Skip to content

release: backend reorganization and verified development changes - #286

Merged
samheutmaker merged 86 commits into
mainfrom
development
Oct 17, 2026
Merged

samheutmaker merged 86 commits into
mainfrom
development

Conversation

@anish-inf

@anish-inf anish-inf commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

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 in docs/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:

  • Full Go, SDK, web, mobile, security, desktop, and distribution CI.
  • Linux/macOS compatibility against the frozen reference and main, including Unix/WebSocket transcripts, ordering/cancellation/reconnect, retained parent/child REPL state, schema integrity, traces and rollback.
  • Successful docs preview build, deployment and live Worker verification.
  • Isolated staged desktop acceptance plus Chromium/Firefox REPL, provider/effort, child-agent, activity and history-recovery acceptance. Local checks use temporary homes and fixture providers; no personal provider accounts or sessions are used.

The release PR CI, development CI, security checks, and docs preview deployment/live verification passed before merging. Merge commit 1c5da05987d2869687dfc95fc9128ad2ad68e0f0 has the exact validated source tree 03eef4c4569125da0c7d6a7508f9ab2e07ec523e.

samheutmaker and others added 25 commits September 26, 2026 17:15
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

@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 .github/workflows/ci.yml
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5aa9a81. Configure here.

feat(theme): add neon-city-dark and seti themes from PR #155

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

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');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Reviewed by Cursor Bugbot for commit 271c0f8. Configure here.

@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 .github/workflows/ci.yml
# 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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Reviewed by Cursor Bugbot for commit 58860dc. Configure here.

AmarUCLA and others added 3 commits October 16, 2026 19:02
# 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(web): make REPL Latest navigation win over stale anchor restoration
@samheutmaker samheutmaker changed the title Development release: backend reorganization and verified development changes Oct 17, 2026

@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 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");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Reviewed by Cursor Bugbot for commit 23012e9. Configure here.

@samheutmaker
samheutmaker merged commit 1c5da05 into main Oct 17, 2026
62 checks passed

This branch had an error being deployed

1 failed and 1 active deployments
desktop-signing — 23012e95 Deployed Oct 17, 2026 by samheutmaker via desktop / package #35
docs-preview — 23012e95 Deployed Oct 17, 2026 by samheutmaker via deploy #25
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.

3 participants