Skip to content

test(ui-react): pair the remaining Cartesian theme cells in answer and journal runtime - #3345

Open
schlessera wants to merge 126 commits into
epic/1849-test-costfrom
test/3329-pair-theme-cells
Open

schlessera wants to merge 126 commits into
epic/1849-test-costfrom
test/3329-pair-theme-cells

Conversation

@schlessera

@schlessera schlessera commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

Refs #3329
This closes #3329 when the epic/1849-test-cost batch lands on main.

What changed

Two runtime families stop running the full 320/1280 × dark/light matrix. Each now runs only the paired cells 320px dark and 1280px light: 1887: Other has explicit current-text Submit in answer-delivery-runtime.test.ts, and journal configuration coverage keeps small changes first and preserves inspection in turn-journal-runtime.test.ts, which now uses the file's existing journalRuntimeCells. The scenario bodies are unchanged; the answer-delivery diff is a re-indent (git diff -w shows the loop header only). Their theme paint moves to central consumer stories that run in both storybook and storybook-light:

  • New Conversation/Answer theme (Phone 320, Desktop 1280) mounts the actual consumer AskUserCard with the built consumer stylesheet. It opens Other and checks painted, the field's computed color-scheme, blank Submit disabled, then enabled, the ≥44px Submit, field and Submit bounds, and scoped WCAG A/AA, contrast included, on the field and the Submit. It also checks the submit dispatch.
  • Conversation/Journal theme now renders configurationCoverage: "incomplete" with two inputs. It checks badge visibility and truncation, the ≥44px coverage disclosure and search, the inventory count and bounds, and scoped WCAG A/AA on the journal with coverage open.
  • docs/process/theme-parity.md gains an "Answer and journal coverage runtime cells" section with the assertion map, and adds AnswerTheme.stories to the focused run command.

Why this shape

Option 1 from the issue (pair the cells). Both are option 1 because no theme-specific failure mode exists. The theme selector sets only color-scheme, which picks one half of each light-dark() colour token (packages/ui-kit/src/tokens.css, [data-theme] blocks). No geometry, focus, key or settlement path in these surfaces branches on it. The ui-react card wraps the kit card in theme-independent space-y-* utilities only.

The issue's fallback applied to the Other card. The existing kit Decisions/AskUserCard/OtherOpen story cannot own the theme paint, because its meta disables axe color-contrast for a known selected-option gap. Mutation A below shows it stays green. So I added the minimal consumer story rather than keeping a runtime copy. That story's accessible is scoped to the Other field and Submit, so it does not inherit that gap. The journal stories did not render configuration coverage at all, so I extended them the same minimal way.

Assertion map (removed copies: 320 light, 1280 dark)

Removed-copy assertion Retained owner
OTHER_LAYOUT_USES_REAL_THEME Runtime 320 dark + 1280 light (both themes). Answer theme/Phone, Desktop (painted + field color-scheme) in both projects
One explicit Submit, BLANK_OTHER_SUBMIT_DISABLED, blank Enter sends nothing, draft kept across switching, OTHER_SUBMIT_KEYBOARD_REACHABLE, OTHER_SUBMIT_TOUCH_HEIGHT, OTHER_CARD_NO_OVERFLOW Retained cell at the same width. The story repeats aria-disabled, ≥44px and bounds, and adds scoped contrast in both themes
Tap at 320 / Space at 1280, OTHER_CURRENT_TEXT_SETTLES_ORIGINAL_REQUEST_ONCE, one ask_user_response frame Retained cell at the same width (the story dispatch is a fixture callback only)
Journal coverage: badge description/visible/outside truncation/not clipped, row counts and visibility, index group, Turn facts, ≥44px disclosure/search, complete height-bounded inventory, filter, debounce, refetch retention, refetch error, no egress Retained cell at the same width. Journal theme/Phone, Desktop add badge, ≥44px, inventory and scoped contrast in both projects

Settlement, frames, Revert/Undo, download bytes, keyboard and touch paths still run at both widths. This change touches only the two families.

Proof

Tested base origin/epic/1849-test-cost = 3bee06dce265; head e3df429bf5d5.

bun scripts/proof-system.ts classify --base 3bee06dce265 --head e3df429bf5d5  # not a proof-system change
NODE_OPTIONS=--max-old-space-size=8192 bun run typecheck                     # pass (tsconfig includes packages/*/stories)
bun run lint                                                                 # all gates clean
bun run test tests/decision-citations.test.ts                                # 44 pass
bun run check:pr --base origin/epic/1849-test-cost                           # Local preflight passed

Storybook (pinned image via scripts/visual.mjs): --project=storybook --project=storybook-light --filter=AnswerTheme.stories --filter=JournalTheme.stories → 8 passed (4 files).

Mutation A (Other card): AskOtherField input color: light-dark(#fbf8f1, <ink>), a light-only break, then rebuilt. Answer theme/Phone and Desktop failed in storybook-light only: expected [ { id: 'color-contrast', nodes: [ [ 'input' ] ] } ] to deeply equal [] at accessible (_consumer-parity.tsx:126). storybook stayed green. Under the same mutation the kit Decisions/AskUserCard stories stay green (22/22) in storybook-light. Restored.

Mutation B (journal coverage): inventory <ul> color: light-dark(#fbf8f1, var(--bk-color-ink)). Journal theme/Phone and Desktop failed in storybook-light only on color-contrast (.font-mono > .break-all) at the new accessible call (JournalTheme.stories.tsx:125 in the play). With the journal story as it was before this PR, the same mutation stays green (2/2). Restored, then rebuilt.

Runtime (turn-journal-runtime.test.ts), run in the pinned Playwright image with --network=none:
(pass) journal configuration coverage … dark 320, (pass) … light 1280; 21 pass. The one failure, keep-both refreshes the actual disposable index, is untouched by this PR. It failed because the image lacks bubblewrap (Agent worker boundary unavailable: bubblewrap is missing).

Case counts: the Other family goes from 4 to 2 cases, and the journal coverage family from 4 to 2. That is 8 → 4 runtime cases, about 11 s at the audit's hosted run 41382713082. The new central cost is 4 story executions (Answer theme: about 0.3–0.5 s each locally). The journal stories gain the coverage steps within their existing 4 executions.

Refresh on 2026-11-03 (head 7ff87a9a9)

Merged epic/1849-test-cost at dfc528d80 and origin/main at 559fe7b41, which carries the merged blocker #3385. Both merges were clean; this PR's test, story and theme-parity changes are byte-identical to e3df429bf.

bun install --frozen-lockfile; bun run typecheck; bun run build; bun run lint   # all exit 0
bun run check:pr --base origin/epic/1849-test-cost   # exit 0; affected fast invariants 1346 pass, 0 fail

The two runtime files have no new local receipt. This host has no system Chrome. With the Playwright Chromium and its missing libraries supplied, Chromium still exits No usable sandbox! because AppArmor restricts unprivileged user namespaces (apparmor_restrict_unprivileged_userns=1). The tests keep chromiumSandbox: true, so the hosted ready proof is their receipt.

Checklist

  • Focused local preflight/behavioral receipts and complete selected hosted proof passed; actual head/base/job checkout evidence is in Proof. (local done; hosted proof pending, see below)
  • Acceptance criteria from the issue are all met, or the gap is named below.
  • The PR and completed/advanced issues have verified release milestones under docs/process/github.md.
  • A changeset is included, or this changes nothing a consumer can see. (tests, stories and docs only)
  • No personal data anywhere in the diff — the leakage gate covers the whole tree.
  • The integration contract index or relevant component is updated in this same commit, or nothing in the machine surface moved.
  • Docs under docs/ reflect the change, or nothing documented moved.
  • Provider history stays append-only (docs/decisions/provider-history.md): the provider-history guard passes, or nothing on a turn's path changed.

Anything left open

  • answer-delivery-runtime.test.ts has no local runtime receipt. It requires sandboxed Chrome (chromiumSandbox: true). The host has no Chrome libraries. The pinned image runs only as root under rootless Docker, and Chrome refuses root without --no-sandbox. A --no-sandbox wrapper hung newContext for every case, untouched ones included. Hosted ready-candidate runtime proof is the receipt for the paired 320px in dark / 1280px in light cases.
  • Hosted ready-candidate proof is pending. Main is currently red (ci: main red at c14846a66e1d: unit #3315), and the controller incident ci: the red-main controller cancels PR proof whose merge ref does not contain the bad commit #3321 cancels PR proof runs.
  • Collision report at head e3df429bf5d5: closure-only (shared affected test closure), no path collisions; see the PR comment.

🤖 Generated with Claude Code

schlessera and others added 14 commits November 2, 2026 16:22
A bind mount does not change the type readdir reports for its mount point,
so a socket bind-mounted onto a file path read as the empty regular file
bwrap made for it and the walk never reported it. The walk now lstats every
mount point listed in the walking process's /proc/self/mountinfo, in the
fixture's baseline and in each worker. A socket file-bound into a path every
profile inherits is the positive control.

Closes #2745

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…3290)

GitHub's workflow dispatch answers with an empty body unless the request
sets return_run_details, so request-heavy-feedback.ts dispatched a run and
then failed without printing its URL. The test fake now answers like GitHub.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…-owned authentication (#3297)

* feat(ui-sdk): withhold host-only secrets from every subprocess

Declare APIFY_TOKEN as a host-only secret (#3159). Unlike a server-only
descriptor entry, neither BRAIN_UI_SUBPROCESS_ENV_EXTRA nor a per-spawn
extra name can admit it, so no agent, brain CLI or cron child receives it.
Shift the decision citations the inserted lines moved.

Refs #3159

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CONTRACT: feat(ui-server): require explicit Apify enablement and host-owned authentication

Add the strict optional governance block `apify: { enabled }`, false by
default, reported in `brain policy check --json` as `effective.apify`.
An invalid policy disables Apify rather than keeping an earlier enablement.

The server reads APIFY_TOKEN only on the host and contacts no Apify endpoint
while the policy leaves the provider disabled. When enabled, it binds the
account GET /v2/users/me reports on the fixed origin, sends the token only
as a bearer header, refuses redirects and token query parameters, and keeps
the token out of statuses, errors, records and URLs. Authority is an opaque
generation: disablement, a missing or rejected token, or a different account
starts a new one, so earlier bindings go stale; a token rotated to the same
account keeps it, and a different account needs an owner's confirmation.
Owned cleanup (one run's status or abort) stays possible after disablement
while the account still matches.

Internal GET /api/apify and POST /api/apify/account expose the disabled,
authentication_required, configured and unavailable states to live human
owners only. Document the policy family, the environment input, the routes
and the contract entry, and shift the citations the change moved.

Refs #3159

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…so reading it taints the session (#3287)

* CONTRACT: feat(ui-server): let a user mark a file untrusted so reading it taints the session

Marks are host records in the UI database keyed by brain-relative path. Any
principal with read access may mark a file; only the owner may clear one or
see retained missing records and marker labels. An admitted file-tool read or
search naming a marked file records taint with source user_marked.

Refs #2673

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui-react): mark a file untrusted from Files, with cues and the Marked untrusted list

The toolbar gains Mark untrusted between Copy path and Share, the owner a
pressed Untrusted: clear mark, and every reader a purple untrusted cue on the
file and its tree row. A Marked untrusted list replaces the disabled
Untrusted only filter. Every visible change waits for the host; races,
refusals and uncertain replies use the approved copy and focus table.

Refs #2673

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui-react): keep Files focus and the viewer's single status with untrusted marks

The Marked untrusted entry row now waits for the host's list, so a row that
an unsupported server would remove never takes the drawer's initial focus.
The marks announcer is a polite live region without the status role, so the
viewer's own File status stays the surface's one status.

Refs #2673

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui-react): draw the Marked untrusted entry only from a host answer, and inventory its runtime proof

An unreachable or unsupported host draws no entry row, so the row never
appears late inside a settled Files drawer. The Chrome runtime test joins the
runtime build inventory.

Refs #2673

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…rete Actor consent (#3300)

* feat(ui-server): reach the Apify catalog through the hosted MCP server

Add ApifyAuthority.catalog for the two catalog tools the contract selects,
search-actors and fetch-actor-details, on the fixed MCP origin. The URL
loads only those tools and turns Apify's telemetry off, so no execution
helper is ever connected and no session id or use-case text is reported.
The exchange is a minimal Streamable HTTP session (initialize, initialized,
one tools/call, a best-effort close) that accepts JSON or event-stream
replies and refuses redirects, oversize bodies, unknown protocol versions
and malformed envelopes as upstream_incompatible.

The token still leaves the module only as a bearer header, now on one
shared transport for the REST and MCP origins, and a call refuses unless
the binding is current, exactly like a REST request.

Refs #3160

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CONTRACT: feat(ui-server): separate Apify catalog discovery from concrete Actor consent

The governance policy's apify block gains discovery and actors, each
deny/ask/allow and ask by default, and actor_grants in the contract's exact
shape {account_id, actor_id, build_number, profile_id, schema_sha256,
pricing_sha256, origins, expires_at, max_run_usd}. brain policy check
--json reports them under effective.apify. Grants name the immutable Actor
id and an exact build number, never a name or tag; an unquoted YAML
timestamp normalizes to the same ISO instant.

createApifyConsent is the host-side mechanism the CLI/MCP operations and
both backend bridges will call (#3162):

- discover sends a bounded catalog query (a few plain words, no addresses,
  links or long numbers, at most 10 results, no use-case text) under the
  discovery mode, after the provider-folder routing check (G2). Results are
  untrusted: the caller is told before it receives them (G6).
- inspect follows discovery policy because it sends the chosen identifier,
  and reads build, input schema, permission level and the full price list
  from the REST API, refusing a redirect alias, a renamed Actor or a
  substituted build.
- prepare checks input against the inspected schema (no unknown, secret or
  missing fields; no credentials, private, local or link-local targets) and
  binds account, authority generation, principal, session, turn, policy
  hash, Actor id, build id and number, schema and pricing fingerprints,
  qualified profile, exact input, target origins and the run cap.
- admitRun re-reads all of it before and after the approval, admits a run
  without review only for an exact unexpired grant under actors: allow,
  asks otherwise through the existing per-use command approval (never a
  rememberable tool grant), refuses declined, edited, cancelled and
  surfaceless approvals, and consumes the proposal once before handing a
  frozen ticket to the dispatch #3161 will own.

No build is qualified yet, so every Actor can be inspected and none can run.
Document the policy fields, the CLI envelope addition and the contract
entry, add the changeset, and shift the citations the schema moved.

Refs #3160

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… incident issue (#3279)

* feat(ci): record a red main centrally, gate doomed proof and coordinate incidents

Adds the repository-level red-main record to the shared coordination ref
(red-main.json beside reservation.json, exact --force-with-lease CAS), a
deterministic flake estimator, a coordinator workflow on main CI completion
(one incident issue per record, triage and bisect probes, cancellation of
doomed PR proof, detection of squashes that land while red, clearing on a
green run that included the red set), a hosted gate as the first PR step,
integrate.ts main-status/declare-fix and merge refusal while red, and main
runs that select the open red set and run proof even when metadata fails.

Refs #2942

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(ci): record the red-main controller decisions and permissions

Refs #2942

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: follow the moved CI install citation

Refs #2942

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ci): replay the red-main coordinator read-only against a recorded run

Refs #2942

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ci): refuse draft heavy feedback on a head containing main's recorded bad commit

The feedback workflow's admission job takes the red-main gate step from
ci.yml by its command, so the generator fails if ci.yml loses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ci): pin red-set widening with a stub planner instead of two whole-repo plans

The hosted unit shard timed the test out at 35.7 s. The real planner still runs
end to end in the executable test against a fixture repository.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…stem squashes (#3296)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…Actor lifecycles (#3306)

* feat(ui-server): add the Apify run ledger and qualified-profile checks

Two internal building blocks for metered Apify runs.

The ledger is private operational state: one host-owned SQLite file
(mode 0600, outside every brain, never brain.db) in WAL mode. Every
process that opens it shares its ledger_id. Admission is a single
BEGIN IMMEDIATE transaction that checks the run, turn, session,
account-wide day and brain-day allowances, the runs-per-turn and
account concurrency bounds and the turn's request count, then writes
the dispatch intent. Pending reservations count as spent. An unsettled
run holds liability on its start day and every later UTC day. Charge
reports are recorded once by content and only ever raise the liability
they belong to. Result cursors are HMAC-bound to one run.

A qualified profile pins one exact build's input schema and price list.
It qualifies only all-in pay-per-event builds with a verified currency
cap and every event priced, exactly-targeted fetching, mapped target and
work-limit fields, a site authorization (robots or a written
permission), fixed run options, and a bounded lifecycle (requests, abort
and remote retention). profileInput validates the full input against
that schema, collects URLs only from the target fields and bounds the
work. No production build is qualified.

Refs #3161

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CONTRACT: feat(ui-server): meter and bound Apify requests and remote Actor lifecycles

The governance policy's apify block gains limits: run_usd, turn_usd,
session_usd, day_usd, max_runs_per_turn, max_requests_per_turn,
max_concurrent_runs, max_runtime_seconds, max_result_items,
max_result_bytes and max_retention_seconds. brain policy check --json
reports them as effective.apify.limits, which is null when unset. With
no limits there is no spending entitlement and no run is admitted.
Once present, every field is required and bounded, and zero refuses
what it bounds. day_usd and max_concurrent_runs are account-wide.

createApifyRuns is the trusted run service that #3162 exposes:

- run: admitRun consumes the proposal. The dispatch then validates the
  input against the build's qualified profile, checks site permission
  and the minimum charge, and intersects apify.limits with the global
  budgets less inference already spent. It reserves the cap plus the
  bounded lifecycle and storage in the shared ledger, then sends one
  start, never retried, with build, timeout, memory, maxTotalChargeUsd,
  waitForFinish=0 and restartOnError=false as run options. It waits a
  bounded time within the run's reserved status reads.
- A lost, 5xx or unreadable start is remote_outcome_unknown. Its
  reservation is kept and it is never sent again. An operator resolves
  it with the run's own identity or an attestation that stays visible.
- The returned run must match the account, Actor, build and options,
  with no metamorph. Otherwise it is aborted and its results withheld.
- Settlement uses the run's charged-event report at the higher of the
  qualified and reported prices, exactly once. A missing or unpriced
  report keeps the reservation. After settlement the lifecycle requests
  and remote storage stay owed; local expiry releases nothing.
- status, results and abort require an owned opaque handle (brain,
  principal, session). Results come only from the validated run's
  dataset, within item, byte, request and retention bounds, behind the
  untrusted hook. Redirects and signed URLs are never followed.
- Cancellation, stop (timeout or budget exhaustion), disablement and
  reconcile send an owned abort through cleanupRequest. A failed abort
  is unresolved, and no abort claims a zero bill.
- coverage reports per-scope liability, known charges, held
  reservations, residuals and unknown runs beside APIFY_COVERAGE.

Consent now:

- pins a profile to its exact schema and price fingerprints;
- validates prepare input through the profile;
- refuses a cap below Apify's minimum charge;
- adds the fingerprints to the run ticket;
- counts actual requests in the shared durable meter under
  max_requests_per_turn.

New refusal codes: accounting_unavailable, remote_outcome_unknown and
result_expired. This adds no CLI command, MCP tool, wire frame or
route. The contract index, supported inputs, CLI envelope note and
changeset ship here. Citations moved by the schema are updated.

Refs #3161

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…with a kept disposition (#3288)

* CONTRACT: feat(core): count media awaiting review from one inventory with a kept disposition (#2780)

One media inventory now feeds both offline `brain sync`'s count and hygiene
review: the assessed uncommitted MEDIA/LARGE files plus conversation-pinned
scratch files (review-only, even when media.ignore matches; no scratch walk;
5,000-path bound with a trusted-host override). Each file is one `media`
finding (MEDIA info, LARGE warning) fingerprinted by bytes, classes, policy
entries, references and the pins of its own path.

A fifth hygiene state, `kept` (context/hygiene/kept.md, never created empty),
holds while the copy exists and is tracked. `mediaReviewCounts` applies the
disposition predicates without trusting the containing file. Sync reports
only `Media awaiting review: N` and no longer hands media to the agent.
Review's Leave pending is a per-pass deferral (`next --defer`).

Additive contract per rev 6.1: list gains copy/copySha256/keptOn, reconcile
gains kept, next gains counts.kept and media, run.leftovers gains
mediaPending/mediaSetAside; the review state, effect and outcome schemas gain
their defer/kept fields with parse defaults. Citations moved by these edits
are updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui-server): gate hygiene next --defer on the media CLI release floor (#2780)

Review sends --defer only to leave a media finding pending, which only a
0.45.0 CLI reports. Record its release evidence as a feature floor and fail
the 0.45.0 version bump until MIN_BRAIN_CLI_VERSION reaches it, as for the
index directory. Also: the env descriptor list, a same-pass deferral lapse
test (the earlier one passed with deferrals matched by ID only), the
Keep-a-permanent-copy eligibility fixture, and moved citations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CONTRACT: docs(api-report): record the ui-sdk review state, defer effect and deferred outcome (#2780)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CONTRACT: feat(core): record a kept media original with brain hygiene keep-copy (#2780)

The kept state's write path (rev 5 §5.7): reconcile's expected-fingerprint
request {kind: "kept", id, expectFingerprint, copy, copySha256, journal},
refused with nothing written unless the finding is media and the copy
already holds - a contained regular file outside .brain/ and workspaces/,
kept by media.track, with the stated bytes. Keep a permanent copy (#2781)
calls it after making and verifying the copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(core): prove sync and review agree for snoozed, due and every pin snapshot state (#2780)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#3298)

* CONTRACT: feat(ui-sdk): negotiate native history removal on the backend seam

Add the optional `historyRemoval` capability and `removeSessionHistory()`
method to `AgentBackend` (#3229), with content-free `removed`, `partial` and
`unknown` outcomes that map onto the retirement ledger's per-store outcomes.
A backend that does not advertise and implement both is unsupported, so a
host refuses deletion before retiring anything; the conformance suite checks
that the flag and method agree.

The shared internal fence writes a durable, content-free tombstone before
anything is removed, holds sessions for turns and readers, keeps one removal
per session across a bounded caller wait, and removes only named artifacts,
refusing links, hard links, special files and redirected roots. The display,
prompt and observation stores expose the names they already used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(backends): remove a retired chat's Claude and Pi native history

Both first-party backends advertise history removal (#3229). Claude removes
the live transcript and subordinate tree, the archived, staged and conflict
copies and the index entry; Pi removes every SessionManager tree whose own
header names the session in this brain, under the canonical and the older
SDK-managed name, so inactive branches go too. Both remove display and prompt
metadata and file observations, and confirm through the installed runtime's
own listing.

Listing, reading, resume, follow-up, archive and restore refuse a retired
session, after a restart too. Turns and history reads hold their session, so a
removal waits for their last native write, and a read never publishes what
it found once the chat is retired.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui-server): record native removal under the chat retirement

`removableBackendIds` selects the retirement's removable backends from the
capability and the method together, and `removeRetiredNativeHistory` asks the
chat's backend to remove each retired session under the retirement's own
operation, recording the native store's combined outcome (#3229). A missing or
unsupported backend, a rejection or an elapsed wait never completes the store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: re-anchor moved backend citations and record the built native removal

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backends): await refusals and name the ownership assertions in native removal tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…al turn budgets (#3323)

The global turn, conversation and UTC-day budgets saw only inference and
relayed CLI providers, so a turn could keep spending inference up to
block_usd after Apify had already reserved part of the same budget.

createTurnBudgets now takes an optional ApifyBudgetSource, read where each
call is made and passed to the ledger's transaction (so it crosses to the
write worker). A read adds the ledger's turn, session and brain-day
liability to each scope's costUsd once, reported again in the new optional
apifyUsd, and every admission, mid-turn stop, relay refusal and the SDK
backstop see it. apifyBudgetSource builds the source from the shared ledger.
A failed or non-finite read keeps inference's behaviour and states Apify
accounting is unavailable; it is never read as zero. Without a source the
receipt is unchanged.

inferenceSpend reports inference without Apify's own liability, for Apify
admission's ApifyInferenceSpend, so neither side counts it twice.

TurnBudgetCoverage gains two values (additive, as #1673's coverage change).
The wire component and contract index describe them; shifted citations move
with the code.

Refs #3307

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ttached and settled (#3334)

Main run 41488767631 failed REDUCED_MOTION_STATIC_LOADING once at the light
1280 case; three hosted probes of the file then passed. The check stays a
single strict measurement, now taken after an attached ghost and two frames,
and a failure prints each ghost's animation name. The cause is #3333.

Refs #3315

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…d journal runtime

The Other Submit family in answer-delivery-runtime and the configuration
coverage family in turn-journal-runtime ran all four 320/1280 x dark/light
cells with no theme-specific reason. Theme selects only colour tokens and
color-scheme, so both now run 320px dark and 1280px light, keeping every
width-dependent oracle at both widths and both painted themes once.

The removed copies' theme paint moves to central consumer stories that run
in both Storybook projects: a new Conversation/Answer theme story mounts the
actual consumer AskUserCard Other field and Submit (the kit story disables
contrast at its meta), and Conversation/Journal theme now renders
configuration coverage, its badge, search and inventory.

Refs #3329

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e cells

Refs #3329

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@schlessera schlessera added this to the 0.45.0 milestone Nov 2, 2026
@schlessera schlessera added type: test Coverage, harness or proof work with no behaviour change. area: ui-react React chat/files/voice components, stores, WebSocket transport. labels Nov 2, 2026
@uploads-sh

uploads-sh Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

No uploads on this PR yet

This PR changes UI files (packages/module-diagrams/src/ui.tsx, packages/ui-kit/src/chrome/Composer.tsx, packages/ui-kit/src/chrome/StatusIndicator.tsx and 24 more). Screenshots help reviewers see what changed.

  • From the CLI: uploads put before.png after.png --pr 3345
  • From an agent: ask it to attach screenshots with uploads.sh
  • By hand: drag images into the PR description and they're imported automatically
Tips for agents

Label each shot so reviewers know what they're seeing. Labels apply to every file in a call, so run one put per shot:

uploads put after.png --pr 3345 --meta path=/settings --state after --caption "Saved settings toast"
uploads put chat.png --pr 3345 --app ios --screen "Chat" --state after

No CLI? With the Uploads MCP, call sign_upload_url for files on disk (or put) with repo, pr: 3345 and the same screen, state and caption fields.

This comment updates with each upload, so you don't need to post the images yourself. A shot or two in the PR description is still welcome. Check what's attached with uploads list --pr 3345. More in the github-screenshots skill or the agent docs.

Workspace admins can turn this comment off in Settings → GitHub · docs

@schlessera

Copy link
Copy Markdown
Owner Author

Collision report (bun scripts/collisions.ts --json --pr 3345) at head e3df429bf5d5eda1b4b51bc9788a127b092605a6, base epic/1849-test-cost @ 3bee06dce265: no path collisions. Closure-only overlaps (shared affected test closure for the two runtime files) with #3054, #3056, #3059, #3069, #3116, #3292, #3293, #3295, #3305, #3309, #3310, #3311, #3312, #3316, #3317, #3319, #3320, #3325, #3327, #3328, #3335, #3336, #3338, #3341, #3342, #3343, #3344. No other open PR edits the changed files. (Correction to the claim comment on #3329: #3056/#3059/#3116 share the test closure, not the files.)

schlessera and others added 4 commits November 2, 2026 20:59
…ence (#3311)

* feat(ci): claim the red-main fix atomically and collect incident evidence (#2944)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): narrow the fix-claim liveness check for the typechecker (#2944)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(ci): record the conditional concurrency direction for #2934

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…CP and both chat backends (#3346)

* refactor(core): move the Apify service into core so every front door shares it

The authority, consent, ledger, profile and run modules move unchanged in
behaviour from ui-server to core (`@schlessera/brain/internal/apify`), so the
`brain` CLI, the core MCP server and the hosted server can call one
implementation (#3162; the contract has core own the operations).

Consent no longer names a UI type: the approval surface is a structural
`requestPermission` (each decision field read once), and provider-folder
routing (G2) is passed in by the host, which supplies the rules turn
admission uses. Without routing, any provider rule refuses as unproven.
Candidates and proposals sit behind `ApifyConsentState`: in this process by
default, or in the shared ledger for a standalone process, where a
proposal's single admission is decided under the ledger's write lock. The
ledger gains the reference and private-settings tables that needs.

Refs #3162

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CONTRACT: feat(core): expose governed Apify operations through CLI, MCP and both chat backends

Seven fixed operations reach Apify, discover, inspect, prepare, run, status,
results and abort, with one strict schema each in
`@schlessera/brain-common/internal/apify-operations`. `brain apify <op>
--json`, the core MCP server's `brain_apify_*` tools and the same-named
bridge tools in the Claude and Pi backends validate that schema and call one
dispatcher over the core consent and run services, so no transport has a
second consent, account or billing path. Every door returns the same
envelope; refusals keep the contract's codes, plus the additive
`provider_unavailable` and `invalid_input`.

The hosted server builds each call's context from the authenticated turn
(principal, named session, turn, posture, the turn's own approval surface,
recorded provider exposures), records the session's taint as the new
`apify_result` source before remote text returns, takes inference spend from
the turn budgets, opens the shared ledger at `BRAIN_APIFY_LEDGER` or beside
the UI database, reconciles open runs at boot and every five minutes, and
aborts a cancelled, timed-out or budget-stopped turn's runs. Both backends
register the tools in every interactive session's fixed roster and auto-allow
them; voice, no-grant-surface and restricted autonomous turns keep their
memberships, and the host refuses any non-interactive turn.

A standalone CLI or MCP process uses its own `APIFY_TOKEN` and the shared
ledger, keeps candidates and proposals there under the operator's session,
never asks on a terminal, and admits a run only by an exact standing grant.
Neither variable reaches a worker, so a shell `brain apify` in a chat turn
refuses.

The integration-contract index, CLI, MCP and package-API components, the
user CLI/MCP references, supported inputs, the governance-event taint list,
the decision record, API reports, moved citations and the MCP schema
snapshot are updated here. Tests cover every front door against a loopback
Apify, the server's own wiring, a real Pi worker over the worker protocol,
and both backends' provider-history guards through the installed Claude CLI
and a native Pi worker.

Refs #3162

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CONTRACT: fix(common): keep the Apify service loadable without the optional core peer

The UI server reaches `@schlessera/brain` only as an optional peer (#697),
so a static import of core's Apify modules broke a server installed without
core (the packed ui-server probe could not load `app.ts`). The authority,
consent, ledger, profile and run modules and the dispatcher move to
`@schlessera/brain-common/internal/apify`, which every front door already
depends on; core keeps the standalone CLI/MCP glue at
`@schlessera/brain/internal/apify`. Behaviour is unchanged. The package-API
entry list, the decision record and the API reports follow.

Refs #3162

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CONTRACT: test(backends): count the fixed Apify tools in the installed Claude turn surface

The installed-CLI surface capture now sees the seven `mcp__brain-ui__brain_apify_*`
bridge tools loaded eagerly (31 bridge tools) and the core server's seven
`mcp__brain__brain_apify_*` tools deferred like every core tool. The MCP
component says why the worker-side core tools are harmless: a worker holds
neither the token nor the ledger, so they refuse.

Refs #3162

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…cache (#3359)

A request that awaited its cache read first could miss, resolve after another
request had fetched, written and finished, and fetch the same geometry again.
The cache read now runs inside the shared in-flight promise.

Closes #3358

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…main failure (#3344)

A failing unit shard carries only tmp/unit-worker-<k>-report.xml; the merged
unit-report.xml exists only once every worker's report reconciles. The
coordinator looked for the merged name alone, so every unit failure was
recorded with test: null. It now prefers the merged report and otherwise
reads every worker report. The test fake now mirrors the real artifact.

Closes #3324

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
schlessera and others added 2 commits November 2, 2026 22:04
) (#3310)

* refactor(common): hold the Claude subscription billing guard once (#2493)

core's CLI runner and the chat backend each carried a copy of the cleared
credential table, the neutralised flag settings, the settings refusal and
the account check, and core justified it with a comment saying the two
packages share no dependency. Both depend on @schlessera/brain-common, so
the guard now lives in its new ./internal/claude-subscription entry and
both callers import it. A newly found credential or routing variable is
added in one place.

The shared shape is the backend's: settingsRefusal(report, pinned = {})
and a { ok, via } subscriptionVerdict. core adapts the verdict to its
reason string locally; behaviour is unchanged for both callers. The
backend keeps its own message, SDK error classes, observed billing and
credential fields.

Refs #2493

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(changeset): note the single Claude subscription guard (#2493)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ain video watch in keyless workers (#3312)

The host's provider relay now carries the Files lifecycle a local video
needs (#2738): a resumable upload start, the bytes to an upload URL the
host validated and registered under a token, the poll and delete of the
handles this relay's own uploads created, and a generation naming them.
A worker-chosen upload URL, a foreign handle (poll, delete or fileUri)
and a blocked budget are refused before contact; poll and delete never
wait on a budget, and the turn's leftover files are deleted with the
host key when its relay stops. Files attempts are priced at a known
zero. The core Gemini provider stops refusing relayed video and routes
its cleanup delete through the relay.

Contract (additive): docs/integration-contract/cli.md lists the relayed
Files routes; a relayed worker no longer reports capability=video.

Refs #2738

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
schlessera and others added 12 commits November 3, 2026 16:03
CONTRACT: feat(core): land epic #1777 batch 2 — governed media effects and Undo from hygiene review
# Conflicts:
#	docs/integration-contract.md
#	packages/ui-sdk/src/internal.ts
…use and record a skip (#3382)

`brain-ui-cron crontab` added module jobs only when discovery succeeded at
the moment cron started; a transient failure fell back to core jobs with no
signal until the next restart (#3381).

Discovery moves into `cron/module-discovery.ts`. Each attempt qualifies the
host afresh and reports why it yielded nothing: the `brain module list` exit
code and stderr tail, unreadable output, a host not reconciled, or a missing
native configuration. Retryable causes get two more attempts, after 5s and
15s. The final outcome is recorded as a finished `modules` job: a cron_runs
row (`/api/status` cronJobs) and a `cron` root span in Activity, `error` with
`module cron skipped: <cause>` or `success`. It writes only to a UI database
outside the brain that exists or sits behind a qualified host, so the
unqualified fallback still creates no UI state. Successful crontab bytes are
unchanged.

Additive contract: integration-contract index and wire component updated.

Closes #3381

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…e release (#3408)

* fix(ui-sdk): keep a gated payload out when the gate holder dies before release

A status-gated live launch gated its payload with bubblewrap's --block-fd,
which starts the sandbox on the gate's end-of-file. The server holds the
gate's only writer, so a server that died between spawn and release let the
unqualified payload run.

The launch now drops --block-fd and keeps --json-status-fd 3. The fixed host
launch shell still opens the gate FIFO as fd 4 after the bootstrap's
descriptor filter; inside the sandbox the trusted /bin/sh reads one line from
fd 4, requires exactly `go`, closes fd 4 and only then execs the payload.
End-of-file, a partial line or any other line exits 125. release() writes
`go\n`. #3387's kill-before-close ordering stays as a second guard.

Records the ruling in policy-write-boundary.md, updates the STATE_GATE
comment and "Worker startup status", and moves the citations the launcher
edit shifted.

Closes #3392

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: re-anchor a citation moved by the main merge

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Any PR that moves lines in a cited file drifts other PRs' citations, so
failing the merge queue on drift turned main red for documentation alone
(#3496). Maintainer ruling, 2026-11-03: drift is a pre-release check.

- experiments/decision-citations.test.ts now holds the tree-wide drift
  checks: every citation anchored or excepted, exceptions exact, and the
  convention's own example. The experiment tier runs it, and
  `bun run release` refuses to publish without that tier's receipt for the
  exact tree.
- tests/decision-citations.test.ts keeps the checker's rules on in-memory
  trees and the one tree-wide rule that cannot drift with code: no
  citation into a private repository (#303).
- AGENTS.md, docs/decisions/README.md and the release skill say so.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	docs/integration-contract.md
…g a running session clears nothing (#3416)

* test(ui-react): end the phone's scroll-up swipes at rest, so no fling swallows Scroll to latest

The 390 phone case scrolled C's transcript up with three fast finger
swipes that each lifted mid-motion. Chromium turned them into a fling that
kept moving the transcript after scrollUp returned. A tap that lands on a
running fling only stops it, and Chromium suppresses that tap, so the
press on "Scroll to latest" could do nothing and C never read as seen.

Each swipe now carries its own event times and holds the finger still for
200ms before lifting, so the gesture leaves no fling at any dispatch
speed. Every assertion and deadline is unchanged.

Refs #3377

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui-react): an answer still being written is not seen, so reopening a running session clears nothing

The seen observer matched the last message's turnId against the latest
turn without asking whether that message had ended. While a session
streams in view, its live frames stamp their turn onto the answer; the
history replay that follows a reopen strips it, since history links a
turn only once it has ended. When the observer's first committed render
after selecting a running session beat that replay, the stale frame turn
matched and the observer recorded proof for a turn still running: the
tracker read cleared before the turn ended (#3406).

The observer now refuses a last message that is still being written.
That is what it already saw once the replay arrived, so only the race
window closes; a turn that ends while the reader is at its end is still
seen.

Closes #3406

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…orker (#2865) (#3494)

The masked taxonomy and private-index workers passed the fallback's bare
`bun` to the worker launcher, which admits only an absolute executable, so
a protected fallback refused as an unavailable worker boundary.
workerBrainCliCommand resolves that `bun` to the server's own runtime and
keeps the script relative, so it resolves in the immutable startup view.
Other relative executables refuse by name before launch, and a failed run's
detail skips Bun's trailing version banner. Shifted client.ts citations
move with the code.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…g it (#3493)

The per-figure SVG/PNG/PDF case clicked the share trigger and immediately
looked up the menu item; when the menu commits after the click resolves,
the lookup returned undefined and WebKit threw at the second click. Poll
for the actual requested item under the ordinary expect.poll deadline.

Refs #2946

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ff the event loop (#3495)

* fix(backend-claude): wait on the idle-session archive move's fsyncs off the event loop

The sweep's per-session move, source removal, conflict set-aside and
reindex now await their file and directory syncs. Removal rechecks the
live turn set and the archived copy in the same synchronous step as its
validation and deletion, and an index write overtaken by a restore or
removal is redone. Restore stays synchronous as the scoped residual.

Refs #2898

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(backend-claude): follow moved transcript-archive citations and record the changeset

Refs #2898

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…waits instead of refusing (#3497)

The policy publisher rechecks the turn inside its own journal observation.
A scope cap that observation itself reached was a terminal refusal: waiting
there would hold the observation, which keeps Continue refused, and throwing
out of it would fail the observation and stop the turn's workers.

The callback now stages nothing and returns, so the observation completes
and releases its hold. The publisher then reports the hold outside it, the
brain lock unwinds, and the existing bounded wait runs under the request's
one deadline. Continue starts a fresh attempt that rechecks authority,
validation, ancestry and the base hash under a new observation. Stop,
cancellation, revocation and expiry publish nothing; a no-publication
attempt records no policy-updated event.

Refs #2870

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…the boundary on read-only writes (#3384)

A scheduled module job runs in a maintenance worker while the host execution
boundary holds. The host, homes and XDG state are read-only or per-run there,
so a module that kept state in a host directory failed on its first mkdir with
a bare EROFS (#3380).

Per the maintainer's 2026-11-02 ruling, core now defines a per-module state
area, `<brain>/.brain-ui/modules/<module-name>/`, gitignored by its own
`.gitignore` and published through output staging like any brain write. A
module command reaches it as the new optional `CommandContext.stateDir()`,
resolved against the CLI's brain root, which is the worker's writable data
root rather than its read-only configuration view. `@schlessera/brain/module`
exports `MODULE_STATE_DIR`, `moduleStatePath`, `moduleStateDir` and
`ModuleStateError`.

A module command's EROFS outside the brain now exits 2 with a message naming
the path, the host execution boundary and the state area. An EROFS inside the
brain keeps the `read_only_brain` refusal. The cron runner adds the same
explanation when a worker job's own subprocess prints an unexplained EROFS.

Contract: docs/integration-contract.md and the package-api component; the
ruling is recorded in docs/decisions/governed-writable-workers.md. Moved
ModuleContribution citations follow the type.

Refs #3380

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
schlessera and others added 3 commits November 3, 2026 15:17
# Conflicts:
#	docs/integration-contract.md
…pare the turn journal while idle (#3386)

* CONTRACT: feat(ui-server): attribute governed admission and journal begin to timed steps

`ActivityTurnLifecycle` gains optional `subPhases` (#3355). Governed
admission records the inference-origin lookup, gate acquire, startup
checkpoint(s), boundary prepare, sandbox resolution, private index, data
tree and record commits; journal begin records the hold wait, initialize,
observe (checkpoint, delta, metadata, capture, record, commit), expiry,
turn insert and policy. Steps ride on the next milestone or terminal
write, so timing adds no write to the turn path, and an unfinished step
follows the phase failure rule.

Refs #3355

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(native-proof): time admission steps in cold scenarios under --observation-timing

The observation-timing run now reads each turn's lifecycle record and
writes its phases and steps to observation-timing.json for the process's
first governed turn, the first turn after 300 notes changed while no turn
ran, and the first turn after a backlog of turns past retention, beside
five warm turns (#3355). The backlog comes from a journal clock held 31
days behind while those turns run.

Refs #3355

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(ui-server): prepare the turn journal while no turn is active

The first admission after start or idle paid for the observer's full
metadata walk, the delta of everything changed while no turn ran and
the expiry of every turn past retention (#3355). The hosted app now runs
that preparation once startup recovery commits and every minute while no
turn is active. With no active turn an observation attributes nothing
and retains no content, so the journal records what it did before; the
admission keeps the same steps, now with little left to do.

Refs #3355

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui-server): close abandons an idle journal pass, and app-close tests scope their own checkpoint

Closing the journal aborts an in-flight idle observation instead of
waiting for its native work. The app-close lease tests now let the
journal's startup observation settle and hold only their own
checkpoint's release, since the journal now holds an observed root from
startup (#3355).

Refs #3355

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: follow citations and API reports past the admission timing lines

Refs #3355

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	docs/decisions/governed-writable-workers.md
#	packages/ui-server/src/brain/governed-turn.ts
schlessera and others added 6 commits November 3, 2026 16:23
…iptors (#3476) (#3500)

The operator restore checked `.brain-ui/inbox` and each arrival with
realpath once, then created directories, reaped leftover temporaries and
published files by pathname, so a link swapped in after the check was
followed. Restore now opens the staging chain and each arrival without
following links (the #2907 helpers), reaps and publishes through those
descriptors, re-verifies the visible chain before each change and at the
end, and reads the final comparison through the descriptors too. A link
refuses with the existing `inbox_staging_symlink` code.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…3503)

#3484 added packages/ui-react/tests/ended-question-runtime.test.ts with a
runtimePackageBuild hook but did not list it in scripts/runtime-test-builds.ts,
so tests/runtime-build-selection.test.ts fails on main (expected 18 hooks,
found 19).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…s busy refusal (#3488) (#3501)

When two concurrent resumes of one non-resident session both open a worker,
the one that returns second now decides busy or reuse (and claims a reused
entry) synchronously, then awaits its own worker's shared inactive abort
before re-raising the original outcome. The refusal settles only after the
losing worker has exited, its turn temp is closed and its stdio release has
been awaited. A refused cleanup is logged as a warning through the backend
log and cannot replace the busy refusal; the reservation stays charged and
the abort stays retryable.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
CONTRACT: fix(ui-server): land epic #1645 batch 1 — provider-relay taint exemption, managed-storage turn refusal and mount-point socket walk

This branch has not been deployed

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

Labels

area: ui-react React chat/files/voice components, stores, WebSocket transport. type: test Coverage, harness or proof work with no behaviour change.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant