Land the compaction-loop fix, last_turn counters, and UI cleanup on whip-rlm - #149
samheutmaker wants to merge 72 commits into
Conversation
…s it surfaced Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he late-ACK fix Move whip-eval campaigns onto Modal VM Sandboxes without changing the native Harbor/Pier workers, task bytes, model settings, deadlines, or grading: one Docker-capable VM per attempt, a detached coordinator function, immutable allowlisted bundles, and a CLI (whip-eval modal submit/status/logs/fetch/ cancel/reconcile) that survives client exit. Reports label cloud runs as a separately qualified environment; nothing promotes the native baseline. Qualification results recorded under evals/reports/modal-*: infrastructure, 90-way credential-free lifecycle (90/90), and the two-task real-model smoke (2/2 passed, $5.32 estimated). That smoke exposed the one production defect, a controller export acknowledgement racing a worker that had already exited; modal_cloud.py now skips the obsolete ACK on an exact-owned integer exit and only suppresses a typed NotFoundError after ownership re-proof. 102 tests pass. Plan and resume plan live in .ai-docs/plans/modal-evals. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Inference.net now lists the pinned route as moonshotai/kimi-k3 while the bare kimi-k3 id still serves completions at high effort. prepare.catalog falls back to a listing whose last path segment equals the pin, keeps kimi-k3 as the id whip calls and caches so routing and pricing lookups stay aligned, and records the listed id as listed_id. Exact listings still win; ambiguous or near-miss listings still fail closed. 103 tests pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…-size limit Inference.net now rejects prompt + max_tokens above the 1,048,576-token context, and the pinned whip binary sends max_tokens equal to the catalog's max_completion_tokens when the config leaves maxOut at 0, so every smoke trial died with a 403 before its first model call. The frozen eval configuration now sets maxOut to 262,144: it cannot realistically bind and leaves about 786K tokens for prompts. Documented as a deviation from the native cohort, which ran effectively unbounded. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
| // A fold point inside a turn means the live agent pinned that turn's | ||
| // opening user message verbatim after the summary (see agent.compact); | ||
| // the raw row sits before the cutoff, so re-derive the pin here or a | ||
| // resumed agent would continue its turn on a paraphrase of its orders. |
There was a problem hiding this comment.
🟡 warning This re-pin fires for ANY fold whose first kept row isn't a user message, but the live agent only pins when compactTailStart returned split=true. Two fold shapes reach a non-user fold point WITHOUT a live pin: (a) the clamp in compact() (tailStart = sysIdx+2, splitting turn 1 at its first assistant row) when the walk ended with start == 1 and split=false — live view is [sys, summary, a1, ...] with no pinned q1; (b) rollback/reload of compaction rows recorded by the previous release, where pinning didn't exist. In both cases a restarted session now reloads [sys, summary, q1, ..., a1, ...] — a user message the pre-shutdown agent never had in its view, with the summary already covering that content. That's a view-across-restart divergence, and it grows context on every reload of an old compacted session. Fix: persist a pinned flag (or the pinned raw seq) on the compaction row and only re-derive the pin when the fold actually split, defaulting legacy rows to no pin.
🔍 loupe · go-review✅ no issues · 53 files · The shown diff is the MCP-import feature set (OpenCode discovery source, project/.mcp.json gate, daemon mcp.import.candidates/apply RPCs, CLI rewrite onto mcp.Candidates/mcp.Apply, web import screen + Welcome/Settings integration, protocol 6.6→6.7) plus login-flow touch-ups; the headline compaction/session changes from the PR description are not in the presented diff, so they could not be reviewed here. I checked the behavior-changed exports and their callers: Merge (now variadic, precedence now opencode < ~/.claude.json < codex < .mcp.json < whip) in config.go/config_test/config_trust_test — consistent; ImportPolicyFrom (project off by default, zero-value ImportPolicy now imports nothing) against its callers in cmd/whip/mcp.go and LoadMerged, which both go through ImportPolicyFrom; mcpImportCLI rewritten onto Candidates/Apply, with the CLI honoring source gates and the web screen deliberately bypassing them after explicit ticks; clientMCPImport/importState mirrors ImportPolicyFrom defaults (project off, others on) including the nil-Enabled case; SelectLoginTeam now also accepting choose_project resets projects/ProjectID correctly; the {env:VAR}→${VAR} rewrite in opencodeReferences resolves correctly under Go's regexp replacement rules (verified Run details
Last reviewed commit: |
…ion log Runs -a and -b are local preparation failures (key absent, provider model rename); -c ran end to end with the reviewed controller (complete, proven cleanup) but every trial died on the provider's new total-token limit; -d passed all six terminal tasks after the maxOut cap, with both repository tasks lost to a provider-side 30-second no-token stall. The plan's log carries the diagnosis and the pending decision. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Brings the detached Modal eval workflow, its qualification record, the late-ACK controller fix, and the 2026-09-13 smoke reruns onto the branch the whip fixes are landing on. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Inference.net's kimi-k3 route now stalls on long calls (a 30-second no-token cutoff killed both repository tasks in today's smoke), so the pinned route moves to kimi-k3-fast. The model id was spread across the protocol file, whip's config writer, the observer's CLI arguments, the native runner's model label, and an adapter guard; it now lives in whip_evals.common.MODEL, the observer takes it from the contract's configuration, and a test asserts frontier/protocol.json agrees. The track name changes with it, so cloud reports form a separate environment from the native kimi-k3 cohort. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…no defaultModel The runtime-ab tests drive observe.run with a stub configuration; canonical trials still take the model from the contract. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Supersedes the 2026-09-11 MCP review with recorded decisions: repairs plus a truthfulness pass, definition server list as a boundary, eager connect with a separate project import source, project-over-user-import precedence, import confers trust, ACP-only additive attach, structured content and image handles, and a five-server acceptance matrix. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The suite no longer reads the developer's ~/.codex/config.toml by default, and the opt-in run logs header counts instead of header values. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The daemon-managed stdio path copied cfg.Env into the process manager verbatim, so a documented "$VAR" reference reached the child as the literal string while the fallback path resolved it. Both transports now call one connectSecrets step. "!cmd" helpers run under the connect's own context via ResolveSecretContext, so a cancelled connect no longer leaves a helper running. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
headerTransport re-adds configured headers after Go's redirect logic has already stripped Authorization for a cross-origin hop, so a redirecting endpoint handed the bearer token to another host. The remote client now refuses redirects that leave the endpoint's scheme and host; same-origin redirects keep the headers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The daemon reads null as every host server and [] as none, but the SDK's generic list helper collapsed both undefined and [] to null, so an agent computed with an empty allowlist requested all servers. Only the MCP field changes; other list fields keep nil-equals-empty semantics. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Connect and catalog refresh read only the first page, so anything past it did not exist to the model. listAllTools follows cursors under the startup deadline, bounded to 64 pages, rejects a repeated cursor, and returns the whole catalog so callers publish it atomically. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
kickAutoReconnect only ran from the disconnect watcher, so one failed redial ended the chain at a single attempt despite the three-attempt budget. A failed automatic attempt now schedules the next one; a startup or manual attempt still stays failed for a human, and a manual reconnect resets the automatic budget. The give-up test asserts the exact count. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Codex TOML reader parsed the whole file, so one [[array]] table or an unsupported top-level value anywhere in ~/.codex/config.toml failed all MCP discovery, and the daemon showed "no servers" for "failed to parse". Only [mcp_servers...] tables are parsed now; foreign sections are skipped and malformed server tables still error. Discovery errors become failed status rows named by source, are logged at daemon startup, and are printed by whip mcp import. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…er imports The repository's .mcp.json shared the claude gate with the user's global ~/.claude.json, and a user-global Codex entry silently overrode a project file of the same name. The project file is now a third import source, project, off unless enabled because a repository author wrote it, and the merge order is native, project, codex, claude-global. Blocked rows name their source. The daemon, TUI, web panel, and protocol status carry the new source; schemas and generated TypeScript are regenerated. Also adds SourceErrors to the client-control test fake so it satisfies the interface extended in the previous commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Materialized imports dropped their Origin and Source, so an imported server copied into whip's own config is now trusted like a hand-written entry, no per-call consent. This is the one path from imported-and-prompting to native-and-trusted; the command says so on both dry-run and apply. Closes mcp-polish item 8 (overlay entries): a materialized import is native, not a patch over its source. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…itive Startup filtered discovery by the definition's server list while mcp.attach rebuilt the manager from every native server, so one authorized attach widened a restricted agent. mcp.Select is now the single selection step for the factory and for attachment. Attach no longer replaces the running manager or re-reads native configuration: attachments are untrusted and additive across names, names outside the definition's list or belonging to a native server are recorded as blocked rows, and re-attaching a non-native name replaces that entry. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
flattenResult dropped structuredContent whenever any text existed and reduced images to placeholders. A call's text now keeps every text part and appends structured content as JSON; image, audio and binary resource parts travel as attachments that the calling agent's services store as content handles named in the text. Image parts also reach the root's next turn through the existing screenshot sink; children get the handle only. CallChecked returns a result struct so nothing is lost between the manager and the dispatcher handler. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Policy-filtered and attach-refused servers showed as "disabled" and unreadable discovery sources as "failed", so clients could not tell a row the daemon can act on from one it cannot. Blocked rows now carry status "blocked" and source rows "unreadable"; both are never live servers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Removes the private JSON attach box: it duplicated whip mcp add with a
sensitive one-shot input, and attach is an ACP-facing operation now.
Renames "Available tools" to "Built-in tools" since it lists whip's own
schemas, not MCP tools. Each server row offers only the controls the
daemon can honor for its state (none for blocked or unreadable rows),
every control names its subject and scope ("Reconnect docs", "Disable docs
for this session"), and import toggles live under "Host import defaults"
with the project source.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The palette derived its server rows from local config, so imported, attached and remote-daemon servers appeared in status but not in the palette, and a stale local name offered controls the daemon could not honor. The model now keeps the last mcp.status rows and the palette offers each server only the actions its state supports, with blocked and unreadable rows pointing at the status view that explains them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Brings the live session trace and OTLP export (spans table, span events, trace.page/trace.export, the trace view kind, and whip sessions export). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
| Settle: func(result llm.ModelAttemptResult) error { | ||
| Settle: func(result llm.ModelAttemptResult) (settleErr error) { | ||
| var settled sessionstore.ModelCallSettlement | ||
| defer func() { s.modelCallSpanEnd(agentID, reservation.ID, attempt, settled, result, settleErr) }() |
There was a problem hiding this comment.
LLM spans close before final settlement
High Severity
Settle always ends the LLM span, including when accounting is only parked in pendingAccounting or when a later model.call.corrected write has the real tokens and cost. endSpanTx refuses to update a span that already has end_ns != 0, so those retries never land. Cancelled calls that interruptOpenSpansTx closes first show up as interrupted with no usage, even though durable model-call accounting was corrected.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit d24b088. Configure here.
| // keep nanoseconds, sort lexicographically as text, and still parse with | ||
| // time.RFC3339. Every stamp that is stored or compared in SQL goes through | ||
| // formatStamp so no two columns disagree on width. | ||
| const stampLayout = "2006-01-02T15:04:05.000000000Z07:00" |
There was a problem hiding this comment.
🟡 warning Switching the stored stamp layout from time.RFC3339 (2026-09-14T19:04:05Z) to a fixed nine-digit fraction (2026-09-14T19:04:05.000000000Z) changes string width and mixes formats with rows already stored in users' databases. The comment says these stamps are 'stored or compared in SQL' — and lexicographically an old stamp sorts after a new stamp at the same second ('Z' > '.'), so any WHERE expires <= ? / ORDER BY created_at-style comparison across pre- and post-upgrade rows can invert or exclude rows at a same-second boundary (e.g. an old …T19:04:05Z fails <= …T19:04:05.000000000Z even though it's equal-or-earlier). Go's time.Parse handles both formats per-row, so this is only a problem where SQL compares the text. If internal/session/migrations.go does not already rewrite existing rows to the new layout (I could not verify), either add a one-time normalization of stored stamps or compare via datetime()/parsed values rather than raw text.
The merge of session-trace into this branch failed lint, runtime, sdk and the coverage gate. Each failure had a small cause: - gofmt/golangci findings in the new trace files and four pre-existing ones (unused results, bodyclose, string conversions). - The large-grants runtime fixture registers five fixed tools, so the receipt count is toolCount-5, not toolCount-4. - The model picker acceptance script now matches OpenRouter's dark logo variant (#openrouter-dark). - Coverage sat at 89.6% against the 90% floor; the CLI export, trace RPCs, export fallbacks and span store error paths now have tests (90.1%). The new tests exposed three real defects, fixed here: - `whip sessions export <root> -o -` ignored every flag after the root id because the standard parser stops at the first positional, so it wrote <root>.otlp.json into the working directory. Flags now parse on either side of the root. - `llm.Message.CallID` never reached the messages table because the wire struct lacked it, so the exporter could not attach the produced assistant message to its LLM span. - `StoreContent` always creates a reference, so the inline branch of trace.export was dead; the result now always carries a content handle (protocol regenerated, docs updated). The web workflow inventory gains rows for trace.page and trace.export, which its test checks against the generated manifest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
| // The partial answer cannot be resumed; within the budget it is | ||
| // discarded and the whole message is generated again. | ||
| if regenerated >= c.regenerations() { | ||
| return msg, total, err |
There was a problem hiding this comment.
🟡 warning Regenerating a stream that failed after its first delta re-runs streamOnce with the same onText/onThink/onToolCall callbacks, so every consumer of the stream receives the discarded partial output a second time (e.g. par + ok for a stream that stalled mid-answer). The returned Message is clean because msg is rebuilt per attempt, but persisted text/thinking events and live UI streams are fed by the callbacks, not by the final message. The new RetryEvent.Regenerating/Discarded fields imply the intended contract is 'consumers rewind Discarded characters on a regeneration' — but nothing in the agent/session turn loop appears to do that, so a mid-stream provider stall (exactly the inference.net 30 s no-token case this targets) appends phantom deltas to the event log. Either rewind the discarded bytes in the stream consumer before retrying, or add a rewind signal the TUI/web transcript handles; at minimum cover the duplicate-delta behavior in a test so the contract is explicit.
Why: whip now records a session trace in sessions.db (spans table, schema 19). The eval observer already backs up the whole database after freezing the daemon, so every span comes with the evidence; the report now says so per trial (span_count, open_span_count) instead of leaving it to a manual audit. The README explains how to export a fetched trial as OTLP offline. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
| row[key] = metrics.get(key) if usage_complete else None | ||
| # Token totals are lower bounds when usage calls are unknown; they are shown, | ||
| # and the unknown-usage count beside them says how firm they are. | ||
| for key in ("unknown_cost_calls", "unknown_usage_calls", "model_calls", "input_tokens", "output_tokens", "cache_tokens"): |
There was a problem hiding this comment.
🟡 warning row[key] for calls/cost/tokens is now taken straight from state.json with no integrity check left anywhere. This diff deleted verify_snapshot (sessions.db ↔ state.json model_calls and content-bodies agreement), the per-body digest/size checks, and the accounting_snapshot_mismatch cross-check — and integrity.py/result_evidence.py were removed in the same PR. I verified upstream too: adapter.py's whip_accounting_complete now gates only on observer exit code, cleanup, evidence_errors, and final_accounting_complete, so nothing re-establishes the lost coverage. Concrete failure mode: a fetched trial tarball whose state.json call rows were edited (or a partial/desynced artifact upload produced by an interrupted cloud sync) passes missing_required_evidence, identity, and finality checks, so evidence_complete=True and the fabricated known_cost_usd/token totals flow into summarize, arms, and the baseline unchanged — previously the same artifact failed with snapshot_inconsistent/accounting_snapshot_mismatch/content_digest_mismatch. At minimum restore the cheap half (compare model_calls in the copied sessions.db against state["calls"] — it was ~5 of the removed lines) or stop labeling that path evidence-complete so promotion gates don't consume unverified numbers.
| "input_tokens": None, "output_tokens": None, "cache_tokens": None, | ||
| "cleanup_complete": None, "error_codes": [], "artifacts": {}, "phase_seconds": {}} | ||
|
|
||
|
|
There was a problem hiding this comment.
🟡 warning termination() scans diagnostic — the agent's whole cli.stderr plus ndjson error lines — for these substrings before classifying any agent_error. "429" and "rate limit" match ordinary task output: a terminal-bench agent that curls an endpoint and logs "429 Too Many Requests" or writes about "rate limit" in its own work, then fails for an unrelated agent reason, gets termination_source = "provider_error", inflating status_detail.provider_errors and the report's provider-error counts. The old tuple's "429"/"502"/"503"/"520" had the same weakness; the expansion ("rate limit", "stream timed out" etc.) widens it. Restrict the numeric/status tokens to lines matching whip's own error shape (api error: prefix, the ndjson-structured errors) and keep free-form stderr matches to unambiguous whip/provider phrases.
Eight-task smoke on the session-trace build (386e587): complete, 7/8 passed, 8/8 evidence and accounting complete, and every trial database carries its full trace (1,518 spans, none open, one llm span per dispatched model call). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
OpenCode's global config files (~/.config/opencode/{config,opencode}.json[c])
now feed discovery below every other import, with their own mcpImport gate,
palette toggles, status field and panel row. An entry that opts into OAuth
imports disabled with a sign-in note, the way the legacy Claude sse transport
does, instead of failing on every connect.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mcp.Candidates lists every discovered server once with a state (importable, native, disabled in its source, excluded by an only/exclude list, or unsupported) and a secret-free brand hint; mcp.Apply writes the chosen names as native entries. whip mcp import now runs on the same core: it imports the importable, gate-admitted candidates and no longer copies servers a source turned off or ones whip cannot run. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mcp.import.candidates lists the servers other agents configured on a host, with a state per row and no command lines, env or headers on the wire; mcp.import.apply copies the chosen names into the native mcp block through the versioned config write and records that the offer was answered. Both sit beside config.get/update because the New session screen has a host but no session yet. The SDK exposes them as client.mcpImport. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One flat list of the servers other agents configured on a host: importable servers first, already-native ones last, a monogram tile per row, and a caveat only where one exists. Ticking is the choice, Import writes native entries through mcp.import.apply, Skip records the answer. No icons yet; the daemon's brand hint is there for a later, web-only change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Once a provider is ready, a host that has servers configured for other agents shows the import screen instead of the composer, once; Skip or Import returns the composer. Settings › Configuration gains "Servers from other agents", which opens the same screen for the selected host and reports what was imported. An older daemon gets one explanatory line instead of the screen. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An empty cwd no longer reads a relative .mcp.json from the daemon's own
working directory; the CLI and daemon test binaries clear XDG_CONFIG_HOME so
OpenCode discovery cannot reach a developer's real files; a repeated Skip or
an apply with nothing new leaves config.json untouched; duplicate names in
one apply count once; the always-true offered field left the apply result;
OpenCode {env:NAME} placeholders import as ${NAME} references.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Four review lanes (reuse, simplification, efficiency, altitude) over the PR, applied together. Go: Merge is variadic lowest-first and both callers use it; discovery is one `discovered` value with a `sources(policy)` table shared by Candidates and LoadMergedFiltered; ImportSourcePolicy.Admits/listed replace three copies of the only/exclude test. Candidate drops SourcePath and Transport (nothing rendered them) and gains Gated so the CLI filters instead of re-deriving the gate; Apply returns the added entries plus an error for an unknown name so the CLI dry-run and the daemon share one path. config.ParseJSONC is the definition rather than a shim. config.get carries mcp_import_offered so a New session tab on an answered host never calls mcp.import.candidates again. Web: the Settings entry is a sibling MCPImportSettings component instead of a prop threaded through the host-defaults form; `included` folds into `overrides`; one source table and Intl.ListFormat for the intro; shared layout styles replace local copies; the post-apply invalidation marks other cwd variants stale without refetching. Tests share one fake. Kept on purpose: brand_hint (the hook for the icon work). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…wer copies
Why: a four-angle review (reuse, simplification, efficiency, altitude) of the
harness found the earlier simplification had stopped at module boundaries.
The observer still serialised the whole transcript four times a second onto
the evidence volume, partial cost was reconstructed from an adapter-side
mirror of a file the observer already writes, the bundle was verified four
times and its manifest written four times, cancellation rode on three
variables the two backends resolved differently, and several report fields
and marker keys had no reader.
Observer: one read-only connection gates on PRAGMA data_version, so the full
snapshot and its two files are refreshed only when the daemon commits;
state.json carries only what the report and the finality rules read (the
database backup at finality is the record); one export routine; aggregate
drops the pricing cross-check and route counts; catalog fetch and cache
shape are shared with prepare.py. Adapter: no 2 s probe loop or
metrics.interim.json; report.normalize_trial keeps the observer's last
metrics.json as the cost lower bound when a job produced no native result,
for both backends. Report: finality is one code, accounting completeness is
computed from the evidence files alone, status_detail comes from summarize,
seven unread row fields are gone, normalize_or_error replaces three
divergent fallbacks. Cancellation: one Event set only by a human; undispatched
trials are cancelled only if the run was. Bundle: create-time verification
only, {name: sha256} inventory, no extract_bundle, one manifest and one
schedule.json in the bundle and beside it, controller modules named once.
Worker: no re-verify, no normalized.json/phase.json, marker without the row.
Fetch: normalizes against the fetch root (no path rewriting), one attempt
scan, no separate presence listing, prune iterates keys. tree_files skips
symlinks instead of blanking an attempt's inventory. `modal logs` removed.
Fixture provider keeps only the qualification path doctor uses.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
| value = cleanup(args.run_id) | ||
| print(json.dumps(value, indent=2, allow_nan=False)) | ||
| return 2 if value.get("status") in ("partial", "failed") else 0 | ||
| return 2 if value.get("status") in ("cancelled", "failed") else 0 |
There was a problem hiding this comment.
🟡 warning Behavior change: exit-code parity with the old status vocabulary is lost. Previously a partial run (some trials ungraded / evidence incomplete) exited 2; now build_result emits incomplete for that exact case, and only cancelled/failed exit 2 — so a campaign that finished with missing results, unproven cleanup, or provider errors returns 0, indistinguishable from a complete run to any scripted caller. If that is deliberate, fine; otherwise add "incomplete" to the tuple (the status_detail fields you added make the distinction visible in the JSON payload regardless).
modal-smoke-20260915-simplify (ref a41fa09): complete, 6/8 passed, 8/8 graded, evidence and accounting complete, 1,390 spans across eight databases with none open, fetched through the new path and pruned. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
| return nil // too little history to fold; rely on the reactive retry | ||
| if errors.Is(err, errNoHistory) { | ||
| // Nothing left to fold and the view is unchanged: stall rather | ||
| // than ask again every round; the reactive retry covers the edge. |
There was a problem hiding this comment.
🟡 warning This stalls on any errNoHistory, but errNoHistory also means "nothing foldable yet this round", not "at the floor forever". Concrete trigger: early in a turn the newest assistant/tool pair alone exceeds the tail budget while sys+pinned+that pair push the estimate over the threshold (e.g. pair ≈30% of the window, threshold 50%, tail budget 25%). history=[user] and pinned=[user], so len(history)-len(pinned) <= 0 → errNoHistory → compactStalled = true for the rest of the turn. As the turn then accumulates older pairs that are foldable, proactive compaction never runs again; the turn grows until a provider context-limit rejection and must burn its single reactive retry to survive. Note this branch by definition made no summary call (errNoHistory returns before buildSummaryPrompt/cli.Complete), so stalling here doesn't save the per-round model call the comment cites — that cost is what compactOutcome handles after a real fold. Fix: return nil without setting compactStalled here (re-checking next round is now cheap), and keep the stall only in compactOutcome for the post-fold-still-over-threshold case — matching the PR's own stated split of the two mechanisms.
| @@ -618,7 +633,7 @@ func (a *Agent) turn(ctx context.Context, input string, parts []llm.ContentPart, | |||
| } | |||
| } | |||
| a.running.Store(false) | |||
There was a problem hiding this comment.
🟡 warning compactStalled (and retriedOverflow) are documented as per-turn ("stalls further proactive folds for the turn", "has been spent this turn") but are only reset on the success paths (here and finalAnswer). A turn that fails after stalling — any model error once compactStalled is true — leaks the flag into the next turn, which then runs with no proactive compaction even though the failed turn's messages are new foldable material; the PR description promises the stall is per-turn. Likewise a failed turn that spent retriedOverflow leaves the next turn without its one reactive retry, so a context-limit rejection surfaces raw instead of folding. Fix: reset the three guards at turn entry (top of turn()) rather than only where the turn succeeds.
MCP import screen for whipcode and OpenCode as an import source
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 4 total unresolved issues (including 3 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 13bce10. Configure here.
Every row carries the vendor's mark instead of a bare letter. The app bundles 199 marks keyed by registrable domain (the MCP vendors in Executor's catalogue, fetched once from integrations.sh by scripts/mcp-brands.mjs, one lazy chunk). For domains the bundle lacks the daemon's new mcp.brand.icons query asks DuckDuckGo's icon endpoint once per domain, caches the answer under ~/.whip/icons, and returns data: URIs: fixed host, no redirects, 48 KiB cap, type sniffed from the bytes with SVG refused, no credentials, four fetches in flight, concurrent callers share one fetch. Anything unresolved shows a first-letter monogram on a stable theme tint; native and unsupported rows keep the quiet grey. The daemon derives brand_key (publicsuffix eTLD+1 of the server URL) next to brand_hint; local hosts, IP literals and tailnet names yield none and never leave the machine. brandIcons: false in the host config, or the new "Server logos" switch in Settings › Agents & execution › MCP servers, keeps every lookup on the host. CSP is untouched: everything drawn is a data URI. Decisions (Sam, 2026-09-15): bundle the 200 + DuckDuckGo, tinted monogram, import screen only. Plan and research: .ai-docs/plans/mcp-logos/README.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CI on the compaction branch now runs golangci-lint v2.13.1, gofumpt and the SDK's own tests, and all three were red: the resolver had an unchecked Body.Close, a hand-rolled WaitGroup loop, a De Morgan candidate and a classic for loop; the merged import code appended to an unsized slice; three trace test files predating this branch were not gofumpt-formatted; and the SDK services test's runtime configuration fixture lacked mcp_import_offered (since PR #152) and brand_icons. golangci-lint v2.13.1 reports 0 issues locally with the repo toolchain, and the SDK check passes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The compaction branch's UI cleanup renamed the Add server dialog's submit button to Connect (cac044b); the web acceptance scripts followed, the desktop smoke did not, and nobody noticed because the desktop job only runs once lint, sdk and test are green, which they were not on this branch until the previous commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Show logos on the MCP import screen
|
See the Actions run logs for details. |


Summary
Four commits from the local
whip-rlmbranch, routed through a PR becausewhip-rlmrequires thego,govulncheck, andcodeqlchecks and rejects direct pushes.internal/agent/agent.go): a fold may now cut inside an oversized turn on an assistant/tool-pair boundary, pinning the turn's opening user message verbatim after the summary. A fold with nothing left to fold makes no model call; a fold that cannot get back under the threshold stalls further proactive folds for the turn; a provider context-limit rejection with nothing left to fold fails the turn withErrCompactionExhausted. The reactive one-retry is decoupled from proactive folds. Background and evidence:.ai-docs/plans/compaction-loop/.internal/session/session.go):applyCompactionre-derives the pinned message when the fold point is not a user message.internal/session/turn_outcome.go, guide fragment, web agent list):last_turncarriesmodel_calls,compactions, andlast_activity_atfor the running turn; protocol types regenerated.cmd/whip/daemon.go): onecompaction.fallbackline in whip.log when summaries fall back to the conversation model.docs/agent-loop.md; Loupe SDK-ergonomics records; distribution test fix.packages/app,apps/web,apps/desktop, andpackages/ui.Merging into
whip-rlmtriggersrelease-whipcode.yml.Test plan
go test ./internal/... ./cmd/...green (one daemon acceptance test flaked once under full-suite load withdatabase is locked, passed on every isolated rerun).npm run checkinpackages/protocol(tsc, interop, drift),tsc -p packages/app/tsconfig.json --noEmit,npm run test:web(602 tests) green.🤖 Generated with Claude Code
Note
High Risk
Changes core context compaction and turn-failure behavior in
internal/agent, plus livelast_turnprojection on every model call; mistakes can truncate agent context or mis-report child health during long tool-heavy turns.Overview
This PR lands the compaction-loop work in the agent and session layers, plus live child health on
agents.listand a round of web/desktop UI polish documented in the accompanying plans.Compaction can now fold inside a single oversized turn (assistant/tool-pair boundary) while pinning the opening user message verbatim after the summary;
applyCompactionon reload re-derives that pin when the cutoff is not a user message. Proactive compaction skips no-op re-folds, stalls when a fold still leaves the prompt at/above the threshold, and surfacesErrCompactionExhaustedon context-limit retry when nothing remains to fold; the reactive overflow retry stays independent.cmd/whip/daemonlogscompaction.fallbackonce per session when the compact model cannot be resolved.last_turngainsmodel_calls,compactions, andlast_activity_atfrommodel.call.*events (guide + web agent rows); protocol types are regenerated.The diff also adds extensive
.ai-docs/plans/write-ups and evidence (compaction incident, empty-workspace front door, loading-state rules, eval-harness simplification record, MCP contracts implementation log, Loupe SDK notes).UI cleanup (per PR scope, not in the markdown-only diff snippet): honest session-opening placeholders and picker skeletons, New Chat / empty-workspace routing, provider-readiness priming, and related test/doc updates across
packages/appand siblings.Reviewed by Cursor Bugbot for commit dd8944f. Bugbot is set up for automated code reviews on this repo. Configure here.