Repository navigation
release: backend reorganization and verified development changes - #286
Conversation
When a turn hits the tool-call cap, finalAnswer returned the model's tools-disabled reply without ev.CheckFinal, so a definition with an output contract could end a capped turn "succeeded" with no output: the daemon's correction round is what usually pushes a reviewer to the cap, and the answer it forced skipped the very check that asked for it. The capped answer now goes through the same check the loop runs on a no-tools reply, with the one correction round the check may ask for. A second miss fails the turn, as it does in the loop. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Store.Fork's INSERT column list omitted permission_mode, so forks silently reset to the schema default 'prompt' regardless of the source session's consent mode. A Full Access (automatic) session forked into one that prompted again — including file-access authority, which reads permission_mode per root. Add permission_mode to the fork copy so the fork inherits the mode exactly, add TestForkCarriesPermissionMode, and drop TestPermissionModeForkStartsWithDefault, which codified the bug.
session.model, session.effort and workspace.set write model, provider, effort and cwd to the sessions row, but the root's agents row only copies those columns in EnsureRootAuthority at open. agents.inspect, root snapshots and RootAgentViews therefore reported the pre-change values until the daemon reopened the session. Read the root's selection from sessions in the two queries that can return the root row. Children keep their admitted values. The physical columns stay populated for older binaries; no schema change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…mirror Session kept a full session.Meta captured at open. Its ID was used everywhere; the rest mixed identity, the installed runner's selection, saved facts read at decision time, and two write-only mirrors nobody read. Turn completion wrote model and provider back to sessions from that mirror, which was harmless only because model changes are refused mid-turn. Replace the mirror with immutable identity (id, kind, engine, definition revision) and the applied selection (model, provider, effort). Read the goal at turn completion and the directory on request through the store. Drop the model/provider fields from RootTurnCommit and its sessions writeback; the selection commands own that row. Two ordering fixes ride along: session.effort persists before it applies the runner level, so a rejected save leaves every reader at the previous value, and goal.set/goal.run refuse while a client operation is busy, matching effort, model and workspace, so a goal formulation and an explicit goal cannot overwrite each other. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Effort had two vocabularies: explicit values (wire, sessions.effort, config, definitions) were off or a level with blank meaning not chosen, while applied values (runner, catalog helper, TUI state, HTTP field) used blank to mean off. Seven translators sat between them and the child spawn path had none, so session.effort.get and the web picker reported off for a reasoning session, and a child spawned with effort=off sent the literal to chat-completions. Standardize on off or a level everywhere. session.create accepts effort (protocol 6.9, additive) and the daemon stores a concrete value from creation: the request, else the definition default, else the configured default, resolved against the model's catalog entry by the new config.ResolveEffort. Rows saved blank before this are resolved once at open. Both llm encoders turn off into an omitted parameter. The catalog helper, compatibility rule, TUI and web app use the same values; the daemon label function, the TUI's translators and the Responses none special case are deleted. The web app sends effort on create instead of a create-then-session.effort two-step. Sessions no longer follow later changes to the global default; it is resolved when the session is created. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…cabulary Session state ownership: root projection, no row mirror, one effort vocabulary (protocol 6.9)
The web app labelled the off level "Default" and the welcome picker used off to stand for "no explicit choice". With effort now stored as a concrete value, the two paths diverged: sending without touching the picker omitted effort and the daemon resolved the model default (usually low), while clicking "Default" sent off and created a session with no reasoning. The same label produced two outcomes. Label off as "Off". Give the draft a real "Default" entry, shown when nothing was chosen and no usable configured default exists, that clears the draft choice so create omits effort on both paths. A model change that makes the chosen level unsupported falls back to Default instead of forcing off. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fix(web): give the welcome draft an explicit Default effort entry
Claude models on inference.net advertise supported_endpoints: ["messages"]
only, but whip posted OpenAI chat-completions to them; the gateway's
OpenAI-to-Anthropic translation is lossy for thinking models, and the
second tool round of a thinking-enabled session 400s as an opaque
upstream error (Anthropic requires signature-carrying thinking blocks
replayed with tool_use; chat history cannot represent them).
Add a third API flavor, anthropic-messages, opt-in per provider entry:
- internal/llm/messages.go: encodeMessages translates history natively
(system hoisting, tool_use/tool_result blocks, native tool schemas,
Anthropic SSE parsing with thinking+signature capture), and
messagesOnce posts to {baseURL}/messages with Bearer + x-api-key and
anthropic-version headers.
- Message.Thinking []ThinkingBlock persists captured thinking with
signatures in the session store (legacy sessions decode with it nil);
encodeMessages replays thinking before tool_use, and the chat encoder
strips it via a copy so captured blocks never reach a chat endpoint.
- reasoning_effort maps to thinking.budget_tokens (1024-32768 by level,
floored at the 1024 minimum, capped at max_tokens/2 so thinking cannot
squeeze out all visible output); off/none omit thinking; unknown
levels error loudly instead of guessing.
- config.APIMessages + KnownAPI/GenericChatAPI replace the scattered
API != openai-completions validation so the flavor is admitted across
daemon gates; daemon.ModelClient builds a messages client when the
provider entry opts in.
Live-validated against api.inference.net/v1/messages with claude-opus-5-5:
200 OK, correct Anthropic response and usage shape. Sessions revert to
chat-completions by removing the api field; no session schema changes
beyond the additive Thinking field.
- rename shadowed predeclared cap -> budgetTokens (predeclared) - replace manual map loop with maps.Copy (modernize) - align struct literal fields (gofumpt) - use canonical header names X-Api-Key / Anthropic-Version in Set/Get calls (canonicalheader); behavior unchanged since net/http canonicalizes keys regardless
1. Parallel tool results (High): consecutive tool-role messages were
each emitted as separate user turns; Anthropic requires all
tool_result blocks for one assistant turn inside a single user
message. Merge them before flushing on the next non-tool role.
2. Complete ignores Messages flavor (High): Complete posted a
non-streaming request to /chat/completions even on the messages
flavor, where encodeMessages always sets stream:true. Route
FlavorMessages through Stream with nil callbacks instead, matching
the responses/openAI path.
3. Messages stream drops usage (Medium): parseMessagesSSE read
input_tokens (message_start) and output_tokens (message_delta)
but always returned Usage{}. Capture both and return a populated
Usage so token counts, cost display, and budget settlement work.
Read message_start usage from the nested message.usage field.
4. Thinking budget equals max tokens (Medium): the 1024 budget floor
could meet or exceed max_tokens on small calls, which Anthropic
rejects. Bump max_tokens above the budget when needed, and always
include max_tokens when thinking is enabled (Anthropic requires
the field even when MaxTokens was zero).
Add tests for each fix: merged parallel results, usage reporting from
SSE, and budget-below-max-tokens across small/zero MaxTokens values.
Lint (golangci-lint) — 21 issues across 6 files: - errorlint: nolint on intentional direct type assertion in compactionRejected (excludes wrapped errors by design) - nilnil: nolint on 6 return nil,nil in title.go (nil is a valid no-op signal for title initialization) - noctx: use net.ListenConfig/DialContext in title_notifications_test - perfsprint: fmt.Sprint -> strconv.Itoa/FormatBool in test files - staticcheck: convert params to ArchiveResult, copy() instead of loop, tagged switches instead of if/else-if chains - tparallel: add t.Parallel() to subtests - usestdlibvars: http.Status constants instead of string literals - usetesting: keep os.MkdirTemp (short path for macOS socket limit) with nolint directive Docs (test:docs) — 1 type error: - session.command() inferred CommandHandle<string> instead of CommandHandle<"submit">; add explicit return type CommandHandle<O> Test (TestResumeActiveIsolatesFailuresAndAllowsRetry): - open() persisted resolved Effort to store before the factory ran, so a failed restore mutated the saved session. Move SetEffort after the factory succeeds.
development gitignored packages/protocol/generated/ (previously committed on main) but the docs CI job was never updated to generate them. The sdk job got an "Initialize protocol artifacts and SDK" step but docs did not, so sdk-examples.test.ts failed with Cannot find module @whip/protocol. Add Go setup plus the same generate/build step to the docs job.
…vor-dev feat(llm): native anthropic-messages API flavor
The desktop release packaging step times out on slow macOS CI runners because a single 30s AbortSignal covers four sequential cold-daemon operations: connect, create session, submit a tool round trip, and snapshot. 30s is not enough headroom on a cold runner; bump to 60s.
fix(desktop): increase startup verification timeout to 60s
| - uses: actions/setup-go@v5 | ||
| with: | ||
| go-version-file: go.mod | ||
| cache: true |
There was a problem hiding this comment.
Docs CI uses unpinned old setup-go
Low Severity
The new docs generate path installs Go with floating actions/setup-go@v5. Other jobs in this change pin setup-go v7 by commit SHA, so this job can pick up an unreviewed v5 revision and drift from the Go toolchain the rest of CI uses.
Reviewed by Cursor Bugbot for commit 5aa9a81. Configure here.
feat(theme): add neon-city-dark and seti themes from PR #155
| await page.locator('[data-session-info-bar]').getByRole('button', { name: 'Session actions', exact: true }).click(); | ||
| assert.equal(await page.getByRole('menuitem', { name: /^(Trace|Session details|Hide session details)$/ }).count(), 0); | ||
| await page.keyboard.press('Escape'); | ||
| checks.push('tabs omit ellipsis controls; right-click and keyboard menus keep close actions last; session overflow omits Trace and details'); |
There was a problem hiding this comment.
Session details assertion contradicts overflow menu
Medium Severity
The new session-tabs check expects the Session actions menu to omit Session details, but SessionTopBar now places that item in the overflow and live sessions always pass onDetails. The same menu is opened and used for details in browser-toolbar and the top-bar unit tests.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 271c0f8. Configure here.
…y-unblock fix(deps): repair security blockers before backend reorganization merge
refactor(backend): separate responsibilities with a frozen client contract
refactor: reuse shared definitions with frozen client behavior
| # PRs use their immediate parent branch; initial push/reusable runs | ||
| # without a previous commit use the current commit for attribution. | ||
| if [ "$COMPATIBILITY_BASE" = 0000000000000000000000000000000000000000 ]; then COMPATIBILITY_BASE="$GITHUB_SHA"; fi | ||
| node scripts/backend-compat/run.mjs --candidate "$GITHUB_WORKSPACE" --base "$COMPATIBILITY_BASE" --output "$RUNNER_TEMP/backend-compat" |
There was a problem hiding this comment.
Compat job may miss required SHAs
Medium Severity
The new required compatibility job checks out github.sha and then git rev-parse/git archive both the frozen manifest.revision and COMPATIBILITY_BASE (pull_request.base.sha, else github.event.before, else HEAD). It never fetches those objects explicitly. With ref set to a SHA, fetch-depth: 0 still follows that commit’s ancestry, so a force-push before SHA or a frozen development revision that is not an ancestor of this ref makes git archive fail and turns the required go gate red.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 58860dc. Configure here.
(cherry picked from commit 8884403)
# Conflicts: # .github/workflows/ci.yml # .github/workflows/mobile.yml # .github/workflows/publish-cli.yml
…ironment fix(process): preserve Go build settings in managed children
fix(agent): run the output contract check on a capped final answer
…ctions-16d1d447e5 chore(deps): bump the actions group across 1 directory with 6 updates
fix(ci): repair docs deployment and validate the reviewed release transition
fix(web): make REPL Latest navigation win over stale anchor restoration
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 23012e9. Configure here.
| await page.getByRole('button', { name: 'REPL', exact: true }).click(); | ||
| const notebook = page.getByRole('region', { name: 'REPL executions', exact: true }); | ||
| await expect(notebook.locator('[data-history-gap]')).toContainText("Couldn't load messages"); | ||
| await expect(await findGap(notebook)).toContainText("Couldn't load messages"); |
There was a problem hiding this comment.
REPL gap hunt can refetch history
Medium Severity
The failed-range check now calls findGap on the REPL notebook, which wheels and forces scrollTop to 0. REPL reading lists auto-load older pages on any near-top scroll, unlike chat, so this can send a new history.page and fail the assertion that chat and REPL share the failed range without refetching.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 23012e9. Configure here.


Promote the reviewed development branch to main, including the backend responsibility split and shared definitions (#320, #321), dependency/security repairs (#329, #325), the capped-answer output-contract fix (#192), and preserved Go build settings in managed children (#330).
The backend reorganization preserves the SDK, protocol, event order, acceptance/cancellation/reconnect behavior, and persisted-data contract frozen at pre-reorganization development
271c0f8d2a35648d1b45056d57432590b783483c. This release also contains intentional work already present before that baseline: protocol 6.9 with concrete effort defaults, admission-owned session titles and ordered title notifications, native folder creation, and conversation-based compaction defaults. The exact differences from the older 6.8 main and their source commits are documented indocs/release-readiness.md; the compatibility test validates both the independently built frozen reference and the release candidate.Release-readiness repairs are separate: #331 restores protocol/SDK generation before docs deployment tests and checks the exact historical release transition. #333 fixes a pre-existing REPL Latest navigation race by canceling an obsolete anchor restore after the history read; it also adds the existing REPL, settings, child-agent, activity, and history-recovery checks to CI using isolated fixtures. Neither repair changes SDK, backend, provider, or database semantics.
Validation passed for exact development head
23012e9522c5db7a9d52206326c71056fa4977b1:The release PR CI, development CI, security checks, and docs preview deployment/live verification passed before merging. Merge commit
1c5da05987d2869687dfc95fc9128ad2ad68e0f0has the exact validated source tree03eef4c4569125da0c7d6a7508f9ab2e07ec523e.