Skip to content

Sync with upstream pingdotgg/t3code main (ad4bb6d6fa) - #6

Merged
kupiainen merged 2763 commits into
mainfrom
sync/upstream-2026-11-04
Nov 4, 2026
Merged

kupiainen merged 2763 commits into
mainfrom
sync/upstream-2026-11-04

Conversation

@kupiainen

@kupiainen kupiainen commented Nov 3, 2026 •

Copy link
Copy Markdown

Problem

Round 2 (after review round 1): a second merge brings the branch to upstream ad4bb6d6fa, and an unanswered Stop cancel now offers Force stop. See Round 2 below; sections 1–3 describe round 1 and still hold.

The fork last merged upstream at 325a7e2ae4 (2026-10-19, #3). Upstream pingdotgg/t3code main has since moved 2,638 first-parent commits, to 7c22e24e3f (2026-11-03, "fix(antigravity): rate-limited turns keep their reset time"). The owner wants T3 Code's latest agent harness with the cell provider working on top of it. Upstream's commits rewrite files the fork's pieces live in (deletion planning, turn start, run execution, recovery, the replay harness, the adapter interface), move client registration into packages/client-runtime, delete or split three files the fork had edited, move to Effect 4.1, and add lint rules the cell code predates.

Fix

A merge, not a rebase, as in #3. The merge commit holds only the conflict resolution; the provider-cell port and each adoption follow as separate commits.

1. Merge commit 66c3964411 (upstream/main 7c22e24 into fork main cfa469e)

32 files conflicted. Each decision:

File Decision Reason
apps/web/package.json, apps/mobile/package.json upstream's Upstream removed the provider package deps from web and mobile (pingdotgg#24205): both read clients from @t3tools/client-runtime/provider-clients.
apps/mobile/src/lib/providerClients.ts (deleted upstream) deleted Same move.
apps/web/src/components/settings/providerDriverMeta.ts upstream's re-export Same move.
packages/client-runtime/src/providerClients.ts + package.json (no conflict) ported: cellClient registered after piClient, dep added The fork's web and mobile registration now lives here, once.
apps/server/src/orchestration-v2/builtInProviderAdapterDrivers.ts (deleted upstream, pingdotgg#22557) deleted It existed only for a test upstream deleted as low-signal; CellAdapterV2Driver loses its only outside user and becomes private (port commit).
apps/server/src/orchestration-v2/testkit/ProviderTurnStartMemory.fixture.mjs (deleted upstream, pingdotgg#22557) deleted Its memory test was deleted with it; the fork's lines there were incidental.
apps/server/src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts (split upstream, pingdotgg#22717) deleted; new CellReplayFixtures.integration.test.ts Upstream runs each driver's fixtures from <Provider>ReplayFixtures.integration.test.ts; the cell's seven run from their own file (and cell joins the runner's guard list in the port commit).
apps/server/package.json scripts union record:cell-replay beside upstream's new record:fx-replay, record:kiro-replay, generate*.
package.json knip:check upstream's + --workspace packages/provider-cell
knip.jsonc upstream's + the three live-cell-* entries Upstream deleted evaluate-thread-titles.ts and measure-pr-preview.ts.
docs/README.md, docs/user/install.md, docs/user/permission-modes.md union Cell rows beside upstream's new fx and Kiro rows.
apps/server/src/provider/providerStatusCache.ts union Upstream's fx, kiro, then the fork's pi, muse, cell.
apps/server/src/settingsSecrets.ts union cellClient beside upstream's new kiroClient.
packages/contracts/src/settings.ts union cell stays default-disabled beside upstream's fx, kiro.
packages/contracts/src/settings.test.ts upstream's (test deleted) Upstream deleted the default-enabled test the fork had extended (pingdotgg#22557).
apps/server/src/provider/model-manifest.json upstream's updatedAt, cell entry after fx
packages/provider-core/src/server/ProviderAdapter.ts both Upstream's forceNewNativeThread and the fork's openNativeSubagentItems are independent optional inputs.
apps/server/src/orchestration-v2/Orchestrator.ts (thread.delete) upstream's planDeletion (side chats) + providerThreads, turnItems in its record list (filtered to open subagent items in 0a0394af2c) The fork's deletion settles a cell task's rows and roster from those records.
ProviderSessionManager.ts (failSubscribers) upstream's Effect.fn form + the fork's reason / ProviderSessionReleasedError cause
ProviderRuntimeRecoveryService.ts (3 hunks) the fork's backgroundWorkSurvivesRestart + upstream's one-line comment; both item filters (surviving native subagent, upstream's link_request)
ProviderTurnStartService.ts (2 hunks) resume fallback passes both forceNewNativeThread and openNativeSubagentItems; upstream moved the restart-note computation earlier (goal-command aware) and the fork's workSurvivesRestart is grafted there
RunExecutionService.ts (2 hunks) upstream's restructured ingestion; the fork's INFO downgrade ("provider event stream ended after its run settled") re-applied in upstream's new failure handler
testkit/ProviderReplayHarness.ts the fork's recovery layer + upstream's NodeCrypto
testkit/fixtures/index.ts, shared.ts union
ProviderRuntimeRecoveryService.test.ts, ProviderSessionManager.test.ts fork tests kept; neighbours upstream deleted stay deleted
RunExecutionService.test.ts both
ProviderTurnStartService.test.ts (11 hunks) upstream's file + the fork's one resume-fallback test ported onto its harness Its harness changed throughout (side chats, previous turns).
apps/server/src/orchestration-v2/runtimeLayer.ts (auto-merged, broken) layerProviderAdapterRegistryProvided → ProviderAdapterRegistry.layerFromProviderInstanceRegistry Upstream inlined the alias the fork's recovery layer referenced.
pnpm-lock.yaml upstream's, regenerated with pnpm install --lockfile-only Differs from upstream's only by the provider-cell importer, its two workspace links (server, client-runtime) and the ws / @types/ws catalog entries, as at #3. provider-cell's dependencies are unchanged.

The fork's comments in these files are cut to upstream's new four-line comment rule.

The fork's three server mechanisms are all kept (backgroundWorkSurvivesRestart, cancelBackgroundWork on deletion, ProviderSessionReleasedError): upstream still has no capability for work that survives a restart, no deletion-time hook, and no deliberate-release marker. The nearest upstream pieces (hasPendingBackgroundWork, backgroundTasksEndWithProcess, RestartBackgroundNote.runOutcome) solve other problems.

2. Follow-up commits

  • cba5e77f2d fix(cell): build the cell provider on upstream's Effect 4.1 and its per-provider replay fixture files: NodeSocket.layerWebSocketConstructorWS → NodeWS.layerWebSocketConstructor (same ws-backed constructor that carries the bearer token); CellAdapterV2Driver private; cell in the replay runner's guard list.
  • c71ca38741 style(cell): follow upstream's Effect.fn, comment-length and test-wait lint rules: Effect.fn("Owner.op") for the reusable effects, the 38 long comments cut to four lines (the rules stay in docs/internals/provider-cell.md), justified disables on four real-clock waits in adapter.test.ts that prove an absence or wait on counters the driven cell does not signal, decodeSync in the testkit. No behaviour change.
  • ca27bf6e1d fix(cell): publish a cell notice's node with it: found by the live run. vim main's cell now retries a transient model failure (upperformance/vim#20) and reports each retry as a run-scoped error; the adapter's system_notice item named a node it never published, which fails the projection integrity check. The notice now publishes a system node first, as Pi's do; the adapter test checks it (fails without the fix). cell_wake_after_failure is re-recorded live: the four retry notices are the only wire change. The docs' live run sets MODEL_RETRY_DELAY_MS=1 (see below).
  • be2232456b feat(cell): report a Stop the cell does not confirm as upstream's unconfirmed interrupt: the adoption below.
  • 6672913836 fix(cell): typecheck with tsc: upstream dropped the tsc-rs binary name; vp run -r typecheck failed on provider-cell before.
  • 614643f7fd style(server): format the two turn-start files the merge resolution left unformatted.
  • 7ff5cfdd61 fix(cell): list the cell provider among the drivers a managed policy may name: upstream's new managed policy keeps its own driver list.
  • 0a0394af2c fix(server): read only a deleted thread's open native subagent items, not its whole history: upstream's control-reads test requires deletion to read no unrelated history; both deletion paths now read turn items filtered to open subagent items.

3. Upstream's provider-facing changes since 325a7e2: adopt / skip

Upstream change Decision Reason
15 s bound on interruptTurn, ProviderAdapterInterruptTimeoutError, unresponsive run + Force stop (pingdotgg#27252) adopt The cell waited up to 30 s for its cancel and 30 s for the confirming done, then failed the turn itself; the manager now cuts it off at 15 s first. Cancel 5 s, confirmation 8 s; unconfirmed Stop returns ProviderAdapterInterruptTimeoutError and leaves the turn open (a late done still ends it interrupted), so the user decides on Force stop, as with ACP. New test fails on the old code.
Per-provider replay fixture files (pingdotgg#22717) adopt CellReplayFixtures.integration.test.ts.
Effect.fn, comment-length, test-wait lint rules; effect-plugin diagnostics adopt Required for lint and typecheck to pass.
Usage-limit failure with ISO resetAt, auto-resume and usage refresh (7c22e24, 6f41364, 8298664) no change needed The cell already reports class: "usage_limit" with an ISO resetAt (live limit: resetAt: 2026-11-04T00:05:35Z). Usage refresh needs a usage reader the cell has none of.
awaited background tasks / awaitBackgroundTasks (91ea0e1) skip Only for command and monitor kinds; a cell task is subagent, which always holds Working.
hasPendingBackgroundWorkForThread({ includeSubagents }) (07242ab) skip The cell's session-wide hasPendingBackgroundWork is the documented fallback; holding the stream open per thread would be wrong while a wake waits as a new run.
forceNewNativeThread (07242ab) skip (passed through) The context-overflow handoff path; the cell reports no context window, so it never triggers. The fork's resume fallback passes both fields.
background_work.updated, pendingBackgroundWork on provider_thread.updated skip For work with no row / OpenCode's evidence only; cell tasks have subagent rows.
reason on completed/interrupted terminals (9b72088) skip Read only for interrupted turns without a Stop reason; a cell turn is interrupted only by a user Stop.
requiresSignIn on ProviderAdapterOpenSessionError (99ff58a) skip (stream 2) The cell's openSession makes no request; a rejected token surfaces later. Login handling is the login-era stream's.
maxRuntimeMode managed-policy ceiling skip The cell already refuses everything below Full access.
Approval feedback, removed response / reasoningEffort skip The cell has no approvals; never used the removed fields.
Token usage fields (autoCompactThreshold, promptCacheExpiresAt, per-request counting) skip until the cell reports usage vim main's done now carries usage (requests, tokens); reporting it on provider_turn.updated is a follow-up (owner question 2).
Secret-provider references for credentials skip No provider's settings use them yet; the cell token stays a secretSetting.
Longer status-probe timeouts for CLI probes (1c748ed) no change needed The cell's HTTP probe allows 8 s already.

No upstream change adds a required member to an interface provider-cell implements.

Verification

All from /srv/t3/worktrees/t3code/t3code-sync, TMPDIR under /srv/t3/tmp/ready-2026-11-04/t3code-sync/tmp, logs in that directory.

Check Command Result
Install pnpm install --frozen-lockfile (after pnpm install --lockfile-only regenerated the lock) OK
Typecheck, every package vp run -r --concurrency-limit 4 typecheck 41/41 pass (apps/web included, which failed on upstream at the last sync)
Lint vp lint --report-unused-disable-directives from the root 0 errors, 4 warnings, all in upstream-only files (markdownWindowsPaths.ts ×2, AntigravityAcpSupport.test.ts, T3ChatAccounts.test.ts)
Format / Knip vp fmt --check, vp run knip:check clean / clean; the pre-commit hook (fmt, lint, knip) passed on every commit after the merge
provider-cell vp test run in packages/provider-cell 3 files, 96 tests pass (95 at fork main, plus the Stop test; the notice test is an added assertion)
Cell integration + fixtures vp test run on CellRestartAdoption, CellThreadDeletion, CellReplayFixtures integration tests 3 files, 9 tests pass (7 fixtures)
Fork mechanisms vp test run on RunExecutionService, ProviderSessionManager, ThreadDeletion, ProviderRuntimeRecoveryService, ProviderTurnStartService tests, ProjectService.deletion 6 files, 186 tests pass
Whole server suite vp test run in apps/server, CLAUDE_CONFIG_DIR unset 465 files: 463 pass; 6396 tests pass, 1 failed, 25 skipped (586 s). The 2 failing files fail on pure upstream 7c22e24 too (below).
Contracts / provider-core / client-runtime vp test run in each 29 files 157 tests / 21 files 194 tests / 145 files 1600 tests, all pass
Web model selection vp test run src/modelSelection.test.ts in apps/web 41 pass

The first whole-suite run also failed ManagedPolicy.test.ts and Orchestrator.control-reads.test.ts, which pass on pure upstream; both were ours and are fixed by 7ff5cfdd61 (the cell missing from the managed policy's driver list) and 0a0394af2c (deletion read the whole turn-item history; now only open subagent items, which upstream's test requires).

Failing on pure upstream 7c22e24 as well (checked in a second worktree with the same command), not ours:

  • src/preview/ServerBrowserPage.test.ts (2 tests): Playwright's headless shell is not downloaded on this machine (chromium_headless_shell-1223 missing).
  • src/storageCleanup.test.ts "compares the checked-out branch without a full status": the worktree classification differs in this machine's git setup.

Live run against the cell of upperformance/vim main 2e7f884 (the round's prebuilt dist), celld 0.6.2 dev --clean --no-watch on 9701, .dev.vars OPENAI_API_KEY=sk-test, OPENAI_BASE_URL=http://127.0.0.1:9702/v1, MODEL_RETRY_DELAY_MS=1, scripts/live-cell-api.mjs 9702 20000, following docs/internals/provider-cell.md (Testing) step by step (logs live/, driver live/run-live.sh):

  • live-cell-probe.ts: the live cell ready, version 0.3.0, model gpt-6-astra; the six stand-ins (no protocol, protocol 2, HTML, 404, no API key, closed port) give their expected status and message.
  • live-cell-driver.ts, all eight scenarios exit 0 with the expected projection: shell completed; interrupt interrupted after POST cancel with x-run-id answered 204; limit failed as usage_limit with an ISO resetAt, then a second turn completed; delegate three completed turns (prompt, wake, follow-up); reconnect completed after the 25 s hold; restart the second runtime's continuation took the run back and completed with no cancelled-background note (same projection as at Sync with upstream pingdotgg/t3code main (325a7e2ae4) #3); attach-retry completed on the second connection; delete-delegate (API restarted with CHILD_DELAY_MS=30000, run last) sent the task's cancel, the cell answered 200 and GET tasks lists it as cancelled, and the stream end logged at INFO "provider event stream ended after its run settled".
  • record-cell-replay-fixture.ts --out <scratch> for all seven fixtures: each wrote and verified by replay (2–9 s each). Compared with the checked-in transcripts, only cell_wake_after_failure changed on the wire (the cell's four retry error events), so only it was refreshed with --write-fixture --version 0.3.0 (22 entries).
  • The first recording of cell_wake_after_failure found the notice-node bug (ca27bf6e1d) and the retry-delay trap (owner question 4); logs of that round in live-r1/.
  • All servers stopped by port afterwards.

UI smoke (headless Chrome 152 through playwright-core, the merged server serve --port 9703 with its own --base-dir, telemetry off, web built from this branch): the cell provider added in Settings → Providers with the celld URL (status Available, v0.3.0), a cell thread in a scratch project with gpt-6-astra: the streamed answer, a delegated review subagent row Running then "Subagent "review" finished" and the wake's reply, and a Stop ("Run interrupted · Stopped by user", 144 ms). Screenshots ui/shots/15-cell-answer.png, 16a-subagent-running.png, 16b-subagent-done.png, 19-stopped.png (plus 11-cell-added.png). The server log has no warning or error.

Round 2

4. Review fix: an unanswered cancel is an unconfirmed Stop (377d0d39de)

Round 1's Stop adoption returned ProviderAdapterInterruptTimeoutError only when the cell answered the cancel but sent no done within 8 s. When POST cancel itself went unanswered (the 5 s Stop bound, the adapter's shorter requestTimeoutMs, or a transport failure), the adapter failed with a protocol error wrapped in ProviderAdapterInterruptError. The server retries that error instead of marking the run unresponsive, so Force stop was never offered.

The rule now, as upstream's Stop bound defines it: every way a Stop can stay unconfirmed returns ProviderAdapterInterruptTimeoutError and leaves the turn open. That covers a cancel the cell never answers (hung, or lost at the transport) and a cancel answered with no done in time. A late done still ends the turn as interrupted. Only a cancel the cell answers with an error status other than 409 counts as a refusal; it keeps ProviderAdapterInterruptError. (Narrowed in round 3, section 6: only a 4xx refuses; a 5xx is unconfirmed.) The 409 "another run" handling is unchanged. The adapter's request helper fails only when no answer arrives, so the cancel's failures (Effect.timeout(min(requestTimeoutMs, 5 s)) plus the helper's own timeout) all map to the unconfirmed error, logged at WARN with the cause.

Regression test in adapter.test.ts, next to the acknowledged-cancel/no-done test: "a Stop whose cancel the cell never answers, hung or lost, is reported unconfirmed like a missing done, and a definite refusal keeps its own error". It sets requestTimeoutMs: 300, below the 5 s bound, so the request helper's timeout is the one that fires. It then runs three Stops on one open turn: a held cancel and a transport-failed cancel both give ProviderAdapterInterruptTimeoutError, and a 500 gives ProviderAdapterInterruptError. No terminal is emitted in between, and a late cancelled done ends the turn as interrupted. The driven cell gained holdCancel, and answerCancel(0, …) now fails at the transport. The test fails on 0a0394af2c (expected 'ProviderAdapterInterruptError' to equal 'ProviderAdapterInterruptTimeoutError') and passes now. I also reran the reviewer's standalone reproduction (requestTimeoutMs: 10000, so the real 5 s bound applies) against this head. It returns ProviderAdapterInterruptTimeoutError after 5000 ms and the late done ends the turn interrupted (r2/repro/stop-cancel-timeout.log). The Testing doc's Stop paragraph states the rule.

5. Second merge commit 1da56ecf60 (upstream/main ad4bb6d)

Upstream moved 112 first-parent commits past 7c22e24 (more than the 58 to 940afd65e0 when the review was written). This is a merge commit on the branch, not a rebase.

File Decision Reason
ProviderTurnStartService.test.ts (the one conflict, 4 hunks) both Upstream added the resumeFailure harness option and "fails the run instead of replacing a native thread another client holds" (pingdotgg#21045). The fork's resumeFallbackWithOpenItem option and its resume-fallback test stay beside them: the fork's fallback session is chosen first, and resumeFailure shares the plain fallback session.
Auto-merged files both sides touched: Orchestrator.ts, ProviderTurnStartService.ts, ProviderRuntimeRecoveryService.ts (+ test), RestartBackgroundNote.ts, provider-core ProviderAdapter.ts both The fork's lines in them are unchanged: for every fork-touched file except the resolved test, the fork's delta over upstream is line-for-line the same before and after the merge (r2/fork-delta-compare.log).
pnpm-lock.yaml upstream's, regenerated with pnpm install --lockfile-only Same 43-line delta over upstream as the first merge.

Port commit a17ce0d72f fix(server): give upstream's another-client resume test a session that carries its provider session. Upstream's new test died at the fork's session.providerSession.capabilities read (backgroundWorkSurvivesRestart): the harness's mocked fallback session has no providerSession, which real sessions always carry and upstream's own code reads later. The fix is in the harness only; it passes on pure upstream and now passes here.

Upstream's provider-facing changes in these 112 commits, adopt / skip:

Upstream change Decision Reason
A queued provider switch no longer ends background work (pingdotgg#30401) adopted for free The cell adapter's existing session-wide hasPendingBackgroundWork probe takes part in upstream's background-work protection: a queued restart_and_resume holds the queue while the cell's tasks run; a context handoff (create_with_handoff, which a driver change selects) proceeds and keeps the busy old session attached instead of detaching it. Upstream's ProviderSwitch.integration.test.ts covers both and passes here.
ProviderAdapterThreadInUseError (pingdotgg#21045) skip A cell accepts several clients on one thread, and its 409 for a second prompt is already a refusal of the turn, not of the thread.
Subagent titles and restart-note labels no longer split an emoji (pingdotgg#29906, pingdotgg#29647) nothing to port The cell's child titles go through provider-core's subagentThreadTitle. Restart notes are skipped for the cell.
Restart-cut messages no longer stay streaming (pingdotgg#28646), delegated threads get their own event log (pingdotgg#29770), provider settings mid-refresh (pingdotgg#29894) no change needed Server-side; the cell needs nothing extra.

Verification (round 2, head a17ce0d72f)

All from /srv/t3/worktrees/t3code/t3code-sync, TMPDIR=/srv/t3/tmp/ready-2026-11-04/t3code-sync/r2/tmp, logs in r2/.

Check Result
pnpm install --frozen-lockfile OK (install.log)
vp run -r --concurrency-limit 4 typecheck 41/41 (typecheck-all.log)
vp lint --report-unused-disable-directives, vp fmt --check, vp run knip:check 0 errors and the same 4 upstream-only warnings / clean / clean; the pre-commit hook passed on all three new commits
provider-cell vp test run, vp run typecheck 3 files, 97 tests pass (96 + the new Stop test) / OK
Cell integration + CellReplayFixtures 3 files, 9 tests pass (test-cell-integration.log)
Fork-mechanism files (RunExecutionService, ProviderSessionManager, ThreadDeletion, ProviderRuntimeRecoveryService, ProviderTurnStartService, ProjectService.deletion, Orchestrator.control-reads, ManagedPolicy, ProviderTurnControlService) 9 files, 213 tests pass (server-focused.log)
Whole server suite (CLAUDE_CONFIG_DIR unset) 466 files: 464 pass; 6475 tests pass, 1 failed, 25 skipped (584 s, server-suite.log). The 2 failing files (ServerBrowserPage.test.ts: headless shell not downloaded; storageCleanup.test.ts: "compares the checked-out branch without a full status") fail identically on pure upstream ad4bb6d6fa (known-failures-upstream.log).

Live run against vim main 2e7f884's cell (fresh copy of the round's dist, celld 0.6.2 --clean --no-watch on 9701, live-cell-api.mjs on 9702, MODEL_RETRY_DELAY_MS=1, docs' steps 2–7, script r2/live/run-live.sh, summary r2/live/summary.txt):

  • Probe: live cell ready, v0.3.0, gpt-6-astra; all six stand-ins give their expected status.
  • All eight driver scenarios exit 0: shell, interrupt, limit, delegate, reconnect, restart, attach-retry, and delete-delegate (run last with CHILD_DELAY_MS=30000: cancel answered 200, GET tasks lists the task as cancelled).
  • All seven --out recordings were written and verified by replay (10–23 entries). Normalized for run ids, timestamps and vim main's usage fields, they match the checked-in transcripts, so no --write-fixture refresh was needed.

UI smoke (server serve --port 9703, web built from this head, headless Chrome on 9704; scratch home copied from round 1 with its cell provider instance): a cell thread with gpt-6-astra streamed the answer (r2/ui/shots/03-cell-answer.png). A held turn showed Working (04a-working.png), and "Stop generation" settled it as "Run interrupted · Stopped by user" in 119 ms (04b-stopped.png). All servers were stopped by port afterwards.

Round 3 (head 6cb702632e)

6. Review fix: a 5xx answer to a Stop's cancel is unconfirmed (6cb702632e)

Round 2 still read any non-409 error status as the cell's refusal, so a gateway's 502/503 (the upstream connection broke, the cell unreachable) produced ProviderAdapterInterruptError. The server retries that error at once and never marks the run unresponsive, so Force stop was never offered.

The rule now: only a 4xx answer refuses the cancel. 409 keeps its meaning (the run ended; its done settles the turn live or by replay). Any other 4xx is ProviderAdapterInterruptError; 401 stays there as today, and its handling as access loss is fork PR #5's. Everything else is unconfirmed: any 5xx, whether the cell's own or a gateway's (neither says whether the run was cancelled, and the transport sees only status and body, so the body is not consulted), a transport failure, no answer within the 5 s bound, or an accepted cancel with no done within 8 s. All of these return ProviderAdapterInterruptTimeoutError and leave the turn open. The 5xx case returns at once and logs a WARN. A late cancelled done still ends the turn interrupted. One added branch in interruptTurn (adapter.ts), and the Stop paragraph and test list in docs/internals/provider-cell.md now state the rule.

Regression: the round-2 Stop test is now "a Stop whose cancel is hung, lost or answered with any 5xx is reported unconfirmed like a missing done, and only a 4xx refuses it". On one open turn it runs a held cancel, a transport failure, 500 ({"error":"no"}), 502 (HTML gateway page), 503 (plain text) and 504 (a body shaped like the cell's refusal). Each one gives ProviderAdapterInterruptTimeoutError. Then a 403 with the cell's refusal body gives ProviderAdapterInterruptError. There are seven cancels, all naming run A, and no terminal before a late cancelled done, which ends the turn interrupted. The 409 case stays covered by "a cancel the cell refuses because another run is active means the run ended: the replayed done settles the turn". Without the new branch the test fails at the first 5xx (500: expected 'ProviderAdapterInterruptError' to equal 'ProviderAdapterInterruptTimeoutError', r3/regression-before.log).

Section 5's pingdotgg#30401 row is corrected: only restart_and_resume holds the queue; a context handoff proceeds and keeps the busy session.

Upstream: upstream/main is now 66e6217b93, 3 first-parent commits past ad4bb6d6fa, none touching packages/provider-core, packages/contracts or apps/server/src/orchestration-v2. Not merged in this round.

Verification (round 3, head 6cb702632e)

All from /srv/t3/worktrees/t3code/t3code-sync, TMPDIR=/srv/t3/tmp/ready-2026-11-04/t3code-sync/r3/tmp, logs in r3/.

Check Result
provider-cell vp test run, vp run typecheck 3 files, 97 tests pass / OK (provider-cell.log)
vp run -r --concurrency-limit 4 typecheck 41/41 (typecheck-all.log)
vp lint --report-unused-disable-directives, vp fmt --check on the changed files 0 errors, the same 4 upstream-only warnings / clean (lint.log, fmt-check.log); pre-commit hook (fmt, lint, knip) passed
Cell integration + CellReplayFixtures + ProviderTurnControlService 4 files, 10 tests pass (server-cell.log)
The round-2 review's 14-file focused server set (cell integration/replay with all seven fixtures, fork mechanisms, turn control, restart note, upstream provider switch) 14 files, 274 tests pass (server-focused.log)
Live, vim main 2e7f884's cell (fresh dist copy, celld 0.6.2 --clean --no-watch on 9701, live-cell-api.mjs 9702 20000, MODEL_RETRY_DELAY_MS=1) probe exit 0 (live/probe.out); --scenario interrupt exit 0: cancel answered 204, cancelled done, run and provider turn interrupted, command_execution interrupted (live/driver-interrupt.out, projection in live/driver-interrupt/)

The whole server suite was not rerun for this one-branch adapter change; round 2's run stands (r2/server-suite.log).

Owner questions (provisional decisions)

  1. Force stop on a cell (from the Stop adoption): T3's Force stop settles the run in T3 and detaches the session; it does not cancel the cell's run, which may go on on the tenant until it ends. Provisional: accepted, since the cell already refused to confirm the cancel; a later turn's resume reads the run back from the thread's head. Should Force stop also try POST cancel once more, or cancelBackgroundWork?

  2. Token usage: vim main's done and state now carry the run's usage (requests, input/cached/output/reasoning tokens). Upstream's turn usage shape could show it per turn. Not adopted here (it is a new feature, not a sync need) — a follow-up for the cell provider?

  3. Retry notices: each model retry is now a visible notice item in the turn ("The API responded 500: …; trying again in 0.5s (retry 1 of 4)"). Provisional: shown, as the cell reports them. Collapse them, or hide all but the last?

  4. MODEL_RETRY_DELAY_MS=1 in the live-run docs: at the cell's default backoff the retries (~8 s) outlast the scripted child (4 s), its completion reaches the socket before the failed run's done behind the fixture's gate, and the cell_wake_after_failure recording never finishes. Provisional: the docs set the variable for the live run instead of changing the fixture's gate or the scripted API's child delay.

  5. Cancel answered 5xx — answered in round 3 by the Stop contract: only a 4xx answer is the cell's definite refusal of a cancel (409 keeps its meaning; 401 as access loss is left to fork PR feat(cell): the cell provider against the login-era cell: refused tokens end the session's reading with what to do, allowance and run bounds reported, run usage as token usage #5). Every 5xx, a transport failure or no answer within the bound is unconfirmed: ProviderAdapterInterruptTimeoutError, turn open, Force stop offered, a late done still ends it interrupted. See section 6.

Not done

  • Upstream moves daily; this PR now merges ad4bb6d6fa (fetched 2026-11-04 ~01:51 UTC+2).
  • No --write-fixture refresh of the six fixtures whose wire is unchanged (only the known 202-versus-opener order differs).
  • The four justified real-clock waits in adapter.test.ts could become event-driven if the driven cell signalled its requests; left for the login-era stream, which owns that harness.

🤖 Generated with Claude Code

saphid and others added 30 commits November 3, 2026 01:39
…in half (pingdotgg#29572)

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>
…r approvals (pingdotgg#29567)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…erged (pingdotgg#29595)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…archived (pingdotgg#29582)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>
…cs (pingdotgg#29100)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…mpty (pingdotgg#29579)

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…them (pingdotgg#29573)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…overwrite (pingdotgg#28351)

Co-authored-by: PR Batch Tester <agent@local.test>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ffect 4.1.2 upgrade (pingdotgg#29780)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>
…ut tail splits an emoji (pingdotgg#29168)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>
…hanged test (pingdotgg#29839)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>
…ments early (pingdotgg#29717)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…tatus (pingdotgg#29627)

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…y Running (pingdotgg#28356)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ed parents (pingdotgg#27951)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…server (pingdotgg#27703)

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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…s skip-worktree or assume-unchanged (pingdotgg#23624)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
romil-bijarnia and others added 24 commits November 3, 2026 23:42
…race (pingdotgg#29889)

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… the catalog (pingdotgg#30051)

Co-authored-by: maria-rcks <maria@kuuro.net>
…point (pingdotgg#30033)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…go (pingdotgg#30071)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: maria-rcks <maria@kuuro.net>
…he runtime starts (pingdotgg#30079)

Co-authored-by: wenshen <wlum0002@student.monash.edu>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ingdotgg#30142)

Co-authored-by: SAPHID <saphid@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ttle (pingdotgg#29847)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ngs (pingdotgg#29853)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…t hooks (pingdotgg#29907)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…backports (pingdotgg#30161)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… lost, as upstream's unconfirmed interrupt, so the user is offered Force stop

An unanswered POST cancel (the 5 s Stop bound, or the adapter's shorter
request timeout, or a transport failure) failed as a protocol error wrapped
in ProviderAdapterInterruptError, which the server retries instead of
marking the run unresponsive. Every way a Stop goes unconfirmed now returns
ProviderAdapterInterruptTimeoutError and leaves the turn open; a late done
still ends it interrupted; a cancel the cell answers with an error keeps
ProviderAdapterInterruptError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream moved 112 first-parent commits past the first merge of this sync
(7c22e24). One conflict, resolved by keeping both sides:

- ProviderTurnStartService.test.ts: upstream added a resumeFailure harness
  option and the "another client holds the native thread" test (pingdotgg#21045);
  the fork's resumeFallbackWithOpenItem option and its open-subagent-rows
  resume-fallback test stay beside them. The fork's fallback session is
  checked first, upstream's resumeFailure shares the plain fallback session.

Auto-merged files both sides touched (Orchestrator, ProviderTurnStartService,
ProviderRuntimeRecoveryService, RestartBackgroundNote, provider-core's
ProviderAdapter) keep both changes; the fork's mechanisms are untouched by
upstream's edits there.

pnpm-lock.yaml took upstream's copy and was regenerated with
pnpm install --lockfile-only; its delta over upstream is the same 43 lines
as the first merge (provider-cell importer, two workspace links, ws catalog).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t carries its provider session, which the fork's restart check reads at turn start

Upstream's new "fails the run instead of replacing a native thread another
client holds" test (pingdotgg#21045) opens the harness's resume-fallback session,
which has no providerSession. The fork's turn start reads
session.providerSession.capabilities (backgroundWorkSurvivesRestart) before
it resumes, so the test died there. Real sessions always carry it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kupiainen kupiainen changed the title Sync with upstream pingdotgg/t3code main (7c22e24e3f) Sync with upstream pingdotgg/t3code main (ad4bb6d6fa) Nov 4, 2026
…fusal, so a 5xx, the cell's own or a gateway's, leaves the Stop unconfirmed and the user is offered Force stop

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kupiainen
kupiainen merged commit dd2d084 into main Nov 4, 2026
kupiainen added a commit that referenced this pull request Nov 4, 2026
…e login-era cell — a refused token ends the session's reading, judged for every answer in the one request function and the socket's close under the event permit, without a reconnect loop; the tenant's allowance as the usage limit with its reset time, the runs bound and run bounds as readable failures, and each run's usage as the turn's token usage

Re-applies 8e98fbf, 2cc5586 and 823a00d (PR #5 rounds 1-3) on
the upstream sync dd2d084, inside its Effect 4.1 / Effect.fn adapter and
under upstream's lint rules. A Stop's cancel refines #6's rule: 401 is
access loss, any other 4xx a refusal (403 with the cell's reason), every
5xx unconfirmed. One notice-node emission (#6's); the live revoke check
requires no socket after the 4401; the allowance check requires the
prompt-time usage_limit that vim main sends since vim pingdotgg#53.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kupiainen added a commit that referenced this pull request Nov 4, 2026
…ens end the session's reading with what to do, allowance and run bounds reported, run usage as token usage (#5)

The fork's cell provider was finished on 2026-10-20 against a cell with no login. This makes it work, and report clearly, against vim `main` (2e7f884 when opened; round 2 rechecked against 4ce3350; round 4 against 25abf41): login and API tokens, 401/403/503, 4401 on a revoked session, suspension, run bounds, the per-tenant daily allowance and runs bound, and per-run usage. The diff stays inside `packages/provider-cell`, its design record, the `live-cell-*` scripts and one cell fixture, so the rebase onto the upstream merge (stream 1) stayed small. **Round 4: rebased onto the upstream sync (fork main dd2d084, #6), see below.**

## What broke against the login-era cell (vim main, login on, before this PR)

- A missing, stale or revoked token made the thread fail with "cell provider protocol error: The cell tenant rejected the access token.", with no word on what to do. The probe said the token was rejected even when none was set.
- A tenant failing closed (base domain, no `SESSION_SECRET`, 503) showed up as "The cell answered 503 to GET state" in a thread, and as "not a cell tenant" in the probe.
- A revoked token (suspension, new invite code) during a run: the socket closes 4401, the reconnect handshake is refused 401, and the adapter reconnected with backoff (up to 10 s) for ever.
- A spent daily allowance is refused **at the prompt** (409 `{ runId: null, error }`). The prompt route re-reads the account, so this is the usual path, not the run's `done`. T3 showed it as a provider error ending in "..".
- `done.usage` was ignored.
- Vim main now retries a model 500 four times and emits each retry as a run-scoped `error`. The adapter published those notices as items without a node, which fails the projection integrity check. The checked-in `cell_wake_after_failure` no longer recorded live: the retries also reorder the child's completion ahead of the run's `done`.

Already fine, and verified live: the eight existing scenarios with a token, the mid-run allowance `done` (`usage_limit` + `resetAt`), the runs-bound `done` ("The run did not start: … send the prompt again", thread reusable), and the run-bound `done` texts. Node's fetch and the `ws` client send no `Origin` and no `Sec-Fetch-Site`, so the cell's other-site rule never refuses T3.

## Design

- **One error for "the tenant refuses T3 itself"**: `CellAccessError` in `errors.ts`, built from the status and the cell's text. The message names the tenant by host, says the token is missing or was refused and why tokens end, gives the steps (log in at `<base>/login`, Create an API token, Settings → Providers → Cell → Access token), and quotes the cell's reason. The cell's generic "log in at /login, or send … Authorization: Bearer" tail is cut from the quote: the steps say it in T3's terms, and T3's failure redaction would mangle it. The probe (auth `unauthenticated` on 401, `authenticated` when a token's state read succeeds), an attach, a prompt and the socket all use it. Codes: `cell_unauthorized` (401/403), `cell_unavailable` (503).
- **Access loss is a terminal state of the session's reader** (reviews 1 and 2 made this hold by construction). It is entered once, by one function (`loseAccess`), under the event permit, and judged in two places only. **The session's one `request` function classifies every answer before its caller sees it**: a 401 to any request (the attach's `GET state`, a prompt, the `GET state` after a lost prompt, a `GET events` page of a replay, a Stop's cancel, a deleted thread's task cancels) enters `loseAccess` right there, whether or not the turn that asked is still in progress, and the caller gets only `CellAccessLost`. If the requesting fiber already holds the permit (a replay, the lost-prompt recovery), the entry happens directly; otherwise it takes the permit. A turn that had already ended gets no second terminal, but the session still reports the error. The other place is **the socket's close handling**: after a 4401 close, or a socket that never opened, its `GET state` answers 401 (handled by `request`) or 403. Entering it discards what is buffered (the turn's candidates, the queued wakes). After it no wake is queued, offered or dispatched (one queueing function, the offer and the dispatch each check the state), nothing already received is read, no request leaves (the request function checks it), the socket is not reopened, and an attach is refused at once. Each turn in progress ends with exactly one terminal, failed with the refusal; every later turn ends at once with it (a start *error* would be shown by the server as its generic "could not start this turn"). A 403/503 refuses one request, not the token: the turn fails with the refusal's message (a Stop: 403 refused with the cell's reason, 503 unconfirmed per #6's rule) and the session reads on. Changing the token rebuilds the instance, so the next message starts clean.
- **One admission check on the socket**: after a connection the cell closed with 4401, or one that could not be opened (a refused handshake tells a client nothing), `GET state` is asked once. The event consumer asks it under the permit, after that connection's frames, so a `done` read before the close is applied first. A 401/403 there ends the reading; anything else reconnects with the existing backoff. Ordinary drops cost nothing extra.
- **Allowance at the prompt**: a refusal may carry `code: "usage_limit"` and `resetAt`, which the adapter reports as T3's usage limit with the reset time. vim main sends them since vim pingdotgg#53 (25abf41), checked live in round 4; an older cell's words still show, and they name when it renews.
- **Usage**: `done.usage` is read leniently (a bad count reports nothing and never makes the `done` unreadable) into `providerTurn.turnTokenUsage`: complete for a completed run, partial otherwise, with `hasSubagents` from the turn's tasks.
- **Fake cell**: cell-worded 401s, refused handshakes answered with their status rather than dropped, `revokeToken` (4401 and the stopped run where a run is in progress; idle sockets stay open and deaf, as in the cell), `loginUnset` (503), `usage` on every `done`, `spendAllowance`. The replay transport now records and replays a socket's close code.
- **Live tooling**: driver scenarios `access`, `revoke --suspend/--resume <operator token>`, `allowance` and `runs-bound`, each checking its outcome and exiting non-zero otherwise. The probe gains `--token` and 401/503 stand-ins. The scripted API reports usage.
- **Docs** (`docs/internals/provider-cell.md`): a new "The login" section (connecting T3 to a deployed cell, what each answer means and what T3 shows, the 30 s policy-cache timing), protocol notes for `usage` and the refusal code, the socket rule, the probe, and a live-check recipe with the login on.

## Review round 1 fixes (2cc5586)

Both must-fixes were ways around the refused-token rule. The fix makes the rule hold by construction instead of adding checks at each site. It is the access-loss state described under Design.
1. **A wake buffered during the prompt was dispatched after the prompt's 401.** Before, `loseAccess` cleared the wakes and then released the turn's candidates, which queued and offered the buffered wake again; its dispatch guard did not look at access loss. Now entering the state discards the candidates. The single function that queues a wake, the offer and the dispatch each check the state, so a wake offered *before* the 401 does not dispatch either. Test: `review r1: a prompt refused 401 discards the wakes it buffered and the ones already offered`.
2. **A cancel answered 401 produced no terminal** ("The cell answered 401 to cancel."). Now it enters access loss under the event permit and the turn ends with the refusal as its one terminal. A 403 or 503 on cancel fails the Stop with the actionable `CellAccessError` message and the run goes on. Tests: `review r1: a cancel refused 401 ends the turn with the refusal and the reading`, `a cancel refused 403 or 503 says why and the run goes on`.
3. **Sweep**: `every cell request answered 401 enters access loss once, by one rule` runs every remaining path on a hand-driven socket: `GET state` after a lost prompt, `GET events` during a replay (before the fix this looped reconnecting), 4401 followed by `GET state`, a task's cancel with no turn in progress, and the attach (a retried attach asks nothing). The prompt and Stop paths are covered by the two regression tests above. Each path checks the same things: the session error is entered once, each turn in progress gets one terminal with `cell_unauthorized`, a wake sent afterwards is not read and no continuation is offered, no socket opens past the backoff, no request leaves afterwards, and a later turn fails at once.

Negative control: all four tests fail against 8e98fbf (`r2/logs/negative-control.log`: no terminal on cancel 401, a wake offered after the prompt's 401, `ProviderAdapterProtocolError` on a 403 cancel, and the sweep hangs in the `GET events` reconnect loop).

## Review round 2 fix (823a00d)

**A prompt left pending, a reset that ended its turn, then the prompt answered 401: no access loss was recorded.** The session then offered and dispatched a wake and sent another prompt. Cause: the prompt's handler returned early on the inactive turn before it looked at the status. Fix: instead of one more check at that call site, the 401 check moved into `request`, which every session request goes through. It runs before any caller logic (turn liveness, candidate release, retries). `tokenRefused`, `loseAccessNow` and the per-caller 401 branches are gone. The permit is now taken through `underPermit`, which records the holding fiber, so a request made under the permit enters access loss without taking the permit a second time. A task cancel's shorter bound is now a `timeoutMs` on the request, so the bound can never cut off the entry into access loss. The socket's post-close `GET state` now goes through the event consumer (`check_access` inbox entry), so it is ordered behind the connection's frames.

Test: `a 401 to a request whose turn had already ended still enters access loss, with no second terminal`. It covers the reviewer's ordering (prompt pending → reset ends the turn → 401) and the same ordering for a Stop's cancel and for a `GET events` page (the page before it carries the reset). It also checks that a run's `done` followed by a 4401 close completes the turn before access is lost. Each case checks one session error, no extra terminal, no wake offered or dispatched, no request or socket afterwards, and a later turn failing at once. The driven cell gained `parksCancels()`. Negative control: against 2cc5586's adapter the test fails, timing out on the prompt ordering (no session error ever comes) (`r3/logs/negative-control.log`).

## Round 4: rebase onto the upstream sync (head 8e4f01e, base dd2d084)

**How:** re-applied as one new commit on dd2d084, not a commit-by-commit rebase. #6 rewrote the adapter: Effect 4.1, `Effect.fn` conversions, upstream's comment cuts, and the new Stop path. Each of my three commits touched the same Stop and task-cancel regions, so a sequential rebase would have resolved them three times. I did a 3-way merge of the squashed change (cfa469e → 823a00d) against dd2d084: 17 conflict hunks in 9 files. The old commits stay reachable as 8e98fbf / 2cc5586 / 823a00d. The commit message names them.

**Reconciled with #6:**
- **Stop rule, refined:** a 401 to a Stop's cancel is access loss. `request` enters it, the turn ends with the refusal as its one terminal, and Stop returns. Any other 4xx is a refusal (`ProviderAdapterInterruptError`; a 403 carries the cell's reason as `CellAccessError`), and 409 keeps its meaning. Every 5xx (503 included, which round 3 treated as a refusal), a transport failure or no answer is `ProviderAdapterInterruptTimeoutError`. #6's 5 s cancel + 8 s confirmation bounds are kept. The cancel bound is passed as `request`'s `timeoutMs`, so it never cuts off the entry into access loss. The test `a cancel refused 403 says why, a 503 leaves it unconfirmed, and the run goes on` replaces round 3's 403/503 test.
- **Task cancels on deletion:** #6's `Effect.fn` form plus the access-loss guards and `timeoutMs` inside `request`.
- **Notice node:** one emission (#6's). My live notice test duplicated #6's driven-socket test, so I dropped it and folded its one extra assertion (the node is a `system` node) into #6's test.
- **Close codes:** the `closeCode` mapping moved into #6's NodeWS socket reader. The replay transport still records `ws_closed` with its code.
- **`cell_wake_after_failure`:** I kept #6's recording and #6's recipe (`MODEL_RETRY_DELAY_MS=1` in `.dev.vars`, no `CHILD_DELAY_MS`), and dropped my `CHILD_DELAY_MS=15000` text from the docs. I re-recorded it against a fresh vim 25abf41 cell with the login off. It differs from the checked-in transcript only by ids and timestamps (4 retry notices, 22 entries, replay-verified), so this PR no longer changes the fixture. No new fixture, so nothing new to register in CellReplayFixtures.
- **Upstream lint:** I cut 11 long comments to ≤ 4 lines (the design stays in docs), used `catchTags`, and un-exported `cellRefusalText` (knip). The four real-clock waits are the absence-of-reconnect checks after access loss. They keep `// oxlint-disable-next-line t3code/no-real-time-wait-in-tests` with main's existing justification ("an absence has no event to await"). There's no cheap event: making them event-driven would need a production hook that exposes the socket loop's end. The fourth is inside the `assertAccessLost` helper, which the rule does not flag.
- **`requiresSignIn`: not adopted.** It is a flag on `ProviderAdapterOpenSessionError`, and the cell's openSession makes no request. The tenant is first asked at the thread attach, which needs the project. The server's only use of the flag is to skip the start worker's retries (100+200+400+800 ms). Those retries already send nothing, because the refusal is cached in the session. Adopting it would add a `GET state` to every session open, and it could not cover a 401 that arrives mid-session.
- **Round-3 should-fix:** the live `revoke` check now requires `socketsAfterClose === 0`. **`allowance`** now requires the prompt-time `usage_limit` with `resetAt`, which vim main sends since pingdotgg#53.

## Verification
Round 4 (head 8e4f01e, on dd2d084; logs `/srv/t3/tmp/ready-2026-11-04/t3code-login-era/r4/logs/`):
- Root: `vp run -r --concurrency-limit 4 typecheck` **41/41**. `vp lint --report-unused-disable-directives` exits 0 with main's four warnings and none in touched files. `vp fmt --check` is clean. `pnpm run knip:check` is clean. The pre-commit hook (fmt, lint, knip) passed.
- `packages/provider-cell`: `pnpm test` **113 passed** (3 files; #6 main has 97). `pnpm typecheck` (tsc) is clean.
- `apps/server`, `CLAUDE_CONFIG_DIR` unset, by path: CellRestartAdoption, CellThreadDeletion, **CellReplayFixtures**, RunExecutionService, ProviderSessionManager, ThreadDeletion, ProviderRuntimeRecoveryService, ProviderTurnStartService, ProjectService.deletion, Orchestrator.control-reads, ManagedPolicy, ProviderTurnControlService, RestartBackgroundNote and ProviderSwitch.integration: **14 files, 274 passed**.
- The review r2/r3 scratch orderings (11 tests: late prompt 401 after a reset, late 202 after socket loss, socket/prompt and cancel/done races, concurrent task cancels past their bound under a held permit, closing with an interrupted requester, prompt 403/503, 4401 then 403 vs 503) were inserted temporarily into adapter.test.ts: **13 passed** with the two own `review r1` tests (`review-orderings.log`).
- **Live, vim main 25abf41** (built fresh, index.js sha256 3b7e9109…), celld 0.6.2, scripted API, `MODEL_RETRY_DELAY_MS=1`, `bash r4/live/run-live.sh r4`: **35/35 exit 0**.
  - Login on: probe; `shell`, `interrupt`, `access` (no token, bogus token, fail-closed tenant); `revoke` (4401 at +30 s, **0 sockets after it**, refusal names the suspension, next message fails at once, resume posted); `shell` after re-login; `delete-delegate` (cancel 200, listed cancelled); 6 recordings; `allowance` at the prompt (**409 carries `code: usage_limit`, turn fails as `usage_limit`, `resetAt` 2026-11-05T00:00Z**) and mid-run; `runs-bound`; fail-closed probe.
  - Login off: probe; `shell`, `interrupt`, `limit`, `delegate`, `reconnect`, `restart`, `attach-retry`, `delete-delegate`; all **7** recordings, `turn_interrupt` and `cell_wake_after_failure` included.
  - Live usage on the turn: input 4088 / output 59.


Round 3 (head 823a00d):
- `packages/provider-cell`: `pnpm test` **112 passed** (111 before, +1 sweep test with four orderings). `pnpm typecheck` clean. `vp lint --report-unused-disable-directives` and `vp fmt --check` clean.
- `apps/server` cell suites by path: **10 passed** (143 skipped).
- **Live against vim main 4ce3350** (the round-2 build; vim main is now 7888f18, but cell code is unchanged since 4ce3350), login on, full `run-live.sh`. Passed: the probe, `shell`/`interrupt`/`limit`/`delegate`/`reconnect`/`restart`/`attach-retry`, `access` (no token, bogus token, fail-closed tenant), `revoke` (refusal names the suspension, next message fails at once, **0 sockets after the 4401**), `shell` after re-login, `delete-delegate` (the cell lists the task as cancelled), allowance at the prompt and mid-run, and `runs-bound`. 24 of 25 exit 0; `turn_interrupt` with the login on fails as before (owner question 3).

Round 2 (head 2cc5586):
- `packages/provider-cell`: `pnpm test` **111 passed** (107 before, +4). `pnpm typecheck` clean. `vp lint` and `vp fmt --check` clean.
- `apps/server` cell suites by path (`CellRestartAdoption`, `CellThreadDeletion`, `OrchestratorReplayFixtures -t cell`): **10 passed**.
- **Live against vim main 4ce3350** (built fresh, celld 0.6.2, login on, tenant `default`, API token via `/login` + `/login/token`): probe; `shell`, `interrupt`, `limit`, `delegate`, `reconnect`, `restart`, `attach-retry` with the token; `access` with no token, a bogus one, and against a fail-closed tenant; `revoke` (refusal names the suspension, next message fails at once, **0 sockets after the 4401**); `shell` after re-login; `delete-delegate`; `allowance` at the prompt and mid-run; `runs-bound`. All exit 0. Six of seven recordings pass; `turn_interrupt` with the login on fails as in round 1 (owner question 3).

Round 1 (head 8e98fbf, vim main 2e7f884):

- `packages/provider-cell`: `pnpm test` went from **95 to 107 passed** (3 files): +2 protocol, +1 scripted socket (4401, then `GET state` 401, nothing after), +9 over the fake cell (probe answers, refused attach, revoke mid-run with no reconnect, prompt 401 on a deaf socket, refused reconnect, allowance refusal, runs bound, notice node, usage). `pnpm typecheck` is clean.
- `apps/server`: the cell suites by path (`CellRestartAdoption`, `CellThreadDeletion`, `OrchestratorReplayFixtures -t cell`): **10 passed** before and after, with the refreshed fixture. `pnpm typecheck` shows no diagnostics in `scripts/live-cell-*` or provider-cell.
- `vp lint --report-unused-disable-directives` and `vp fmt --check` on the changed files: clean.
- **Live, vim main 2e7f884 on celld 0.6.2, login on** (`CELL_BASE_DOMAIN=vim.test`, `SESSION_SECRET`, `INVITE_SECRET`, tenant `default` at the apex, API token minted through `/login` + `/login/token`), full run `run-live.sh r3`. Passed:
  - the probe;
  - `shell`, `interrupt`, `limit`, `delegate`, `reconnect`, `restart` and `attach-retry` with the token;
  - `access` with no token, with a bogus one, and against a fail-closed tenant (no prompt posted in each);
  - `revoke`: 4401 at +30 s, **0 sockets after it**, the second message ended at once with the refusal, resume posted;
  - `shell` after re-login, `delete-delegate`;
  - `allowance` at the prompt (`TENANT_DAILY_TOKENS=1000`) and mid-run (`41500`, `usage_limit` + `resetAt`);
  - `runs-bound` (`TENANT_MAX_RUNS=1`);
  - six of seven fixture recordings with the token;
  - live usage on the turn (`inputTokens 4060, outputTokens 59`).
- `cell_wake_after_failure` was re-recorded against that cell (`CHILD_DELAY_MS=15000`, `--write-fixture --version 0.3.0`) and checked in. The transcript carries no token.
- `turn_interrupt` records with the login **off** (5/5) but not on (0/5); see owner questions.

## Owner questions (provisional decisions)

1. ~~The allowance refusal's code~~: settled by vim pingdotgg#53 (the prompt's 409 carries `code: "usage_limit"` and `resetAt`), mapped live in round 4.
2. **A refused token ends the session's reading** rather than retrying it. I chose this because a refused token never holds again. Is a session-level stop right, or should later turns retry the attach (costing one more 401 each)?
3. **`turn_interrupt` with the login on**: the shared input presses Stop right after the prompt, which with the login on lands during the first model request's reservation, so the shell row never appears. Adding `waitForTurnItemType: "command_execution"` to the shared input would fix it, but that touches every provider's fixture and `fixtures/index.ts`; I left it.
4. Failure codes `cell_unauthorized` / `cell_unavailable` are new strings. The class stays `provider_error`, since contracts have no auth class.





Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.