Skip to content

Land the compaction-loop fix, last_turn counters, and UI cleanup on whip-rlm - #149

Open
samheutmaker wants to merge 72 commits into
whip-rlmfrom
compaction-loop-and-ui-cleanup
Open

samheutmaker wants to merge 72 commits into
whip-rlmfrom
compaction-loop-and-ui-cleanup

Conversation

@samheutmaker

@samheutmaker samheutmaker commented Sep 14, 2026

Copy link
Copy Markdown

Summary

Four commits from the local whip-rlm branch, routed through a PR because whip-rlm requires the go, govulncheck, and codeql checks and rejects direct pushes.

  • Compaction loop fix (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 with ErrCompactionExhausted. The reactive one-retry is decoupled from proactive folds. Background and evidence: .ai-docs/plans/compaction-loop/.
  • Reload parity (internal/session/session.go): applyCompaction re-derives the pinned message when the fold point is not a user message.
  • Visible child health (internal/session/turn_outcome.go, guide fragment, web agent list): last_turn carries model_calls, compactions, and last_activity_at for the running turn; protocol types regenerated.
  • Compaction-model fallback log (cmd/whip/daemon.go): one compaction.fallback line in whip.log when summaries fall back to the conversation model.
  • Docs: compaction note in docs/agent-loop.md; Loupe SDK-ergonomics records; distribution test fix.
  • UI cleanup across packages/app, apps/web, apps/desktop, and packages/ui.

Merging into whip-rlm triggers release-whipcode.yml.

Test plan

  • go test ./internal/... ./cmd/... green (one daemon acceptance test flaked once under full-suite load with database is locked, passed on every isolated rerun).
  • npm run check in packages/protocol (tsc, interop, drift), tsc -p packages/app/tsconfig.json --noEmit, npm run test:web (602 tests) green.
  • Replay of the incident's real 64-message turn through the new tail selection: 136,638 estimated tokens fold to 18,805 with the opening message pinned and no orphaned tool result.

🤖 Generated with Claude Code


Note

High Risk
Changes core context compaction and turn-failure behavior in internal/agent, plus live last_turn projection 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.list and 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; applyCompaction on 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 surfaces ErrCompactionExhausted on context-limit retry when nothing remains to fold; the reactive overflow retry stays independent. cmd/whip/daemon logs compaction.fallback once per session when the compact model cannot be resolved.

last_turn gains model_calls, compactions, and last_activity_at from model.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/app and siblings.

Reviewed by Cursor Bugbot for commit dd8944f. Bugbot is set up for automated code reviews on this repo. Configure here.

Sam Hogan and others added 8 commits September 11, 2026 09:34
…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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown

🔍 loupe · go-review

✅ no issues · 53 files · ⚠️ degraded run

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 ${$1} emits $+{+group+}); Apply drops provenance, clears Enabled, and never exposes headers/env over the Candidate/protocol surface. The design points that looked risky — project-authored .mcp.json importable into trusted native config via the web screen, secrets copied from source configs into ~/.whip/config.json, and the one-shot 'Offered' flag set even on an empty skip — are all deliberate, tested, and documented in the diff.

Run details
  • review: headless fallback (agentic run failed)
  • verification: skipped
  • scope: delta
  • dropped: 0 malformed finding(s), 0 malformed concern(s), 0 out of scope, 0 below profile, 0 rejected by verification
  • off-diff notes published: 0

Last reviewed commit: 13bce10

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread internal/agent/agent.go
Sam Hogan and others added 19 commits September 13, 2026 19:56
…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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread internal/daemon/budget.go
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) }()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d24b088. Configure here.

Comment thread internal/session/mailbox.go
// 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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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>
Comment thread internal/llm/openai.go
// 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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"):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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": {}}


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Sam Hogan and others added 11 commits September 14, 2026 20:47
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>
Comment thread evals/whip_evals/cli.py
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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).

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread evals/whip_evals/run.py
Sam Hogan and others added 2 commits September 15, 2026 08:58
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>
Comment thread internal/agent/agent.go
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment thread internal/agent/agent.go
@@ -618,7 +633,7 @@ func (a *Agent) turn(ctx context.Context, input string, parts []llm.ContentPart,
}
}
a.running.Store(false)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Fix All in Cursor

❌ 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.

Comment thread packages/app/src/mcp-import.tsx
samheutmaker and others added 4 commits September 15, 2026 16:51
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
@github-actions

Copy link
Copy Markdown

⚠️ loupe · go-review could not complete this review — whip error: "context deadline exceeded (Client.Timeout or context cancellation while reading body)"

See the Actions run logs for details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant