From 596fa473175b2b555cb30656b3d944d10c109f5f Mon Sep 17 00:00:00 2001 From: Etan Joseph Heyman Date: Sat, 1 Aug 2026 19:42:58 +0300 Subject: [PATCH 1/5] feat(t3layer): replace private runtime client with stock T3 HTTP baseline Phase 3 of the stock-T3 realignment: T3Layer becomes a channel-agnostic external orchestration overlay for an unmodified stock T3 server. - Remove @t3tools/runtime-client and all private-fork prerequisites; delete the hand-written RPC protocol module - Add narrow local stock HTTP contracts (descriptor/auth/shell/detail/ dispatch) with fail-closed validation at pinned upstream d3037064 - Add bounded adaptive shell/detail poller with strict cadence, rate, concurrency, and evidence budgets; injected clock throughout - Rewrite nativeRuntime/facade for two-stage HTTP spawn (thread.create -> bootstrap-free thread.turn.start), receipt-targeted causal wait, expiring send leases, exhaustive typed partial/pending outcomes, and the no-throw-after-durable-mutation invariant across all three stock mutations (project/thread/turn) - Caller-held project-create identity for cross-runtime idempotency; stock-compatible workspace-root ingress canonicalization - Isolated live-proof harness with exact-SHA stock worktree, forgery- rejecting receipts, and shell-hardened teardown; stock-only gate script - 222 tests / 727 assertions green at pinned Bun 1.3.11; typecheck clean; ShellCheck clean; exact-stock characterization green at d3037064 Nine adversarial review rounds plus boundary-convergence and final pre-commit gates; final independent verdict SHIP 9/10 with zero HIGH/CRITICAL/MEDIUM findings. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 232 +- bun.lock | 48 - docs/operations/stock-t3-first-release.md | 55 + package.json | 17 +- scripts/check-stock-only.sh | 63 + scripts/stock-proof-cli.ts | 27 + scripts/stock-t3-canary-drill.sh | 217 ++ scripts/stock-t3-exact-characterization.sh | 201 ++ scripts/stock-t3-live-harness.sh | 331 ++ src/adaptivePoller.ts | 571 ++++ src/facade.ts | 821 +---- src/nativeRuntime.ts | 3389 ++++++++++++++------ src/protocol.ts | 170 - src/stockProof.ts | 317 ++ src/stockT3Contracts.ts | 367 +++ src/stockT3HttpClient.ts | 338 ++ test/adaptive-poller.test.ts | 422 +++ test/boundary-convergence.test.ts | 922 ++++++ test/facade.contract.test.ts | 193 -- test/facade.send.test.ts | 867 ----- test/facade.spawn.test.ts | 918 ------ test/facade.stock-http.test.ts | 891 +++++ test/facade.wait.test.ts | 1259 -------- test/native-runtime-adapter.test.ts | 1909 +++-------- test/protocol.test.ts | 168 - test/r3-runtime-regressions.test.ts | 438 +++ test/r4-runtime-regressions.test.ts | 442 +++ test/r5-runtime-regressions.test.ts | 338 ++ test/r6-runtime-regressions.test.ts | 399 +++ test/r7-runtime-regressions.test.ts | 466 +++ test/r8-runtime-regressions.test.ts | 604 ++++ test/r9-runtime-regressions.test.ts | 664 ++++ test/stock-only-gate.test.ts | 93 + test/stock-t3-contracts.test.ts | 172 + test/stock-t3-exact-stock-negative.test.ts | 44 + test/stock-t3-http-client.test.ts | 243 ++ test/stock-t3-live-harness.test.ts | 446 +++ test/stock-t3-live.test.ts | 95 + test/stock-t3-sequence.test.ts | 200 ++ 39 files changed, 12350 insertions(+), 7007 deletions(-) create mode 100644 docs/operations/stock-t3-first-release.md create mode 100755 scripts/check-stock-only.sh create mode 100644 scripts/stock-proof-cli.ts create mode 100644 scripts/stock-t3-canary-drill.sh create mode 100755 scripts/stock-t3-exact-characterization.sh create mode 100755 scripts/stock-t3-live-harness.sh create mode 100644 src/adaptivePoller.ts delete mode 100644 src/protocol.ts create mode 100644 src/stockProof.ts create mode 100644 src/stockT3Contracts.ts create mode 100644 src/stockT3HttpClient.ts create mode 100644 test/adaptive-poller.test.ts create mode 100644 test/boundary-convergence.test.ts delete mode 100644 test/facade.contract.test.ts delete mode 100644 test/facade.send.test.ts delete mode 100644 test/facade.spawn.test.ts create mode 100644 test/facade.stock-http.test.ts delete mode 100644 test/facade.wait.test.ts delete mode 100644 test/protocol.test.ts create mode 100644 test/r3-runtime-regressions.test.ts create mode 100644 test/r4-runtime-regressions.test.ts create mode 100644 test/r5-runtime-regressions.test.ts create mode 100644 test/r6-runtime-regressions.test.ts create mode 100644 test/r7-runtime-regressions.test.ts create mode 100644 test/r8-runtime-regressions.test.ts create mode 100644 test/r9-runtime-regressions.test.ts create mode 100644 test/stock-only-gate.test.ts create mode 100644 test/stock-t3-contracts.test.ts create mode 100644 test/stock-t3-exact-stock-negative.test.ts create mode 100644 test/stock-t3-http-client.test.ts create mode 100644 test/stock-t3-live-harness.test.ts create mode 100644 test/stock-t3-live.test.ts create mode 100644 test/stock-t3-sequence.test.ts diff --git a/README.md b/README.md index fb23d41..13e4ec2 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,216 @@ # T3Layer -T3Layer is an experimental TypeScript control-plane facade for native -[T3 Code](https://t3.codes/) projects, threads, turns, and lifecycle events. -It is designed to coordinate coding agents through T3 Code's structured APIs -without parsing terminal output or maintaining a second agent registry. +T3Layer is an experimental TypeScript orchestration facade for an unmodified +stock [T3 Code](https://t3.codes/) server. Stock T3 remains the only durable +owner of projects, threads, messages, turns, sessions, approvals, checkpoints, +and provider state. T3Layer keeps only bounded process-local receipts, causal +wait leases, polling cursors, evidence, and orchestration policy. > [!WARNING] -> T3Layer is in early development. It has no stable public API or release yet. -> The narrow protocol code currently in this repository is prototype evidence, -> not a supported T3 Code client. +> T3Layer is pre-release software. Use disposable workspaces and a scoped, +> short-lived T3 bearer while testing it. -T3Layer is an independent project and is not an official T3 Code product. +T3Layer is independent and is not an official T3 Code product. -## Design principles +## Stock HTTP boundary -- T3 Code remains the source of truth for agent identity and lifecycle. -- A native T3 thread ID is the durable agent ID. -- Transport adaptation, compatibility checks, timeouts, and bounded resource - policy belong in T3Layer. -- Agent transcripts, approvals, provider sessions, and UI state do not. -- Lifecycle decisions come from structured state and events, never terminal - panes, titles, or model prose. +The baseline transport uses only the public stock endpoints below: -## Intended API +- `GET /.well-known/t3/environment` +- `POST /oauth/token` when a bootstrap credential must be exchanged +- `GET /api/orchestration/shell` +- `GET /api/orchestration/threads/:threadId` +- `POST /api/orchestration/dispatch` -The planned facade is deliberately small: +Dispatch always uses authenticated HTTP. Observation uses one coalesced shell +poller per environment plus conditional thread-detail reads. Socket framing, +server source imports, direct database access, copied runtime internals, and a +second transcript/lifecycle store are outside the baseline. + +Configure a runtime directly: ```ts -spawn(input): Promise -send(agentId, message): Promise -wait(agentId, condition): AsyncIterable -getState(agentId): Promise -interrupt(agentId): Promise -stop(agentId): Promise +import { createStockT3Facade } from "./src/facade"; +import { createStockT3NativeRuntime } from "./src/nativeRuntime"; + +const runtime = createStockT3NativeRuntime({ + baseUrl: "http://127.0.0.1:3774", + bearerToken: process.env.T3_STOCK_HTTP_TOKEN, + connectionProfile: "local", +}); +const t3 = createStockT3Facade(runtime); ``` -These signatures describe project direction, not a released implementation. +Never log the bearer, authorization headers, bootstrap credentials, provider +keys, prompts, or raw responses. The live harness accepts a 1Password reference +through `T3_STOCK_PROVIDER_SECRET_REF`; the resolved value is scoped only to the +owned isolated server child. + +## Causal API + +Project lookup remains stock-authoritative. If a unique project for +`workspaceRoot` is already visible, `spawn` may resolve it without a creation +identity. If no project exists, the caller must preallocate and retain the full +immutable `projectCreateIdentity`: cryptographically random project and command +IDs plus `createdAt`, workspace root, project title, and default model selection. +The same object must be reused after a timeout, runtime recreation, or by +simultaneous runtime objects participating in the same logical attempt. +Identity-free creation fails before dispatch as +`identity_conflict/project_create_identity_required`; T3Layer never derives a +deterministic ID from a workspace root or relies on a process-local map for +duplicate prevention. -## Development +```ts +import { + allocateProjectCreateIdentity, + parseProjectCreateIdentity, +} from "./src/facade"; + +const projectCreateIdentity = allocateProjectCreateIdentity({ + workspaceRoot, + title: "My stock project", + defaultModelSelection: modelSelection, +}); +const restoredIdentity = parseProjectCreateIdentity( + JSON.parse(JSON.stringify(projectCreateIdentity)), + { workspaceRoot }, +); + +await t3.spawn({ + workspaceRoot, + projectCreateIdentity: restoredIdentity, + title: "worker", + message: "Start", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, +}); +``` -Declared toolchain target: +HTTP spawn is a two-stage operation: + +```text +thread.create -> shell/detail identity reconciliation + -> fresh empty-thread preflight + -> bootstrap-free thread.turn.start +``` -- [Bun](https://bun.sh/) 1.3.11 -- TypeScript 6.0.x +`spawn` returns either a fully reconciled `spawned` result, a truthful partial +result for a durable thread whose initial turn is not proven, a +`create_reconciliation_pending` result with its provisional scoped reference, +or a ref-preserving protocol failure. Pending create reconciliation is read-only +until identity is established; it never hides or deletes the stock thread. + +`send` returns a `TurnReceipt`. Only `wait(receipt, ...)` may claim causal turn +completion. One expiring send lease is allowed per scoped thread. A bare thread +reference can be observed but cannot be upgraded into a causal completion claim +after process restart or lease loss. Receipts use `leaseState: "active"` while +executable. External-writer terminal partials retain the complete accepted or +ambiguous receipt as `leaseState: "released"`; that evidence is not executable +and `wait` rejects it as `receipt_expired`. + +Workspace roots cross one stock-compatible ingress seam before lookup, identity +validation, payload construction, or comparison: whitespace is trimmed, `~` is +expanded, relative paths become absolute, trailing separators are normalized, +and Windows drive/UNC comparison is case-insensitive. The canonical root is +stored in project-create identities and evidence. `createdAt` remains +allocator-owned because an ambiguous stock command retry must replay the entire +original command byte-for-byte. + +The distinct-ID correlation contract fails closed on observable overlap: +`superseded`, `concurrent_writer`, or `causality_unverifiable`. Deliberate reuse +of the exact target message ID with indistinguishable time and payload is outside +the deterministic guarantee because stock exposes no public command-to-turn +causation field. + +## Polling and request budgets + +- Healthy shell cadence after dispatch: 250 ms, 500 ms, 1 s, then 2 s. +- Shell starts: at most 32 in minute one, then 30 per rolling minute. +- Detail amplification: at most four reads per active wait/minute and no + overlapping detail read for one thread. +- Capacity: eight active waits and eight total HTTP requests per client. +- Aggregate ceiling for eight fast waits: 64 starts in minute one and 62 in a + later full minute; slow requests reduce starts because attempts do not overlap. +- Attempt deadline: 5 s for `local`; 15 s independently for `relay` and + `tunnel`, always capped by the operation deadline. +- Default operation/lease deadline: 15 minutes; no unbounded wait. +- Snapshot failure backoff: 500 ms, 1 s, 2 s, 4 s, then 8 s; `Retry-After` is + capped at 8 s. +- Evidence cap: 256 KiB per wait, with terminal identity retained. + +Capacity, transport, protocol-decode, and shell/detail observation failures do +not prove a causal turn outcome. They leave the receipt active so the caller can +retry `wait` with the same receipt; a duplicate `send` remains blocked. Only +explicit cancellation/release, environment invalidation, inclusive lease +expiry, a proven causal terminal outcome, or successful completion releases the +lease. Every terminal result/error embeds a structurally released receipt. + +Exact received dispatch errors are surfaced as `command_rejected` (400), +`authentication_failed` (401), `permission_denied` (403), or `server_internal` +(500). A received first-attempt error is not retried. Only a request with no +trustworthy response can be retried once with identical IDs and payload; a retry +error cannot erase ambiguity about the original attempt. + +An environment-ID change invalidates only the old environment's receipts and +slot claims, then re-pins the runtime to the newly discovered stable +environment. The operation that observes the roll returns `environment_changed`; +new-environment work cannot be blocked or cleared by a colliding old ref. Scoped +old project-create evidence fails closed, while stable work may reuse the same +unscoped caller-held project command identity without minting a second ID. + +## Development and proof + +Declared toolchain target: Bun 1.3.11 and TypeScript 6.0.x. ```bash -bun install +bun install --frozen-lockfile bun test bun run typecheck +bash scripts/check-stock-only.sh +bash scripts/stock-t3-canary-drill.sh --dry-run ``` -Do not point experimental code at an important T3 Code environment. Some future -operations may run agents with broad filesystem access; use disposable projects -and worktrees while developing integrations. +The opt-in exact-stock live proof is isolated from normal user state: -## Contributing - -See [CONTRIBUTING.md](CONTRIBUTING.md). Please report security issues privately -as described in [SECURITY.md](SECURITY.md). - -## License +```bash +set -a +. ./.env.stock-proof +set +a +bash scripts/stock-t3-live-harness.sh +``` -Licensed under the [Apache License 2.0](LICENSE). +`.env.stock-proof` is ignored and must contain only a secret reference, never a +secret value. The harness pins the adopted stock SHA, builds in detached clean +worktrees, uses a dedicated base directory/workspace/bearer, validates exact PID +birth and working-directory identity during teardown, and accepts proof freshness +only for the caller-held current `{runId, candidateSha}` pair. Its TypeScript +validator requires clean-install/build provenance, executable exact-stock +characterization, the HTTP negative, redacted endpoint/status observations, +actual request/poll counters, scoped IDs and sequences, terminal outcomes, +isolation, and complete teardown before atomic mode-0600 publication. Literal +stock SHA/provenance commands and isolation basenames are pinned, and final +validation runs from the archived candidate rather than the mutable worktree. + +Because the harness exports `git archive HEAD`, it proves only a reviewed +checkpoint commit. An uncommitted working tree cannot produce a current proof +for those edits. + +## First stock-only release + +The first stock-only release has no compatible earlier binary rollback target. +Canary and promotion therefore use the same immutable artifact and +`stock-http-v1` configuration. A configuration rollback restores the +canary-validated config on that same artifact. A code failure turns T3Layer +routing off, cancels in-flight receipt waits without replay, and leaves stock T3 +untouched while a forward fix is built. After a second stock-only artifact passes +the same gate, rollback may target a previously accepted stock-only artifact. + +See [docs/operations/stock-t3-first-release.md](docs/operations/stock-t3-first-release.md). + +## Contributing and license + +See [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md). Licensed +under the [Apache License 2.0](LICENSE). diff --git a/bun.lock b/bun.lock index 60b32a3..f16c9ff 100644 --- a/bun.lock +++ b/bun.lock @@ -4,10 +4,6 @@ "workspaces": { "": { "name": "t3layer", - "dependencies": { - "@t3tools/runtime-client": "https://github.com/EtanHey/t3code/releases/download/runtime-client-v0.0.31-rpc.2/t3tools-runtime-client-0.0.31-rpc.2.tgz", - "effect": "4.0.0-beta.102", - }, "devDependencies": { "@types/bun": "1.3.11", "typescript": "~6.0.3", @@ -15,58 +11,14 @@ }, }, "packages": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], - - "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="], - - "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="], - - "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="], - - "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="], - - "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], - - "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@t3tools/runtime-client": ["@t3tools/runtime-client@https://github.com/EtanHey/t3code/releases/download/runtime-client-v0.0.31-rpc.2/t3tools-runtime-client-0.0.31-rpc.2.tgz", { "peerDependencies": { "effect": "4.0.0-beta.102" } }, "sha512-+GRWl2WZxg9MJfQNKDc+BsnGAj8gD8zmlwWPa9Qe/TbEV3v/HRsoTZ8uZfuJX/m4YJnARIGzkhONN4eg9QeqTw=="], - "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], "@types/node": ["@types/node@26.1.2", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg=="], "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - - "effect": ["effect@4.0.0-beta.102", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-z8Y+Q76Hh/kjLFZrXu8tGn6e+tDsg45R+UHhxd190pXxD53OGwf/G/zDxXTkse4HJ5mobNZfitLfUCp4fMvu6w=="], - - "fast-check": ["fast-check@4.9.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg=="], - - "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], - - "ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="], - - "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], - - "msgpackr": ["msgpackr@2.0.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA=="], - - "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="], - - "multipasta": ["multipasta@0.2.8", "", {}, "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q=="], - - "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], - - "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="], - - "toml": ["toml@4.3.0", "", {}, "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A=="], - "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], - - "uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="], - - "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], } } diff --git a/docs/operations/stock-t3-first-release.md b/docs/operations/stock-t3-first-release.md new file mode 100644 index 0000000..e8c4505 --- /dev/null +++ b/docs/operations/stock-t3-first-release.md @@ -0,0 +1,55 @@ +# Stock T3 First-Release Runbook + +This runbook describes operator-side routing around an already-built Phase 3 +artifact. It does not add routing state to T3Layer and does not control or mutate +the stock T3 server. + +## Preconditions + +- Record the reviewed candidate SHA, artifact SHA-256, and configuration digest. +- Require configuration schema `stock-http-v1` and acceleration `off`. +- Attach passing deterministic, typecheck, stock-only, exact-SHA, and current + `{runId,candidateSha}` live-proof receipts. +- Supply explicit executable paths for routing off, canary, promotion, prior + config restoration, readiness, descriptor inspection, and an existing-thread + read, plus a cancellation command that returns redacted + `{cancelled, replayed: 0}` evidence. Also supply the immutable artifact and + redacted configuration files. + The drill refuses shell snippets. + +Without a real routing controller and these commands, only `--dry-run` is valid +and release remains blocked. + +## Drill + +Run the same immutable artifact through: + +```text +off -> canary -> promoted -> canary (prior config) -> off +``` + +The execute receipt re-hashes the artifact after every stage and records every +command exit status, before/after configuration digests, one unchanged +descriptor environment identity, one unchanged canonical thread identity, +actual cancellation/no-replay evidence, and acceleration=`off`. Its mode-0600 +checksum envelope is reread before success. Promotion changes only the routing +percentage or allowlist. Configuration rollback restores the byte-identical +canary-validated redacted configuration. + +## Failure behavior + +- Any transition, readiness, descriptor, or thread-read failure: invoke the + already-armed recovery trap, attempt prior-configuration restoration, and + finish with the supplied routing-off command. +- Code failure or first-release incident: keep routing off, cancel outstanding + receipt waits with typed outcomes, and do not replay them. +- Never roll back T3 data, start an incompatible predecessor, or infer causal + completion after a lost receipt. +- Treat a capacity, HTTP, or projection observation error as retryable + observation failure: retry `wait` with the same active receipt. Do not issue a + replacement send while that receipt owns the thread slot. Terminal evidence + must carry `leaseState: "released"` before a new send is admitted. +- Build and validate a forward fix while users continue against stock T3. + +After a second stock-only artifact independently passes the same gate, ordinary +binary rollback may select only a previously accepted stock-only artifact. diff --git a/package.json b/package.json index 0544f33..4c372a7 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,15 @@ { "name": "t3layer", - "private": true, - "type": "module", + "devDependencies": { + "@types/bun": "1.3.11", + "typescript": "~6.0.3" + }, "packageManager": "bun@1.3.11", + "private": true, "scripts": { + "check:stock-only": "bash scripts/check-stock-only.sh", "test": "bun test", "typecheck": "tsc --noEmit" }, - "devDependencies": { - "@types/bun": "1.3.11", - "typescript": "~6.0.3" - }, - "dependencies": { - "@t3tools/runtime-client": "https://github.com/EtanHey/t3code/releases/download/runtime-client-v0.0.31-rpc.2/t3tools-runtime-client-0.0.31-rpc.2.tgz", - "effect": "4.0.0-beta.102" - } + "type": "module" } diff --git a/scripts/check-stock-only.sh b/scripts/check-stock-only.sh new file mode 100755 index 0000000..ab5f31d --- /dev/null +++ b/scripts/check-stock-only.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +set -euo pipefail + +candidate_root=${STOCK_ONLY_CANDIDATE_ROOT:-$PWD} +historical_path=${STOCK_ONLY_HISTORICAL_PATH:-/Users/etanheyman/Gits/t3layer/test/p2-live-proof-runner.test.ts} +historical_sha=${STOCK_ONLY_HISTORICAL_SHA256:-0202d976418d6da8d21eb708025f8b1e378ed34b83daa5201cf1a9255a5691ee} + +if [[ ! -d "$candidate_root" ]]; then + echo "ERROR: candidate root is not a directory" >&2 + exit 2 +fi +if [[ ! -f "$historical_path" ]]; then + echo "ERROR: historical evidence file is missing" >&2 + exit 2 +fi + +private_scope='@t3tools' +private_name='runtime''-client' +private_needle="${private_scope}/${private_name}" +release_host='github.com/' +release_owner='Etan''Hey/t3code' +release_needle="${release_host}${release_owner}" +fork_marker='ORCHESTRATION_''WS_METHODS' +factory_marker='makeRpc''SessionFactory' + +paths_file=$(mktemp "${TMPDIR:-/tmp}/t3layer-stock-paths.XXXXXX") +cleanup() { + rm -f -- "$paths_file" +} +trap cleanup EXIT INT TERM + +/usr/bin/git -C "$candidate_root" ls-files --cached --others --exclude-standard -z -- \ + package.json bun.lock src test README.md scripts docs >"$paths_file" + +violation=0 +while IFS= read -r -d '' relative_path; do + candidate_path="$candidate_root/$relative_path" + [[ -f "$candidate_path" ]] || continue + for needle in "$private_needle" "$release_needle" "$fork_marker" "$factory_marker"; do + if /usr/bin/grep -F -q -- "$needle" "$candidate_path"; then + echo "ERROR: forbidden candidate reference in $relative_path" >&2 + violation=1 + break + fi + done +done <"$paths_file" + +if [[ "$violation" -ne 0 ]]; then + exit 1 +fi + +if command -v sha256sum >/dev/null 2>&1; then + actual_historical_sha=$(sha256sum "$historical_path" | /usr/bin/awk '{print $1}') +else + actual_historical_sha=$(shasum -a 256 "$historical_path" | /usr/bin/awk '{print $1}') +fi +if [[ "$actual_historical_sha" != "$historical_sha" ]]; then + echo "ERROR: historical evidence SHA-256 mismatch" >&2 + exit 1 +fi + +echo "STOCK_ONLY_CHECK: PASS" +echo "HISTORICAL_ALLOWLIST_SHA256: $actual_historical_sha" diff --git a/scripts/stock-proof-cli.ts b/scripts/stock-proof-cli.ts new file mode 100644 index 0000000..c7f483f --- /dev/null +++ b/scripts/stock-proof-cli.ts @@ -0,0 +1,27 @@ +import { + canonicalProofEnvelopeJson, + canonicalProvisionalProof, + proofChecksum, + validateProofEnvelope, +} from "../src/stockProof"; + +const [command, source, first, second] = process.argv.slice(2); +if (!command || !source) throw new TypeError("usage: stock-proof-cli.ts ..."); +const value = await Bun.file(source).json(); + +if (command === "validate-provisional") { + if (!first) throw new TypeError("expected run ID is required"); + canonicalProvisionalProof(value, first); +} else if (command === "publish") { + if (!first || !second) throw new TypeError("output path and expected identity are required"); + const candidateSha = process.argv[6]; + if (!candidateSha) throw new TypeError("candidate SHA is required"); + const checksum = await proofChecksum(value); + await Bun.write(first, canonicalProofEnvelopeJson(value, checksum)); + await validateProofEnvelope(await Bun.file(first).json(), { runId: second, candidateSha }); +} else if (command === "validate-envelope") { + if (!first || !second) throw new TypeError("expected identity is required"); + await validateProofEnvelope(value, { runId: first, candidateSha: second }); +} else { + throw new TypeError("unknown stock proof command"); +} diff --git a/scripts/stock-t3-canary-drill.sh b/scripts/stock-t3-canary-drill.sh new file mode 100644 index 0000000..9b78367 --- /dev/null +++ b/scripts/stock-t3-canary-drill.sh @@ -0,0 +1,217 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + echo "usage: stock-t3-canary-drill.sh --dry-run|--execute" >&2 +} + +mode=${1:-} +transitions='off -> canary -> promoted -> canary (prior config) -> off' + +if [[ "$mode" == "--dry-run" ]]; then + echo "STOCK_T3_CANARY_DRILL: DRY_RUN" + echo "$transitions" + echo "schema=stock-http-v1 acceleration=off artifact=unchanged" + echo "release_blocked=true reason=operator_commands_not_executed" + exit 0 +fi +if [[ "$mode" != "--execute" ]]; then + usage + exit 2 +fi + +: "${T3_STOCK_ROUTE_OFF_COMMAND:?required executable path}" +: "${T3_STOCK_ROUTE_CANARY_COMMAND:?required executable path}" +: "${T3_STOCK_ROUTE_PROMOTE_COMMAND:?required executable path}" +: "${T3_STOCK_ROUTE_PRIOR_CONFIG_COMMAND:?required executable path}" +: "${T3_STOCK_READINESS_COMMAND:?required executable path}" +: "${T3_STOCK_DESCRIPTOR_COMMAND:?required executable path}" +: "${T3_STOCK_THREAD_READ_COMMAND:?required executable path}" +: "${T3_STOCK_CANCEL_WAITS_COMMAND:?required executable path}" +: "${T3_STOCK_ARTIFACT_PATH:?required artifact path}" +: "${T3_STOCK_CONFIG_PATH:?required redacted config path}" +: "${T3_STOCK_DRILL_RECEIPT_PATH:?required receipt path}" + +commands=( + "$T3_STOCK_ROUTE_OFF_COMMAND" + "$T3_STOCK_ROUTE_CANARY_COMMAND" + "$T3_STOCK_ROUTE_PROMOTE_COMMAND" + "$T3_STOCK_ROUTE_PRIOR_CONFIG_COMMAND" + "$T3_STOCK_READINESS_COMMAND" + "$T3_STOCK_DESCRIPTOR_COMMAND" + "$T3_STOCK_THREAD_READ_COMMAND" + "$T3_STOCK_CANCEL_WAITS_COMMAND" +) +for command_path in "${commands[@]}"; do + if [[ ! -x "$command_path" ]]; then + echo "ERROR: operator command is not executable" >&2 + exit 2 + fi +done +if [[ ! -f "$T3_STOCK_ARTIFACT_PATH" || ! -f "$T3_STOCK_CONFIG_PATH" ]]; then + echo "ERROR: artifact/config must be files" >&2 + exit 2 +fi + +artifact_digest=$(shasum -a 256 "$T3_STOCK_ARTIFACT_PATH" | /usr/bin/awk '{print $1}') +config_before=$(shasum -a 256 "$T3_STOCK_CONFIG_PATH" | /usr/bin/awk '{print $1}') +command_statuses='[]' +descriptor_evidence='[]' +thread_evidence='[]' +drill_complete=false +recovery_armed=true +artifact_evidence='[]' +expected_environment_id='' +expected_thread_id='' +cancellation_evidence='' + +verify_artifact() { + artifact_stage=$1 + current_digest=$(shasum -a 256 "$T3_STOCK_ARTIFACT_PATH" | /usr/bin/awk '{print $1}') + artifact_evidence=$(/usr/bin/jq -c --arg stage "$artifact_stage" --arg digest "$current_digest" '. + [{stage:$stage,digest:$digest}]' <<<"$artifact_evidence") + if [[ "$current_digest" != "$artifact_digest" ]]; then + echo "ERROR: artifact drift detected at $artifact_stage" >&2 + return 2 + fi +} + +record_status() { + command_statuses=$(/usr/bin/jq -c --arg name "$1" --argjson status "$2" '. + [{name:$name,status:$status}]' <<<"$command_statuses") +} + +run_step() { + step_name=$1 + step_command=$2 + step_status=0 + "$step_command" || step_status=$? + record_status "$step_name" "$step_status" + if [[ "$step_status" -ne 0 ]]; then return "$step_status"; fi + if [[ ${T3_STOCK_FAIL_AT:-} == "$step_name" ]]; then + echo "ERROR: injected transition failure: $step_name" >&2 + return 91 + fi + verify_artifact "$step_name" +} + +recover() { + exit_status=$? + trap - EXIT INT TERM + if [[ "$drill_complete" != true && "$recovery_armed" == true ]]; then + prior_status=0 + "$T3_STOCK_ROUTE_PRIOR_CONFIG_COMMAND" || prior_status=$? + record_status recovery-prior-config "$prior_status" + off_status=0 + "$T3_STOCK_ROUTE_OFF_COMMAND" || off_status=$? + record_status recovery-off "$off_status" + cancel_status=0 + "$T3_STOCK_CANCEL_WAITS_COMMAND" >/dev/null || cancel_status=$? + record_status recovery-cancel-waits "$cancel_status" + echo "CANARY_RECOVERY: prior_config=$prior_status routing_off=$off_status cancel_waits=$cancel_status" >&2 + fi + exit "$exit_status" +} + +# Recovery is armed before the first routing mutation. +trap recover EXIT INT TERM + +verify_health() { + stage=$1 + run_step "$stage-readiness" "$T3_STOCK_READINESS_COMMAND" + descriptor=$($T3_STOCK_DESCRIPTOR_COMMAND) + if ! /usr/bin/jq -e 'type == "object" and (.environmentId|type == "string" and length > 0) and (.serverVersion|type == "string" and length > 0)' <<<"$descriptor" >/dev/null; then + echo "ERROR: invalid descriptor evidence" >&2 + return 2 + fi + descriptor_evidence=$(/usr/bin/jq -c --arg stage "$stage" --argjson value "$descriptor" '. + [{stage:$stage,environmentId:$value.environmentId,serverVersion:$value.serverVersion}]' <<<"$descriptor_evidence") + current_environment_id=$(/usr/bin/jq -r '.environmentId' <<<"$descriptor") + if [[ -z "$expected_environment_id" ]]; then + expected_environment_id=$current_environment_id + elif [[ "$current_environment_id" != "$expected_environment_id" ]]; then + echo "ERROR: environment identity changed during drill" >&2 + return 2 + fi + record_status "$stage-descriptor" 0 + if [[ ${T3_STOCK_FAIL_AT:-} == "$stage-descriptor" ]]; then return 91; fi + thread=$($T3_STOCK_THREAD_READ_COMMAND) + if ! /usr/bin/jq -e 'type == "object" and (.threadId|type == "string" and length > 0) and .readable == true' <<<"$thread" >/dev/null; then + echo "ERROR: existing thread is not readable" >&2 + return 2 + fi + thread_evidence=$(/usr/bin/jq -c --arg stage "$stage" --argjson value "$thread" '. + [{stage:$stage,threadId:$value.threadId,readable:true}]' <<<"$thread_evidence") + current_thread_id=$(/usr/bin/jq -r '.threadId' <<<"$thread") + if [[ -z "$expected_thread_id" ]]; then + expected_thread_id=$current_thread_id + elif [[ "$current_thread_id" != "$expected_thread_id" ]]; then + echo "ERROR: canonical thread identity changed during drill" >&2 + return 2 + fi + record_status "$stage-thread" 0 + if [[ ${T3_STOCK_FAIL_AT:-} == "$stage-thread" ]]; then return 91; fi + verify_artifact "$stage-health" +} + +run_step route-off "$T3_STOCK_ROUTE_OFF_COMMAND" +run_step route-canary "$T3_STOCK_ROUTE_CANARY_COMMAND" +verify_health canary +run_step route-promote "$T3_STOCK_ROUTE_PROMOTE_COMMAND" +verify_health promoted +run_step restore-prior-config "$T3_STOCK_ROUTE_PRIOR_CONFIG_COMMAND" +config_after_restore=$(shasum -a 256 "$T3_STOCK_CONFIG_PATH" | /usr/bin/awk '{print $1}') +if [[ "$config_after_restore" != "$config_before" ]]; then + echo "ERROR: prior configuration digest was not restored" >&2 + exit 2 +fi +run_step route-prior-canary "$T3_STOCK_ROUTE_CANARY_COMMAND" +verify_health prior-canary +run_step final-route-off "$T3_STOCK_ROUTE_OFF_COMMAND" +cancellation_status=0 +cancellation_evidence=$($T3_STOCK_CANCEL_WAITS_COMMAND) || cancellation_status=$? +record_status cancel-waits "$cancellation_status" +if [[ "$cancellation_status" -ne 0 ]] || ! /usr/bin/jq -e 'type == "object" and (.cancelled|type == "number" and . >= 0) and .replayed == 0' <<<"$cancellation_evidence" >/dev/null; then + echo "ERROR: invalid cancellation/no-replay evidence" >&2 + exit 2 +fi +if [[ ${T3_STOCK_FAIL_AT:-} == cancel-waits ]]; then + echo "ERROR: injected transition failure: cancel-waits" >&2 + exit 91 +fi +verify_artifact cancel-waits +config_after=$(shasum -a 256 "$T3_STOCK_CONFIG_PATH" | /usr/bin/awk '{print $1}') +[[ "$config_after" == "$config_before" ]] + +receipt_dir=$(dirname "$T3_STOCK_DRILL_RECEIPT_PATH") +mkdir -p -- "$receipt_dir" +body_staging=$(mktemp "$receipt_dir/.stock-t3-drill-body.XXXXXX") +staging=$(mktemp "$receipt_dir/.stock-t3-drill.XXXXXX") +chmod 600 "$body_staging" "$staging" +/usr/bin/jq -cS -n \ + --arg transitions "$transitions" \ + --arg digest "$artifact_digest" \ + --arg before "$config_before" \ + --arg after "$config_after" \ + --argjson statuses "$command_statuses" \ + --argjson descriptors "$descriptor_evidence" \ + --argjson threads "$thread_evidence" \ + --argjson artifacts "$artifact_evidence" \ + --argjson cancellation "$cancellation_evidence" \ + '{success:true,transitions:$transitions,artifactDigest:$digest,configDigestBefore:$before,configDigestAfter:$after,schema:"stock-http-v1",acceleration:"off",cancellation:$cancellation,commandStatuses:$statuses,descriptors:$descriptors,threadReadability:$threads,artifactChecks:$artifacts}' >"$body_staging" +checksum=$(shasum -a 256 "$body_staging" | /usr/bin/awk '{print $1}') +/usr/bin/jq -cS --arg checksum "$checksum" '. + {checksum:$checksum}' "$body_staging" >"$staging" +mv -f -- "$staging" "$T3_STOCK_DRILL_RECEIPT_PATH" +chmod 600 "$T3_STOCK_DRILL_RECEIPT_PATH" +rm -f -- "$body_staging" +if [[ $(/usr/bin/stat -f '%Lp' "$T3_STOCK_DRILL_RECEIPT_PATH") != 600 ]]; then + echo "ERROR: canary receipt mode mismatch" >&2 + exit 2 +fi +reread_body=$(mktemp "$receipt_dir/.stock-t3-drill-reread.XXXXXX") +/usr/bin/jq -cS 'del(.checksum)' "$T3_STOCK_DRILL_RECEIPT_PATH" >"$reread_body" +reread_checksum=$(shasum -a 256 "$reread_body" | /usr/bin/awk '{print $1}') +rm -f -- "$reread_body" +if [[ "$reread_checksum" != "$checksum" ]] || ! /usr/bin/jq -e --arg checksum "$checksum" '.success == true and .schema == "stock-http-v1" and .acceleration == "off" and .checksum == $checksum and .cancellation.replayed == 0' "$T3_STOCK_DRILL_RECEIPT_PATH" >/dev/null; then + echo "ERROR: canary receipt checksum or reread mismatch" >&2 + exit 2 +fi +drill_complete=true +trap - EXIT INT TERM +echo "STOCK_T3_CANARY_DRILL: PASS" diff --git a/scripts/stock-t3-exact-characterization.sh b/scripts/stock-t3-exact-characterization.sh new file mode 100755 index 0000000..d446cb1 --- /dev/null +++ b/scripts/stock-t3-exact-characterization.sh @@ -0,0 +1,201 @@ +#!/usr/bin/env bash +set -euo pipefail + +stock_tree=${1:?exact stock worktree path is required} +expected_sha=d3037064e61a9f059eafbd4f9869679779bd2a7c +generated_relative=apps/server/src/orchestration/Layers/T3LayerStockProjectionCharacterization.generated.test.ts +generated_path="$stock_tree/$generated_relative" + +actual_sha=$(/usr/bin/git -C "$stock_tree" rev-parse HEAD) +if [[ "$actual_sha" != "$expected_sha" ]]; then + echo "ERROR: exact stock SHA mismatch" >&2 + exit 2 +fi + +cleanup() { + rm -f -- "$generated_path" +} +trap cleanup EXIT INT TERM + +/bin/cat >"$generated_path" <<'CHARACTERIZATION' +import { + CommandId, + DEFAULT_PROVIDER_INTERACTION_MODE, + MessageId, + ProjectId, + ProviderInstanceId, + ThreadId, + type OrchestrationEvent, +} from "@t3tools/contracts"; +import { normalizeProjectPathForComparison } from "@t3tools/shared/path"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as ManagedRuntime from "effect/ManagedRuntime"; +import * as Stream from "effect/Stream"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; +import { expect, test } from "vite-plus/test"; + +import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts"; +import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts"; +import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts"; +import * as RepositoryIdentityResolver from "../../project/RepositoryIdentityResolver.ts"; +import { ServerConfig } from "../../config.ts"; +import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; +import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts"; +import { OrchestrationEngineLive } from "./OrchestrationEngine.ts"; +import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts"; +import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts"; +import * as WorkspacePaths from "../../workspace/WorkspacePaths.ts"; + +test("two accepted same-ID/equal-time turns collapse to the later stock projection", async () => { + const config = ServerConfig.layerTest(process.cwd(), { + prefix: "t3layer-stock-characterization-", + }); + const layer = Layer.mergeAll( + OrchestrationEngineLive.pipe( + Layer.provide(OrchestrationProjectionSnapshotQueryLive), + Layer.provide(OrchestrationProjectionPipelineLive), + ), + OrchestrationProjectionSnapshotQueryLive, + WorkspacePaths.layer, + ).pipe( + Layer.provide(OrchestrationEventStoreLive), + Layer.provide(OrchestrationCommandReceiptRepositoryLive), + Layer.provide(RepositoryIdentityResolver.layer), + Layer.provideMerge(SqlitePersistenceMemory), + Layer.provideMerge(config), + Layer.provideMerge(NodeServices.layer), + ); + const runtime = ManagedRuntime.make(layer); + try { + const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService)); + const sql = await runtime.runPromise(Effect.service(SqlClient.SqlClient)); + const snapshotQuery = await runtime.runPromise(Effect.service(ProjectionSnapshotQuery)); + const workspacePaths = await runtime.runPromise(Effect.service(WorkspacePaths.WorkspacePaths)); + const createdAt = "2026-07-31T18:00:00.000Z"; + const threadId = ThreadId.make("thread-t3layer-stock-characterization"); + const messageId = MessageId.make("message-t3layer-stock-characterization"); + + const normalizedWorkspaceRoot = await runtime.runPromise( + workspacePaths.normalizeWorkspaceRoot(` ${process.cwd()}/ `), + ); + expect(normalizedWorkspaceRoot).toBe(process.cwd()); + expect(normalizeProjectPathForComparison(`${process.cwd()}/`)).toBe(process.cwd()); + expect(normalizeProjectPathForComparison("C:/Users/Etan/Project/")) + .toBe("c:\\users\\etan\\project"); + const projectCommand = { + type: "project.create" as const, + commandId: CommandId.make("cmd-project-characterization"), + projectId: ProjectId.make("project-t3layer-stock-characterization"), + title: "characterization", + workspaceRoot: normalizedWorkspaceRoot, + defaultModelSelection: { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5-codex", + }, + createdAt, + }; + const [firstProject, exactReplay] = await Promise.all([ + runtime.runPromise(engine.dispatch(projectCommand)), + runtime.runPromise(engine.dispatch(projectCommand)), + ]); + expect(exactReplay.sequence).toBe(firstProject.sequence); + await runtime.runPromise( + engine.dispatch({ + type: "thread.create", + commandId: CommandId.make("cmd-thread-characterization"), + threadId, + projectId: ProjectId.make("project-t3layer-stock-characterization"), + title: "characterization", + modelSelection: { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5-codex", + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt, + }), + ); + + const first = await runtime.runPromise( + engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.make("cmd-turn-first"), + threadId, + message: { messageId, role: "user", text: "first", attachments: [] }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt, + }), + ); + const second = await runtime.runPromise( + engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.make("cmd-turn-second"), + threadId, + message: { messageId, role: "user", text: "second", attachments: [] }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt, + }), + ); + expect(second.sequence).toBeGreaterThan(first.sequence); + + const events = await runtime.runPromise( + Stream.runCollect(engine.readEvents(0)).pipe( + Effect.map((chunk): OrchestrationEvent[] => Array.from(chunk)), + ), + ); + expect(events.filter((event) => event.commandId === "cmd-turn-first")).toHaveLength(2); + expect(events.filter((event) => event.commandId === "cmd-turn-second")).toHaveLength(2); + + const messageRows = await runtime.runPromise(sql<{ + readonly messageId: string; + readonly text: string; + readonly createdAt: string; + }>` + SELECT + message_id AS "messageId", + text, + created_at AS "createdAt" + FROM projection_thread_messages + WHERE thread_id = ${threadId} + `); + expect(messageRows).toEqual([{ messageId, text: "second", createdAt }]); + + const pendingRows = await runtime.runPromise(sql<{ + readonly messageId: string; + readonly requestedAt: string; + }>` + SELECT + pending_message_id AS "messageId", + requested_at AS "requestedAt" + FROM projection_turns + WHERE thread_id = ${threadId} + AND turn_id IS NULL + AND state = 'pending' + `); + expect(pendingRows).toEqual([{ messageId, requestedAt: createdAt }]); + + const snapshot = await runtime.runPromise(snapshotQuery.getSnapshot()); + expect(snapshot.projects.filter((entry) => entry.id === projectCommand.projectId)).toHaveLength(1); + expect(snapshot.projects.find((entry) => entry.id === projectCommand.projectId)?.workspaceRoot) + .toBe(process.cwd()); + const projected = snapshot.threads.find((entry) => entry.id === threadId); + expect(projected?.messages.filter((entry) => entry.id === messageId)).toHaveLength(1); + expect(projected?.messages.find((entry) => entry.id === messageId)?.text).toBe("second"); + } finally { + await runtime.dispose(); + } +}); +CHARACTERIZATION + +if [[ ${T3_STOCK_EXACT_FAIL_AT:-} == after-generated-fixture ]]; then + echo "ERROR: injected failure: after-generated-fixture" >&2 + exit 91 +fi + +(cd "$stock_tree" && corepack pnpm --filter t3 exec vp test run src/orchestration/Layers/T3LayerStockProjectionCharacterization.generated.test.ts) diff --git a/scripts/stock-t3-live-harness.sh b/scripts/stock-t3-live-harness.sh new file mode 100755 index 0000000..bab8ea3 --- /dev/null +++ b/scripts/stock-t3-live-harness.sh @@ -0,0 +1,331 @@ +#!/usr/bin/env bash +set -euo pipefail + +T3_STOCK_SHA=d3037064e61a9f059eafbd4f9869679779bd2a7c +stock_repo=/Users/etanheyman/Gits/t3code +candidate_repo=/Users/etanheyman/Gits/t3layer-stock-http-runtime +proof_target=${T3_STOCK_PROOF_TARGET:-/Users/etanheyman/Gits/t3layer/docs.local/audits/t3layer-stock-t3-realignment/phase-3-stock-live-proof.json} +proof_root='' +stock_tree='' +server_pid='' +server_birth='' +cleanup_root_valid=false +pid_stopped=false +worktree_removed=false +root_removed=false +proof_ready=false +provisional_json='' +candidate_sha='' +artifact_digest='' +actual_stock_sha='' +run_id='' +test_mode=${T3_STOCK_HARNESS_TEST_MODE:-0} + +run_stage_seam() { + stage=$1 + if [[ "$test_mode" == 1 ]]; then + : "${T3_STOCK_HARNESS_COMMAND_RUNNER:?required in test mode}" + "$T3_STOCK_HARNESS_COMMAND_RUNNER" "$stage" "$proof_root" + fi + fail_at "$stage" +} + +fail_at() { + if [[ ${T3_STOCK_FAIL_AT:-} == "$1" ]]; then + echo "ERROR: injected failure: $1" >&2 + exit 91 + fi +} + +cleanup() { + cleanup_status=$? + set +e + if [[ -n "$server_pid" && -n "$server_birth" && "$server_pid" =~ ^[0-9]+$ ]]; then + current_birth=$(/bin/ps -o lstart= -p "$server_pid" 2>/dev/null | /usr/bin/xargs) + current_cwd=$(/usr/sbin/lsof -a -p "$server_pid" -d cwd -Fn 2>/dev/null | /usr/bin/sed -n 's/^n//p') + if [[ -n "$current_birth" && "$current_birth" == "$server_birth" && "$current_cwd" == "$workspace" ]]; then + /bin/kill -TERM "$server_pid" 2>/dev/null + for _stop_attempt in {1..20}; do + if ! /bin/kill -0 "$server_pid" 2>/dev/null; then break; fi + sleep 0.1 + done + if /bin/kill -0 "$server_pid" 2>/dev/null; then + /bin/kill -KILL "$server_pid" 2>/dev/null + fi + wait "$server_pid" 2>/dev/null + if ! /bin/kill -0 "$server_pid" 2>/dev/null; then pid_stopped=true; fi + fi + else + pid_stopped=true + fi + if [[ -n "$stock_tree" ]]; then + registered_paths=$(/usr/bin/git -C "$stock_repo" worktree list --porcelain 2>/dev/null) + if /usr/bin/grep -F -x -q -- "worktree $stock_tree" <<<"$registered_paths"; then + /usr/bin/git -C "$stock_repo" worktree remove --force "$stock_tree" >/dev/null 2>&1 + fi + registered_after=$(/usr/bin/git -C "$stock_repo" worktree list --porcelain 2>/dev/null) + if ! /usr/bin/grep -F -x -q -- "worktree $stock_tree" <<<"$registered_after"; then + worktree_removed=true + fi + elif [[ -z "$stock_tree" || ! -e "$stock_tree" ]]; then + worktree_removed=true + fi + if [[ "$cleanup_root_valid" == true && -n "$proof_root" && "$proof_root" != / && "$proof_root" != "$HOME" && ! -L "$proof_root" ]]; then + rm -rf -- "$proof_root" + root_removed=true + fi + if [[ "$cleanup_status" -eq 0 && "$proof_ready" == true && "$pid_stopped" == true && "$worktree_removed" == true && "$root_removed" == true ]]; then + proof_dir=$(dirname "$proof_target") + mkdir -p -- "$proof_dir" + final_body_staging=$(mktemp "$proof_dir/.phase-3-stock-live-proof-body.XXXXXX") + final_staging=$(mktemp "$proof_dir/.phase-3-stock-live-proof.XXXXXX") + chmod 600 "$final_body_staging" "$final_staging" + final_body=$(/usr/bin/jq -cS -n \ + --arg runId "$run_id" \ + --arg candidateSha "$candidate_sha" \ + --arg stockSha "$actual_stock_sha" \ + --arg artifactDigest "$artifact_digest" \ + --argjson live "$provisional_json" \ + --argjson negativeShellStatus "$negative_shell_status" \ + --argjson negativeDetailStatus "$negative_detail_status" \ + '{runId:$runId,candidateSha:$candidateSha,stockSha:$stockSha,success:true,cleanBeforeBuild:true,artifactDigest:$artifactDigest,privateResolution:false,provenance:{stockInstall:{command:"corepack pnpm install --frozen-lockfile",status:0},stockBuild:{command:"corepack pnpm --filter t3 build:bundle",status:0},candidateInstall:{command:"bun install --frozen-lockfile",status:0},exactCharacterization:{command:"corepack pnpm --filter t3 exec vp test run src/orchestration/Layers/T3LayerStockProjectionCharacterization.generated.test.ts",status:0},isolatedBasenames:["stock-tree","t3layer-clean","server-home","workspace"]},exactHttpNegative:{status:500,shellStatus:$negativeShellStatus,detailStatus:$negativeDetailStatus,code:"internal_error",reason:"orchestration_dispatch_failed",threadAbsent:true},live:($live|del(.provisional,.success,.runId)),teardown:{pidStopped:true,worktreeRemoved:true,rootRemoved:true}}') + printf '%s\n' "$final_body" >"$final_body_staging" + if [[ ${T3_STOCK_FAIL_AT:-} == before-final-body-validation ]]; then + rm -f -- "$final_body_staging" "$final_staging" + echo "ERROR: injected failure: before-final-body-validation" >&2 + exit 91 + fi + if ! bun "$t3layer_clean/scripts/stock-proof-cli.ts" publish "$final_body_staging" "$final_staging" "$run_id" "$candidate_sha"; then + rm -f -- "$final_body_staging" "$final_staging" + exit 2 + fi + if [[ ${T3_STOCK_FAIL_AT:-} == after-final-body-validation ]]; then + rm -f -- "$final_body_staging" "$final_staging" + echo "ERROR: injected failure: after-final-body-validation" >&2 + exit 91 + fi + [[ $(/usr/bin/stat -f '%Lp' "$final_staging") == 600 ]] + staging_bytes=$(shasum -a 256 "$final_staging" | /usr/bin/awk '{print $1}') + if ! node -e 'const fs=require("node:fs");const [source,target]=process.argv.slice(1);const fd=fs.openSync(source,"r+");fs.fsyncSync(fd);fs.closeSync(fd);fs.renameSync(source,target);const check=fs.openSync(target,"r");fs.fsyncSync(check);fs.closeSync(check)' "$final_staging" "$proof_target"; then + rm -f -- "$final_body_staging" "$final_staging" + exit 2 + fi + rm -f -- "$final_body_staging" + if [[ ${T3_STOCK_FAIL_AT:-} == after-final-rename ]]; then + rm -f -- "$proof_target" + echo "ERROR: injected failure: after-final-rename" >&2 + exit 91 + fi + chmod 600 "$proof_target" + final_bytes=$(shasum -a 256 "$proof_target" | /usr/bin/awk '{print $1}') + if [[ $(/usr/bin/stat -f '%Lp' "$proof_target") != 600 || "$final_bytes" != "$staging_bytes" ]] || ! bun "$t3layer_clean/scripts/stock-proof-cli.ts" validate-envelope "$proof_target" "$run_id" "$candidate_sha"; then + rm -f -- "$proof_target" + echo "ERROR: final proof bytes, mode, checksum, identity, or teardown mismatch" >&2 + exit 2 + fi + fi + echo "cleanup root_removed=$root_removed worktree_removed=$worktree_removed pid_stopped=$pid_stopped" >&2 + exit "$cleanup_status" +} + +: "${T3_STOCK_PROVIDER_SECRET_REF:?T3_STOCK_PROVIDER_SECRET_REF is required}" +proof_root=$(mktemp -d "${TMPDIR:-/tmp}/t3layer-stock-proof.XXXXXX") +trap cleanup EXIT INT TERM + +canonical_root=$(cd "$proof_root" && pwd -P) +canonical_temp_base=$(cd "${TMPDIR:-/tmp}" && pwd -P) +expected_prefix="$canonical_temp_base/t3layer-stock-proof." +case "$canonical_root" in + "$expected_prefix"*) ;; + *) echo "ERROR: invalid proof root" >&2; exit 2 ;; +esac +if [[ -L "$proof_root" || "$canonical_root" == / || "$canonical_root" == "$HOME" ]]; then + echo "ERROR: unsafe proof root" >&2 + exit 2 +fi +cleanup_root_valid=true +run_id=$(/usr/bin/uuidgen | /usr/bin/tr '[:upper:]' '[:lower:]') +stock_tree="$proof_root/stock-tree" +t3layer_clean="$proof_root/t3layer-clean" +server_home="$proof_root/server-home" +workspace="$proof_root/workspace" +server_log="$proof_root/server.log" +provisional="$proof_root/provisional.json" +mkdir -p -- "$t3layer_clean" "$server_home" "$workspace" +: >"$server_log" +: >"$provisional" +chmod 600 "$server_log" "$provisional" + +run_stage_seam after-proof-root +if [[ "$test_mode" == 1 ]]; then + stock_tree='' + actual_stock_sha=$T3_STOCK_SHA +else + /usr/bin/git -C "$stock_repo" worktree add --detach "$stock_tree" "$T3_STOCK_SHA" +fi +run_stage_seam after-worktree-add +if [[ "$test_mode" != 1 ]]; then + actual_stock_sha=$(/usr/bin/git -C "$stock_tree" rev-parse HEAD) + [[ "$actual_stock_sha" == "$T3_STOCK_SHA" ]] + [[ -z $(/usr/bin/git -C "$stock_tree" status --short) ]] + (cd "$stock_tree" && corepack pnpm install --frozen-lockfile) +fi +run_stage_seam after-stock-install +if [[ "$test_mode" != 1 ]]; then + (cd "$stock_tree" && corepack pnpm --filter t3 build:bundle) +fi +run_stage_seam after-stock-build +if [[ "$test_mode" == 1 ]]; then + candidate_sha=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + artifact_digest=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + mkdir -p -- "$t3layer_clean/scripts" "$t3layer_clean/src" + /bin/cp "$candidate_repo/scripts/stock-proof-cli.ts" "$t3layer_clean/scripts/stock-proof-cli.ts" + /bin/cp "$candidate_repo/src/stockProof.ts" "$t3layer_clean/src/stockProof.ts" +else + candidate_sha=$(/usr/bin/git -C "$candidate_repo" rev-parse HEAD) + /usr/bin/git -C "$candidate_repo" archive HEAD | /usr/bin/tar -x -C "$t3layer_clean" + artifact_digest=$(/usr/bin/git -C "$candidate_repo" archive HEAD | shasum -a 256 | /usr/bin/awk '{print $1}') +fi +run_stage_seam after-archive-extract +if [[ "$test_mode" != 1 ]]; then + (cd "$t3layer_clean" && bun install --frozen-lockfile) +fi +run_stage_seam after-candidate-install + +legacy_scope='@t3tools' +legacy_name='runtime''-client' +if [[ "$test_mode" != 1 ]] && (cd "$t3layer_clean" && bun -e "await import('${legacy_scope}/${legacy_name}')" >/dev/null 2>&1); then + echo "ERROR: archived candidate resolves retired package" >&2 + exit 2 +fi + +exact_failure='' +if [[ ${T3_STOCK_FAIL_AT:-} == after-generated-fixture ]]; then + exact_failure=after-generated-fixture +fi +if [[ "$test_mode" != 1 ]]; then + T3_STOCK_EXACT_FAIL_AT="$exact_failure" bash "$t3layer_clean/scripts/stock-t3-exact-characterization.sh" "$stock_tree" +fi +run_stage_seam after-exact-characterization +if [[ "$test_mode" == 1 ]]; then + http_token=test-mode-redacted +else + http_token=$(node "$stock_tree/apps/server/dist/bin.mjs" auth session issue --base-dir "$server_home" --ttl 30m --label t3layer-stock-proof --subject t3layer-stock-proof --token-only) +fi +run_stage_seam after-bearer-issue +if [[ "$test_mode" != 1 ]] && /usr/sbin/lsof -nP -iTCP:3774 -sTCP:LISTEN >/dev/null 2>&1; then + echo "ERROR: port 3774 already bound" >&2 + exit 2 +fi +set +x +if [[ "$test_mode" == 1 ]]; then + provider_key=test-mode-redacted +else + provider_key=$(op read "$T3_STOCK_PROVIDER_SECRET_REF") +fi +run_stage_seam after-secret-read +if [[ "$test_mode" != 1 ]]; then + (cd "$workspace" && ANTHROPIC_API_KEY="$provider_key" exec node "$stock_tree/apps/server/dist/bin.mjs" serve --host 127.0.0.1 --port 3774 --base-dir "$server_home") >"$server_log" 2>&1 & + server_pid=$! + server_birth=$(/bin/ps -o lstart= -p "$server_pid" | /usr/bin/xargs) + unset provider_key + server_cwd=$(/usr/sbin/lsof -a -p "$server_pid" -d cwd -Fn | /usr/bin/sed -n 's/^n//p') + [[ "$server_cwd" == "$workspace" ]] +else + unset provider_key +fi +run_stage_seam after-server-launch + +ready=false +if [[ "$test_mode" == 1 ]]; then + ready=true +else + for _attempt in {1..30}; do + /bin/kill -0 "$server_pid" 2>/dev/null || break + descriptor_body=$(/usr/bin/curl --silent --fail --max-time 0.5 http://127.0.0.1:3774/.well-known/t3/environment 2>/dev/null || true) + if /usr/bin/jq -e 'type == "object" and (.environmentId|type == "string" and length > 0) and (.serverVersion|type == "string" and length > 0)' <<<"$descriptor_body" >/dev/null 2>&1; then + ready=true + break + fi + sleep 0.5 + done +fi +[[ "$ready" == true ]] +run_stage_seam after-readiness + +if [[ "$test_mode" == 1 ]]; then + negative_shell_status=200 + negative_detail_status=404 +else + negative_thread_id=$(/usr/bin/uuidgen | /usr/bin/tr '[:upper:]' '[:lower:]') + negative_command_id=$(/usr/bin/uuidgen | /usr/bin/tr '[:upper:]' '[:lower:]') + negative_message_id=$(/usr/bin/uuidgen | /usr/bin/tr '[:upper:]' '[:lower:]') + negative_body="$proof_root/exact-http-negative.json" + : >"$negative_body" + chmod 600 "$negative_body" + negative_payload=$(/usr/bin/jq -n \ + --arg commandId "$negative_command_id" \ + --arg threadId "$negative_thread_id" \ + --arg messageId "$negative_message_id" \ + --arg createdAt "$(date -u '+%Y-%m-%dT%H:%M:%S.000Z')" \ + --arg workspace "$workspace" \ + '{type:"thread.turn.start",commandId:$commandId,threadId:$threadId,message:{messageId:$messageId,role:"user",text:"negative",attachments:[]},runtimeMode:"full-access",interactionMode:"default",bootstrap:{createThread:{projectId:"00000000-0000-4000-8000-000000000000",title:"negative",modelSelection:{instanceId:"claudeAgent",model:"claude-sonnet-4-5"},runtimeMode:"full-access",interactionMode:"default",branch:null,worktreePath:null,createdAt:$createdAt}},createdAt:$createdAt}') +negative_status=$(/usr/bin/curl --silent --show-error --output "$negative_body" --write-out '%{http_code}' \ + --max-time 5 \ + --request POST \ + --header "Authorization: Bearer $http_token" \ + --header 'Content-Type: application/json' \ + --data "$negative_payload" \ + http://127.0.0.1:3774/api/orchestration/dispatch) + if [[ "$negative_status" != 500 ]] || ! /usr/bin/jq -e '.code == "internal_error" and .reason == "orchestration_dispatch_failed"' "$negative_body" >/dev/null; then + echo "ERROR: exact HTTP bootstrap negative did not match stock" >&2 + exit 2 +fi +negative_shell_body="$proof_root/negative-shell.json" +negative_shell_status=$(/usr/bin/curl --silent --show-error --output "$negative_shell_body" --write-out '%{http_code}' \ + --max-time 5 \ + --header "Authorization: Bearer $http_token" \ + http://127.0.0.1:3774/api/orchestration/shell) +negative_detail_body="$proof_root/negative-detail.json" +negative_detail_status=$(/usr/bin/curl --silent --show-error --output "$negative_detail_body" --write-out '%{http_code}' \ + --max-time 5 \ + --header "Authorization: Bearer $http_token" \ + "http://127.0.0.1:3774/api/orchestration/threads/$negative_thread_id") + if [[ "$negative_shell_status" != 200 || "$negative_detail_status" != 404 ]] || /usr/bin/jq -e --arg id "$negative_thread_id" '.threads[]? | select(.id == $id)' "$negative_shell_body" >/dev/null; then + echo "ERROR: direct HTTP bootstrap unexpectedly created a thread" >&2 + exit 2 + fi +fi +run_stage_seam after-http-negative + +export T3_STOCK_BASE_URL=http://127.0.0.1:3774 +export T3_STOCK_HTTP_TOKEN="$http_token" +export T3_STOCK_WORKSPACE_ROOT="$workspace" +export T3_STOCK_RECEIPT_PATH="$provisional" +export T3_STOCK_RUN_ID="$run_id" +if [[ "$test_mode" == 1 ]]; then + provisional_json=$(/usr/bin/jq -cS -n --arg runId "$run_id" '{provisional:true,success:false,runId:$runId,environmentId:"environment-fixture",serverVersion:"stock",endpointStatusTrace:[{method:"GET",path:"/.well-known/t3/environment",status:200},{method:"GET",path:"/api/orchestration/shell",status:200},{method:"GET",path:"/api/orchestration/shell",status:200},{method:"GET",path:"/api/orchestration/shell",status:200},{method:"GET",path:"/api/orchestration/threads/thread-id",status:200},{method:"GET",path:"/api/orchestration/threads/thread-id",status:200},{method:"POST",path:"/api/orchestration/dispatch",status:200},{method:"POST",path:"/api/orchestration/dispatch",status:200},{method:"POST",path:"/api/orchestration/dispatch",status:200}],ids:{projectId:"project-id",threadId:"thread-id",createCommandId:"create-id",initialCommandId:"initial-id",initialMessageId:"initial-message",followupCommandId:"followup-id",followupMessageId:"followup-message"},sequences:{create:1,initial:2,followup:3},counters:{requests:9,shellPolls:3,detailPolls:2,peakInFlight:1},terminalKinds:["completed","completed"],timestamps:{startedAt:"2026-07-31T00:00:00.000Z",completedAt:"2026-07-31T00:01:00.000Z"}}') + printf '%s\n' "$provisional_json" >"$provisional" +else + (cd "$t3layer_clean" && T3_STOCK_LIVE=1 bun test test/stock-t3-live.test.ts --timeout 120000) +fi +run_stage_seam after-live-test + +if [[ "$test_mode" != 1 ]]; then + bun "$t3layer_clean/scripts/stock-proof-cli.ts" validate-provisional "$provisional" "$run_id" +fi +run_stage_seam after-provisional-validation + +if [[ "$test_mode" != 1 ]]; then + provisional_json=$(/usr/bin/jq -cS \ + --arg runId "$run_id" \ + 'select(.provisional == true and .success == false and .runId == $runId)' \ + "$provisional") +fi +if [[ -z "$provisional_json" ]]; then + echo "ERROR: invalid live provisional evidence" >&2 + exit 2 +fi +proof_ready=true +run_stage_seam before-normal-exit +echo "LIVE PROVISIONAL WRITTEN runId=$run_id candidateSha=$candidate_sha artifactDigest=$artifact_digest target=$proof_target" diff --git a/src/adaptivePoller.ts b/src/adaptivePoller.ts new file mode 100644 index 0000000..5f29d8c --- /dev/null +++ b/src/adaptivePoller.ts @@ -0,0 +1,571 @@ +import type { ShellSnapshot, ThreadDetailSnapshot } from "./stockT3Contracts"; +import { StockT3HttpError } from "./stockT3HttpClient"; + +const POLICY = Object.freeze({ + firstMinuteShellStarts: 32, + laterMinuteShellStarts: 30, + detailStartsPerWaitMinute: 4, + maxActiveWaits: 8, + maxHttpInFlight: 8, + firstMinuteAggregateCeiling: 64, + laterMinuteAggregateCeiling: 62, + intervalMs(attempt: number): number { + return [250, 500, 1_000, 2_000][Math.min(Math.max(0, attempt), 3)] ?? 2_000; + }, + backoffMs(failure: number, retryAfterMs: number): number { + const base = + [500, 1_000, 2_000, 4_000, 8_000][Math.min(Math.max(0, failure), 4)] ?? + 8_000; + return Math.min(8_000, Math.max(base, Math.max(0, retryAfterMs))); + }, +}); + +export class PollerError extends Error { + constructor( + readonly code: + | "cancelled" + | "timeout" + | "capacity" + | "closed" + | "transport_unavailable", + ) { + super(code); + this.name = "PollerError"; + } +} + +type Evaluation = + | { readonly done: true; readonly value: T } + | { readonly done: false; readonly detail?: boolean }; + +export interface PollObservation { + readonly shell: ShellSnapshot; + readonly detail?: ThreadDetailSnapshot; +} + +export interface WaitForOptions { + readonly environmentId: string; + readonly threadId: string; + readonly deadlineMs: number; + readonly signal?: AbortSignal; + readonly evaluate: (observation: PollObservation) => Evaluation; +} + +export interface AdaptivePollerOptions { + readonly getShell: (options: { + readonly deadlineMs: number; + readonly signal?: AbortSignal; + }) => Promise; + readonly getThread: ( + threadId: string, + options: { + readonly deadlineMs: number; + readonly signal?: AbortSignal; + readonly minimumSequence?: number; + }, + ) => Promise; + readonly now?: () => number; + readonly sleep?: (milliseconds: number, signal: AbortSignal) => Promise; + /** Returns a signed jitter delta. It is clamped to +/-10% of the delay. */ + readonly jitter?: (delayMs: number, failureIndex: number) => number; +} + +interface Subscriber { + readonly id: number; + readonly threadId: string; + readonly deadlineMs: number; + readonly signal?: AbortSignal; + readonly evaluate: (observation: PollObservation) => Evaluation; + readonly resolve: (value: T) => void; + readonly reject: (error: Error) => void; + readonly onAbort: () => void; + detailWindowStartedAt: number; + detailStarts: number; +} + +interface ThreadDetailState { + detail: ThreadDetailSnapshot | undefined; + shellSequence: number | null; +} + +interface EnvironmentState { + readonly environmentId: string; + readonly subscribers: Map>; + readonly details: Map; + controller: AbortController; + running: boolean; + cadenceIndex: number; + failureIndex: number; + failureDelayMs: number | null; + shellStartTimes: number[]; + firstStartAt: number | null; + lastScheduledStart: number | null; + lastCompletionAt: number; + lastShellSequence: number | null; +} + +interface SlotWaiter { + readonly deadlineMs: number; + readonly signal?: AbortSignal; + readonly resolve: (release: () => void) => void; + readonly reject: (error: PollerError) => void; + readonly onAbort: () => void; +} + +function defaultSleep(milliseconds: number, signal: AbortSignal): Promise { + return new Promise((resolve, reject) => { + if (signal.aborted) { + reject(new PollerError("cancelled")); + return; + } + const timer = setTimeout(resolve, Math.max(0, milliseconds)); + const onAbort = () => { + clearTimeout(timer); + reject(new PollerError("cancelled")); + }; + signal.addEventListener("abort", onAbort, { once: true }); + }); +} + +function retryMetadata(error: unknown): { retryAfterMs: number } | null { + if (!(error instanceof StockT3HttpError) || error.code !== "transport_unavailable") { + return null; + } + if ( + error.status !== null && + ![429, 502, 503, 504].includes(error.status) && + error.detail.transient !== true + ) { + return null; + } + const retryAfterMs = + typeof error.detail.retryAfterMs === "number" && + Number.isFinite(error.detail.retryAfterMs) + ? Math.max(0, error.detail.retryAfterMs) + : 0; + return { retryAfterMs }; +} + +export function createAdaptivePoller(options: AdaptivePollerOptions) { + const now = options.now ?? Date.now; + const sleep = options.sleep ?? defaultSleep; + const jitter = + options.jitter ?? + ((delayMs: number) => delayMs * (Math.random() * 0.2 - 0.1)); + const environments = new Map(); + let nextSubscriberId = 1; + let closed = false; + let activeWaits = 0; + let peakActiveWaits = 0; + let httpInFlight = 0; + let peakHttpInFlight = 0; + let shellStarts = 0; + let detailStarts = 0; + let throttledCycles = 0; + const slotWaiters: SlotWaiter[] = []; + + function pumpSlots(): void { + while (httpInFlight < POLICY.maxHttpInFlight && slotWaiters.length > 0) { + const waiter = slotWaiters.shift()!; + waiter.signal?.removeEventListener("abort", waiter.onAbort); + if (waiter.signal?.aborted) { + waiter.reject(new PollerError("cancelled")); + continue; + } + if (now() >= waiter.deadlineMs) { + waiter.reject(new PollerError("timeout")); + continue; + } + httpInFlight += 1; + peakHttpInFlight = Math.max(peakHttpInFlight, httpInFlight); + let released = false; + waiter.resolve(() => { + if (released) return; + released = true; + httpInFlight -= 1; + pumpSlots(); + }); + } + } + + function acquireSlot(deadlineMs: number, signal?: AbortSignal): Promise<() => void> { + if (signal?.aborted) return Promise.reject(new PollerError("cancelled")); + if (now() >= deadlineMs) return Promise.reject(new PollerError("timeout")); + return new Promise((resolve, reject) => { + const waiter: SlotWaiter = { + deadlineMs, + signal, + resolve, + reject, + onAbort: () => { + const index = slotWaiters.indexOf(waiter); + if (index >= 0) slotWaiters.splice(index, 1); + reject(new PollerError("cancelled")); + }, + }; + slotWaiters.push(waiter); + signal?.addEventListener("abort", waiter.onAbort, { once: true }); + pumpSlots(); + }); + } + + function finish( + state: EnvironmentState, + subscriber: Subscriber, + error?: Error, + value?: T, + ): void { + if (!state.subscribers.delete(subscriber.id)) return; + activeWaits -= 1; + subscriber.signal?.removeEventListener("abort", subscriber.onAbort); + if (error !== undefined) subscriber.reject(error); + else subscriber.resolve(value as T); + if (state.subscribers.size === 0) state.controller.abort(); + } + + function expireSubscribers(state: EnvironmentState): void { + for (const subscriber of [...state.subscribers.values()]) { + if (subscriber.signal?.aborted) { + finish(state, subscriber, new PollerError("cancelled")); + } else if (now() >= subscriber.deadlineMs) { + finish(state, subscriber, new PollerError("timeout")); + } + } + } + + function rateDelay(state: EnvironmentState, instant: number): number { + state.shellStartTimes = state.shellStartTimes.filter( + (start) => instant - start < 60_000, + ); + const firstMinute = + state.firstStartAt === null || instant - state.firstStartAt < 60_000; + const cap = firstMinute + ? POLICY.firstMinuteShellStarts + : POLICY.laterMinuteShellStarts; + if (state.shellStartTimes.length < cap) return 0; + throttledCycles += 1; + return Math.max(0, 60_000 - (instant - state.shellStartTimes[0]!)); + } + + async function tracked( + operation: () => Promise, + deadlineMs: number, + signal?: AbortSignal, + ): Promise { + const release = await acquireSlot(deadlineMs, signal); + try { + return await operation(); + } finally { + release(); + } + } + + function evaluate( + state: EnvironmentState, + subscriber: Subscriber, + observation: PollObservation, + ): Evaluation | null { + if (!state.subscribers.has(subscriber.id)) return null; + if (subscriber.signal?.aborted) { + finish(state, subscriber, new PollerError("cancelled")); + return null; + } + if (now() > subscriber.deadlineMs) { + finish(state, subscriber, new PollerError("timeout")); + return null; + } + try { + const result = subscriber.evaluate(observation); + if (result.done) finish(state, subscriber, undefined, result.value); + return result; + } catch (error) { + finish( + state, + subscriber, + error instanceof Error ? error : new PollerError("transport_unavailable"), + ); + return null; + } + } + + function admitDetail(subscriber: Subscriber): boolean { + const instant = now(); + if (instant - subscriber.detailWindowStartedAt >= 60_000) { + subscriber.detailWindowStartedAt = instant; + subscriber.detailStarts = 0; + } + if (subscriber.detailStarts >= POLICY.detailStartsPerWaitMinute) return false; + subscriber.detailStarts += 1; + return true; + } + + async function detailCycle( + state: EnvironmentState, + shell: ShellSnapshot, + threadId: string, + subscribers: readonly Subscriber[], + ): Promise { + const cached = state.details.get(threadId); + if (cached?.shellSequence === shell.snapshotSequence) { + if (cached.detail !== undefined) { + for (const subscriber of subscribers) { + evaluate(state, subscriber, { shell, detail: cached.detail }); + } + } + return; + } + const admitted = subscribers.filter(admitDetail); + if (admitted.length === 0) return; + const deadlineMs = Math.min(...admitted.map((entry) => entry.deadlineMs)); + if (now() >= deadlineMs) { + expireSubscribers(state); + return; + } + detailStarts += 1; + try { + const detail = await tracked( + () => + options.getThread(threadId, { + deadlineMs, + signal: state.controller.signal, + }), + deadlineMs, + state.controller.signal, + ); + state.details.set(threadId, { + detail, + shellSequence: shell.snapshotSequence, + }); + if (detail === undefined) return; + for (const subscriber of subscribers) { + evaluate(state, subscriber, { shell, detail }); + } + } catch (error) { + if (state.controller.signal.aborted) return; + if (retryMetadata(error) !== null) { + state.details.delete(threadId); + return; + } + const failure = + error instanceof Error ? error : new PollerError("transport_unavailable"); + for (const subscriber of subscribers) finish(state, subscriber, failure); + } + } + + async function processShell( + state: EnvironmentState, + shell: ShellSnapshot, + ): Promise { + if ( + state.lastShellSequence !== null && + shell.snapshotSequence < state.lastShellSequence + ) { + throw new StockT3HttpError("protocol_mismatch", 200, { + reason: "shell_sequence_regression", + }); + } + state.lastShellSequence = shell.snapshotSequence; + const detailGroups = new Map(); + for (const subscriber of [...state.subscribers.values()]) { + const result = evaluate(state, subscriber, { shell }); + if (result?.done === false && result.detail === true) { + const group = detailGroups.get(subscriber.threadId) ?? []; + group.push(subscriber); + detailGroups.set(subscriber.threadId, group); + } + } + await Promise.all( + [...detailGroups].map(([threadId, subscribers]) => + detailCycle(state, shell, threadId, subscribers), + ), + ); + } + + function nextDelay(state: EnvironmentState): number { + const instant = now(); + if (state.failureDelayMs !== null) return state.failureDelayMs; + const interval = POLICY.intervalMs(state.cadenceIndex); + const scheduled = + state.lastScheduledStart === null + ? instant + interval + : Math.max(state.lastScheduledStart + interval, state.lastCompletionAt); + return Math.max(0, scheduled - instant); + } + + async function run(state: EnvironmentState): Promise { + state.running = true; + try { + while (!closed && state.subscribers.size > 0) { + expireSubscribers(state); + if (state.subscribers.size === 0) break; + const earliestDeadline = Math.min( + ...[...state.subscribers.values()].map((entry) => entry.deadlineMs), + ); + const desiredDelay = Math.max(nextDelay(state), rateDelay(state, now())); + const delay = Math.min(desiredDelay, Math.max(0, earliestDeadline - now())); + try { + await sleep(delay, state.controller.signal); + } catch { + if (state.subscribers.size === 0 || closed) break; + } + expireSubscribers(state); + if (state.subscribers.size === 0 || closed) break; + + const requestDeadline = Math.min( + ...[...state.subscribers.values()].map((entry) => entry.deadlineMs), + ); + if (now() >= requestDeadline) continue; + const startedAt = now(); + if (state.firstStartAt === null) state.firstStartAt = startedAt; + state.lastScheduledStart = startedAt; + state.shellStartTimes.push(startedAt); + shellStarts += 1; + try { + const shell = await tracked( + () => + options.getShell({ + deadlineMs: requestDeadline, + signal: state.controller.signal, + }), + requestDeadline, + state.controller.signal, + ); + state.lastCompletionAt = now(); + await processShell(state, shell); + state.failureIndex = 0; + state.failureDelayMs = null; + state.cadenceIndex = Math.min(3, state.cadenceIndex + 1); + } catch (error) { + state.lastCompletionAt = now(); + if (state.controller.signal.aborted) break; + const retry = retryMetadata(error); + if (retry === null) { + const failure = + error instanceof Error + ? error + : new PollerError("transport_unavailable"); + for (const subscriber of [...state.subscribers.values()]) { + finish(state, subscriber, failure); + } + break; + } + state.failureIndex = Math.min(5, state.failureIndex + 1); + const base = POLICY.backoffMs( + state.failureIndex - 1, + retry.retryAfterMs, + ); + const maximumJitter = base * 0.1; + const delta = Math.max( + -maximumJitter, + Math.min(maximumJitter, jitter(base, state.failureIndex)), + ); + state.failureDelayMs = Math.max(0, base + delta); + } + } + } finally { + state.running = false; + if (state.subscribers.size === 0) { + environments.delete(state.environmentId); + } else if (!closed) { + if (state.controller.signal.aborted) state.controller = new AbortController(); + void run(state); + } + } + } + + function getEnvironment(environmentId: string): EnvironmentState { + const existing = environments.get(environmentId); + if (existing !== undefined) return existing; + const state: EnvironmentState = { + environmentId, + subscribers: new Map(), + details: new Map(), + controller: new AbortController(), + running: false, + cadenceIndex: 0, + failureIndex: 0, + failureDelayMs: null, + shellStartTimes: [], + firstStartAt: null, + lastScheduledStart: null, + lastCompletionAt: now(), + lastShellSequence: null, + }; + environments.set(environmentId, state); + return state; + } + + return { + waitFor(input: WaitForOptions): Promise { + if (closed) return Promise.reject(new PollerError("closed")); + if (activeWaits >= POLICY.maxActiveWaits) { + return Promise.reject(new PollerError("capacity")); + } + const state = getEnvironment(input.environmentId); + if (state.controller.signal.aborted) state.controller = new AbortController(); + return new Promise((resolve, reject) => { + const id = nextSubscriberId++; + const subscriber: Subscriber = { + id, + threadId: input.threadId, + deadlineMs: input.deadlineMs, + signal: input.signal, + evaluate: input.evaluate, + resolve, + reject, + onAbort: () => finish(state, subscriber, new PollerError("cancelled")), + detailWindowStartedAt: now(), + detailStarts: 0, + }; + state.subscribers.set(id, subscriber); + activeWaits += 1; + peakActiveWaits = Math.max(peakActiveWaits, activeWaits); + input.signal?.addEventListener("abort", subscriber.onAbort, { once: true }); + if (input.signal?.aborted) subscriber.onAbort(); + if (!state.running && state.subscribers.size > 0) void run(state); + }); + }, + + dispatchObserved(environmentId: string): void { + const state = environments.get(environmentId); + if (state !== undefined) { + state.cadenceIndex = 0; + state.failureIndex = 0; + state.failureDelayMs = null; + } + }, + + metrics() { + return { + shellStarts, + detailStarts, + throttledCycles, + activeWaits, + activeEnvironments: environments.size, + peakActiveWaits, + httpInFlight, + peakHttpInFlight, + }; + }, + + close(): void { + if (closed) return; + closed = true; + for (const state of environments.values()) { + for (const subscriber of [...state.subscribers.values()]) { + finish(state, subscriber, new PollerError("closed")); + } + state.controller.abort(); + } + for (const waiter of slotWaiters.splice(0)) { + waiter.signal?.removeEventListener("abort", waiter.onAbort); + waiter.reject(new PollerError("closed")); + } + environments.clear(); + }, + }; +} + +createAdaptivePoller.policy = () => POLICY; + +export type AdaptivePoller = ReturnType; diff --git a/src/facade.ts b/src/facade.ts index 10aab14..14230be 100644 --- a/src/facade.ts +++ b/src/facade.ts @@ -1,782 +1,41 @@ -import type { ExperimentConfig } from "./config"; - -type RuntimeMode = ExperimentConfig["runtimeMode"]; -type InteractionMode = ExperimentConfig["interactionMode"]; - -export interface NativeProject { - readonly projectId: string; - readonly workspaceRoot: string; -} - -export interface NativeThreadSnapshot { - readonly threadId: string; - readonly projectId: string; - readonly snapshotSequence: number; - readonly session: { - readonly status: string; - readonly activeTurnId: string | null; - }; - readonly latestUserMessageId?: string; - readonly latestTurn: { - readonly turnId: string; - readonly status: string; - readonly userMessageId?: string; - readonly assistantMessage?: { - readonly content: string; - readonly streaming: boolean; - } | null; - } | null; - readonly pendingApproval?: unknown; - readonly pendingInput?: unknown; -} - -export interface NativeStartThreadInput { - readonly commandId: string; - readonly projectId: string; - readonly threadId: string; - readonly messageId: string; - readonly title: string; - readonly message: string; - readonly modelSelection: ModelSelection; - readonly runtimeMode: RuntimeMode; - readonly interactionMode: InteractionMode; - readonly branch: string | null; - readonly worktreePath: string | null; - readonly createdAt: string; - readonly attachments: readonly []; -} - -export interface NativeCreateProjectInput { - readonly commandId: string; - readonly projectId: string; - readonly title: string; - readonly workspaceRoot: string; - readonly createWorkspaceRootIfMissing: false; - readonly defaultModelSelection: ModelSelection; - readonly createdAt: string; -} - -export interface NativeStartTurnInput { - readonly commandId: string; - readonly threadId: string; - readonly messageId: string; - readonly message: string; - readonly runtimeMode: RuntimeMode; - readonly interactionMode: InteractionMode; - readonly createdAt: string; - readonly attachments: readonly []; -} - -export interface NativeRuntime { - readonly listProjects: () => Promise; - readonly createProject: ( - input: NativeCreateProjectInput, - ) => Promise<{ readonly sequence: number }>; - readonly startThread: ( - input: NativeStartThreadInput, - ) => Promise<{ readonly sequence: number }>; - readonly startTurn: ( - input: NativeStartTurnInput, - ) => Promise<{ readonly sequence: number }>; - readonly getThread: ( - threadId: string, - ) => Promise; - readonly subscribeThread: ( - threadId: string, - input: { readonly afterSequence?: number }, - ) => AsyncIterable; -} - -export interface NativeThreadObservation { - readonly sequence: number; - readonly snapshot: NativeThreadSnapshot; -} - -export interface ModelSelection { - readonly instanceId: string; - readonly model: string; - readonly options?: ReadonlyArray<{ - readonly id: string; - readonly value: string | boolean; - }>; -} - -export interface SpawnInput { - readonly workspaceRoot: string; - readonly title: string; - readonly message: string; - readonly modelSelection: ModelSelection; - readonly runtimeMode: RuntimeMode; - readonly interactionMode: InteractionMode; - readonly branch: string | null; - readonly worktreePath: string | null; -} - -export interface AgentSnapshot { - readonly agentId: string; - readonly projectId: string; - readonly sequence: number; - readonly native: NativeThreadSnapshot; -} - -interface TurnReceiptBase { - readonly agentId: string; - readonly commandId: string; - readonly messageId: string; - readonly sequence: number; -} - -export type TurnReceipt = - | (TurnReceiptBase & { - readonly recovered: false; - readonly sequenceSource: "dispatch"; - }) - | (TurnReceiptBase & { - readonly recovered: true; - readonly sequenceSource: "projection"; - }); - -export type AgentLifecycle = - | "starting" - | "running" - | "ready" - | "awaiting_input" - | "completed" - | "interrupted" - | "stopped" - | "error" - | "unknown"; - -export interface AgentEvent { - readonly agentId: string; - readonly sequence: number; - readonly lifecycle: AgentLifecycle; - readonly assistantContent?: string; - readonly native: NativeThreadSnapshot; -} - -export interface WaitCondition { - readonly kind: "terminal"; - readonly timeoutMs: number; - readonly maxEvidenceBytes: number; -} - -export type FacadeErrorCode = - | "empty_assistant_response" - | "transport_unavailable" - | "timeout" - | "buffer_exhausted" - | "turn_error"; - -export class FacadeError extends Error { - readonly code: FacadeErrorCode; - readonly sequence: number; - readonly structuralSnapshot: Readonly>; - - constructor(code: FacadeErrorCode, snapshot: NativeThreadSnapshot) { - super(code); - this.name = "FacadeError"; - this.code = code; - this.sequence = snapshot.snapshotSequence; - this.structuralSnapshot = { - threadId: snapshot.threadId, - projectId: snapshot.projectId, - snapshotSequence: snapshot.snapshotSequence, - session: { - status: snapshot.session.status, - activeTurnId: snapshot.session.activeTurnId, - }, - latestTurn: - snapshot.latestTurn === null - ? null - : { - turnId: snapshot.latestTurn.turnId, - status: snapshot.latestTurn.status, - userMessageId: snapshot.latestTurn.userMessageId, - assistantMessage: - snapshot.latestTurn.assistantMessage == null - ? null - : { - streaming: snapshot.latestTurn.assistantMessage.streaming, - contentBytes: new TextEncoder().encode( - snapshot.latestTurn.assistantMessage.content, - ).byteLength, - }, - }, - hasPendingApproval: snapshot.pendingApproval != null, - hasPendingInput: snapshot.pendingInput != null, - }; - } -} - -export interface FacadeOptions extends Pick< - ExperimentConfig, - "runtimeMode" | "interactionMode" -> { - readonly id?: () => string; - readonly now?: () => string; - readonly evidence?: (record: Readonly>) => void; -} - -export class AmbiguousDispatchError extends Error { - constructor() { - super("native dispatch outcome is ambiguous"); - this.name = "AmbiguousDispatchError"; - } -} - -function defaultId(): string { - return crypto.randomUUID(); -} - -function unavailableSnapshot( - threadId: string, - projectId = "", -): NativeThreadSnapshot { - return { - threadId, - projectId, - snapshotSequence: 0, - session: { status: "unknown", activeTurnId: null }, - latestTurn: null, - pendingApproval: null, - pendingInput: null, - }; -} - -async function callRuntime( - operation: () => Promise, - snapshot: NativeThreadSnapshot, -): Promise { - try { - return await operation(); - } catch (error) { - if ( - error instanceof FacadeError || - error instanceof AmbiguousDispatchError - ) { - throw error; - } - throw new FacadeError("transport_unavailable", snapshot); - } -} - -function toAgentSnapshot(snapshot: NativeThreadSnapshot): AgentSnapshot { - return { - agentId: snapshot.threadId, - projectId: snapshot.projectId, - sequence: snapshot.snapshotSequence, - native: snapshot, - }; -} - -function matchesMessageIdentity( - snapshot: NativeThreadSnapshot, - messageId: string, -): boolean { - const observedMessageIds = [ - snapshot.latestUserMessageId, - snapshot.latestTurn?.userMessageId, - ].filter((candidate): candidate is string => candidate !== undefined); - return ( - observedMessageIds.length > 0 && - observedMessageIds.every((candidate) => candidate === messageId) - ); -} - -function matchesSpawnIdentity( - snapshot: NativeThreadSnapshot, - threadId: string, - projectId: string, - messageId: string, -): boolean { - return ( - snapshot.threadId === threadId && - snapshot.projectId === projectId && - matchesMessageIdentity(snapshot, messageId) - ); -} - -function toAgentEvent(snapshot: NativeThreadSnapshot): AgentEvent { - const assistant = snapshot.latestTurn?.assistantMessage; - let lifecycle: AgentLifecycle; - if ( - snapshot.session.status === "interrupted" || - snapshot.latestTurn?.status === "interrupted" - ) { - lifecycle = "interrupted"; - } else if ( - snapshot.session.status === "error" || - snapshot.latestTurn?.status === "error" - ) { - lifecycle = "error"; - } else if ( - snapshot.pendingApproval != null || - snapshot.pendingInput != null - ) { - lifecycle = "awaiting_input"; - } else if (snapshot.session.status === "stopped") { - lifecycle = "stopped"; - } else if ( - snapshot.session.status === "ready" && - snapshot.latestTurn?.status === "completed" && - assistant !== null && - assistant !== undefined && - !assistant.streaming && - assistant.content.trim().length > 0 - ) { - lifecycle = "completed"; - } else if ( - snapshot.session.status === "starting" && - snapshot.session.activeTurnId === null - ) { - lifecycle = "starting"; - } else if ( - snapshot.session.status === "running" || - snapshot.session.activeTurnId !== null || - assistant?.streaming === true - ) { - lifecycle = "running"; - } else if (snapshot.session.status === "ready") { - lifecycle = "ready"; - } else { - lifecycle = "unknown"; - } - - return { - agentId: snapshot.threadId, - sequence: snapshot.snapshotSequence, - lifecycle, - ...(assistant !== null && - assistant !== undefined && - !assistant.streaming && - assistant.content.trim().length > 0 - ? { assistantContent: assistant.content } - : {}), - native: snapshot, - }; -} - -function requireSendableThread( - snapshot: NativeThreadSnapshot | undefined, - agentId: string, - unavailableBoundary: NativeThreadSnapshot, -): NativeThreadSnapshot { - if (snapshot === undefined) { - throw new FacadeError("transport_unavailable", unavailableBoundary); - } - if (snapshot.threadId !== agentId) { - throw new FacadeError("transport_unavailable", snapshot); - } - if ( - snapshot.session.activeTurnId !== null || - snapshot.session.status === "starting" || - snapshot.session.status === "running" || - snapshot.latestTurn?.status === "running" || - snapshot.pendingApproval != null || - snapshot.pendingInput != null - ) { - throw new FacadeError("turn_error", snapshot); - } - return snapshot; -} - -function isTerminal(event: AgentEvent): boolean { - return ( - event.lifecycle === "completed" || - event.lifecycle === "interrupted" || - event.lifecycle === "stopped" || - event.lifecycle === "error" || - event.lifecycle === "awaiting_input" - ); -} - -function eventEvidenceBytes(event: AgentEvent): number { - return new TextEncoder().encode(JSON.stringify(event)).byteLength; -} - -function assertNonEmptyTerminal(snapshot: NativeThreadSnapshot): void { - const assistant = snapshot.latestTurn?.assistantMessage; - if ( - snapshot.pendingApproval == null && - snapshot.pendingInput == null && - snapshot.session.status === "ready" && - snapshot.latestTurn?.status === "completed" && - (assistant == null || - (!assistant.streaming && assistant.content.trim().length === 0)) - ) { - throw new FacadeError("empty_assistant_response", snapshot); - } -} - -export function createT3Facade(runtime: NativeRuntime, options: FacadeOptions) { - const id = options.id ?? defaultId; - const now = options.now ?? (() => new Date().toISOString()); - const evidence = options.evidence ?? (() => undefined); - - return { - async spawn(input: SpawnInput): Promise { - const projectBoundary = unavailableSnapshot("unallocated"); - const projects = await callRuntime( - () => runtime.listProjects(), - projectBoundary, - ); - let project = projects.find( - (candidate) => candidate.workspaceRoot === input.workspaceRoot, - ); - const createdAt = now(); - if (project === undefined) { - const projectId = id(); - const projectCommand: NativeCreateProjectInput = { - commandId: id(), - projectId, - title: input.title, - workspaceRoot: input.workspaceRoot, - createWorkspaceRootIfMissing: false, - defaultModelSelection: input.modelSelection, - createdAt, - }; - const requestedProject = { - projectId, - workspaceRoot: input.workspaceRoot, - }; - try { - await callRuntime( - () => runtime.createProject(projectCommand), - projectBoundary, - ); - project = requestedProject; - } catch (error) { - if (!(error instanceof AmbiguousDispatchError)) throw error; - project = ( - await callRuntime(() => runtime.listProjects(), projectBoundary) - ).find( - (candidate) => candidate.workspaceRoot === input.workspaceRoot, - ); - if (project === undefined) { - try { - await callRuntime( - () => runtime.createProject(projectCommand), - projectBoundary, - ); - project = requestedProject; - } catch (retryError) { - if (!(retryError instanceof AmbiguousDispatchError)) { - throw retryError; - } - project = ( - await callRuntime(() => runtime.listProjects(), projectBoundary) - ).find( - (candidate) => candidate.workspaceRoot === input.workspaceRoot, - ); - if (project === undefined) throw retryError; - } - } - } - } - - const threadId = id(); - const commandId = id(); - const messageId = id(); - const command: NativeStartThreadInput = { - commandId, - projectId: project.projectId, - threadId, - messageId, - title: input.title, - message: input.message, - modelSelection: input.modelSelection, - runtimeMode: input.runtimeMode, - interactionMode: input.interactionMode, - branch: input.branch, - worktreePath: input.worktreePath, - createdAt, - attachments: [], - }; - const threadBoundary = unavailableSnapshot(threadId, project.projectId); - - evidence({ - operation: "spawn", - commandId, - projectId: project.projectId, - threadId, - messageId, - workspaceRoot: input.workspaceRoot, - modelSelection: { - instanceId: input.modelSelection.instanceId, - model: input.modelSelection.model, - optionCount: input.modelSelection.options?.length ?? 0, - }, - runtimeMode: input.runtimeMode, - interactionMode: input.interactionMode, - branch: input.branch, - worktreePath: input.worktreePath, - createdAt, - attachments: 0, - messageBytes: new TextEncoder().encode(input.message).byteLength, - }); - try { - await callRuntime(() => runtime.startThread(command), threadBoundary); - } catch (error) { - if (!(error instanceof AmbiguousDispatchError)) throw error; - const recovered = await callRuntime( - () => runtime.getThread(threadId), - threadBoundary, - ); - if (recovered !== undefined) { - if ( - !matchesSpawnIdentity( - recovered, - threadId, - project.projectId, - messageId, - ) - ) { - throw error; - } - return toAgentSnapshot(recovered); - } - try { - await callRuntime(() => runtime.startThread(command), threadBoundary); - } catch (retryError) { - if (!(retryError instanceof AmbiguousDispatchError)) { - throw retryError; - } - const finalSnapshot = await callRuntime( - () => runtime.getThread(threadId), - threadBoundary, - ); - if ( - finalSnapshot === undefined || - !matchesSpawnIdentity( - finalSnapshot, - threadId, - project.projectId, - messageId, - ) - ) { - throw retryError; - } - return toAgentSnapshot(finalSnapshot); - } - } - - const snapshot = await callRuntime( - () => runtime.getThread(threadId), - threadBoundary, - ); - if (snapshot === undefined) { - throw new FacadeError("transport_unavailable", threadBoundary); - } - if ( - !matchesSpawnIdentity(snapshot, threadId, project.projectId, messageId) - ) { - throw new FacadeError("transport_unavailable", snapshot); - } - return toAgentSnapshot(snapshot); - }, - - async send(agentId: string, message: string): Promise { - const sendBoundary = unavailableSnapshot(agentId); - const current = requireSendableThread( - await callRuntime(() => runtime.getThread(agentId), sendBoundary), - agentId, - sendBoundary, - ); - - const commandId = id(); - const messageId = id(); - const createdAt = now(); - const command: NativeStartTurnInput = { - commandId, - threadId: agentId, - messageId, - message, - runtimeMode: options.runtimeMode, - interactionMode: options.interactionMode, - createdAt, - attachments: [], - }; - evidence({ - operation: "send", - commandId, - threadId: agentId, - messageId, - runtimeMode: options.runtimeMode, - interactionMode: options.interactionMode, - createdAt, - attachments: 0, - messageBytes: new TextEncoder().encode(message).byteLength, - }); - let receipt: { readonly sequence: number }; - try { - receipt = await callRuntime(() => runtime.startTurn(command), current); - } catch (error) { - if (!(error instanceof AmbiguousDispatchError)) throw error; - const snapshot = await callRuntime( - () => runtime.getThread(agentId), - current, - ); - if ( - snapshot?.threadId === agentId && - matchesMessageIdentity(snapshot, messageId) - ) { - return { - agentId, - commandId, - messageId, - sequence: snapshot.snapshotSequence, - sequenceSource: "projection", - recovered: true, - }; - } - requireSendableThread(snapshot, agentId, current); - try { - receipt = await callRuntime( - () => runtime.startTurn(command), - current, - ); - } catch (retryError) { - if (!(retryError instanceof AmbiguousDispatchError)) { - throw retryError; - } - const finalSnapshot = await callRuntime( - () => runtime.getThread(agentId), - current, - ); - if ( - finalSnapshot?.threadId !== agentId || - !matchesMessageIdentity(finalSnapshot, messageId) - ) { - throw retryError; - } - return { - agentId, - commandId, - messageId, - sequence: finalSnapshot.snapshotSequence, - sequenceSource: "projection", - recovered: true, - }; - } - } - return { - agentId, - commandId, - messageId, - sequence: receipt.sequence, - sequenceSource: "dispatch", - recovered: false, - }; - }, - - async *wait( - agentId: string, - condition: WaitCondition, - ): AsyncIterable { - const deadline = Date.now() + condition.timeoutMs; - const initialBoundary = unavailableSnapshot(agentId); - let initialTimer: ReturnType | undefined; - const initialTimeout = new Promise((_, reject) => { - initialTimer = setTimeout( - () => reject(new FacadeError("timeout", initialBoundary)), - Math.max(0, deadline - Date.now()), - ); - }); - let initial: NativeThreadSnapshot | undefined; - try { - initial = await Promise.race([ - callRuntime(() => runtime.getThread(agentId), initialBoundary), - initialTimeout, - ]); - } finally { - if (initialTimer !== undefined) clearTimeout(initialTimer); - } - if (initial === undefined) { - throw new FacadeError("transport_unavailable", initialBoundary); - } - if (initial.threadId !== agentId) { - throw new FacadeError("transport_unavailable", initial); - } - - assertNonEmptyTerminal(initial); - const initialEvent = toAgentEvent(initial); - let evidenceBytes = eventEvidenceBytes(initialEvent); - if (evidenceBytes > condition.maxEvidenceBytes) { - throw new FacadeError("buffer_exhausted", initial); - } - yield initialEvent; - if (isTerminal(initialEvent)) return; - - let lastSnapshot = initial; - let lastObservationSequence = initial.snapshotSequence; - let iterator: AsyncIterator; - try { - iterator = runtime - .subscribeThread(agentId, { - afterSequence: initial.snapshotSequence, - }) - [Symbol.asyncIterator](); - } catch { - throw new FacadeError("transport_unavailable", lastSnapshot); - } - try { - while (true) { - const remainingMs = deadline - Date.now(); - if (remainingMs <= 0) { - throw new FacadeError("timeout", lastSnapshot); - } - let timer: ReturnType | undefined; - const timeout = new Promise((_, reject) => { - timer = setTimeout( - () => reject(new FacadeError("timeout", lastSnapshot)), - remainingMs, - ); - }); - let next: IteratorResult; - try { - try { - next = await Promise.race([iterator.next(), timeout]); - } catch (error) { - if (error instanceof FacadeError) throw error; - throw new FacadeError("transport_unavailable", lastSnapshot); - } - } finally { - if (timer !== undefined) clearTimeout(timer); - } - if (next.done) { - throw new FacadeError("transport_unavailable", lastSnapshot); - } - if (next.value.snapshot.threadId !== agentId) { - throw new FacadeError("transport_unavailable", next.value.snapshot); - } - if ( - next.value.sequence !== next.value.snapshot.snapshotSequence || - next.value.sequence <= lastObservationSequence - ) { - throw new FacadeError("transport_unavailable", lastSnapshot); - } - lastObservationSequence = next.value.sequence; - lastSnapshot = next.value.snapshot; - assertNonEmptyTerminal(lastSnapshot); - const event = toAgentEvent(lastSnapshot); - evidenceBytes += eventEvidenceBytes(event); - if (evidenceBytes > condition.maxEvidenceBytes) { - throw new FacadeError("buffer_exhausted", lastSnapshot); - } - yield event; - if (isTerminal(event)) return; - } - } finally { - try { - const teardown = iterator.return?.(); - if (teardown !== undefined) { - void teardown.catch(() => undefined); - } - } catch { - // Teardown must not replace the caller-visible wait outcome. - } - } - }, - }; +import type { + AgentRef, + CreateReconciliationPending, + RuntimeOperationOptions, + StockSpawnInput, + T3NativeRuntime, + TurnReceipt, +} from "./nativeRuntime"; + +export { + allocateProjectCreateIdentity, + canonicalizeWorkspaceRoot, + parseProjectCreateIdentity, +} from "./nativeRuntime"; +export type { + ProjectCreateIdentity, + ProjectCreateIdentityAllocationOptions, + ProjectCreateIdentityExpectation, + ProjectCreateIdentityInput, + WorkspaceCanonicalizationOptions, +} from "./nativeRuntime"; + +/** Public receipt-targeted facade over the stock T3 HTTP runtime. */ +export function createStockT3Facade(runtime: T3NativeRuntime) { + return Object.freeze({ + spawn: (input: StockSpawnInput, options?: RuntimeOperationOptions) => + runtime.spawn(input, options), + resumeCreateReconciliation: ( + pending: CreateReconciliationPending, + input: StockSpawnInput, + options?: RuntimeOperationOptions, + ) => runtime.resumeCreateReconciliation(pending, input, options), + send: (ref: AgentRef, message: string, options?: RuntimeOperationOptions) => + runtime.send(ref, message, options), + wait: (receipt: TurnReceipt, options?: RuntimeOperationOptions) => + runtime.wait(receipt, options), + observe: (ref: AgentRef, options?: RuntimeOperationOptions) => + runtime.observe(ref, options), + releaseReceipt: (receipt: TurnReceipt) => runtime.releaseReceipt(receipt), + }); } diff --git a/src/nativeRuntime.ts b/src/nativeRuntime.ts index bcc9e55..e2337cc 100644 --- a/src/nativeRuntime.ts +++ b/src/nativeRuntime.ts @@ -1,1163 +1,2492 @@ +import type { + EnvironmentDescriptor, + ShellSnapshot, + StockMessage, + StockThreadDetail, + StockThreadShell, + ThreadDetailSnapshot, +} from "./stockT3Contracts"; +import { homedir } from "node:os"; +import { posix, win32 } from "node:path"; import { - ClientOrchestrationCommand, - EnvironmentId, - ORCHESTRATION_WS_METHODS, - OrchestrationSubscribeThreadInput, - applyShellStreamEvent, - applyThreadDetailEvent, - makeRpcSessionFactory, - type OrchestrationShellSnapshot, - type OrchestrationShellStreamItem, - type OrchestrationSubscribeShellInput, - type OrchestrationThread, - type OrchestrationThreadShell, - type OrchestrationThreadStreamItem, - type RuntimeClientRpcSessionFactory, -} from "@t3tools/runtime-client"; -import * as Cause from "effect/Cause"; -import * as Effect from "effect/Effect"; -import * as Exit from "effect/Exit"; -import * as Option from "effect/Option"; -import * as Schema from "effect/Schema"; -import * as Scope from "effect/Scope"; -import * as Stream from "effect/Stream"; -import * as Socket from "effect/unstable/socket/Socket"; -import { - AmbiguousDispatchError, - type NativeCreateProjectInput, - type NativeProject, - type NativeRuntime, - type NativeStartThreadInput, - type NativeStartTurnInput, - type NativeThreadObservation, - type NativeThreadSnapshot, -} from "./facade"; - -export type NativeRuntimeAdapterErrorCode = - | "authorization" - | "version_mismatch" - | "command_rejected" - | "transport_unavailable" - | "projection_invalid"; + StockT3HttpError, + createStockT3HttpClient, + type FetchLike, + type RequestBoundaryOptions, +} from "./stockT3HttpClient"; +import { createAdaptivePoller, PollerError } from "./adaptivePoller"; -export class NativeRuntimeAdapterError extends Error { - readonly code: NativeRuntimeAdapterErrorCode; +export interface AgentRef { + readonly environmentId: string; + readonly threadId: string; +} - constructor(code: NativeRuntimeAdapterErrorCode) { - super(code); - this.name = "NativeRuntimeAdapterError"; - this.code = code; - } +export interface RuntimeModelSelection { + readonly instanceId: string; + readonly model: string; + readonly options?: readonly unknown[]; } -export interface RuntimeClientSession { - readonly dispatchCommand: ( - command: ClientOrchestrationCommand, - ) => Promise<{ readonly sequence: number }>; - readonly subscribeShell: ( - input: OrchestrationSubscribeShellInput, - ) => AsyncIterable; - readonly subscribeThread: ( - input: OrchestrationSubscribeThreadInput, - ) => AsyncIterable; - readonly close: () => Promise; +export interface StockSpawnInput { + readonly workspaceRoot: string; + readonly projectId?: string; + readonly projectCreateIdentity?: ProjectCreateIdentity; + readonly title: string; + readonly message: string; + readonly modelSelection: RuntimeModelSelection; + readonly runtimeMode: "approval-required" | "auto-accept-edits" | "auto" | "full-access"; + readonly interactionMode: "default" | "plan"; + readonly branch: string | null; + readonly worktreePath: string | null; } -export interface RuntimeClientSessionFactory { - readonly connect: (connection: { - readonly environmentId: string; - readonly label: string; - readonly socketUrl: string; - readonly timeoutMs?: number; - }) => Promise; +export interface ProjectCreateIdentity { + readonly projectId: string; + readonly commandId: string; + readonly createdAt: string; + readonly workspaceRoot: string; + readonly title: string; + readonly defaultModelSelection: RuntimeModelSelection; + readonly environmentId?: string; } -export interface T3NativeRuntimeOptions { - readonly environmentId: string; - readonly label: string; - /** - * Acquires a one-use authorized WebSocket URL. The adapter passes the value - * directly into a new scoped session and never retains or reports it. - */ - readonly acquireSocketUrl: () => Promise; - readonly sessionFactory?: RuntimeClientSessionFactory; - readonly connectionTimeoutMs?: number; - readonly alignmentTimeoutMs?: number; +export interface WorkspaceCanonicalizationOptions { + readonly platform?: "darwin" | "linux" | "windows" | "win32"; + readonly cwd?: string; + readonly homeDirectory?: string; +} + +export interface ProjectCreateIdentityInput { + readonly workspaceRoot: string; + readonly title: string; + readonly defaultModelSelection: RuntimeModelSelection; + readonly environmentId?: string; +} + +export interface ProjectCreateIdentityAllocationOptions + extends WorkspaceCanonicalizationOptions { + readonly id?: () => string; + readonly now?: () => string; +} + +export interface ProjectCreateIdentityExpectation + extends WorkspaceCanonicalizationOptions { + readonly workspaceRoot?: string; + readonly projectId?: string; +} + +export type RetryState = + | "not_applicable" + | "eligible_not_sent" + | "identical_retry_sent_no_response" + | "identical_retry_accepted" + | "identical_retry_received_error"; + +export interface CreateAttemptReceipt { + readonly commandId: string; + readonly threadId: string; + readonly projectId: string; + readonly acceptedSequence: number | null; + readonly dispatchState: "accepted" | "outcome_unknown"; + readonly retryState: RetryState; + readonly retryError: SanitizedRetryError | null; } -interface VersionedDetail { - readonly sequence: number; - readonly thread: OrchestrationThread; +export interface SanitizedRetryError { + readonly status: 400 | 401 | 403 | 500; + readonly class: + | "command_rejected" + | "authentication_failed" + | "permission_denied" + | "server_internal"; + readonly code: "invalid_request" | "auth_invalid" | "insufficient_scope" | "internal_error"; + readonly reason: "invalid_command" | "orchestration_dispatch_failed" | null; } -interface VersionedShell { - readonly sequence: number; - readonly snapshot: OrchestrationShellSnapshot; - readonly origin: "seed" | "snapshot" | "event"; +export interface CreateReconciliationState { + readonly reason: + | "projection_pending" + | "retry_error_after_ambiguous_original" + | "cancelled" + | "deadline_exhausted" + | "transport_exhausted"; + readonly projectionState: + | "unobserved" + | "shell_only" + | "detail_only" + | "below_required_sequence" + | "identity_unverified"; + readonly highestShellSequence: number | null; + readonly highestDetailSequence: number | null; + readonly deadlineMs: number; + readonly evidence: readonly Readonly>[]; } -interface ReconciledThreadState { - readonly observation: NativeThreadObservation; - readonly detail: OrchestrationThread; - readonly shellSnapshot: OrchestrationShellSnapshot; +export interface ThreadCreateReceipt { + readonly commandId: string; + readonly threadId: string; + readonly acceptedSequence: number | null; + readonly observedSequence: number; + readonly recovered: boolean; } -interface ReconcileThreadOptions { - readonly emitAfterSequence?: number; - readonly resumeFromSequence?: number; - readonly seed?: ReconciledThreadState; - readonly alignmentTimeoutMs?: number; +export interface TurnReceipt { + readonly agentRef: AgentRef; + readonly leaseId: string; + readonly commandId: string; + readonly messageId: string; + readonly acceptedSequence: number | null; + readonly observedSequence: number; + readonly leaseExpiresAt: number; + readonly leaseState: "active" | "released"; + readonly reconciliationEvidence?: readonly Readonly>[]; } -type TaggedNext = +export type SpawnResult = | { - readonly source: "detail"; - readonly result: IteratorResult; + readonly kind: "spawned"; + readonly agentRef: AgentRef; + readonly createReceipt: ThreadCreateReceipt; + readonly turnReceipt: TurnReceipt; } | { - readonly source: "shell"; - readonly result: IteratorResult; + readonly kind: "partial"; + readonly agentRef: AgentRef; + readonly createReceipt: ThreadCreateReceipt; + readonly initialTurn: { + readonly commandId: string; + readonly messageId: string; + readonly state: + | "not_attempted" + | "initial_turn_rejected" + | "initial_turn_accepted_projection_pending" + | "initial_turn_outcome_unknown" + | "contended_before_start" + | "superseded" + | "concurrent_writer" + | "causality_unverifiable" + | "cancelled" + | "deadline_exhausted"; + readonly turnReceipt: TurnReceipt | null; + readonly leaseExpiresAt: number | null; + readonly safeAction: "new_send" | "wait" | "observe"; + readonly evidence: readonly Readonly>[]; + }; } + | CreateReconciliationPending | { - readonly source: "detail" | "shell"; - readonly error: unknown; + readonly kind: "create_protocol_failure"; + readonly provisionalRef: AgentRef; + readonly createAttempt: CreateAttemptReceipt; + readonly conflict: Readonly>; }; -const DEFAULT_CONNECTION_TIMEOUT_MS = 15_000; -const DEFAULT_ALIGNMENT_TIMEOUT_MS = 5_000; -const MAX_TIMER_TIMEOUT_MS = 2_147_483_647; +export interface CreateReconciliationPending { + readonly kind: "create_reconciliation_pending"; + readonly provisionalRef: AgentRef; + readonly createAttempt: CreateAttemptReceipt; + readonly reconciliation: CreateReconciliationState; + readonly initialTurnContinuation: { + readonly commandId: string; + readonly messageId: string; + readonly inputDigest: string; + }; + readonly safeAction: "resume_create_reconciliation"; +} + +export type StockRuntimeErrorCode = + | "command_rejected" + | "authentication_failed" + | "permission_denied" + | "server_internal" + | "transport_unavailable" + | "protocol_mismatch" + | "environment_changed" + | "identity_conflict" + | "send_in_progress" + | "receipt_expired" + | "correlation_capacity" + | "cancelled" + | "timeout" + | "superseded" + | "concurrent_writer" + | "causality_unverifiable" + | "pending_approval" + | "pending_input" + | "turn_interrupted" + | "turn_error"; -function positiveBound(value: number | undefined, fallback: number): number { - const resolved = value ?? fallback; - if ( - !Number.isSafeInteger(resolved) || - resolved <= 0 || - resolved > MAX_TIMER_TIMEOUT_MS +export class StockRuntimeError extends Error { + constructor( + readonly code: StockRuntimeErrorCode, + readonly evidence: Readonly> = {}, ) { - throw new NativeRuntimeAdapterError("transport_unavailable"); + super(code); + this.name = "StockRuntimeError"; } - return resolved; } -function remainingMillis(deadline: number): number { - const remaining = deadline - Date.now(); - if (remaining <= 0) { - throw new NativeRuntimeAdapterError("transport_unavailable"); - } - return remaining; +function identityConflict(reason: string, evidence: Readonly> = {}): never { + throw new StockRuntimeError("identity_conflict", { + reason: "invalid_project_create_identity", + detail: reason, + ...evidence, + }); } -// Initiates teardown synchronously, then detaches it. Callers may rely on the -// attempt having started, but not on cleanup having completed successfully. -function startBestEffortCleanup(cleanup: (() => unknown) | undefined): void { - if (cleanup === undefined) return; - try { - void Promise.resolve(cleanup()).catch(() => undefined); - } catch { - // Cleanup is initiated but deliberately neither awaited nor reported as - // complete; teardown failures must not mask or delay the request result. - } +function nonBlank(value: unknown, field: string): string { + if (typeof value !== "string" || value.trim().length === 0) identityConflict(`${field}_required`); + return value; } -function withTransportTimeout( - promise: Promise, - timeoutMs: number, - onLateResolve?: (value: T) => void | Promise, - onTimeout?: () => void | Promise, -): Promise { - return new Promise((resolve, reject) => { - let settled = false; - const timer = setTimeout(() => { - settled = true; - startBestEffortCleanup(onTimeout); - reject(new NativeRuntimeAdapterError("transport_unavailable")); - }, timeoutMs); - promise.then( - (value) => { - if (settled) { - if (onLateResolve !== undefined) { - startBestEffortCleanup(() => onLateResolve(value)); - } - return; - } - settled = true; - clearTimeout(timer); - resolve(value); - }, - (error) => { - if (settled) return; - settled = true; - clearTimeout(timer); - reject(error); - }, - ); - }); +function identifier(value: unknown, field: string): string { + const parsed = nonBlank(value, field); + if (parsed !== parsed.trim()) identityConflict(`${field}_has_surrounding_whitespace`); + return parsed; } -function taggedNext( - source: "detail" | "shell", - next: Promise>, -): Promise { - return next.then( - (result) => ({ source, result }) as TaggedNext, - (error) => ({ source, error }), - ); +function jsonValue(value: unknown, field: string): unknown { + if (value === null || typeof value === "string" || typeof value === "boolean") return value; + if (typeof value === "number") { + if (!Number.isFinite(value)) identityConflict(`${field}_must_be_json`); + return value; + } + if (Array.isArray(value)) return value.map((entry, index) => jsonValue(entry, `${field}[${index}]`)); + if (typeof value === "object") { + const prototype = Object.getPrototypeOf(value); + if (prototype !== Object.prototype && prototype !== null) identityConflict(`${field}_must_be_json`); + const result: Record = {}; + for (const [key, entry] of Object.entries(value as Record)) { + if (entry === undefined) identityConflict(`${field}.${key}_must_be_json`); + result[key] = jsonValue(entry, `${field}.${key}`); + } + return result; + } + identityConflict(`${field}_must_be_json`); } -function adapterError( - error: unknown, - fallback: NativeRuntimeAdapterErrorCode, -): NativeRuntimeAdapterError { - if (error instanceof NativeRuntimeAdapterError) return error; - const tagged = error as { - readonly _tag?: unknown; - readonly reason?: unknown; - }; - if (tagged?._tag === "EnvironmentAuthorizationError") { - return new NativeRuntimeAdapterError("authorization"); +function freezeJson(value: unknown): unknown { + if (Array.isArray(value)) { + for (const entry of value) freezeJson(entry); + return Object.freeze(value); } - if (tagged?._tag === "ConnectionBlockedError") { - return new NativeRuntimeAdapterError( - tagged.reason === "version_mismatch" - ? "version_mismatch" - : "authorization", - ); - } - if (tagged?._tag === "OrchestrationDispatchCommandError") { - return new NativeRuntimeAdapterError("command_rejected"); + if (typeof value === "object" && value !== null) { + for (const entry of Object.values(value as Record)) freezeJson(entry); + return Object.freeze(value); } - return new NativeRuntimeAdapterError(fallback); + return value; } -function dispatchError(error: unknown): Error { - const mapped = adapterError(error, "transport_unavailable"); - if ( - mapped.code === "authorization" || - mapped.code === "version_mismatch" || - mapped.code === "command_rejected" - ) { - return mapped; - } - return new AmbiguousDispatchError(); +/** Stock-compatible workspace ingress canonicalization. */ +export function canonicalizeWorkspaceRoot( + value: unknown, + options: WorkspaceCanonicalizationOptions = {}, +): string { + const input = nonBlank(value, "workspace_root").trim(); + const path = options.platform === "windows" || options.platform === "win32" ? win32 : posix; + const home = options.homeDirectory ?? homedir(); + const expanded = input === "~" + ? home + : input.startsWith("~/") || input.startsWith("~\\") + ? path.join(home, input.slice(2)) + : input; + return path.resolve(options.cwd ?? process.cwd(), expanded); } -async function runEffect( - effect: Effect.Effect, - signal?: AbortSignal, -): Promise { - const exit = await Effect.runPromiseExit( - effect, - signal === undefined ? undefined : { signal }, - ); - if (Exit.isSuccess(exit)) return exit.value; - const error = Cause.findErrorOption(exit.cause); - if (Option.isSome(error)) throw error.value; - throw new NativeRuntimeAdapterError("transport_unavailable"); +function workspaceComparisonKey( + value: unknown, + options: WorkspaceCanonicalizationOptions = {}, +): string { + const canonicalRoot = canonicalizeWorkspaceRoot(value, options); + return options.platform === "windows" || options.platform === "win32" + ? canonicalRoot.replaceAll("/", "\\").toLowerCase() + : canonicalRoot; } -function loadRuntimeClientFactory(): Promise { - return runEffect( - makeRpcSessionFactory.pipe( - Effect.provideService( - Socket.WebSocketConstructor, - (url, protocols) => new globalThis.WebSocket(url, protocols), - ), - ), - ); +/** Allocate the one caller-held, exact-replay identity used by project.create. */ +export function allocateProjectCreateIdentity( + input: ProjectCreateIdentityInput, + options: ProjectCreateIdentityAllocationOptions = {}, +): ProjectCreateIdentity { + const id = options.id ?? (() => crypto.randomUUID()); + const now = options.now ?? (() => new Date().toISOString()); + return parseProjectCreateIdentity({ + projectId: id(), + commandId: id(), + createdAt: now(), + workspaceRoot: canonicalizeWorkspaceRoot(input.workspaceRoot, options), + title: input.title, + defaultModelSelection: input.defaultModelSelection, + ...(input.environmentId === undefined ? {} : { environmentId: input.environmentId }), + }, options); } -export function createDefaultSessionFactory( - loadFactory: () => Promise = loadRuntimeClientFactory, - defaultTimeoutMs = DEFAULT_CONNECTION_TIMEOUT_MS, -): RuntimeClientSessionFactory { - let factoryPromise: Promise | undefined; - const getFactory = () => { - if (factoryPromise === undefined) { - const attempt = loadFactory().catch((error) => { - if (factoryPromise === attempt) factoryPromise = undefined; - throw error; - }); - factoryPromise = attempt; - } - return factoryPromise; +/** Parse plain JSON and validate every immutable project.create replay field. */ +export function parseProjectCreateIdentity( + value: unknown, + expectation: ProjectCreateIdentityExpectation = {}, +): ProjectCreateIdentity { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + identityConflict("identity_must_be_object"); + } + const record = value as Record; + const selection = record.defaultModelSelection; + if (typeof selection !== "object" || selection === null || Array.isArray(selection)) { + identityConflict("default_model_selection_required"); + } + const model = selection as Record; + const createdAt = nonBlank(record.createdAt, "created_at"); + if (!Number.isFinite(Date.parse(createdAt))) identityConflict("created_at_invalid"); + const modelOptions = model.options === undefined + ? undefined + : freezeJson(jsonValue(model.options, "model_options")) as readonly unknown[]; + if (modelOptions !== undefined && !Array.isArray(modelOptions)) identityConflict("model_options_must_be_array"); + const defaultModelSelection = Object.freeze({ + instanceId: identifier(model.instanceId, "model_instance_id"), + model: identifier(model.model, "model"), + ...(modelOptions === undefined ? {} : { options: modelOptions }), + }); + const parsed: ProjectCreateIdentity = { + projectId: identifier(record.projectId, "project_id"), + commandId: identifier(record.commandId, "command_id"), + createdAt, + workspaceRoot: canonicalizeWorkspaceRoot(record.workspaceRoot, expectation), + title: nonBlank(record.title, "title"), + defaultModelSelection, + ...(record.environmentId === undefined + ? {} + : { environmentId: identifier(record.environmentId, "environment_id") }), }; + if (expectation.workspaceRoot !== undefined && + workspaceComparisonKey(parsed.workspaceRoot, expectation) !== + workspaceComparisonKey(expectation.workspaceRoot, expectation)) { + identityConflict("workspace_root_mismatch", { + expectedWorkspaceRoot: canonicalizeWorkspaceRoot(expectation.workspaceRoot, expectation), + actualWorkspaceRoot: parsed.workspaceRoot, + }); + } + if (expectation.projectId !== undefined && parsed.projectId !== expectation.projectId) { + identityConflict("project_id_mismatch", { + expectedProjectId: expectation.projectId, + actualProjectId: parsed.projectId, + }); + } + return Object.freeze(parsed); +} - return { - async connect(connection) { - const timeoutMs = positiveBound( - connection.timeoutMs, - positiveBound(defaultTimeoutMs, DEFAULT_CONNECTION_TIMEOUT_MS), - ); - const deadline = Date.now() + timeoutMs; - const factoryAttempt = getFactory(); - let factory: RuntimeClientRpcSessionFactory; - try { - factory = await withTransportTimeout( - factoryAttempt, - remainingMillis(deadline), - ); - } catch (error) { - if (factoryPromise === factoryAttempt) factoryPromise = undefined; - throw error; - } - const scope = await runEffect(Scope.make()); - let closed = false; - const close = async () => { - if (closed) return; - closed = true; - await runEffect(Scope.close(scope, Exit.succeed(undefined))); - }; - try { - const connectAbort = new AbortController(); - const session = await withTransportTimeout( - runEffect( - factory - .connect({ - environmentId: Schema.decodeUnknownSync(EnvironmentId)( - connection.environmentId, - ), - label: connection.label, - socketUrl: connection.socketUrl, - }) - .pipe(Effect.provideService(Scope.Scope, scope)), - connectAbort.signal, - ), - remainingMillis(deadline), - undefined, - () => connectAbort.abort(), - ); - const readyAbort = new AbortController(); - await withTransportTimeout( - runEffect(session.ready, readyAbort.signal), - remainingMillis(deadline), - undefined, - () => readyAbort.abort(), - ); - return { - dispatchCommand: (command) => - runEffect( - session.client[ORCHESTRATION_WS_METHODS.dispatchCommand](command), - ), - subscribeShell: (input) => - Stream.toAsyncIterable( - session.client[ORCHESTRATION_WS_METHODS.subscribeShell](input), - ), - subscribeThread: (input) => - Stream.toAsyncIterable( - session.client[ORCHESTRATION_WS_METHODS.subscribeThread](input), - ), - close, - }; - } catch (error) { - startBestEffortCleanup(close); - throw adapterError(error, "transport_unavailable"); - } - }, +export interface StockT3RuntimeClient { + readonly getDescriptor: (options?: RequestBoundaryOptions) => Promise; + readonly getShell: (options?: RequestBoundaryOptions) => Promise; + readonly getThread: ( + threadId: string, + options?: RequestBoundaryOptions, + ) => Promise; + readonly dispatch: ( + command: Readonly>, + options?: RequestBoundaryOptions, + ) => Promise<{ readonly sequence: number }>; + readonly observations?: () => { + readonly requestCount: number; + readonly inFlight: number; + readonly peakInFlight: number; + readonly endpointStatusTrace: readonly { + readonly method: string; + readonly path: string; + readonly status: number | null; + }[]; }; } -function closeQuietly(session: RuntimeClientSession): void { - startBestEffortCleanup(() => session.close()); +export interface StockT3NativeRuntimeOptions { + readonly client?: StockT3RuntimeClient; + readonly baseUrl?: string | URL; + readonly bearerToken?: string; + readonly fetch?: FetchLike; + readonly connectionProfile?: "local" | "relay" | "tunnel"; + readonly id?: () => string; + readonly now?: () => string; + readonly clock?: () => number; } -function latestVersionAt( - versions: readonly T[], - sequence: number, -): T | undefined { - for (let index = versions.length - 1; index >= 0; index -= 1) { - const version = versions[index]; - if (version !== undefined && version.sequence <= sequence) return version; - } - return undefined; +export interface RuntimeOperationOptions { + readonly deadlineMs?: number; + readonly timeoutMs?: number; + readonly signal?: AbortSignal; + readonly maxReconciliationReads?: number; } -function pruneVersionsThrough( - versions: T[], - sequence: number, -): void { - let retainedIndex = 0; - for (let index = 1; index < versions.length; index += 1) { - if (versions[index]!.sequence > sequence) break; - retainedIndex = index; - } - if (retainedIndex > 0) versions.splice(0, retainedIndex); +interface LeaseState { + readonly receipt: TurnReceipt; + readonly slotToken: symbol; + readonly baselineUserIds: readonly string[]; + readonly preflightLatestTurnId: string | null; + readonly expectedInputDigest: string; + boundTurnId: string | null; } -function sameValue(left: unknown, right: unknown): boolean { - return JSON.stringify(left) === JSON.stringify(right); +interface TurnSlotClaim { + readonly token: symbol; + readonly expiresAt: number; + readonly generation: number; } -function shellCanAdvanceDetail( - detail: OrchestrationThread, - shell: OrchestrationThreadShell, -): boolean { - return ( - detail.id === shell.id && - detail.projectId === shell.projectId && - detail.title === shell.title && - sameValue(detail.modelSelection, shell.modelSelection) && - detail.runtimeMode === shell.runtimeMode && - detail.interactionMode === shell.interactionMode && - detail.branch === shell.branch && - detail.worktreePath === shell.worktreePath && - detail.archivedAt === shell.archivedAt && - detail.settledOverride === shell.settledOverride && - detail.settledAt === shell.settledAt && - sameValue(detail.latestTurn, shell.latestTurn) && - (detail.session === null || - sameValue( - { - status: detail.session.status, - providerName: detail.session.providerName, - providerInstanceId: detail.session.providerInstanceId, - runtimeMode: detail.session.runtimeMode, - activeTurnId: detail.session.activeTurnId, - lastError: detail.session.lastError, - }, - shell.session === null - ? null - : { - status: shell.session.status, - providerName: shell.session.providerName, - providerInstanceId: shell.session.providerInstanceId, - runtimeMode: shell.session.runtimeMode, - activeTurnId: shell.session.activeTurnId, - lastError: shell.session.lastError, - }, - )) - ); +interface TargetProjection { + readonly kind: "absent" | "target" | "superseded" | "concurrent_writer" | "causality_unverifiable"; + readonly detail?: ThreadDetailSnapshot; + readonly evidence?: readonly Readonly>[]; } -function shellDeltaIsPendingOnly( - previous: OrchestrationThreadShell, - current: OrchestrationThreadShell, -): boolean { - const { - updatedAt: _previousUpdatedAt, - hasPendingApprovals: previousPendingApprovals, - hasPendingUserInput: previousPendingInput, - hasActionableProposedPlan: previousActionablePlan, - ...previousStructural - } = previous; - const { - updatedAt: _currentUpdatedAt, - hasPendingApprovals: currentPendingApprovals, - hasPendingUserInput: currentPendingInput, - hasActionableProposedPlan: currentActionablePlan, - ...currentStructural - } = current; - const pendingChanged = - previousPendingApprovals !== currentPendingApprovals || - previousPendingInput !== currentPendingInput || - previousActionablePlan !== currentActionablePlan; - return pendingChanged && sameValue(previousStructural, currentStructural); +interface ReconciliationObservation { + readonly shell: ShellSnapshot | null; + readonly detail: ThreadDetailSnapshot | null; + readonly projectionState: CreateReconciliationState["projectionState"]; + readonly conflict: Readonly> | null; + readonly evidence?: readonly Readonly>[]; } -function toNativeSnapshot( - sequence: number, - detail: OrchestrationThread, - shell: OrchestrationThreadShell, -): NativeThreadSnapshot { - const latestTurn = detail.latestTurn; - const latestUserMessage = [...detail.messages] - .reverse() - .find((message) => message.role === "user"); - const userMessage = - latestTurn === null - ? undefined - : [...detail.messages] - .reverse() - .find( - (message) => - message.role === "user" && message.turnId === latestTurn.turnId, - ); - const assistantMessage = - latestTurn?.assistantMessageId == null - ? undefined - : detail.messages.find( - (message) => message.id === latestTurn.assistantMessageId, - ); - const session = detail.session ?? shell.session; +interface ProjectCreateAttemptState { + readonly environmentId: string; + readonly workspaceRoot: string; + readonly projectId: string; + readonly commandId: string; + readonly command: Readonly>; + acceptedSequence: number | null; + dispatchState: "accepted" | "outcome_unknown"; + retryState: RetryState; + retryClass: StockRuntimeErrorCode | null; + readonly readEvidence: Readonly>[]; +} - return { - threadId: detail.id, - projectId: detail.projectId, - snapshotSequence: sequence, - session: { - status: session?.status ?? "unknown", - activeTurnId: session?.activeTurnId ?? null, - }, - ...(latestUserMessage === undefined - ? {} - : { latestUserMessageId: latestUserMessage.id }), - latestTurn: - latestTurn === null - ? null - : { - turnId: latestTurn.turnId, - status: latestTurn.state, - ...(userMessage === undefined - ? {} - : { userMessageId: userMessage.id }), - assistantMessage: - assistantMessage === undefined - ? null - : { - content: assistantMessage.text, - streaming: assistantMessage.streaming, - }, - }, - pendingApproval: shell.hasPendingApprovals ? true : null, - pendingInput: shell.hasPendingUserInput ? true : null, - }; +const DEFAULT_DEADLINE_MS = 15 * 60_000; +const MAX_BASELINE_IDS = 4_096; +const MAX_BASELINE_BYTES = 256 * 1024; +const MAX_EVIDENCE_BYTES = 256 * 1024; + +function isAmbiguous(error: unknown): boolean { + return ( + error instanceof StockT3HttpError && + error.code === "transport_unavailable" && + error.status === null + ); } -function updateDetail( - versions: VersionedDetail[], - item: OrchestrationThreadStreamItem, -): { readonly synchronized: boolean; readonly deleted: boolean } { - if (item.kind === "synchronized") { - return { synchronized: true, deleted: false }; - } - if (item.kind === "snapshot") { - const latest = versions.at(-1); - if ( - latest === undefined || - item.snapshot.snapshotSequence > latest.sequence - ) { - versions.push({ - sequence: item.snapshot.snapshotSequence, - thread: item.snapshot.thread, - }); - } - return { synchronized: false, deleted: false }; +function isAmbiguousDispatch(error: unknown): boolean { + return ( + isAmbiguous(error) || + (error instanceof StockT3HttpError && + error.code === "protocol_mismatch" && + error.status !== null && + error.status >= 200 && + error.status < 300) + ); +} + +type DispatchFailure = + | { readonly kind: "cancelled" } + | { readonly kind: "timeout" } + | { readonly kind: "ambiguous" } + | { readonly kind: "received"; readonly error: StockRuntimeError }; + +function sanitizeRetry(error: StockT3HttpError): SanitizedRetryError | null { + if (![400, 401, 403, 500].includes(error.status ?? 0)) return null; + const status = error.status as 400 | 401 | 403 | 500; + const detail = error.detail; + const code = detail.code; + const reason = detail.reason; + if (status === 400 && error.code === "command_rejected" && code === "invalid_request" && reason === "invalid_command") { + return { status, class: error.code, code, reason }; } - const latest = versions.at(-1); - if (latest === undefined) { - throw new NativeRuntimeAdapterError("projection_invalid"); + if (status === 401 && error.code === "authentication_failed" && code === "auth_invalid") { + return { status, class: error.code, code, reason: null }; } - if (item.event.sequence <= latest.sequence) { - return { synchronized: false, deleted: false }; + if (status === 403 && error.code === "permission_denied" && code === "insufficient_scope") { + return { status, class: error.code, code, reason: null }; } - const reduced = applyThreadDetailEvent(latest.thread, item.event); - if (reduced.kind === "deleted") { - return { synchronized: false, deleted: true }; + if (status === 500 && error.code === "server_internal" && code === "internal_error" && reason === "orchestration_dispatch_failed") { + return { status, class: error.code, code, reason }; } - versions.push({ - sequence: item.event.sequence, - thread: reduced.kind === "updated" ? reduced.thread : latest.thread, - }); - return { synchronized: false, deleted: false }; + return null; } -function updateShell( - versions: VersionedShell[], - item: OrchestrationShellStreamItem, -): boolean { - if (item.kind === "synchronized") return true; - if (item.kind === "snapshot") { - const latest = versions.at(-1); +function mapReceivedError(error: unknown): StockRuntimeError { + if (error instanceof StockRuntimeError) return error; + if (error instanceof StockT3HttpError) { if ( - latest === undefined || - item.snapshot.snapshotSequence > latest.sequence + error.code === "command_rejected" || + error.code === "authentication_failed" || + error.code === "permission_denied" || + error.code === "server_internal" || + error.code === "protocol_mismatch" || + error.code === "transport_unavailable" ) { - versions.push({ - sequence: item.snapshot.snapshotSequence, - snapshot: item.snapshot, - origin: "snapshot", - }); + return new StockRuntimeError(error.code, { status: error.status }); } - return false; } - const latest = versions.at(-1); - if (latest === undefined) { - throw new NativeRuntimeAdapterError("projection_invalid"); + return new StockRuntimeError("transport_unavailable"); +} + +function readFailureEvidence( + error: unknown, + stage: string, +): Readonly> { + const mapped = mapReceivedError(error); + const status = mapped.evidence.status; + return status === undefined + ? { stage, class: mapped.code } + : { stage, class: mapped.code, status }; +} + +function turnReceiptError( + code: StockRuntimeErrorCode, + receipt: TurnReceipt, + evidence: Readonly> = {}, +): StockRuntimeError { + return new StockRuntimeError(code, { ...evidence, receipt }); +} + +function canonical(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`; + if (typeof value === "object" && value !== null) { + const record = value as Record; + return `{${Object.keys(record) + .sort() + .map((key) => `${JSON.stringify(key)}:${canonical(record[key])}`) + .join(",")}}`; } - if (item.sequence <= latest.sequence) return false; - const snapshot = applyShellStreamEvent(latest.snapshot, item); - versions.push({ sequence: item.sequence, snapshot, origin: "event" }); - return false; + return JSON.stringify(value); +} + +function digestSync(value: unknown): string { + return new Bun.CryptoHasher("sha256").update(canonical(value)).digest("hex"); } -async function catchUpDetailThrough( - session: RuntimeClientSession, +export async function digestStockSpawnInput(input: StockSpawnInput): Promise { + return digestSync(canonicalizeSpawnInput(input)); +} + +function canonicalizeSpawnInput( + input: StockSpawnInput, + platform?: EnvironmentDescriptor["platform"]["os"], +): StockSpawnInput { + const canonicalization = { platform: platform === "unknown" ? undefined : platform }; + const workspaceRoot = canonicalizeWorkspaceRoot(input.workspaceRoot, canonicalization); + const projectCreateIdentity = input.projectCreateIdentity === undefined + ? undefined + : parseProjectCreateIdentity(input.projectCreateIdentity, { + ...canonicalization, + workspaceRoot, + ...(input.projectId === undefined ? {} : { projectId: input.projectId }), + }); + return { + ...input, + workspaceRoot, + ...(projectCreateIdentity === undefined ? {} : { projectCreateIdentity }), + }; +} + +function sameThreadIdentity( + thread: StockThreadShell | StockThreadDetail, + input: StockSpawnInput, threadId: string, - afterSequence: number, - versions: VersionedDetail[], - deadline: number, -): Promise<{ readonly deleted: boolean }> { - const iterator = session - .subscribeThread( - Schema.decodeUnknownSync(OrchestrationSubscribeThreadInput)({ - threadId, - afterSequence, - requestCompletionMarker: true, - }), - ) - [Symbol.asyncIterator](); - try { - while (true) { - let next: IteratorResult; - try { - next = await withTransportTimeout( - iterator.next(), - remainingMillis(deadline), - ); - } catch (error) { - throw adapterError(error, "transport_unavailable"); - } - if (next.done) { - throw new NativeRuntimeAdapterError("transport_unavailable"); - } - const update = updateDetail(versions, next.value); - if (update.deleted) return { deleted: true }; - if (update.synchronized) return { deleted: false }; - } - } finally { - startBestEffortCleanup(() => iterator.return?.()); - } + projectId: string, +): boolean { + return ( + thread.id === threadId && + thread.projectId === projectId && + thread.title === input.title && + thread.runtimeMode === input.runtimeMode && + thread.interactionMode === input.interactionMode && + thread.branch === input.branch && + thread.worktreePath === input.worktreePath && + thread.modelSelection.instanceId === input.modelSelection.instanceId && + thread.modelSelection.model === input.modelSelection.model + ); } -async function catchUpShellThrough( - session: RuntimeClientSession, - afterSequence: number, - versions: VersionedShell[], - deadline: number, -): Promise<{ readonly snapshotSequence: number | undefined }> { - const iterator = session - .subscribeShell({ - afterSequence, - requestCompletionMarker: true, - }) - [Symbol.asyncIterator](); - let snapshotSequence: number | undefined; - try { - while (true) { - let next: IteratorResult; - try { - next = await withTransportTimeout( - iterator.next(), - remainingMillis(deadline), - ); - } catch (error) { - throw adapterError(error, "transport_unavailable"); - } - if (next.done) { - throw new NativeRuntimeAdapterError("transport_unavailable"); - } - if (next.value.kind === "snapshot") { - snapshotSequence = Math.max( - snapshotSequence ?? -1, - next.value.snapshot.snapshotSequence, - ); - } - if (updateShell(versions, next.value)) { - return { snapshotSequence }; +function userMessages(detail: ThreadDetailSnapshot): readonly StockMessage[] { + return detail.thread.messages.filter((entry) => entry.role === "user"); +} + +export function createStockT3NativeRuntime(options: StockT3NativeRuntimeOptions) { + if (options.client === undefined && options.baseUrl === undefined) { + throw new TypeError("client or baseUrl is required"); + } + const client: StockT3RuntimeClient = + options.client ?? + createStockT3HttpClient({ + baseUrl: options.baseUrl!, + bearerToken: options.bearerToken, + fetch: options.fetch, + connectionProfile: options.connectionProfile, + clock: options.clock, + }); + const id = options.id ?? (() => crypto.randomUUID()); + const now = options.now ?? (() => new Date().toISOString()); + const clock = options.clock ?? Date.now; + const leases = new Map(); + const turnSlotClaims = new Map(); + const leaseTimers = new Map>(); + const leaseGenerations = new Map(); + let pinnedEnvironmentId: string | null = null; + const poller = createAdaptivePoller({ + getShell: (boundary) => client.getShell(boundary), + getThread: (threadId, boundary) => client.getThread(threadId, boundary), + now: clock, + }); + + function scopedThreadKey(ref: AgentRef): string { + return `${ref.environmentId}\u0000${ref.threadId}`; + } + + function environmentLeaseGeneration(environmentId: string): number { + return leaseGenerations.get(environmentId) ?? 0; + } + + function invalidateEnvironmentLeases(environmentId: string): void { + leaseGenerations.set( + environmentId, + environmentLeaseGeneration(environmentId) + 1, + ); + for (const state of [...leases.values()]) { + if (state.receipt.agentRef.environmentId === environmentId) { + releaseLease(state.receipt.agentRef, state.receipt.leaseId); } } - } finally { - startBestEffortCleanup(() => iterator.return?.()); + const prefix = `${environmentId}\u0000`; + for (const key of turnSlotClaims.keys()) { + if (key.startsWith(prefix)) turnSlotClaims.delete(key); + } } -} -async function alignInitialVersions( - session: RuntimeClientSession, - threadId: string, - detailVersions: VersionedDetail[], - shellVersions: VersionedShell[], - timeoutMs: number, -): Promise<{ readonly sequence: number; readonly deleted: boolean }> { - const deadline = Date.now() + timeoutMs; - let detailValidatedThrough = detailVersions.at(-1)!.sequence; - let shellValidatedThrough = shellVersions.at(-1)!.sequence; - let targetSequence = Math.max(detailValidatedThrough, shellValidatedThrough); - - while ( - detailValidatedThrough < targetSequence || - shellValidatedThrough < targetSequence - ) { - if (detailValidatedThrough < targetSequence) { - const result = await catchUpDetailThrough( - session, - threadId, - detailValidatedThrough, - detailVersions, - deadline, - ); - if (result.deleted) { - return { sequence: targetSequence, deleted: true }; - } - detailValidatedThrough = targetSequence; - } - if (shellValidatedThrough < targetSequence) { - const result = await catchUpShellThrough( - session, - shellValidatedThrough, - shellVersions, - deadline, - ); - shellValidatedThrough = targetSequence; - if ( - result.snapshotSequence !== undefined && - result.snapshotSequence > targetSequence - ) { - targetSequence = result.snapshotSequence; - shellValidatedThrough = result.snapshotSequence; - } + async function descriptor(boundary: RequestBoundaryOptions = {}): Promise { + const current = await client.getDescriptor(boundary).catch((error) => { + throw mapReceivedError(error); + }); + if (pinnedEnvironmentId !== null && current.environmentId !== pinnedEnvironmentId) { + const expectedEnvironmentId = pinnedEnvironmentId; + pinnedEnvironmentId = current.environmentId; + invalidateEnvironmentLeases(expectedEnvironmentId); + throw new StockRuntimeError("environment_changed", { + expectedEnvironmentId, + actualEnvironmentId: current.environmentId, + }); } + pinnedEnvironmentId = current.environmentId; + return current; } - return { sequence: targetSequence, deleted: false }; -} + function operationDeadline(input: RuntimeOperationOptions): number { + return input.deadlineMs ?? clock() + (input.timeoutMs ?? DEFAULT_DEADLINE_MS); + } -async function* reconcileThread( - session: RuntimeClientSession, - threadId: string, - options: ReconcileThreadOptions = {}, -): AsyncIterable { - const resumeFromSequence = options.resumeFromSequence; - const detailIterator = session - .subscribeThread( - Schema.decodeUnknownSync(OrchestrationSubscribeThreadInput)({ - threadId, - ...(resumeFromSequence === undefined - ? {} - : { afterSequence: resumeFromSequence }), - requestCompletionMarker: true, - }), - ) - [Symbol.asyncIterator](); - const shellIterator = session - .subscribeShell({ - ...(resumeFromSequence === undefined - ? {} - : { afterSequence: resumeFromSequence }), - requestCompletionMarker: true, - }) - [Symbol.asyncIterator](); - const detailVersions: VersionedDetail[] = - options.seed === undefined - ? [] - : [ - { - sequence: options.seed.observation.sequence, - thread: options.seed.detail, - }, - ]; - const shellVersions: VersionedShell[] = - options.seed === undefined - ? [] - : [ - { - sequence: options.seed.observation.sequence, - snapshot: { - ...options.seed.shellSnapshot, - snapshotSequence: options.seed.observation.sequence, - }, - origin: "seed", - }, - ]; - let detailSynchronized = false; - let shellSynchronized = false; - let detailDeleted = false; - let detailFailure: NativeRuntimeAdapterError | undefined; - let lastEmitted = options.emitAfterSequence ?? -1; - let initialAlignmentComplete = options.seed !== undefined; - let alignedInitialSequence: number | undefined; - let detailDone = false; - let shellDone = false; - let detailNext: Promise | undefined = taggedNext( - "detail", - detailIterator.next(), - ); - let shellNext: Promise | undefined = taggedNext( - "shell", - shellIterator.next(), - ); + function boundedOperation(input: RuntimeOperationOptions): RuntimeOperationOptions & { + readonly deadlineMs: number; + } { + return { ...input, deadlineMs: operationDeadline(input) }; + } - try { - while (!detailDone || !shellDone) { - const pending = [detailNext, shellNext].filter( - (entry): entry is Promise => entry !== undefined, - ); - if (pending.length === 0) return; - const next = await Promise.race(pending); - if ("error" in next) { - if (next.source === "shell") { - throw adapterError(next.error, "transport_unavailable"); - } - detailFailure = adapterError(next.error, "transport_unavailable"); - detailDone = true; - detailNext = undefined; - } else if (next.source === "detail") { - detailNext = undefined; - if (next.result.done) { - detailDone = true; - } else { - const update = updateDetail(detailVersions, next.result.value); - if (update.synchronized) detailSynchronized = true; - if (update.deleted) detailDeleted = true; - detailNext = taggedNext("detail", detailIterator.next()); - } - } else { - shellNext = undefined; - if (next.result.done) { - shellDone = true; - } else { - if (updateShell(shellVersions, next.result.value)) { - shellSynchronized = true; - } - shellNext = taggedNext("shell", shellIterator.next()); - } - } + function stopCode( + operation: RuntimeOperationOptions & { readonly deadlineMs: number }, + ): "cancelled" | "timeout" | null { + if (operation.signal?.aborted) return "cancelled"; + if (clock() >= operation.deadlineMs) return "timeout"; + return null; + } - if (shellSynchronized && shellVersions.length > 0) { - const targetExists = shellVersions - .at(-1)! - .snapshot.threads.some((candidate) => candidate.id === threadId); - if (!targetExists) return; - if (detailFailure !== undefined) throw detailFailure; - if (detailDone && !detailSynchronized) { - throw new NativeRuntimeAdapterError("transport_unavailable"); - } - } + function classifyDispatchFailure( + error: unknown, + operation: RuntimeOperationOptions & { readonly deadlineMs: number }, + ): DispatchFailure { + if (operation.signal?.aborted) return { kind: "cancelled" }; + if (clock() >= operation.deadlineMs) return { kind: "timeout" }; + if (isAmbiguousDispatch(error)) return { kind: "ambiguous" }; + return { kind: "received", error: mapReceivedError(error) }; + } - if ( - !detailSynchronized || - !shellSynchronized || - detailDeleted || - detailVersions.length === 0 || - shellVersions.length === 0 - ) { - continue; - } - if (!initialAlignmentComplete) { - const alignment = await alignInitialVersions( - session, - threadId, - detailVersions, - shellVersions, - positiveBound( - options.alignmentTimeoutMs, - DEFAULT_ALIGNMENT_TIMEOUT_MS, + function releaseLeaseAndSnapshot(receipt: TurnReceipt): TurnReceipt { + const key = scopedThreadKey(receipt.agentRef); + const state = leases.get(key); + const matchingLeaseState = state?.receipt.leaseId === receipt.leaseId ? state : undefined; + const matchingState = + matchingLeaseState !== undefined && + matchingLeaseState.receipt.commandId === receipt.commandId && + matchingLeaseState.receipt.messageId === receipt.messageId + ? matchingLeaseState + : undefined; + const current = matchingState === undefined + ? receipt + : { + ...matchingState.receipt, + acceptedSequence: + receipt.acceptedSequence ?? matchingState.receipt.acceptedSequence, + observedSequence: Math.max( + receipt.observedSequence, + matchingState.receipt.observedSequence, ), - ); - if (alignment.deleted) return; - alignedInitialSequence = alignment.sequence; - initialAlignmentComplete = true; - } + ...(receipt.reconciliationEvidence === undefined + ? {} + : { reconciliationEvidence: receipt.reconciliationEvidence }), + }; + if (matchingLeaseState !== undefined) { + leases.delete(key); + const timer = leaseTimers.get(key); + if (timer !== undefined) clearTimeout(timer); + leaseTimers.delete(key); + } + return current.leaseState === "released" + ? current + : { ...current, leaseState: "released" }; + } - let detail: VersionedDetail; - let shell: VersionedShell; - let commonSequence: number; - if (alignedInitialSequence !== undefined) { - commonSequence = alignedInitialSequence; - const alignedDetail = latestVersionAt(detailVersions, commonSequence); - const alignedShell = latestVersionAt(shellVersions, commonSequence); - if (alignedDetail === undefined || alignedShell === undefined) { - throw new NativeRuntimeAdapterError("projection_invalid"); - } - detail = alignedDetail; - shell = alignedShell; - } else { - const latestDetail = detailVersions.at(-1)!; - const latestShell = shellVersions.at(-1)!; - const latestShellThread = latestShell.snapshot.threads.find( - (candidate) => candidate.id === threadId, - ); - if (latestShellThread === undefined) return; - if (latestDetail.sequence === latestShell.sequence) { - commonSequence = latestDetail.sequence; - detail = latestDetail; - shell = latestShell; - } else { - const overlappingStateMatches = shellCanAdvanceDetail( - latestDetail.thread, - latestShellThread, - ); - const previousShell = shellVersions - .at(-2) - ?.snapshot.threads.find((candidate) => candidate.id === threadId); - const provenPendingOnlyShellAdvance = - latestShell.sequence > latestDetail.sequence && - latestShell.origin === "event" && - previousShell !== undefined && - shellDeltaIsPendingOnly(previousShell, latestShellThread); - if (!overlappingStateMatches || !provenPendingOnlyShellAdvance) { - continue; - } - // After initial replay alignment, carry detail forward only for a - // shell delta proven to change pending/actionable flags and nothing - // else. Any other skew waits for the matching canonical stream. - commonSequence = latestShell.sequence; - detail = latestDetail; - shell = latestShell; - } - } - if (commonSequence <= lastEmitted) { - alignedInitialSequence = undefined; - continue; - } - const shellThread = shell.snapshot.threads.find( - (candidate) => candidate.id === threadId, - ); - if (shellThread === undefined || detail.thread.id !== threadId) return; + function releaseLease(ref: AgentRef, leaseId?: string): void { + const state = leases.get(scopedThreadKey(ref)); + if (state === undefined || (leaseId !== undefined && state.receipt.leaseId !== leaseId)) return; + releaseLeaseAndSnapshot(state.receipt); + } - const snapshot = toNativeSnapshot( - commonSequence, - detail.thread, - shellThread, - ); - const shellSnapshot = { - ...shell.snapshot, - snapshotSequence: commonSequence, - }; - pruneVersionsThrough(detailVersions, commonSequence); - pruneVersionsThrough(shellVersions, commonSequence); - lastEmitted = commonSequence; - alignedInitialSequence = undefined; - yield { - observation: { sequence: commonSequence, snapshot }, - detail: detail.thread, - shellSnapshot, - }; - } - if (detailFailure !== undefined) throw detailFailure; - } finally { - startBestEffortCleanup(() => detailIterator.return?.()); - startBestEffortCleanup(() => shellIterator.return?.()); + function releaseTurnSlot(ref: AgentRef, token: symbol): void { + const key = scopedThreadKey(ref); + const claim = turnSlotClaims.get(key); + if (claim?.token === token) turnSlotClaims.delete(key); + const lease = leases.get(key); + if (lease?.slotToken === token) releaseLease(ref, lease.receipt.leaseId); } -} -async function readShellSnapshot( - session: RuntimeClientSession, -): Promise { - const iterator = session - .subscribeShell({ requestCompletionMarker: true }) - [Symbol.asyncIterator](); - let snapshot: OrchestrationShellSnapshot | undefined; - try { - while (true) { - let next: IteratorResult; - try { - next = await iterator.next(); - } catch (error) { - throw adapterError(error, "transport_unavailable"); - } - if (next.done) { - throw new NativeRuntimeAdapterError("transport_unavailable"); - } - if (next.value.kind === "synchronized") { - if (snapshot === undefined) { - throw new NativeRuntimeAdapterError("projection_invalid"); - } - return snapshot; - } - if (next.value.kind === "snapshot") { - snapshot = next.value.snapshot; - } else if (snapshot !== undefined) { - snapshot = applyShellStreamEvent(snapshot, next.value); + function claimTurnSlot(ref: AgentRef, deadlineMs: number): TurnSlotClaim { + const key = scopedThreadKey(ref); + const existingLease = leases.get(key); + if (existingLease !== undefined) { + if (existingLease.receipt.leaseExpiresAt <= clock()) { + releaseLease(ref, existingLease.receipt.leaseId); } else { - throw new NativeRuntimeAdapterError("projection_invalid"); + throw new StockRuntimeError("send_in_progress", { threadId: ref.threadId }); } } - } finally { - startBestEffortCleanup(() => iterator.return?.()); + const existingClaim = turnSlotClaims.get(key); + if (existingClaim !== undefined && existingClaim.expiresAt > clock()) { + throw new StockRuntimeError("send_in_progress", { threadId: ref.threadId }); + } + if (existingClaim !== undefined) turnSlotClaims.delete(key); + if (deadlineMs <= clock()) throw new StockRuntimeError("timeout"); + const token = Symbol(ref.threadId); + const claim = { + token, + expiresAt: deadlineMs, + generation: environmentLeaseGeneration(ref.environmentId), + }; + turnSlotClaims.set(key, claim); + return claim; } -} -function projectCommand( - input: NativeCreateProjectInput, -): ClientOrchestrationCommand { - return decodeClientCommand({ - type: "project.create", - commandId: input.commandId, - projectId: input.projectId, - title: input.title, - workspaceRoot: input.workspaceRoot, - createWorkspaceRootIfMissing: input.createWorkspaceRootIfMissing, - defaultModelSelection: input.defaultModelSelection, - createdAt: input.createdAt, - }); -} + function updateLeaseReceipt( + ref: AgentRef, + slotToken: symbol, + receipt: TurnReceipt, + ): boolean { + const key = scopedThreadKey(ref); + const state = leases.get(key); + if ( + state === undefined || + state.slotToken !== slotToken || + state.receipt.leaseExpiresAt <= clock() + ) { + if (state?.slotToken === slotToken) { + releaseLease(ref, state.receipt.leaseId); + } + return false; + } + leases.set(key, { ...state, receipt }); + return true; + } -function spawnCommand( - input: NativeStartThreadInput, -): ClientOrchestrationCommand { - return decodeClientCommand({ - type: "thread.turn.start", - commandId: input.commandId, - threadId: input.threadId, - message: { - messageId: input.messageId, - role: "user", - text: input.message, - attachments: [], - }, - modelSelection: input.modelSelection, - runtimeMode: input.runtimeMode, - interactionMode: input.interactionMode, - bootstrap: { - createThread: { - projectId: input.projectId, - title: input.title, - modelSelection: input.modelSelection, - runtimeMode: input.runtimeMode, - interactionMode: input.interactionMode, - branch: input.branch, - worktreePath: input.worktreePath, - createdAt: input.createdAt, + function pending( + ref: AgentRef, + attempt: CreateAttemptReceipt, + continuation: CreateReconciliationPending["initialTurnContinuation"], + reason: CreateReconciliationState["reason"], + observation: ReconciliationObservation, + deadlineMs: number, + ): CreateReconciliationPending { + return { + kind: "create_reconciliation_pending", + provisionalRef: ref, + createAttempt: attempt, + reconciliation: { + reason, + projectionState: observation.projectionState, + highestShellSequence: observation.shell?.snapshotSequence ?? null, + highestDetailSequence: observation.detail?.snapshotSequence ?? null, + deadlineMs, + evidence: observation.evidence ?? [], }, - }, - createdAt: input.createdAt, - }); -} - -function turnCommand(input: NativeStartTurnInput): ClientOrchestrationCommand { - return decodeClientCommand({ - type: "thread.turn.start", - commandId: input.commandId, - threadId: input.threadId, - message: { - messageId: input.messageId, - role: "user", - text: input.message, - attachments: [], - }, - runtimeMode: input.runtimeMode, - interactionMode: input.interactionMode, - createdAt: input.createdAt, - }); -} - -function decodeClientCommand(input: unknown): ClientOrchestrationCommand { - try { - return Schema.decodeUnknownSync(ClientOrchestrationCommand)(input); - } catch { - throw new NativeRuntimeAdapterError("command_rejected"); + initialTurnContinuation: continuation, + safeAction: "resume_create_reconciliation", + }; } -} - -export function createT3NativeRuntime( - options: T3NativeRuntimeOptions, -): NativeRuntime { - const connectionTimeoutMs = positiveBound( - options.connectionTimeoutMs, - DEFAULT_CONNECTION_TIMEOUT_MS, - ); - const alignmentTimeoutMs = positiveBound( - options.alignmentTimeoutMs, - DEFAULT_ALIGNMENT_TIMEOUT_MS, - ); - const ownsSessionFactory = options.sessionFactory === undefined; - const sessionFactory = - options.sessionFactory ?? - createDefaultSessionFactory(undefined, connectionTimeoutMs); - const openSession = async (): Promise => { - const deadline = Date.now() + connectionTimeoutMs; + async function observeReconciliation( + ref: AgentRef, + projectId: string, + input: StockSpawnInput, + minimumSequence: number | null, + boundary: RequestBoundaryOptions & { readonly deadlineMs: number }, + ): Promise { + let shell: ShellSnapshot | null = null; + let detail: ThreadDetailSnapshot | null = null; + const evidence: Readonly>[] = []; try { - const socketUrl = await withTransportTimeout( - Promise.resolve().then(options.acquireSocketUrl), - remainingMillis(deadline), - ); - const connectPromise = sessionFactory.connect({ - environmentId: options.environmentId, - label: options.label, - socketUrl, - timeoutMs: remainingMillis(deadline), - }); - if (ownsSessionFactory) return await connectPromise; - return await withTransportTimeout( - connectPromise, - remainingMillis(deadline), - closeQuietly, - ); + shell = await client.getShell(boundary); } catch (error) { - throw adapterError(error, "transport_unavailable"); + if (!isAmbiguous(error)) { + evidence.push(readFailureEvidence(error, "create_shell_reconciliation")); + } } - }; - - const dispatch = async ( - command: ClientOrchestrationCommand, - ): Promise<{ readonly sequence: number }> => { - const session = await openSession(); - try { - return await session.dispatchCommand(command); - } catch (error) { - throw dispatchError(error); - } finally { - closeQuietly(session); + if (!boundary.signal?.aborted && clock() < boundary.deadlineMs) { + try { + const received = (await client.getThread(ref.threadId, boundary)) ?? null; + if (clock() <= boundary.deadlineMs) detail = received; + } catch (error) { + if (!isAmbiguous(error)) { + evidence.push(readFailureEvidence(error, "create_detail_reconciliation")); + } + } } - }; + const shellThread = shell?.threads.find((entry) => entry.id === ref.threadId) ?? null; + if (shellThread !== null && !sameThreadIdentity(shellThread, input, ref.threadId, projectId)) { + return { + shell, + detail, + projectionState: "identity_unverified", + conflict: { source: "shell", threadId: ref.threadId }, + evidence, + }; + } + if (detail !== null && !sameThreadIdentity(detail.thread, input, ref.threadId, projectId)) { + return { + shell, + detail, + projectionState: "identity_unverified", + conflict: { source: "detail", threadId: ref.threadId }, + evidence, + }; + } + const required = minimumSequence ?? 0; + const shellReady = shellThread !== null && shell!.snapshotSequence >= required; + const detailReady = detail !== null && detail.snapshotSequence >= required; + return { + shell, + detail, + projectionState: + shellReady && detailReady + ? "identity_unverified" + : shellReady + ? "shell_only" + : detailReady + ? "detail_only" + : shellThread !== null || detail !== null + ? "below_required_sequence" + : "unobserved", + conflict: null, + evidence, + }; + } - return { - async listProjects(): Promise { - const session = await openSession(); + function reconciled( + observation: ReconciliationObservation, + threadId: string, + minimumSequence: number | null, + ): ThreadCreateReceipt | null { + const shellThread = observation.shell?.threads.find((entry) => entry.id === threadId); + if (shellThread === undefined || observation.detail === null) return null; + const required = minimumSequence ?? 0; + if ( + observation.shell!.snapshotSequence < required || + observation.detail.snapshotSequence < required + ) { + return null; + } + return { + commandId: "", + threadId, + acceptedSequence: minimumSequence, + observedSequence: Math.max( + observation.shell!.snapshotSequence, + observation.detail.snapshotSequence, + ), + recovered: minimumSequence === null, + }; + } + + async function reconcileCreate( + ref: AgentRef, + projectId: string, + input: StockSpawnInput, + attempt: CreateAttemptReceipt, + operation: RuntimeOperationOptions & { readonly deadlineMs: number }, + ): Promise< + | { readonly kind: "reconciled"; readonly receipt: ThreadCreateReceipt; readonly detail: ThreadDetailSnapshot } + | { readonly kind: "pending"; readonly observation: ReconciliationObservation } + | { readonly kind: "conflict"; readonly observation: ReconciliationObservation } + > { + const deadlineMs = operation.deadlineMs; + const reads = Math.max(1, operation.maxReconciliationReads ?? 4); + let last: ReconciliationObservation = { + shell: null, + detail: null, + projectionState: "unobserved", + conflict: null, + }; + for (let index = 0; index < reads; index += 1) { + if (operation.signal?.aborted || clock() >= deadlineMs) { + return { kind: "pending", observation: last }; + } + last = await observeReconciliation(ref, projectId, input, attempt.acceptedSequence, { + deadlineMs, + signal: operation.signal, + }); + if (last.conflict !== null) return { kind: "conflict", observation: last }; + const receipt = reconciled(last, ref.threadId, attempt.acceptedSequence); + if (receipt !== null && last.detail !== null && clock() <= deadlineMs) { + return { + kind: "reconciled", + receipt: { ...receipt, commandId: attempt.commandId }, + detail: last.detail, + }; + } + if (clock() >= deadlineMs) break; + } + return { kind: "pending", observation: last }; + } + + function makeLease( + ref: AgentRef, + slotClaim: TurnSlotClaim, + commandId: string, + messageId: string, + acceptedSequence: number | null, + observedSequence: number, + inputText: string, + preflight: ThreadDetailSnapshot, + deadlineMs: number, + leaseId = id(), + ): TurnReceipt { + if (deadlineMs <= clock()) throw new StockRuntimeError("timeout"); + if (slotClaim.generation !== environmentLeaseGeneration(ref.environmentId)) { + throw new StockRuntimeError("environment_changed", { threadId: ref.threadId }); + } + const key = scopedThreadKey(ref); + const claim = turnSlotClaims.get(key); + if (claim?.token !== slotClaim.token || claim.expiresAt <= clock()) { + throw new StockRuntimeError("send_in_progress", { threadId: ref.threadId }); + } + const baselineUserIds = userMessages(preflight).map((entry) => entry.id); + if ( + baselineUserIds.length > MAX_BASELINE_IDS || + new TextEncoder().encode(JSON.stringify(baselineUserIds)).byteLength > MAX_BASELINE_BYTES + ) { + throw new StockRuntimeError("correlation_capacity"); + } + const receipt: TurnReceipt = { + agentRef: ref, + leaseId, + commandId, + messageId, + acceptedSequence, + observedSequence, + leaseExpiresAt: deadlineMs, + leaseState: "active", + }; + turnSlotClaims.delete(key); + leases.set(key, { + receipt, + slotToken: slotClaim.token, + baselineUserIds, + preflightLatestTurnId: preflight.thread.latestTurn?.turnId ?? null, + expectedInputDigest: digestSync({ text: inputText, attachments: [] }), + boundTurnId: null, + }); + const delay = Math.max(0, Math.min(2_147_483_647, deadlineMs - clock())); + const timer = setTimeout(() => releaseLease(ref, leaseId), delay); + (timer as unknown as { unref?: () => void }).unref?.(); + leaseTimers.set(key, timer); + return receipt; + } + + function classifyTargetProjection( + detail: ThreadDetailSnapshot, + state: LeaseState, + ): TargetProjection { + const observedUsers = userMessages(detail); + if (observedUsers.length < state.baselineUserIds.length) { + return { kind: "concurrent_writer", detail }; + } + for (let index = 0; index < state.baselineUserIds.length; index += 1) { + if (observedUsers[index]?.id !== state.baselineUserIds[index]) { + return { kind: "concurrent_writer", detail }; + } + } + const post = observedUsers.slice(state.baselineUserIds.length); + const targetIndex = post.findIndex( + (entry) => entry.id === state.receipt.messageId, + ); + if (targetIndex === -1) { + return post.length === 0 + ? { kind: "absent", detail } + : post.length === 1 + ? { kind: "superseded", detail } + : { kind: "concurrent_writer", detail }; + } + if (targetIndex > 0) return { kind: "superseded", detail }; + if (post.length > 1) return { kind: "concurrent_writer", detail }; + const target = post[0]!; + if ( + digestSync({ text: target.text, attachments: target.attachments }) !== + state.expectedInputDigest + ) { + return { kind: "causality_unverifiable", detail }; + } + return { kind: "target", detail }; + } + + async function reconcileTarget( + ref: AgentRef, + operation: RuntimeOperationOptions & { readonly deadlineMs: number }, + maximumReads: number, + ): Promise { + let last: TargetProjection = { kind: "absent" }; + for (let index = 0; index < Math.max(1, maximumReads); index += 1) { + if (stopCode(operation) !== null) return last; + let detail: ThreadDetailSnapshot | undefined; try { - const snapshot = await readShellSnapshot(session); - return snapshot.projects.map((entry) => ({ - projectId: entry.id, - workspaceRoot: entry.workspaceRoot, - })); - } finally { - closeQuietly(session); + detail = await client.getThread(ref.threadId, { + deadlineMs: operation.deadlineMs, + signal: operation.signal, + }); + } catch (error) { + if (!isAmbiguous(error)) { + last = { + ...last, + evidence: [ + ...(last.evidence ?? []), + readFailureEvidence(error, "target_reconciliation"), + ], + }; + } + continue; } - }, - createProject: (input) => dispatch(projectCommand(input)), - startThread: (input) => dispatch(spawnCommand(input)), - startTurn: (input) => dispatch(turnCommand(input)), - async getThread(threadId): Promise { - const session = await openSession(); + if (clock() >= operation.deadlineMs) return last; + if (detail === undefined) continue; + const state = leases.get(scopedThreadKey(ref)); + if (state === undefined) return last; + const classified = classifyTargetProjection(detail, state); + last = { ...classified, evidence: last.evidence }; + if (last.kind !== "absent") return last; + } + return last; + } + + function boundedUtf8(value: string): { + readonly text: string; + readonly truncated: boolean; + readonly originalBytes: number; + readonly retainedBytes: number; + } { + const encoder = new TextEncoder(); + const bytes = encoder.encode(value); + if (bytes.byteLength <= MAX_EVIDENCE_BYTES) { + return { + text: value, + truncated: false, + originalBytes: bytes.byteLength, + retainedBytes: bytes.byteLength, + }; + } + let end = MAX_EVIDENCE_BYTES; + const decoder = new TextDecoder("utf-8", { fatal: true }); + while (end > 0) { try { - for await (const state of reconcileThread(session, threadId, { - alignmentTimeoutMs, - })) { - return state.observation.snapshot; + const text = decoder.decode(bytes.subarray(0, end)); + return { + text, + truncated: true, + originalBytes: bytes.byteLength, + retainedBytes: end, + }; + } catch { + end -= 1; + } + } + return { + text: "", + truncated: true, + originalBytes: bytes.byteLength, + retainedBytes: 0, + }; + } + + async function continueInitialTurn( + ref: AgentRef, + slotClaim: TurnSlotClaim, + input: StockSpawnInput, + createReceipt: ThreadCreateReceipt, + continuation: CreateReconciliationPending["initialTurnContinuation"], + preflight: ThreadDetailSnapshot, + operation: RuntimeOperationOptions & { readonly deadlineMs: number }, + ): Promise { + const partial = ( + state: + | "not_attempted" + | "initial_turn_rejected" + | "initial_turn_accepted_projection_pending" + | "initial_turn_outcome_unknown" + | "contended_before_start" + | "superseded" + | "concurrent_writer" + | "causality_unverifiable" + | "cancelled" + | "deadline_exhausted", + leaseExpiresAt: number | null, + safeAction: "new_send" | "wait" | "observe", + evidence: readonly Readonly>[] = [], + turnReceipt: TurnReceipt | null = null, + ): SpawnResult => ({ + kind: "partial", + agentRef: ref, + createReceipt, + initialTurn: { + commandId: continuation.commandId, + messageId: continuation.messageId, + state, + turnReceipt, + leaseExpiresAt, + safeAction, + evidence, + }, + }); + if ( + userMessages(preflight).length !== 0 || + preflight.thread.latestTurn !== null || + (preflight.thread.session !== null && preflight.thread.session.status !== "idle") + ) { + return partial("contended_before_start", null, "observe"); + } + const stopped = stopCode(operation); + if (stopped !== null) { + return partial( + stopped === "cancelled" ? "cancelled" : "deadline_exhausted", + null, + "observe", + ); + } + const deadlineMs = operation.deadlineMs; + let lease: TurnReceipt; + try { + lease = makeLease( + ref, + slotClaim, + continuation.commandId, + continuation.messageId, + null, + preflight.snapshotSequence, + input.message, + preflight, + deadlineMs, + ); + } catch (error) { + if (error instanceof StockRuntimeError) { + if (error.code === "timeout") { + return partial("deadline_exhausted", null, "observe"); + } + if (error.code === "environment_changed") { + return partial("not_attempted", null, "observe", [ + { stage: "lease_promotion", class: "environment_changed" }, + ]); + } + if (error.code === "send_in_progress") { + return partial("contended_before_start", null, "observe", [ + { stage: "lease_promotion", class: "send_in_progress" }, + ]); + } + if (error.code === "correlation_capacity") { + return partial("not_attempted", null, "observe", [ + { stage: "lease_promotion", class: "correlation_capacity" }, + ]); } - return undefined; - } finally { - closeQuietly(session); } - }, - async *subscribeThread( + return partial("not_attempted", null, "observe", [ + { stage: "lease_promotion", class: mapReceivedError(error).code }, + ]); + } + const command = { + type: "thread.turn.start", + commandId: continuation.commandId, + threadId: ref.threadId, + message: { + messageId: continuation.messageId, + role: "user", + text: input.message, + attachments: [], + }, + modelSelection: input.modelSelection, + runtimeMode: input.runtimeMode, + interactionMode: input.interactionMode, + createdAt: now(), + }; + let acceptedSequence: number | null = null; + let ambiguous = false; + try { + acceptedSequence = (await client.dispatch(command, { deadlineMs, signal: operation.signal })).sequence; + poller.dispatchObserved(ref.environmentId); + } catch (error) { + const failure = classifyDispatchFailure(error, operation); + if (failure.kind !== "ambiguous") { + const terminalReceipt = releaseLeaseAndSnapshot({ ...lease, acceptedSequence }); + if (failure.kind === "cancelled" || failure.kind === "timeout") { + return partial( + failure.kind === "cancelled" ? "cancelled" : "deadline_exhausted", + null, + "observe", + [{ stage: "initial_turn_dispatch", class: failure.kind }], + terminalReceipt, + ); + } + return partial( + "initial_turn_rejected", + null, + "new_send", + [{ stage: "initial_turn_dispatch", class: failure.error.code }], + terminalReceipt, + ); + } + ambiguous = true; + } + + const updateReceipt = ( + projection: TargetProjection, + ): { readonly receipt: TurnReceipt; readonly retained: boolean } => { + const updated: TurnReceipt = { + ...lease, + acceptedSequence, + observedSequence: projection.detail?.snapshotSequence ?? lease.observedSequence, + ...((projection.evidence?.length ?? 0) === 0 + ? {} + : { reconciliationEvidence: projection.evidence }), + }; + return { + receipt: updated, + retained: updateLeaseReceipt(ref, slotClaim.token, updated), + }; + }; + const classifyProjection = (projection: TargetProjection): SpawnResult | null => { + if (projection.kind === "target") { + const updated = updateReceipt(projection); + return { + kind: "spawned", + agentRef: ref, + createReceipt, + turnReceipt: updated.receipt, + }; + } + if ( + projection.kind === "superseded" || + projection.kind === "concurrent_writer" || + projection.kind === "causality_unverifiable" + ) { + const updated = updateReceipt(projection); + const terminalReceipt = releaseLeaseAndSnapshot(updated.receipt); + return partial( + projection.kind, + null, + "observe", + [{ reason: projection.kind }, ...(projection.evidence ?? [])], + terminalReceipt, + ); + } + return null; + }; + + const firstProjection = await reconcileTarget( + ref, + operation, + ambiguous ? 1 : operation.maxReconciliationReads ?? 4, + ); + const firstResult = classifyProjection(firstProjection); + if (firstResult !== null) return firstResult; + + const afterObservationStop = stopCode(operation); + if (acceptedSequence !== null) { + if (afterObservationStop !== null) { + const terminalReceipt = releaseLeaseAndSnapshot({ ...lease, acceptedSequence }); + return partial( + "initial_turn_accepted_projection_pending", + null, + "observe", + [{ acceptedSequence }, ...(firstProjection.evidence ?? [])], + terminalReceipt, + ); + } + const updated = updateReceipt(firstProjection); + if (!updated.retained) { + const terminalReceipt = releaseLeaseAndSnapshot(updated.receipt); + return partial( + "initial_turn_accepted_projection_pending", + null, + "observe", + [{ acceptedSequence }, ...(firstProjection.evidence ?? [])], + terminalReceipt, + ); + } + return partial( + "initial_turn_accepted_projection_pending", + updated.receipt.leaseExpiresAt, + "wait", + [{ acceptedSequence }, ...(firstProjection.evidence ?? [])], + updated.receipt, + ); + } + if (afterObservationStop !== null) { + const terminalReceipt = releaseLeaseAndSnapshot({ ...lease, acceptedSequence }); + return partial( + afterObservationStop === "cancelled" ? "cancelled" : "deadline_exhausted", + null, + "observe", + firstProjection.evidence ?? [], + terminalReceipt, + ); + } + + let retryEvidence: Readonly> | null = null; + try { + const retry = await client.dispatch(command, { + deadlineMs, + signal: operation.signal, + }); + acceptedSequence = retry.sequence; + poller.dispatchObserved(ref.environmentId); + } catch (error) { + const failure = classifyDispatchFailure(error, operation); + if (failure.kind === "cancelled" || failure.kind === "timeout") { + const terminalReceipt = releaseLeaseAndSnapshot({ ...lease, acceptedSequence }); + return partial( + failure.kind === "cancelled" ? "cancelled" : "deadline_exhausted", + null, + "observe", + [{ stage: "identical_retry", class: failure.kind }], + terminalReceipt, + ); + } + if (failure.kind === "received") { + retryEvidence = { retryClass: failure.error.code }; + } + } + + const secondProjection = await reconcileTarget( + ref, + operation, + operation.maxReconciliationReads ?? 4, + ); + const secondResult = classifyProjection(secondProjection); + if (secondResult !== null) return secondResult; + const evidence = [ + ...(acceptedSequence === null ? [] : [{ acceptedSequence }]), + ...(retryEvidence === null ? [] : [retryEvidence]), + ...(secondProjection.evidence ?? []), + ]; + const afterRetryObservationStop = stopCode(operation); + if (afterRetryObservationStop !== null) { + const terminalReceipt = releaseLeaseAndSnapshot({ ...lease, acceptedSequence }); + return partial( + acceptedSequence === null + ? "initial_turn_outcome_unknown" + : "initial_turn_accepted_projection_pending", + null, + "observe", + evidence, + terminalReceipt, + ); + } + const updated = updateReceipt(secondProjection); + if (!updated.retained) { + const terminalReceipt = releaseLeaseAndSnapshot(updated.receipt); + return partial( + acceptedSequence === null + ? "initial_turn_outcome_unknown" + : "initial_turn_accepted_projection_pending", + null, + "observe", + evidence, + terminalReceipt, + ); + } + return partial( + acceptedSequence === null + ? "initial_turn_outcome_unknown" + : "initial_turn_accepted_projection_pending", + updated.receipt.leaseExpiresAt, + "wait", + evidence, + updated.receipt, + ); + } + + function freshPreflightPartial( + ref: AgentRef, + createReceipt: ThreadCreateReceipt, + continuation: CreateReconciliationPending["initialTurnContinuation"], + state: "not_attempted" | "contended_before_start" | "cancelled" | "deadline_exhausted", + evidence: readonly Readonly>[] = [], + ): SpawnResult { + return { + kind: "partial", + agentRef: ref, + createReceipt, + initialTurn: { + commandId: continuation.commandId, + messageId: continuation.messageId, + state, + turnReceipt: null, + leaseExpiresAt: null, + safeAction: "observe", + evidence, + }, + }; + } + + async function continueAfterFreshPreflight( + ref: AgentRef, + input: StockSpawnInput, + createReceipt: ThreadCreateReceipt, + continuation: CreateReconciliationPending["initialTurnContinuation"], + operation: RuntimeOperationOptions & { readonly deadlineMs: number }, + ): Promise { + const stopped = stopCode(operation); + if (stopped !== null) { + return freshPreflightPartial( + ref, + createReceipt, + continuation, + stopped === "cancelled" ? "cancelled" : "deadline_exhausted", + ); + } + let slotClaim: TurnSlotClaim; + try { + slotClaim = claimTurnSlot(ref, operation.deadlineMs); + } catch (error) { + if (error instanceof StockRuntimeError && error.code === "send_in_progress") { + return freshPreflightPartial( + ref, + createReceipt, + continuation, + "contended_before_start", + [{ reason: "send_in_progress" }], + ); + } + if (error instanceof StockRuntimeError && error.code === "timeout") { + return freshPreflightPartial( + ref, + createReceipt, + continuation, + "deadline_exhausted", + ); + } + return freshPreflightPartial(ref, createReceipt, continuation, "not_attempted", [ + { stage: "slot_claim", class: mapReceivedError(error).code }, + ]); + } + let retainSlot = false; + try { + let fresh: ThreadDetailSnapshot | undefined; + try { + fresh = await client.getThread(ref.threadId, { + deadlineMs: operation.deadlineMs, + signal: operation.signal, + }); + } catch (error) { + const stoppedAfterRead = stopCode(operation); + if (stoppedAfterRead !== null) { + return freshPreflightPartial( + ref, + createReceipt, + continuation, + stoppedAfterRead === "cancelled" ? "cancelled" : "deadline_exhausted", + ); + } + return freshPreflightPartial(ref, createReceipt, continuation, "not_attempted", [ + { stage: "fresh_preflight", class: mapReceivedError(error).code }, + ]); + } + if (fresh === undefined) { + return freshPreflightPartial(ref, createReceipt, continuation, "not_attempted", [ + { stage: "fresh_preflight", class: "not_found" }, + ]); + } + const result = await continueInitialTurn( + ref, + slotClaim, + input, + createReceipt, + continuation, + fresh, + operation, + ); + retainSlot = + result.kind === "spawned" || + (result.kind === "partial" && + result.initialTurn.turnReceipt?.leaseState === "active"); + return result; + } finally { + if (!retainSlot) releaseTurnSlot(ref, slotClaim.token); + } + } + + async function resolveProject( + input: StockSpawnInput, + operation: RuntimeOperationOptions & { readonly deadlineMs: number }, + environmentId: string, + platform: EnvironmentDescriptor["platform"]["os"], + ): Promise { + const deadlineMs = operation.deadlineMs; + const identity = input.projectCreateIdentity; + if ( + identity !== undefined && + identity.environmentId !== undefined && + identity.environmentId !== environmentId + ) { + throw new StockRuntimeError("environment_changed", { + expectedEnvironmentId: identity.environmentId, + actualEnvironmentId: environmentId, + provisionalProjectId: identity.projectId, + }); + } + const workspaceMatches = (value: string): boolean => + workspaceComparisonKey(value, { platform: platform === "unknown" ? undefined : platform }) === + workspaceComparisonKey(input.workspaceRoot, { platform: platform === "unknown" ? undefined : platform }); + let attempt: ProjectCreateAttemptState | undefined = + identity === undefined + ? undefined + : { + environmentId, + workspaceRoot: input.workspaceRoot, + projectId: identity.projectId, + commandId: identity.commandId, + command: { + type: "project.create", + commandId: identity.commandId, + projectId: identity.projectId, + title: identity.title, + workspaceRoot: identity.workspaceRoot, + createWorkspaceRootIfMissing: false, + defaultModelSelection: identity.defaultModelSelection, + createdAt: identity.createdAt, + }, + acceptedSequence: null, + dispatchState: "outcome_unknown", + retryState: "eligible_not_sent", + retryClass: null, + readEvidence: [], + }; + + const recordReadFailure = ( + currentAttempt: ProjectCreateAttemptState, + error: unknown, + ): void => { + currentAttempt.readEvidence.push( + readFailureEvidence(error, "project_create_reconciliation"), + ); + if (currentAttempt.readEvidence.length > 8) currentAttempt.readEvidence.shift(); + }; + const attemptEvidence = (currentAttempt: ProjectCreateAttemptState) => ({ + provisionalProjectId: currentAttempt.projectId, + acceptedSequence: currentAttempt.acceptedSequence, + projectAttempt: { + environmentId: currentAttempt.environmentId, + commandId: currentAttempt.commandId, + projectId: currentAttempt.projectId, + createdAt: currentAttempt.command.createdAt, + workspaceRoot: currentAttempt.command.workspaceRoot, + title: currentAttempt.command.title, + defaultModelSelection: currentAttempt.command.defaultModelSelection, + acceptedSequence: currentAttempt.acceptedSequence, + dispatchState: currentAttempt.dispatchState, + retryState: currentAttempt.retryState, + retryClass: currentAttempt.retryClass, + }, + readEvidence: currentAttempt.readEvidence.map((entry) => ({ ...entry })), + }); + const attemptError = ( + currentAttempt: ProjectCreateAttemptState, + code: StockRuntimeErrorCode, + reason: string, + extra: Readonly> = {}, + ) => + new StockRuntimeError(code, { + reason, + ...attemptEvidence(currentAttempt), + ...extra, + }); + + let current: ShellSnapshot; + try { + current = await client.getShell({ deadlineMs, signal: operation.signal }); + } catch (error) { + if (attempt === undefined) throw mapReceivedError(error); + if (!isAmbiguous(error)) recordReadFailure(attempt, error); + const stopped = stopCode(operation); + throw attemptError( + attempt, + stopped ?? "transport_unavailable", + stopped === "cancelled" + ? "project_reconciliation_cancelled" + : stopped === "timeout" + ? "project_reconciliation_deadline_exhausted" + : "project_projection_pending", + ); + } + if (input.projectId !== undefined) { + const exact = current.projects.find((entry) => entry.id === input.projectId); + if (exact !== undefined) { + if (!workspaceMatches(exact.workspaceRoot)) { + throw new StockRuntimeError("identity_conflict", { + reason: "project_workspace_mismatch", + projectId: input.projectId, + }); + } + return exact.id; + } + if (attempt === undefined) { + throw new StockRuntimeError("identity_conflict", { + reason: "project_not_found", + projectId: input.projectId, + }); + } + } + const matches = current.projects.filter((entry) => workspaceMatches(entry.workspaceRoot)); + if (matches.length > 1) { + if (attempt !== undefined) { + throw attemptError(attempt, "identity_conflict", "multiple_workspace_projects", { + workspaceRoot: input.workspaceRoot, + }); + } + throw new StockRuntimeError("identity_conflict", { workspaceRoot: input.workspaceRoot }); + } + if (matches[0] !== undefined) { + if (attempt !== undefined && matches[0].id !== attempt.projectId) { + throw attemptError(attempt, "identity_conflict", "workspace_project_changed", { + actualProjectId: matches[0].id, + }); + } + return matches[0].id; + } + const stopped = stopCode(operation); + if (stopped !== null) { + if (attempt !== undefined) { + throw attemptError( + attempt, + stopped, + stopped === "cancelled" + ? "project_reconciliation_cancelled" + : "project_reconciliation_deadline_exhausted", + ); + } + throw new StockRuntimeError(stopped); + } + + if (attempt === undefined) { + throw new StockRuntimeError("identity_conflict", { + reason: "project_create_identity_required", + workspaceRoot: input.workspaceRoot, + }); + } + + const reconcile = async (currentAttempt: ProjectCreateAttemptState): Promise => { + const reads = Math.max(1, operation.maxReconciliationReads ?? 4); + let lastObservedSequence: number | null = null; + for (let index = 0; index < reads; index += 1) { + if (stopCode(operation) !== null) return false; + let observed: ShellSnapshot; + try { + observed = await client.getShell({ + deadlineMs, + signal: operation.signal, + }); + } catch (error) { + if (stopCode(operation) !== null) return false; + if (!isAmbiguous(error)) recordReadFailure(currentAttempt, error); + continue; + } + if ( + lastObservedSequence !== null && + observed.snapshotSequence < lastObservedSequence + ) { + throw attemptError(currentAttempt, "protocol_mismatch", "shell_sequence_regression"); + } + lastObservedSequence = observed.snapshotSequence; + const byId = observed.projects.find((entry) => entry.id === currentAttempt.projectId); + if (byId !== undefined) { + if (!workspaceMatches(byId.workspaceRoot)) { + throw attemptError(currentAttempt, "identity_conflict", "project_identity_changed", { + source: "project_create_reconciliation", + }); + } + if ( + currentAttempt.acceptedSequence !== null && + observed.snapshotSequence < currentAttempt.acceptedSequence + ) { + continue; + } + return true; + } + const byRoot = observed.projects.filter( + (entry) => workspaceMatches(entry.workspaceRoot), + ); + if (byRoot.length > 0) { + throw attemptError(currentAttempt, "identity_conflict", "workspace_project_changed", { + source: "project_create_reconciliation", + }); + } + } + return false; + }; + + let originalWasAmbiguous = false; + try { + attempt.acceptedSequence = ( + await client.dispatch(attempt.command, { deadlineMs, signal: operation.signal }) + ).sequence; + attempt.dispatchState = "accepted"; + attempt.retryState = "not_applicable"; + } catch (error) { + const failure = classifyDispatchFailure(error, operation); + if (failure.kind === "received") { + throw attemptError( + attempt, + failure.error.code, + "project_create_received_error", + failure.error.evidence, + ); + } + if (failure.kind === "cancelled" || failure.kind === "timeout") { + throw attemptError( + attempt, + failure.kind, + failure.kind === "cancelled" + ? "project_reconciliation_cancelled" + : "project_reconciliation_deadline_exhausted", + ); + } + originalWasAmbiguous = true; + } + if (await reconcile(attempt)) { + return attempt.projectId; + } + const stoppedAfterReconcile = stopCode(operation); + if (stoppedAfterReconcile !== null) { + throw attemptError( + attempt, + stoppedAfterReconcile, + stoppedAfterReconcile === "cancelled" + ? "project_reconciliation_cancelled" + : "project_reconciliation_deadline_exhausted", + ); + } + if (!originalWasAmbiguous) { + throw attemptError(attempt, "transport_unavailable", "project_projection_pending"); + } + + try { + attempt.acceptedSequence = ( + await client.dispatch(attempt.command, { deadlineMs, signal: operation.signal }) + ).sequence; + attempt.dispatchState = "accepted"; + attempt.retryState = "identical_retry_accepted"; + } catch (error) { + const failure = classifyDispatchFailure(error, operation); + if (failure.kind === "cancelled" || failure.kind === "timeout") { + attempt.retryState = "identical_retry_sent_no_response"; + throw attemptError( + attempt, + failure.kind, + failure.kind === "cancelled" + ? "project_reconciliation_cancelled" + : "project_reconciliation_deadline_exhausted", + ); + } + if (failure.kind === "received") { + attempt.retryState = "identical_retry_received_error"; + attempt.retryClass = failure.error.code; + } else { + attempt.retryState = "identical_retry_sent_no_response"; + } + } + if (await reconcile(attempt)) { + return attempt.projectId; + } + throw attemptError(attempt, "transport_unavailable", "project_create_outcome_unknown"); + } + + async function spawn(input: StockSpawnInput, operation: RuntimeOperationOptions = {}): Promise { + const bounded = boundedOperation(operation); + const initialStop = stopCode(bounded); + if (initialStop !== null) throw new StockRuntimeError(initialStop); + const environment = await descriptor({ deadlineMs: bounded.deadlineMs, signal: bounded.signal }); + input = canonicalizeSpawnInput(input, environment.platform.os); + const projectId = await resolveProject( + input, + bounded, + environment.environmentId, + environment.platform.os, + ); + const createCommandId = id(); + const threadId = id(); + const turnCommandId = id(); + const messageId = id(); + const ref = { environmentId: environment.environmentId, threadId }; + const continuation = { + commandId: turnCommandId, + messageId, + inputDigest: await digestStockSpawnInput(input), + }; + const deadlineMs = bounded.deadlineMs; + const createCommand = { + type: "thread.create", + commandId: createCommandId, threadId, + projectId, + title: input.title, + modelSelection: input.modelSelection, + runtimeMode: input.runtimeMode, + interactionMode: input.interactionMode, + branch: input.branch, + worktreePath: input.worktreePath, + createdAt: now(), + }; + const beforeCreateStop = stopCode(bounded); + if (beforeCreateStop !== null) throw new StockRuntimeError(beforeCreateStop); + let attempt: CreateAttemptReceipt; + try { + const accepted = await client.dispatch(createCommand, { deadlineMs, signal: operation.signal }); + poller.dispatchObserved(ref.environmentId); + attempt = { + commandId: createCommandId, + threadId, + projectId, + acceptedSequence: accepted.sequence, + dispatchState: "accepted", + retryState: "not_applicable", + retryError: null, + }; + } catch (error) { + const failure = classifyDispatchFailure(error, bounded); + if (failure.kind === "received") throw failure.error; + attempt = { + commandId: createCommandId, + threadId, + projectId, + acceptedSequence: null, + dispatchState: "outcome_unknown", + retryState: "eligible_not_sent", + retryError: null, + }; + if (failure.kind === "cancelled" || failure.kind === "timeout") { + return pending( + ref, + attempt, + continuation, + failure.kind === "cancelled" ? "cancelled" : "deadline_exhausted", + { shell: null, detail: null, projectionState: "unobserved", conflict: null }, + deadlineMs, + ); + } + const beforeRetry = await reconcileCreate(ref, projectId, input, attempt, bounded); + if (beforeRetry.kind === "conflict") { + return { kind: "create_protocol_failure", provisionalRef: ref, createAttempt: attempt, conflict: beforeRetry.observation.conflict ?? {} }; + } + if (beforeRetry.kind === "reconciled") { + return continueAfterFreshPreflight( + ref, + input, + beforeRetry.receipt, + continuation, + bounded, + ); + } + const beforeRetryStop = stopCode(bounded); + if (beforeRetryStop !== null) { + return pending( + ref, + attempt, + continuation, + beforeRetryStop === "cancelled" ? "cancelled" : "deadline_exhausted", + beforeRetry.observation, + deadlineMs, + ); + } + try { + const retry = await client.dispatch(createCommand, { deadlineMs, signal: bounded.signal }); + poller.dispatchObserved(ref.environmentId); + attempt = { + ...attempt, + acceptedSequence: retry.sequence, + dispatchState: "accepted", + retryState: "identical_retry_accepted", + }; + } catch (retryError) { + const retryFailure = classifyDispatchFailure(retryError, bounded); + if ( + retryFailure.kind === "ambiguous" || + retryFailure.kind === "cancelled" || + retryFailure.kind === "timeout" + ) { + attempt = { ...attempt, retryState: "identical_retry_sent_no_response" }; + if (retryFailure.kind === "cancelled" || retryFailure.kind === "timeout") { + return pending( + ref, + attempt, + continuation, + retryFailure.kind === "cancelled" ? "cancelled" : "deadline_exhausted", + beforeRetry.observation, + deadlineMs, + ); + } + } else if (retryError instanceof StockT3HttpError) { + const sanitized = sanitizeRetry(retryError); + if (sanitized !== null) { + attempt = { + ...attempt, + retryState: "identical_retry_received_error", + retryError: sanitized, + }; + return pending( + ref, + attempt, + continuation, + "retry_error_after_ambiguous_original", + beforeRetry.observation, + deadlineMs, + ); + } + if (retryFailure.error.code === "protocol_mismatch") { + return { + kind: "create_protocol_failure", + provisionalRef: ref, + createAttempt: attempt, + conflict: { stage: "identical_retry", class: "protocol_mismatch" }, + }; + } + return pending(ref, attempt, continuation, "transport_exhausted", beforeRetry.observation, deadlineMs); + } else { + return pending(ref, attempt, continuation, "transport_exhausted", beforeRetry.observation, deadlineMs); + } + } + } + const result = await reconcileCreate(ref, projectId, input, attempt, bounded); + if (result.kind === "conflict") { + return { kind: "create_protocol_failure", provisionalRef: ref, createAttempt: attempt, conflict: result.observation.conflict ?? {} }; + } + if (result.kind === "pending") { + return pending( + ref, + attempt, + continuation, + bounded.signal?.aborted + ? "cancelled" + : clock() >= deadlineMs + ? "deadline_exhausted" + : (result.observation.evidence?.length ?? 0) > 0 + ? "transport_exhausted" + : attempt.retryState === "identical_retry_sent_no_response" + ? "transport_exhausted" + : "projection_pending", + result.observation, + deadlineMs, + ); + } + return continueAfterFreshPreflight( + ref, + input, + result.receipt, + continuation, + bounded, + ); + } + + async function resumeCreateReconciliation( + pendingResult: CreateReconciliationPending, + input: StockSpawnInput, + operation: RuntimeOperationOptions = {}, + ): Promise { + const bounded = boundedOperation(operation); + const resumeStop = stopCode(bounded); + if (resumeStop !== null) { + return pending( + pendingResult.provisionalRef, + pendingResult.createAttempt, + pendingResult.initialTurnContinuation, + resumeStop === "cancelled" ? "cancelled" : "deadline_exhausted", + { + shell: null, + detail: null, + projectionState: pendingResult.reconciliation.projectionState, + conflict: null, + }, + bounded.deadlineMs, + ); + } + let environment: EnvironmentDescriptor; + try { + environment = await descriptor({ + deadlineMs: bounded.deadlineMs, + signal: bounded.signal, + }); + } catch (error) { + const stoppedAfterDescriptor = stopCode(bounded); + return pending( + pendingResult.provisionalRef, + pendingResult.createAttempt, + pendingResult.initialTurnContinuation, + stoppedAfterDescriptor === "cancelled" + ? "cancelled" + : stoppedAfterDescriptor === "timeout" + ? "deadline_exhausted" + : "transport_exhausted", + { + shell: null, + detail: null, + projectionState: pendingResult.reconciliation.projectionState, + conflict: null, + evidence: [readFailureEvidence(error, "resume_descriptor")], + }, + bounded.deadlineMs, + ); + } + if (environment.environmentId !== pendingResult.provisionalRef.environmentId) { + return pending( + pendingResult.provisionalRef, + pendingResult.createAttempt, + pendingResult.initialTurnContinuation, + "transport_exhausted", + { + shell: null, + detail: null, + projectionState: pendingResult.reconciliation.projectionState, + conflict: null, + evidence: [{ + stage: "resume_descriptor", + class: "environment_changed", + expectedEnvironmentId: pendingResult.provisionalRef.environmentId, + actualEnvironmentId: environment.environmentId, + }], + }, + bounded.deadlineMs, + ); + } + input = canonicalizeSpawnInput(input, environment.platform.os); + if ((await digestStockSpawnInput(input)) !== pendingResult.initialTurnContinuation.inputDigest) { + throw new StockRuntimeError("identity_conflict", { + reason: "input_digest_mismatch", + provisionalRef: pendingResult.provisionalRef, + createAttempt: pendingResult.createAttempt, + }); + } + const result = await reconcileCreate( + pendingResult.provisionalRef, + pendingResult.createAttempt.projectId, + input, + pendingResult.createAttempt, + bounded, + ); + if (result.kind === "conflict") { + return { + kind: "create_protocol_failure", + provisionalRef: pendingResult.provisionalRef, + createAttempt: pendingResult.createAttempt, + conflict: result.observation.conflict ?? {}, + }; + } + if (result.kind === "pending") { + return pending( + pendingResult.provisionalRef, + pendingResult.createAttempt, + pendingResult.initialTurnContinuation, + bounded.signal?.aborted + ? "cancelled" + : clock() >= bounded.deadlineMs + ? "deadline_exhausted" + : (result.observation.evidence?.length ?? 0) > 0 + ? "transport_exhausted" + : "projection_pending", + result.observation, + bounded.deadlineMs, + ); + } + return continueAfterFreshPreflight( + pendingResult.provisionalRef, input, - ): AsyncIterable { - const session = await openSession(); + result.receipt, + pendingResult.initialTurnContinuation, + bounded, + ); + } + + async function send( + ref: AgentRef, + text: string, + operation: RuntimeOperationOptions = {}, + ): Promise { + const bounded = boundedOperation(operation); + const initialStop = stopCode(bounded); + if (initialStop !== null) throw new StockRuntimeError(initialStop); + const deadlineMs = bounded.deadlineMs; + const slotClaim = claimTurnSlot(ref, deadlineMs); + const slotToken = slotClaim.token; + let retainSlot = false; + const retain = (receipt: TurnReceipt): TurnReceipt => { + retainSlot = true; + return receipt; + }; + try { + const [environment, preflight] = await Promise.all([ + descriptor({ deadlineMs, signal: bounded.signal }), + client.getThread(ref.threadId, { deadlineMs, signal: bounded.signal }), + ] as const); + if (environment.environmentId !== ref.environmentId) { + throw new StockRuntimeError("environment_changed"); + } + if (preflight === undefined || preflight.thread.id !== ref.threadId) { + throw new StockRuntimeError("identity_conflict", { threadId: ref.threadId }); + } + if ( + preflight.thread.latestTurn?.state === "running" || + preflight.thread.session?.status === "starting" || + preflight.thread.session?.status === "running" + ) { + throw new StockRuntimeError("send_in_progress", { source: "stock" }); + } + const preDispatchStop = stopCode(bounded); + if (preDispatchStop !== null) throw new StockRuntimeError(preDispatchStop); + const commandId = id(); + const messageId = id(); + const leaseId = id(); + let receipt = makeLease( + ref, + slotClaim, + commandId, + messageId, + null, + preflight.snapshotSequence, + text, + preflight, + deadlineMs, + leaseId, + ); + const command = { + type: "thread.turn.start", + commandId, + threadId: ref.threadId, + message: { messageId, role: "user", text, attachments: [] }, + runtimeMode: preflight.thread.runtimeMode, + interactionMode: preflight.thread.interactionMode, + createdAt: now(), + }; try { - if (input.afterSequence === undefined) { - for await (const state of reconcileThread(session, threadId, { - alignmentTimeoutMs, - })) { - yield state.observation; + const accepted = await client.dispatch(command, { + deadlineMs, + signal: bounded.signal, + }); + poller.dispatchObserved(ref.environmentId); + receipt = { ...receipt, acceptedSequence: accepted.sequence }; + updateLeaseReceipt(ref, slotToken, receipt); + return retain(receipt); + } catch (error) { + const failure = classifyDispatchFailure(error, bounded); + if (failure.kind !== "ambiguous") { + const terminalReceipt = releaseLeaseAndSnapshot(receipt); + if (failure.kind === "cancelled" || failure.kind === "timeout") { + throw turnReceiptError(failure.kind, terminalReceipt, { stage: "turn_dispatch" }); } - return; + throw turnReceiptError(failure.error.code, terminalReceipt, { + stage: "turn_dispatch", + ...failure.error.evidence, + }); + } + } + + const projection = await reconcileTarget(ref, bounded, 1); + if (projection.kind === "target") { + receipt = { + ...receipt, + observedSequence: projection.detail?.snapshotSequence ?? receipt.observedSequence, + ...((projection.evidence?.length ?? 0) === 0 + ? {} + : { reconciliationEvidence: projection.evidence }), + }; + updateLeaseReceipt(ref, slotToken, receipt); + return retain(receipt); + } + if (projection.kind !== "absent") { + const terminalReceipt = releaseLeaseAndSnapshot(receipt); + throw turnReceiptError(projection.kind, terminalReceipt, { stage: "send_reconciliation" }); + } + const beforeRetryStop = stopCode(bounded); + if (beforeRetryStop !== null) { + const terminalReceipt = releaseLeaseAndSnapshot(receipt); + throw turnReceiptError(beforeRetryStop, terminalReceipt, { stage: "before_identical_retry" }); + } + try { + const retried = await client.dispatch(command, { + deadlineMs, + signal: bounded.signal, + }); + poller.dispatchObserved(ref.environmentId); + receipt = { ...receipt, acceptedSequence: retried.sequence }; + updateLeaseReceipt(ref, slotToken, receipt); + return retain(receipt); + } catch (error) { + const failure = classifyDispatchFailure(error, bounded); + if (failure.kind === "ambiguous") return retain(receipt); + if ( + failure.kind === "received" && + failure.error.code !== "protocol_mismatch" && + error instanceof StockT3HttpError && + [400, 401, 403, 500].includes(error.status ?? 0) + ) { + return retain(receipt); } + const terminalReceipt = releaseLeaseAndSnapshot(receipt); + if (failure.kind === "cancelled" || failure.kind === "timeout") { + throw turnReceiptError(failure.kind, terminalReceipt, { stage: "identical_retry" }); + } + throw turnReceiptError(failure.error.code, terminalReceipt, { + stage: "identical_retry", + ...failure.error.evidence, + }); + } + } finally { + if (!retainSlot) releaseTurnSlot(ref, slotToken); + } + } - let initial: ReconciledThreadState | undefined; - for await (const state of reconcileThread(session, threadId, { - alignmentTimeoutMs, - })) { - initial = state; - break; + function validateReceipt(receipt: TurnReceipt): LeaseState { + const state = leases.get(scopedThreadKey(receipt.agentRef)); + if ( + receipt.leaseState !== "active" || + state === undefined || + state.receipt.leaseId !== receipt.leaseId || + state.receipt.leaseExpiresAt <= clock() + ) { + releaseLease(receipt.agentRef, receipt.leaseId); + throw new StockRuntimeError("receipt_expired"); + } + return state; + } + + async function wait( + receipt: TurnReceipt, + operation: RuntimeOperationOptions = {}, + ): Promise<{ + readonly kind: "completed"; + readonly receipt: TurnReceipt; + readonly assistantContent: string; + readonly snapshotSequence: number; + readonly evidence: { + readonly truncated: boolean; + readonly originalBytes: number; + readonly retainedBytes: number; + }; + }> { + const bounded = boundedOperation(operation); + const deadlineMs = Math.min(bounded.deadlineMs, receipt.leaseExpiresAt); + try { + validateReceipt(receipt); + const environment = await descriptor({ deadlineMs, signal: bounded.signal }); + if (environment.environmentId !== receipt.agentRef.environmentId) { + throw new StockRuntimeError("environment_changed", { + expectedEnvironmentId: receipt.agentRef.environmentId, + actualEnvironmentId: environment.environmentId, + }); + } + validateReceipt(receipt); + return await poller.waitFor({ + environmentId: receipt.agentRef.environmentId, + threadId: receipt.agentRef.threadId, + deadlineMs, + signal: bounded.signal, + evaluate: ({ shell: shellSnapshot, detail: detailSnapshot }) => { + const state = validateReceipt(receipt); + const shellThread = shellSnapshot.threads.find( + (entry) => entry.id === receipt.agentRef.threadId, + ); + if (shellThread === undefined) { + throw new StockRuntimeError("protocol_mismatch", { reason: "shell_thread_missing" }); + } + if (detailSnapshot === undefined) return { done: false, detail: true }; + if ( + detailSnapshot.thread.id !== shellThread.id || + detailSnapshot.thread.projectId !== shellThread.projectId + ) { + throw new StockRuntimeError("protocol_mismatch", { + reason: "shell_detail_identity_conflict", + }); + } + if (detailSnapshot.snapshotSequence < shellSnapshot.snapshotSequence) { + return { done: false, detail: true }; + } + const targetProjection = classifyTargetProjection(detailSnapshot, state); + if (targetProjection.kind === "absent") return { done: false, detail: true }; + if (targetProjection.kind !== "target") { + throw new StockRuntimeError(targetProjection.kind); + } + const observedUsers = userMessages(detailSnapshot); + const post = observedUsers.slice(state.baselineUserIds.length); + const target = post[0]!; + const latest = detailSnapshot.thread.latestTurn; + if ( + state.boundTurnId === null && + latest !== null && + latest.turnId !== state.preflightLatestTurnId && + latest.requestedAt === target.createdAt + ) { + state.boundTurnId = latest.turnId; + } else if ( + state.boundTurnId === null && + latest !== null && + latest.requestedAt > target.createdAt + ) { + throw new StockRuntimeError("superseded"); + } + if (state.boundTurnId !== null && latest?.turnId !== state.boundTurnId) { + throw new StockRuntimeError("concurrent_writer", { reason: "turn_changed" }); + } + if (state.boundTurnId === null) return { done: false, detail: true }; + const shellLatest = shellThread.latestTurn; + if ( + state.boundTurnId !== null && + shellLatest !== null && + (shellLatest.turnId !== state.boundTurnId || + shellLatest.requestedAt !== target.createdAt) + ) { + throw new StockRuntimeError("concurrent_writer", { + reason: "shell_detail_turn_conflict", + }); + } + if (shellThread.hasPendingApprovals) throw new StockRuntimeError("pending_approval"); + if (shellThread.hasPendingUserInput) throw new StockRuntimeError("pending_input"); + if (latest?.state === "interrupted") throw new StockRuntimeError("turn_interrupted"); + if (latest?.state === "error") throw new StockRuntimeError("turn_error"); + if ( + latest?.state !== "completed" || + state.boundTurnId === null || + shellLatest?.turnId !== state.boundTurnId || + shellLatest.requestedAt !== target.createdAt || + shellLatest.state !== "completed" + ) { + return { done: false, detail: true }; + } + const assistant = detailSnapshot.thread.messages.find( + (entry) => + entry.id === latest.assistantMessageId && + entry.role === "assistant" && + entry.turnId === state.boundTurnId && + !entry.streaming, + ); + if (assistant === undefined) return { done: false, detail: true }; + const retained = boundedUtf8(assistant.text); + const terminalReceipt = releaseLeaseAndSnapshot(receipt); + return { + done: true, + value: { + kind: "completed" as const, + receipt: terminalReceipt, + assistantContent: retained.text, + snapshotSequence: detailSnapshot.snapshotSequence, + evidence: { + truncated: retained.truncated, + originalBytes: retained.originalBytes, + retainedBytes: retained.retainedBytes, + }, + }, + }; + }, + }); + } catch (error) { + if (error instanceof StockRuntimeError) { + if ( + error.code === "environment_changed" || + error.code === "receipt_expired" || + error.code === "cancelled" || + error.code === "superseded" || + error.code === "concurrent_writer" || + error.code === "causality_unverifiable" || + error.code === "turn_interrupted" || + error.code === "turn_error" + ) { + const terminalReceipt = releaseLeaseAndSnapshot(receipt); + throw turnReceiptError(error.code, terminalReceipt, error.evidence); } - if (initial === undefined) return; - if (initial.observation.sequence > input.afterSequence) { - yield initial.observation; + throw error; + } + if (error instanceof PollerError) { + if (error.code === "cancelled" || error.code === "closed") { + const terminalReceipt = releaseLeaseAndSnapshot(receipt); + throw turnReceiptError("cancelled", terminalReceipt, { reason: error.code }); } - const resumeFromSequence = initial.observation.sequence; - for await (const state of reconcileThread(session, threadId, { - seed: initial, - resumeFromSequence, - emitAfterSequence: Math.max(input.afterSequence, resumeFromSequence), - alignmentTimeoutMs, - })) { - yield state.observation; + if (error.code === "timeout") { + if (clock() >= receipt.leaseExpiresAt) { + const terminalReceipt = releaseLeaseAndSnapshot(receipt); + throw turnReceiptError("receipt_expired", terminalReceipt); + } + throw new StockRuntimeError("timeout", { receipt }); } - } finally { - closeQuietly(session); + throw new StockRuntimeError("transport_unavailable", { reason: error.code }); + } + throw mapReceivedError(error); + } + } + + return { + client, + spawn, + resumeCreateReconciliation, + send, + wait, + async observe(ref: AgentRef, operation: RuntimeOperationOptions = {}) { + const bounded = boundedOperation(operation); + const stopped = stopCode(bounded); + if (stopped !== null) throw new StockRuntimeError(stopped); + const environment = await descriptor({ deadlineMs: bounded.deadlineMs, signal: bounded.signal }); + if (environment.environmentId !== ref.environmentId) { + throw new StockRuntimeError("environment_changed", { + expectedEnvironmentId: ref.environmentId, + actualEnvironmentId: environment.environmentId, + }); + } + return client.getThread(ref.threadId, { + deadlineMs: bounded.deadlineMs, + signal: bounded.signal, + }); + }, + releaseReceipt(receipt: TurnReceipt): void { + const state = leases.get(scopedThreadKey(receipt.agentRef)); + if (state?.receipt.leaseId === receipt.leaseId) { + releaseLease(receipt.agentRef, receipt.leaseId); + } + }, + pollMetrics: () => poller.metrics(), + httpObservations: () => + client.observations?.() ?? { + requestCount: 0, + inFlight: 0, + peakInFlight: 0, + endpointStatusTrace: [], + }, + close(): void { + poller.close(); + for (const state of leases.values()) { + releaseLease(state.receipt.agentRef, state.receipt.leaseId); } }, }; } + +export const createT3NativeRuntime = createStockT3NativeRuntime; +export type T3NativeRuntime = ReturnType; diff --git a/src/protocol.ts b/src/protocol.ts deleted file mode 100644 index 8d8911a..0000000 --- a/src/protocol.ts +++ /dev/null @@ -1,170 +0,0 @@ -export type Header = readonly [name: string, value: string]; - -export interface RequestFrame { - readonly _tag: "Request"; - readonly id: string; - readonly tag: string; - readonly payload: unknown; - readonly headers: ReadonlyArray
; -} - -export interface AckFrame { - readonly _tag: "Ack"; - readonly requestId: string; -} - -export interface InterruptFrame { - readonly _tag: "Interrupt"; - readonly requestId: string; -} - -export interface SuccessfulResponse { - readonly _tag: "Success"; - readonly requestId: string; - readonly value: unknown; -} - -export interface FailedResponse { - readonly _tag: "Failure"; - readonly requestId: string; - readonly cause: ReadonlyArray; -} - -export interface ChunkResponse { - readonly _tag: "Chunk"; - readonly requestId: string; - readonly values: readonly [unknown, ...unknown[]]; -} - -export type ResponseFrame = - | SuccessfulResponse - | FailedResponse - | ChunkResponse; - -export class ProtocolError extends TypeError { - constructor(message: string) { - super(message); - this.name = "ProtocolError"; - } -} - -const NUMERIC_STRING = /^\d+$/; - -function requireRequestId(value: unknown): string { - if (typeof value !== "string" || !NUMERIC_STRING.test(value)) { - throw new ProtocolError("request ID must be a numeric string"); - } - return value; -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function hasOwn( - value: Record, - key: string, -): boolean { - return Object.prototype.hasOwnProperty.call(value, key); -} - -function malformedFrame(): never { - throw new ProtocolError("malformed Effect RPC server frame"); -} - -export function createRequestFrame(input: { - readonly id: string; - readonly tag: string; - readonly payload: unknown; - readonly headers: ReadonlyArray
; -}): RequestFrame { - const id = requireRequestId(input.id); - if (typeof input.tag !== "string" || input.tag.length === 0) { - throw new ProtocolError("request tag must be a non-empty string"); - } - if ( - !Array.isArray(input.headers) || - !input.headers.every( - (header) => - Array.isArray(header) && - header.length === 2 && - typeof header[0] === "string" && - typeof header[1] === "string", - ) - ) { - throw new ProtocolError("request headers must be string pairs"); - } - - return { - _tag: "Request", - id, - tag: input.tag, - payload: input.payload, - headers: input.headers.map(([name, value]) => [name, value]), - }; -} - -export function createAckFrame(requestId: string): AckFrame { - return { - _tag: "Ack", - requestId: requireRequestId(requestId), - }; -} - -export function createInterruptFrame(requestId: string): InterruptFrame { - return { - _tag: "Interrupt", - requestId: requireRequestId(requestId), - }; -} - -export function decodeResponseFrame(input: unknown): ResponseFrame { - if (!isRecord(input) || typeof input._tag !== "string") { - return malformedFrame(); - } - - if (input._tag === "Chunk") { - const requestId = requireRequestId(input.requestId); - if (!Array.isArray(input.values) || input.values.length === 0) { - return malformedFrame(); - } - return { - _tag: "Chunk", - requestId, - values: input.values as [unknown, ...unknown[]], - }; - } - - if (input._tag === "Exit") { - const requestId = requireRequestId(input.requestId); - if (!isRecord(input.exit) || typeof input.exit._tag !== "string") { - return malformedFrame(); - } - - if (input.exit._tag === "Success") { - if (!hasOwn(input.exit, "value")) { - return malformedFrame(); - } - return { - _tag: "Success", - requestId, - value: input.exit.value, - }; - } - - if (input.exit._tag === "Failure") { - if (!Array.isArray(input.exit.cause)) { - return malformedFrame(); - } - return { - _tag: "Failure", - requestId, - cause: input.exit.cause, - }; - } - - return malformedFrame(); - } - - throw new ProtocolError("unknown Effect RPC server frame"); -} diff --git a/src/stockProof.ts b/src/stockProof.ts new file mode 100644 index 0000000..94be86c --- /dev/null +++ b/src/stockProof.ts @@ -0,0 +1,317 @@ +export interface ExpectedProofIdentity { + readonly runId: string; + readonly candidateSha: string; +} + +export interface LiveProofEvidence { + readonly environmentId: string; + readonly serverVersion: string; + readonly endpointStatusTrace: readonly { + readonly method: string; + readonly path: string; + readonly status: number | null; + }[]; + readonly ids: { + readonly projectId: string; + readonly threadId: string; + readonly createCommandId: string; + readonly initialCommandId: string; + readonly initialMessageId: string; + readonly followupCommandId: string; + readonly followupMessageId: string; + }; + readonly sequences: { + readonly create: number; + readonly initial: number; + readonly followup: number; + }; + readonly counters: { + readonly requests: number; + readonly shellPolls: number; + readonly detailPolls: number; + readonly peakInFlight: number; + }; + readonly terminalKinds: readonly ["completed", "completed"]; + readonly timestamps: { readonly startedAt: string; readonly completedAt: string }; +} + +export interface StockProofBody extends ExpectedProofIdentity { + readonly stockSha: string; + readonly success: true; + readonly cleanBeforeBuild: true; + readonly artifactDigest: string; + readonly privateResolution: false; + readonly provenance: { + readonly stockInstall: CommandResult; + readonly stockBuild: CommandResult; + readonly candidateInstall: CommandResult; + readonly exactCharacterization: CommandResult; + readonly isolatedBasenames: readonly string[]; + }; + readonly exactHttpNegative: { + readonly status: 500; + readonly shellStatus: 200; + readonly detailStatus: 404; + readonly code: "internal_error"; + readonly reason: "orchestration_dispatch_failed"; + readonly threadAbsent: true; + }; + readonly live: LiveProofEvidence; + readonly teardown: { + readonly pidStopped: true; + readonly worktreeRemoved: true; + readonly rootRemoved: true; + }; +} + +interface CommandResult { + readonly command: string; + readonly status: 0; +} + +export interface ProvisionalProofRecord extends LiveProofEvidence { + readonly provisional: true; + readonly success: false; + readonly runId: string; +} + +export class ProofReceiptError extends TypeError { + readonly code = "invalid_proof_receipt" as const; + + constructor(readonly reason: string) { + super("invalid stock proof receipt"); + this.name = "ProofReceiptError"; + } +} + +const SHA40 = /^[0-9a-f]{40}$/; +const SHA256 = /^[0-9a-f]{64}$/; +const HTTP_METHOD = /^(GET|POST)$/; +const ALLOWED_PATH = /^(?:\/\.well-known\/t3\/environment|\/api\/orchestration\/(?:shell|dispatch|threads\/[^/?#]+))$/; +const EXPECTED_STOCK_SHA = "d3037064e61a9f059eafbd4f9869679779bd2a7c"; +const EXPECTED_COMMANDS = Object.freeze({ + stock_install: "corepack pnpm install --frozen-lockfile", + stock_build: "corepack pnpm --filter t3 build:bundle", + candidate_install: "bun install --frozen-lockfile", + exact_characterization: + "corepack pnpm --filter t3 exec vp test run src/orchestration/Layers/T3LayerStockProjectionCharacterization.generated.test.ts", +}); +const EXPECTED_ISOLATED_BASENAMES = Object.freeze([ + "stock-tree", + "t3layer-clean", + "server-home", + "workspace", +]); + +function record(value: unknown, reason = "not_object"): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new ProofReceiptError(reason); + } + return value as Record; +} + +function textField(input: Record, key: string): string { + const value = input[key]; + if (typeof value !== "string" || value.length === 0) throw new ProofReceiptError(key); + return value; +} + +function integerField(input: Record, key: string): number { + const value = input[key]; + if (!Number.isSafeInteger(value) || (value as number) < 0) throw new ProofReceiptError(key); + return value as number; +} + +function assertNoSecretMaterial(value: unknown, path = "receipt"): void { + if (typeof value === "string") { + if (/op:\/\/|bearer\s|api[_-]?key|token/i.test(value)) { + throw new ProofReceiptError(`secret_material:${path}`); + } + return; + } + if (Array.isArray(value)) { + value.forEach((entry, index) => assertNoSecretMaterial(entry, `${path}[${index}]`)); + return; + } + if (typeof value === "object" && value !== null) { + for (const [key, entry] of Object.entries(value)) { + if (/secret|token|prompt|response|authorization|body|header|log/i.test(key)) { + throw new ProofReceiptError(`forbidden_key:${path}.${key}`); + } + assertNoSecretMaterial(entry, `${path}.${key}`); + } + } +} + +function validateLive(value: unknown): LiveProofEvidence { + const live = record(value, "live"); + textField(live, "environmentId"); + textField(live, "serverVersion"); + if (!Array.isArray(live.endpointStatusTrace) || live.endpointStatusTrace.length === 0) { + throw new ProofReceiptError("endpoint_status_trace"); + } + for (const rawEntry of live.endpointStatusTrace) { + const entry = record(rawEntry, "endpoint_trace_entry"); + const method = textField(entry, "method"); + const path = textField(entry, "path"); + if (!HTTP_METHOD.test(method) || !ALLOWED_PATH.test(path)) { + throw new ProofReceiptError("endpoint_trace_value"); + } + if (entry.status !== null && (!Number.isSafeInteger(entry.status) || (entry.status as number) < 100 || (entry.status as number) > 599)) { + throw new ProofReceiptError("endpoint_trace_status"); + } + } + const trace = live.endpointStatusTrace as readonly { + readonly method: string; + readonly path: string; + readonly status: number | null; + }[]; + const shellRequests = trace.filter( + (entry) => entry.method === "GET" && entry.path === "/api/orchestration/shell" && entry.status === 200, + ).length; + const detailRequests = trace.filter( + (entry) => entry.method === "GET" && entry.path.startsWith("/api/orchestration/threads/") && entry.status === 200, + ).length; + const dispatchRequests = trace.filter( + (entry) => entry.method === "POST" && entry.path === "/api/orchestration/dispatch" && entry.status === 200, + ).length; + if (shellRequests === 0 || detailRequests === 0 || dispatchRequests < 3) { + throw new ProofReceiptError("endpoint_trace_coverage"); + } + const ids = record(live.ids, "ids"); + const idKeys = ["projectId", "threadId", "createCommandId", "initialCommandId", "initialMessageId", "followupCommandId", "followupMessageId"]; + const idValues = idKeys.map((key) => textField(ids, key)); + if (new Set(idValues).size !== idValues.length) throw new ProofReceiptError("ids_not_distinct"); + const sequences = record(live.sequences, "sequences"); + const create = integerField(sequences, "create"); + const initial = integerField(sequences, "initial"); + const followup = integerField(sequences, "followup"); + if (!(create < initial && initial < followup)) throw new ProofReceiptError("sequence_order"); + const counters = record(live.counters, "counters"); + for (const key of ["requests", "shellPolls", "detailPolls", "peakInFlight"]) integerField(counters, key); + const requests = counters.requests as number; + const shellPolls = counters.shellPolls as number; + const detailPolls = counters.detailPolls as number; + const peakInFlight = counters.peakInFlight as number; + if (requests === 0 || shellPolls === 0 || detailPolls === 0 || peakInFlight === 0) { + throw new ProofReceiptError("empty_counters"); + } + if ( + requests !== trace.length || + shellPolls > shellRequests || + detailPolls > detailRequests || + peakInFlight > 8 || + peakInFlight > requests + ) { + throw new ProofReceiptError("inconsistent_counters"); + } + if (!Array.isArray(live.terminalKinds) || live.terminalKinds.length !== 2 || live.terminalKinds.some((entry) => entry !== "completed")) { + throw new ProofReceiptError("terminal_kinds"); + } + const timestamps = record(live.timestamps, "timestamps"); + const startedAt = textField(timestamps, "startedAt"); + const completedAt = textField(timestamps, "completedAt"); + if (!Number.isFinite(Date.parse(startedAt)) || !Number.isFinite(Date.parse(completedAt)) || Date.parse(completedAt) < Date.parse(startedAt)) { + throw new ProofReceiptError("timestamps"); + } + return structuredClone(live) as unknown as LiveProofEvidence; +} + +export function canonicalProvisionalProof(value: unknown, expectedRunId: string): ProvisionalProofRecord { + const input = record(value); + if (input.provisional !== true || input.success !== false || input.runId !== expectedRunId) { + throw new ProofReceiptError("provisional_identity"); + } + validateLive(input); + assertNoSecretMaterial(input); + return structuredClone(input) as unknown as ProvisionalProofRecord; +} + +function commandResult( + value: unknown, + key: keyof typeof EXPECTED_COMMANDS, +): void { + const result = record(value, key); + if (result.command !== EXPECTED_COMMANDS[key]) { + throw new ProofReceiptError(`${key}_command`); + } + if (result.status !== 0) throw new ProofReceiptError(`${key}_status`); +} + +export function canonicalProofBody(value: unknown): StockProofBody { + const input = record(value); + if (typeof input.runId !== "string" || input.runId.length < 8) throw new ProofReceiptError("run_id"); + if (typeof input.candidateSha !== "string" || !SHA40.test(input.candidateSha)) throw new ProofReceiptError("candidate_sha"); + if (input.stockSha !== EXPECTED_STOCK_SHA) throw new ProofReceiptError("stock_sha"); + if (input.success !== true) throw new ProofReceiptError("not_success"); + if (input.cleanBeforeBuild !== true) throw new ProofReceiptError("not_clean_before_build"); + if (typeof input.artifactDigest !== "string" || !SHA256.test(input.artifactDigest)) throw new ProofReceiptError("artifact_digest"); + if (input.privateResolution !== false) throw new ProofReceiptError("private_resolution"); + const provenance = record(input.provenance, "provenance"); + commandResult(provenance.stockInstall, "stock_install"); + commandResult(provenance.stockBuild, "stock_build"); + commandResult(provenance.candidateInstall, "candidate_install"); + commandResult(provenance.exactCharacterization, "exact_characterization"); + if ( + !Array.isArray(provenance.isolatedBasenames) || + provenance.isolatedBasenames.length !== EXPECTED_ISOLATED_BASENAMES.length || + provenance.isolatedBasenames.some( + (entry, index) => entry !== EXPECTED_ISOLATED_BASENAMES[index], + ) + ) { + throw new ProofReceiptError("isolated_basenames"); + } + const negative = record(input.exactHttpNegative, "exact_http_negative"); + if (negative.status !== 500 || negative.shellStatus !== 200 || negative.detailStatus !== 404 || negative.code !== "internal_error" || negative.reason !== "orchestration_dispatch_failed" || negative.threadAbsent !== true) { + throw new ProofReceiptError("exact_http_negative"); + } + validateLive(input.live); + const teardown = record(input.teardown, "teardown"); + if (teardown.pidStopped !== true || teardown.worktreeRemoved !== true || teardown.rootRemoved !== true) { + throw new ProofReceiptError("teardown_incomplete"); + } + assertNoSecretMaterial(input); + return structuredClone(input) as unknown as StockProofBody; +} + +export function validateProofReceipt(value: unknown, expected: ExpectedProofIdentity): StockProofBody { + const receipt = canonicalProofBody(value); + if (receipt.runId !== expected.runId || receipt.candidateSha !== expected.candidateSha) { + throw new ProofReceiptError("expected_identity_mismatch"); + } + return receipt; +} + +function canonical(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`; + if (typeof value === "object" && value !== null) { + const input = value as Record; + return `{${Object.keys(input).sort().map((key) => `${JSON.stringify(key)}:${canonical(input[key])}`).join(",")}}`; + } + return JSON.stringify(value); +} + +export function canonicalProofEnvelopeJson(bodyValue: unknown, checksum: string): string { + const body = canonicalProofBody(bodyValue); + if (!SHA256.test(checksum)) throw new ProofReceiptError("checksum"); + return `${canonical({ ...body, checksum })}\n`; +} + +export function canonicalProofJson(value: unknown): string { + return `${canonical(canonicalProofBody(value))}\n`; +} + +export async function proofChecksum(value: unknown): Promise { + const bytes = new TextEncoder().encode(canonicalProofJson(value)); + const hashed = await crypto.subtle.digest("SHA-256", bytes); + return [...new Uint8Array(hashed)].map((entry) => entry.toString(16).padStart(2, "0")).join(""); +} + +export async function validateProofEnvelope(value: unknown, expected: ExpectedProofIdentity): Promise { + const envelope = record(value); + if (typeof envelope.checksum !== "string" || !SHA256.test(envelope.checksum)) throw new ProofReceiptError("checksum"); + const { checksum, ...bodyValue } = envelope; + const body = validateProofReceipt(bodyValue, expected); + if ((await proofChecksum(body)) !== checksum) throw new ProofReceiptError("checksum_mismatch"); + return body; +} diff --git a/src/stockT3Contracts.ts b/src/stockT3Contracts.ts new file mode 100644 index 0000000..431046b --- /dev/null +++ b/src/stockT3Contracts.ts @@ -0,0 +1,367 @@ +export type ConnectionProfile = "local" | "relay" | "tunnel"; + +export class ProtocolMismatchError extends TypeError { + readonly code = "protocol_mismatch" as const; + + constructor(readonly path: string) { + super(`stock T3 protocol mismatch at ${path}`); + this.name = "ProtocolMismatchError"; + } +} + +type JsonObject = Record; + +function object(value: unknown, path: string): JsonObject { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new ProtocolMismatchError(path); + } + return value as JsonObject; +} + +function string(value: unknown, path: string, allowEmpty = false): string { + if (typeof value !== "string" || (!allowEmpty && value.trim().length === 0)) { + throw new ProtocolMismatchError(path); + } + return value; +} + +function boolean(value: unknown, path: string): boolean { + if (typeof value !== "boolean") throw new ProtocolMismatchError(path); + return value; +} + +function integer(value: unknown, path: string): number { + if (!Number.isSafeInteger(value) || (value as number) < 0) { + throw new ProtocolMismatchError(path); + } + return value as number; +} + +function nullableString(value: unknown, path: string): string | null { + return value === null ? null : string(value, path); +} + +function optionalNullableString(value: unknown, path: string): string | null | undefined { + return value === undefined ? undefined : nullableString(value, path); +} + +function iso(value: unknown, path: string): string { + const decoded = string(value, path); + if (!Number.isFinite(Date.parse(decoded))) throw new ProtocolMismatchError(path); + return decoded; +} + +function array(value: unknown, path: string, decode: (entry: unknown, path: string) => T): T[] { + if (!Array.isArray(value)) throw new ProtocolMismatchError(path); + return value.map((entry, index) => decode(entry, `${path}[${index}]`)); +} + +export interface EnvironmentDescriptor { + readonly environmentId: string; + readonly label: string; + readonly platform: { readonly os: "darwin" | "linux" | "windows" | "unknown"; readonly arch: "arm64" | "x64" | "other" }; + readonly serverVersion: string; + readonly capabilities: { readonly repositoryIdentity: boolean }; +} + +export interface StockModelSelection { + readonly instanceId: string; + readonly model: string; + readonly options?: readonly unknown[]; +} + +export interface StockProjectShell { + readonly id: string; + readonly title: string; + readonly workspaceRoot: string; + readonly defaultModelSelection: StockModelSelection | null; + readonly createdAt: string; + readonly updatedAt: string; +} + +export interface StockLatestTurn { + readonly turnId: string; + readonly state: "running" | "interrupted" | "completed" | "error"; + readonly requestedAt: string; + readonly startedAt: string | null; + readonly completedAt: string | null; + readonly assistantMessageId: string | null; +} + +export interface StockSession { + readonly threadId: string; + readonly status: "idle" | "starting" | "running" | "ready" | "interrupted" | "stopped" | "error"; + readonly providerName: string | null; + readonly activeTurnId: string | null; + readonly lastError: string | null; + readonly updatedAt: string; +} + +export interface StockThreadIdentity { + readonly id: string; + readonly projectId: string; + readonly title: string; + readonly modelSelection: StockModelSelection; + readonly runtimeMode: "approval-required" | "auto-accept-edits" | "auto" | "full-access"; + readonly interactionMode: "default" | "plan"; + readonly branch: string | null; + readonly worktreePath: string | null; + readonly latestTurn: StockLatestTurn | null; + readonly createdAt: string; + readonly updatedAt: string; + readonly session: StockSession | null; +} + +export interface StockThreadShell extends StockThreadIdentity { + readonly latestUserMessageAt: string | null; + readonly hasPendingApprovals: boolean; + readonly hasPendingUserInput: boolean; +} + +export interface StockMessage { + readonly id: string; + readonly role: "user" | "assistant" | "system"; + readonly text: string; + readonly attachments: readonly unknown[]; + readonly turnId: string | null; + readonly streaming: boolean; + readonly createdAt: string; + readonly updatedAt: string; +} + +export interface StockThreadDetail extends StockThreadIdentity { + readonly messages: readonly StockMessage[]; + readonly activities: readonly unknown[]; + readonly checkpoints: readonly unknown[]; +} + +export interface ShellSnapshot { + readonly snapshotSequence: number; + readonly projects: readonly StockProjectShell[]; + readonly threads: readonly StockThreadShell[]; + readonly updatedAt: string; +} + +export interface ThreadDetailSnapshot { + readonly snapshotSequence: number; + readonly thread: StockThreadDetail; +} + +function literal(value: unknown, allowed: readonly T[], path: string): T { + if (typeof value !== "string" || !allowed.includes(value as T)) { + throw new ProtocolMismatchError(path); + } + return value as T; +} + +function decodeModelSelection(value: unknown, path: string): StockModelSelection { + const input = object(value, path); + const result: StockModelSelection = { + instanceId: string(input.instanceId ?? input.provider, `${path}.instanceId`), + model: string(input.model, `${path}.model`), + ...(input.options === undefined + ? {} + : { options: array(input.options, `${path}.options`, (entry) => entry) }), + }; + return result; +} + +function decodeLatestTurn(value: unknown, path: string): StockLatestTurn | null { + if (value === null) return null; + const input = object(value, path); + return { + turnId: string(input.turnId, `${path}.turnId`), + state: literal(input.state, ["running", "interrupted", "completed", "error"], `${path}.state`), + requestedAt: iso(input.requestedAt, `${path}.requestedAt`), + startedAt: input.startedAt === null ? null : iso(input.startedAt, `${path}.startedAt`), + completedAt: input.completedAt === null ? null : iso(input.completedAt, `${path}.completedAt`), + assistantMessageId: nullableString(input.assistantMessageId, `${path}.assistantMessageId`), + }; +} + +function decodeSession(value: unknown, path: string): StockSession | null { + if (value === null) return null; + const input = object(value, path); + return { + threadId: string(input.threadId, `${path}.threadId`), + status: literal(input.status, ["idle", "starting", "running", "ready", "interrupted", "stopped", "error"], `${path}.status`), + providerName: nullableString(input.providerName, `${path}.providerName`), + activeTurnId: nullableString(input.activeTurnId, `${path}.activeTurnId`), + lastError: nullableString(input.lastError, `${path}.lastError`), + updatedAt: iso(input.updatedAt, `${path}.updatedAt`), + }; +} + +function decodeThreadIdentity(input: JsonObject, path: string): StockThreadIdentity { + return { + id: string(input.id, `${path}.id`), + projectId: string(input.projectId, `${path}.projectId`), + title: string(input.title, `${path}.title`), + modelSelection: decodeModelSelection(input.modelSelection, `${path}.modelSelection`), + runtimeMode: literal(input.runtimeMode, ["approval-required", "auto-accept-edits", "auto", "full-access"], `${path}.runtimeMode`), + interactionMode: literal(input.interactionMode ?? "default", ["default", "plan"], `${path}.interactionMode`), + branch: nullableString(input.branch, `${path}.branch`), + worktreePath: nullableString(input.worktreePath, `${path}.worktreePath`), + latestTurn: decodeLatestTurn(input.latestTurn, `${path}.latestTurn`), + createdAt: iso(input.createdAt, `${path}.createdAt`), + updatedAt: iso(input.updatedAt, `${path}.updatedAt`), + session: decodeSession(input.session, `${path}.session`), + }; +} + +function assertSequence(sequence: number, minimumSequence: number | undefined, path: string): void { + if (minimumSequence !== undefined && sequence < minimumSequence) { + throw new ProtocolMismatchError(path); + } +} + +export function decodeDescriptor(value: unknown): EnvironmentDescriptor { + const input = object(value, "descriptor"); + const platform = object(input.platform, "descriptor.platform"); + const capabilities = object(input.capabilities, "descriptor.capabilities"); + return { + environmentId: string(input.environmentId, "descriptor.environmentId"), + label: string(input.label, "descriptor.label"), + platform: { + os: literal(platform.os, ["darwin", "linux", "windows", "unknown"], "descriptor.platform.os"), + arch: literal(platform.arch, ["arm64", "x64", "other"], "descriptor.platform.arch"), + }, + serverVersion: string(input.serverVersion, "descriptor.serverVersion"), + capabilities: { + repositoryIdentity: + capabilities.repositoryIdentity === undefined + ? false + : boolean(capabilities.repositoryIdentity, "descriptor.capabilities.repositoryIdentity"), + }, + }; +} + +export function decodeDispatchResult(value: unknown): { readonly sequence: number } { + const input = object(value, "dispatch"); + return { sequence: integer(input.sequence, "dispatch.sequence") }; +} + +function decodeProject(value: unknown, path: string): StockProjectShell { + const input = object(value, path); + return { + id: string(input.id, `${path}.id`), + title: string(input.title, `${path}.title`), + workspaceRoot: string(input.workspaceRoot, `${path}.workspaceRoot`), + defaultModelSelection: + input.defaultModelSelection === null + ? null + : decodeModelSelection(input.defaultModelSelection, `${path}.defaultModelSelection`), + createdAt: iso(input.createdAt, `${path}.createdAt`), + updatedAt: iso(input.updatedAt, `${path}.updatedAt`), + }; +} + +function decodeThreadShell(value: unknown, path: string): StockThreadShell { + const input = object(value, path); + return { + ...decodeThreadIdentity(input, path), + latestUserMessageAt: + input.latestUserMessageAt === null ? null : iso(input.latestUserMessageAt, `${path}.latestUserMessageAt`), + hasPendingApprovals: boolean(input.hasPendingApprovals, `${path}.hasPendingApprovals`), + hasPendingUserInput: boolean(input.hasPendingUserInput, `${path}.hasPendingUserInput`), + }; +} + +export function decodeShellSnapshot( + value: unknown, + options: { readonly minimumSequence?: number } = {}, +): ShellSnapshot { + const input = object(value, "shell"); + const snapshotSequence = integer(input.snapshotSequence, "shell.snapshotSequence"); + assertSequence(snapshotSequence, options.minimumSequence, "shell.snapshotSequence"); + return { + snapshotSequence, + projects: array(input.projects, "shell.projects", decodeProject), + threads: array(input.threads, "shell.threads", decodeThreadShell), + updatedAt: iso(input.updatedAt, "shell.updatedAt"), + }; +} + +function decodeMessage(value: unknown, path: string): StockMessage { + const input = object(value, path); + return { + id: string(input.id, `${path}.id`), + role: literal(input.role, ["user", "assistant", "system"], `${path}.role`), + text: string(input.text, `${path}.text`, true), + attachments: + input.attachments === undefined + ? [] + : array(input.attachments, `${path}.attachments`, (entry) => entry), + turnId: nullableString(input.turnId, `${path}.turnId`), + streaming: boolean(input.streaming, `${path}.streaming`), + createdAt: iso(input.createdAt, `${path}.createdAt`), + updatedAt: iso(input.updatedAt, `${path}.updatedAt`), + }; +} + +export function decodeThreadDetailSnapshot( + value: unknown, + options: { readonly minimumSequence?: number } = {}, +): ThreadDetailSnapshot { + const input = object(value, "detail"); + const snapshotSequence = integer(input.snapshotSequence, "detail.snapshotSequence"); + assertSequence(snapshotSequence, options.minimumSequence, "detail.snapshotSequence"); + const threadInput = object(input.thread, "detail.thread"); + return { + snapshotSequence, + thread: { + ...decodeThreadIdentity(threadInput, "detail.thread"), + messages: array(threadInput.messages, "detail.thread.messages", decodeMessage), + activities: array(threadInput.activities, "detail.thread.activities", (entry) => entry), + checkpoints: array(threadInput.checkpoints, "detail.thread.checkpoints", (entry) => entry), + }, + }; +} + +export type SanitizedDispatchError = + | { readonly status: 400; readonly class: "command_rejected"; readonly code: "invalid_request"; readonly reason: "invalid_command" } + | { readonly status: 401; readonly class: "authentication_failed"; readonly code: "auth_invalid"; readonly reason: "missing_credential" | "invalid_credential" } + | { readonly status: 403; readonly class: "permission_denied"; readonly code: "insufficient_scope"; readonly reason: null } + | { readonly status: 500; readonly class: "server_internal"; readonly code: "internal_error"; readonly reason: "orchestration_dispatch_failed" }; + +export function decodeDispatchError(status: number, value: unknown): SanitizedDispatchError { + const input = object(value, "dispatchError"); + if (status === 400 && input.code === "invalid_request" && input.reason === "invalid_command") { + return { status, class: "command_rejected", code: input.code, reason: input.reason }; + } + if ( + status === 401 && + input.code === "auth_invalid" && + (input.reason === "missing_credential" || input.reason === "invalid_credential") + ) { + return { status, class: "authentication_failed", code: input.code, reason: input.reason }; + } + if (status === 403 && input.code === "insufficient_scope") { + return { status, class: "permission_denied", code: input.code, reason: null }; + } + if ( + status === 500 && + input.code === "internal_error" && + input.reason === "orchestration_dispatch_failed" + ) { + return { status, class: "server_internal", code: input.code, reason: input.reason }; + } + throw new ProtocolMismatchError("dispatchError"); +} + +export function decodeTokenResult(value: unknown): { + readonly accessToken: string; + readonly tokenType: "Bearer"; + readonly expiresIn: number; +} { + const input = object(value, "token"); + return { + accessToken: string(input.accessToken, "token.accessToken"), + tokenType: literal(input.tokenType, ["Bearer"], "token.tokenType"), + expiresIn: integer(input.expiresIn, "token.expiresIn"), + }; +} + +export function nullableOptional(value: unknown, path: string): string | null | undefined { + return optionalNullableString(value, path); +} diff --git a/src/stockT3HttpClient.ts b/src/stockT3HttpClient.ts new file mode 100644 index 0000000..8a6c2fb --- /dev/null +++ b/src/stockT3HttpClient.ts @@ -0,0 +1,338 @@ +import { + ProtocolMismatchError, + type ConnectionProfile, + type EnvironmentDescriptor, + type SanitizedDispatchError, + type ShellSnapshot, + type ThreadDetailSnapshot, + decodeDescriptor, + decodeDispatchError, + decodeDispatchResult, + decodeShellSnapshot, + decodeThreadDetailSnapshot, + decodeTokenResult, +} from "./stockT3Contracts"; + +export type StockT3HttpErrorCode = + | SanitizedDispatchError["class"] + | "not_found" + | "transport_unavailable" + | "protocol_mismatch"; + +export class StockT3HttpError extends Error { + constructor( + readonly code: StockT3HttpErrorCode, + readonly status: number | null, + readonly detail: Readonly> = {}, + ) { + super(code); + this.name = "StockT3HttpError"; + } + + toJSON(): Readonly> { + return { name: this.name, code: this.code, status: this.status, detail: this.detail }; + } +} + +export type FetchLike = ( + input: string | URL | Request, + init?: RequestInit, +) => Promise; + +export interface StockT3HttpClientOptions { + readonly baseUrl: string | URL; + readonly bearerToken?: string; + readonly fetch?: FetchLike; + readonly connectionProfile?: ConnectionProfile; + readonly clock?: () => number; + readonly setTimer?: (callback: () => void, milliseconds: number) => unknown; + readonly clearTimer?: (timer: unknown) => void; +} + +export interface RequestBoundaryOptions { + readonly deadlineMs?: number; + readonly signal?: AbortSignal; + readonly minimumSequence?: number; +} + +export interface EndpointStatusTrace { + readonly method: string; + readonly path: string; + readonly status: number | null; +} + +function normalizeBaseUrl(value: string | URL): URL { + const url = new URL(value); + if (url.protocol !== "http:" && url.protocol !== "https:") { + throw new TypeError("stock T3 base URL must use http or https"); + } + url.pathname = `${url.pathname.replace(/\/+$/, "")}/`; + url.search = ""; + url.hash = ""; + return url; +} + +function requestBudget(profile: ConnectionProfile): number { + return profile === "local" ? 5_000 : 15_000; +} + +const MAX_HTTP_IN_FLIGHT = 8; + +function linkedAttemptSignal( + external: AbortSignal | undefined, + timeoutMs: number, + setTimer: (callback: () => void, milliseconds: number) => unknown, + clearTimer: (timer: unknown) => void, +): { readonly signal: AbortSignal; readonly cleanup: () => void } { + const controller = new AbortController(); + const onAbort = () => controller.abort(external?.reason); + if (external?.aborted) controller.abort(external.reason); + else external?.addEventListener("abort", onAbort, { once: true }); + const timer = setTimer( + () => controller.abort(new DOMException("request deadline exceeded", "TimeoutError")), + Math.max(0, timeoutMs), + ); + return { + signal: controller.signal, + cleanup: () => { + clearTimer(timer); + external?.removeEventListener("abort", onAbort); + }, + }; +} + +export function createStockT3HttpClient(options: StockT3HttpClientOptions) { + const baseUrl = normalizeBaseUrl(options.baseUrl); + const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis); + const profile = options.connectionProfile ?? "local"; + const clock = options.clock ?? Date.now; + const setTimer = options.setTimer ?? ((callback, milliseconds) => setTimeout(callback, milliseconds)); + const clearTimer = options.clearTimer ?? ((timer) => clearTimeout(timer as ReturnType)); + const endpointStatusTrace: EndpointStatusTrace[] = []; + let requestCount = 0; + let inFlight = 0; + let peakInFlight = 0; + const capacityWaiters: Array<() => void> = []; + + async function acquireCapacity(boundary: RequestBoundaryOptions): Promise<() => void> { + if (boundary.signal?.aborted) { + throw new StockT3HttpError("transport_unavailable", null, { reason: "cancelled" }); + } + if (inFlight >= MAX_HTTP_IN_FLIGHT) { + await new Promise((resolve, reject) => { + const onAbort = () => { + const index = capacityWaiters.indexOf(resume); + if (index >= 0) capacityWaiters.splice(index, 1); + reject(new StockT3HttpError("transport_unavailable", null, { reason: "cancelled" })); + }; + const resume = () => { + boundary.signal?.removeEventListener("abort", onAbort); + resolve(); + }; + capacityWaiters.push(resume); + boundary.signal?.addEventListener("abort", onAbort, { once: true }); + }); + } + if ( + boundary.signal?.aborted || + (boundary.deadlineMs !== undefined && clock() >= boundary.deadlineMs) + ) { + throw new StockT3HttpError("transport_unavailable", null, { + reason: boundary.signal?.aborted ? "cancelled" : "deadline", + }); + } + inFlight += 1; + peakInFlight = Math.max(peakInFlight, inFlight); + let released = false; + return () => { + if (released) return; + released = true; + inFlight -= 1; + capacityWaiters.shift()?.(); + }; + } + + async function requestJson( + path: string, + init: RequestInit, + boundary: RequestBoundaryOptions, + authenticated: boolean, + ): Promise<{ readonly response: Response; readonly body: unknown }> { + const remaining = + boundary.deadlineMs === undefined + ? requestBudget(profile) + : Math.max(0, boundary.deadlineMs - clock()); + const timeoutMs = Math.min(requestBudget(profile), remaining); + if (timeoutMs <= 0 || boundary.signal?.aborted) { + throw new StockT3HttpError("transport_unavailable", null, { reason: "deadline_or_cancelled" }); + } + const attempt = linkedAttemptSignal(boundary.signal, timeoutMs, setTimer, clearTimer); + const method = init.method ?? "GET"; + requestCount += 1; + const releaseCapacity = await acquireCapacity(boundary); + const headers = new Headers(init.headers); + headers.set("accept", "application/json"); + if (init.body !== undefined && init.body !== null) headers.set("content-type", "application/json"); + if (authenticated && options.bearerToken !== undefined) { + headers.set("authorization", `Bearer ${options.bearerToken}`); + } + try { + const response = await fetchImpl(new URL(path, baseUrl), { ...init, headers, signal: attempt.signal }); + endpointStatusTrace.push({ method, path, status: response.status }); + let body: unknown; + try { + const text = await response.text(); + body = text.length === 0 ? undefined : JSON.parse(text); + } catch { + if (response.ok) { + throw new StockT3HttpError("protocol_mismatch", response.status, { + reason: "invalid_json", + }); + } + body = undefined; + } + return { response, body }; + } catch (error) { + if (error instanceof StockT3HttpError) throw error; + if (error instanceof ProtocolMismatchError) { + throw new StockT3HttpError("protocol_mismatch", null, { path: error.path }); + } + endpointStatusTrace.push({ method, path, status: null }); + throw new StockT3HttpError("transport_unavailable", null, { + reason: boundary.signal?.aborted ? "cancelled" : "request_failed", + }); + } finally { + releaseCapacity(); + attempt.cleanup(); + } + } + + function decodeOrProtocol(operation: () => T, status: number | null): T { + try { + return operation(); + } catch (error) { + if (error instanceof ProtocolMismatchError) { + throw new StockT3HttpError("protocol_mismatch", status, { path: error.path }); + } + throw error; + } + } + + function retryAfterMs(response: Response): number { + const value = response.headers.get("retry-after"); + if (value === null) return 0; + const seconds = Number(value); + if (Number.isFinite(seconds) && seconds >= 0) return Math.min(8_000, seconds * 1_000); + const at = Date.parse(value); + return Number.isFinite(at) ? Math.min(8_000, Math.max(0, at - clock())) : 0; + } + + function receivedHttpError(response: Response): StockT3HttpError { + if (response.status === 401) { + return new StockT3HttpError("authentication_failed", 401); + } + if (response.status === 403) { + return new StockT3HttpError("permission_denied", 403); + } + if (response.status === 500) { + return new StockT3HttpError("server_internal", 500); + } + if ([429, 502, 503, 504].includes(response.status)) { + return new StockT3HttpError("transport_unavailable", response.status, { + transient: true, + retryAfterMs: retryAfterMs(response), + }); + } + return new StockT3HttpError("protocol_mismatch", response.status, { + reason: "unexpected_http_status", + }); + } + + return { + baseUrl, + connectionProfile: profile, + observations() { + return { + requestCount, + inFlight, + peakInFlight, + endpointStatusTrace: endpointStatusTrace.map((entry) => ({ ...entry })), + }; + }, + + async getDescriptor(boundary: RequestBoundaryOptions = {}): Promise { + const { response, body } = await requestJson( + "/.well-known/t3/environment", + { method: "GET" }, + boundary, + false, + ); + if (!response.ok) throw receivedHttpError(response); + return decodeOrProtocol(() => decodeDescriptor(body), response.status); + }, + + async getShell(boundary: RequestBoundaryOptions = {}): Promise { + const { response, body } = await requestJson( + "/api/orchestration/shell", + { method: "GET" }, + boundary, + true, + ); + if (!response.ok) throw receivedHttpError(response); + return decodeOrProtocol( + () => decodeShellSnapshot(body, { minimumSequence: boundary.minimumSequence }), + response.status, + ); + }, + + async getThread( + threadId: string, + boundary: RequestBoundaryOptions = {}, + ): Promise { + const path = `/api/orchestration/threads/${encodeURIComponent(threadId)}`; + const { response, body } = await requestJson(path, { method: "GET" }, boundary, true); + if (response.status === 404) return undefined; + if (!response.ok) throw receivedHttpError(response); + return decodeOrProtocol( + () => decodeThreadDetailSnapshot(body, { minimumSequence: boundary.minimumSequence }), + response.status, + ); + }, + + async dispatch( + command: Readonly>, + boundary: RequestBoundaryOptions = {}, + ): Promise<{ readonly sequence: number }> { + const { response, body } = await requestJson( + "/api/orchestration/dispatch", + { method: "POST", body: JSON.stringify(command) }, + boundary, + true, + ); + if (!response.ok) { + const decoded = decodeOrProtocol(() => decodeDispatchError(response.status, body), response.status); + throw new StockT3HttpError(decoded.class, decoded.status, { + code: decoded.code, + reason: decoded.reason, + }); + } + return decodeOrProtocol(() => decodeDispatchResult(body), response.status); + }, + + async exchangeToken( + payload: Readonly>, + boundary: RequestBoundaryOptions = {}, + ): Promise<{ readonly accessToken: string; readonly tokenType: "Bearer"; readonly expiresIn: number }> { + const { response, body } = await requestJson( + "/oauth/token", + { method: "POST", body: JSON.stringify(payload) }, + boundary, + false, + ); + if (!response.ok) throw receivedHttpError(response); + return decodeOrProtocol(() => decodeTokenResult(body), response.status); + }, + }; +} + +export type StockT3HttpClient = ReturnType; diff --git a/test/adaptive-poller.test.ts b/test/adaptive-poller.test.ts new file mode 100644 index 0000000..8dc60a2 --- /dev/null +++ b/test/adaptive-poller.test.ts @@ -0,0 +1,422 @@ +import { describe, expect, test } from "bun:test"; + +import { createAdaptivePoller } from "../src/adaptivePoller"; +import { StockT3HttpError } from "../src/stockT3HttpClient"; + +function fakeClock(start = 0) { + let current = start; + const sleeps: number[] = []; + return { + now: () => current, + set: (value: number) => { + current = value; + }, + sleeps, + sleep: async (milliseconds: number, signal: AbortSignal) => { + if (signal.aborted) throw signal.reason; + sleeps.push(milliseconds); + current += milliseconds; + await Promise.resolve(); + }, + }; +} + +const emptyDetail = { + snapshotSequence: 1, + thread: { + id: "thread-1", + projectId: "project-1", + title: "worker", + modelSelection: { instanceId: "claudeAgent", model: "claude-opus-5" }, + runtimeMode: "full-access" as const, + interactionMode: "default" as const, + branch: null, + worktreePath: null, + latestTurn: null, + createdAt: "2026-07-31T18:00:00.000Z", + updatedAt: "2026-07-31T18:00:00.000Z", + session: null, + messages: [], + activities: [], + checkpoints: [], + }, +}; + +describe("environment-coalesced adaptive poller", () => { + test("coalesces eight waiters onto one shell request per cycle", async () => { + let shellStarts = 0; + const poller = createAdaptivePoller({ + getShell: async () => { + shellStarts += 1; + return { snapshotSequence: shellStarts, projects: [], threads: [], updatedAt: new Date().toISOString() }; + }, + getThread: async () => undefined, + }); + + const waiters = Array.from({ length: 8 }, (_, index) => + poller.waitFor({ + environmentId: "env-1", + threadId: `thread-${index}`, + deadlineMs: Date.now() + 2_000, + evaluate: ({ shell }) => + shell.snapshotSequence >= 2 ? { done: true, value: shell.snapshotSequence } : { done: false }, + }), + ); + + expect(await Promise.all(waiters)).toEqual(Array(8).fill(2)); + expect(shellStarts).toBe(2); + expect(poller.metrics()).toMatchObject({ shellStarts: 2, peakActiveWaits: 8, peakHttpInFlight: 1 }); + poller.close(); + }); + + test("uses 250/500/1000/2000 cadence and bounds first/later minute starts", () => { + const policy = createAdaptivePoller.policy(); + expect([0, 1, 2, 3, 4, 5].map((attempt) => policy.intervalMs(attempt))).toEqual([ + 250, 500, 1_000, 2_000, 2_000, 2_000, + ]); + expect(policy.firstMinuteShellStarts).toBe(32); + expect(policy.laterMinuteShellStarts).toBe(30); + expect(policy.detailStartsPerWaitMinute).toBe(4); + expect(policy.maxActiveWaits).toBe(8); + expect(policy.maxHttpInFlight).toBe(8); + expect(policy.firstMinuteAggregateCeiling).toBe(64); + expect(policy.laterMinuteAggregateCeiling).toBe(62); + }); + + test("enforces the global eight-request in-flight cap across environments", async () => { + let active = 0; + let peak = 0; + const poller = createAdaptivePoller({ + getShell: async () => { + active += 1; + peak = Math.max(peak, active); + await new Promise((resolve) => setTimeout(resolve, 10)); + active -= 1; + return { + snapshotSequence: 1, + projects: [], + threads: [], + updatedAt: "2026-07-31T18:00:00.000Z", + }; + }, + getThread: async () => undefined, + }); + const waits = Array.from({ length: 8 }, (_, index) => + poller.waitFor({ + environmentId: `env-${index}`, + threadId: `thread-${index}`, + deadlineMs: Date.now() + 2_000, + evaluate: () => ({ done: true, value: "done" }), + }), + ); + + await expect(Promise.all(waits)).resolves.toEqual(Array(8).fill("done")); + expect(peak).toBeLessThanOrEqual(8); + expect(poller.metrics().peakHttpInFlight).toBe(peak); + poller.close(); + }); + + test("cancels one subscriber without aborting the shared environment poll", async () => { + const first = new AbortController(); + let shellStarts = 0; + const poller = createAdaptivePoller({ + getShell: async () => { + shellStarts += 1; + return { snapshotSequence: shellStarts, projects: [], threads: [], updatedAt: new Date().toISOString() }; + }, + getThread: async () => undefined, + }); + const cancelled = poller.waitFor({ + environmentId: "env-1", + threadId: "one", + deadlineMs: Date.now() + 2_000, + signal: first.signal, + evaluate: () => ({ done: false }), + }); + const survivor = poller.waitFor({ + environmentId: "env-1", + threadId: "two", + deadlineMs: Date.now() + 2_000, + evaluate: ({ shell }) => shell.snapshotSequence >= 2 ? { done: true, value: "ok" } : { done: false }, + }); + first.abort(); + + await expect(cancelled).rejects.toMatchObject({ code: "cancelled" }); + await expect(survivor).resolves.toBe("ok"); + expect(shellStarts).toBe(2); + poller.close(); + }); + + test("restarts a same-environment poll after last-waiter cancellation and same-tick rewait", async () => { + const first = new AbortController(); + let releaseFirstSleep!: () => void; + const firstSleepStarted = new Promise((resolve) => { + releaseFirstSleep = resolve; + }); + let sleepStarts = 0; + let shellStarts = 0; + const poller = createAdaptivePoller({ + sleep: (_milliseconds, signal) => { + sleepStarts += 1; + if (sleepStarts > 1) return Promise.resolve(); + releaseFirstSleep(); + return new Promise((_resolve, reject) => { + signal.addEventListener("abort", () => reject(signal.reason), { once: true }); + }); + }, + getShell: async () => { + shellStarts += 1; + return { + snapshotSequence: 1, + projects: [], + threads: [], + updatedAt: "2026-07-31T18:00:00.000Z", + }; + }, + getThread: async () => undefined, + }); + const cancelled = poller.waitFor({ + environmentId: "env-1", + threadId: "thread-1", + deadlineMs: Date.now() + 1_000, + signal: first.signal, + evaluate: () => ({ done: false }), + }); + await firstSleepStarted; + + first.abort(); + const replacement = poller.waitFor({ + environmentId: "env-1", + threadId: "thread-1", + deadlineMs: Date.now() + 1_000, + evaluate: ({ shell }) => ({ done: true, value: shell.snapshotSequence }), + }); + + await expect(cancelled).rejects.toMatchObject({ code: "cancelled" }); + await expect(replacement).resolves.toBe(1); + expect(shellStarts).toBe(1); + poller.close(); + }, 1_000); + + test("uses bounded snapshot backoff without overlapping requests", () => { + const policy = createAdaptivePoller.policy(); + expect([0, 1, 2, 3, 4, 5].map((failure) => policy.backoffMs(failure, 0))).toEqual([ + 500, 1_000, 2_000, 4_000, 8_000, 8_000, + ]); + expect(policy.backoffMs(4, 20_000)).toBe(8_000); + }); + + test("coalesces same-thread detail work and fans one observation to every waiter", async () => { + const clock = fakeClock(); + let detailStarts = 0; + let detailInFlight = 0; + let peakDetailInFlight = 0; + const poller = createAdaptivePoller({ + now: clock.now, + sleep: clock.sleep, + getShell: async () => ({ + snapshotSequence: 1, + projects: [], + threads: [], + updatedAt: "2026-07-31T18:00:00.000Z", + }), + getThread: async () => { + detailStarts += 1; + detailInFlight += 1; + peakDetailInFlight = Math.max(peakDetailInFlight, detailInFlight); + await Promise.resolve(); + detailInFlight -= 1; + return emptyDetail; + }, + }); + + const waits = ["first", "second"].map((value) => + poller.waitFor({ + environmentId: "env-1", + threadId: "thread-1", + deadlineMs: 2_000, + evaluate: ({ detail }) => + detail === undefined ? { done: false, detail: true } : { done: true, value }, + }), + ); + + expect(await Promise.all(waits)).toEqual(["first", "second"]); + expect(detailStarts).toBe(1); + expect(peakDetailInFlight).toBe(1); + expect(poller.metrics()).toMatchObject({ detailStarts: 1, peakHttpInFlight: 1 }); + poller.close(); + }); + + test("does not refresh detail when the shell sequence has not advanced", async () => { + const clock = fakeClock(); + let detailStarts = 0; + const poller = createAdaptivePoller({ + now: clock.now, + sleep: clock.sleep, + getShell: async () => ({ + snapshotSequence: 1, + projects: [], + threads: [], + updatedAt: "2026-07-31T18:00:00.000Z", + }), + getThread: async () => { + detailStarts += 1; + return emptyDetail; + }, + }); + + await expect( + poller.waitFor({ + environmentId: "env-1", + threadId: "thread-1", + deadlineMs: 2_000, + evaluate: () => ({ done: false, detail: true }), + }), + ).rejects.toMatchObject({ code: "timeout" }); + expect(detailStarts).toBe(1); + poller.close(); + }); + + test("clamps sleep to the inclusive operation deadline", async () => { + const clock = fakeClock(); + const poller = createAdaptivePoller({ + now: clock.now, + sleep: clock.sleep, + getShell: async () => { + throw new Error("request must not start at the deadline"); + }, + getThread: async () => undefined, + }); + + await expect( + poller.waitFor({ + environmentId: "env-1", + threadId: "thread-1", + deadlineMs: 100, + evaluate: () => ({ done: false }), + }), + ).rejects.toMatchObject({ code: "timeout" }); + expect(clock.sleeps).toEqual([100]); + poller.close(); + }); + + test("honors Retry-After with injectable deterministic jitter", async () => { + const clock = fakeClock(); + let attempts = 0; + const poller = createAdaptivePoller({ + now: clock.now, + sleep: clock.sleep, + jitter: (delayMs: number) => Math.round(delayMs * 0.1), + getShell: async () => { + attempts += 1; + if (attempts === 1) { + throw new StockT3HttpError("transport_unavailable", 503, { + transient: true, + retryAfterMs: 3_000, + }); + } + return { + snapshotSequence: 2, + projects: [], + threads: [], + updatedAt: "2026-07-31T18:00:00.000Z", + }; + }, + getThread: async () => undefined, + } as Parameters[0] & { + jitter: (delayMs: number) => number; + }); + + await expect( + poller.waitFor({ + environmentId: "env-1", + threadId: "thread-1", + deadlineMs: 10_000, + evaluate: ({ shell }) => + shell.snapshotSequence === 2 ? { done: true, value: "done" } : { done: false }, + }), + ).resolves.toBe("done"); + expect(clock.sleeps).toEqual([250, 3_300]); + poller.close(); + }); + + test("fails closed without retrying a protocol mismatch", async () => { + const clock = fakeClock(); + let attempts = 0; + const failure = new StockT3HttpError("protocol_mismatch", 200, { + reason: "schema", + }); + const poller = createAdaptivePoller({ + now: clock.now, + sleep: clock.sleep, + getShell: async () => { + attempts += 1; + throw failure; + }, + getThread: async () => undefined, + }); + + await expect( + poller.waitFor({ + environmentId: "env-1", + threadId: "thread-1", + deadlineMs: 10_000, + evaluate: () => ({ done: false }), + }), + ).rejects.toBe(failure); + expect(attempts).toBe(1); + poller.close(); + }); + + test("enforces the 64/62 aggregate ceilings with eight concurrent waits", async () => { + const clock = fakeClock(); + const starts: Array<{ kind: "shell" | "detail"; at: number }> = []; + let shellSequence = 0; + const poller = createAdaptivePoller({ + now: clock.now, + sleep: clock.sleep, + getShell: async () => { + starts.push({ kind: "shell", at: clock.now() }); + shellSequence += 1; + return { + snapshotSequence: shellSequence, + projects: [], + threads: [], + updatedAt: "2026-07-31T18:00:00.000Z", + }; + }, + getThread: async (threadId) => { + starts.push({ kind: "detail", at: clock.now() }); + return { + ...emptyDetail, + snapshotSequence: shellSequence, + thread: { ...emptyDetail.thread, id: threadId }, + }; + }, + }); + + const waits = Array.from({ length: 8 }, (_, index) => + poller + .waitFor({ + environmentId: "env-1", + threadId: `thread-${index}`, + deadlineMs: 120_000, + evaluate: () => ({ done: false, detail: true }), + }) + .catch((error) => error), + ); + const results = await Promise.all(waits); + expect( + results.every( + (entry) => (entry as { readonly code?: unknown })?.code === "timeout", + ), + ).toBe(true); + const firstMinute = starts.filter((entry) => entry.at < 60_000); + const laterMinute = starts.filter((entry) => entry.at >= 60_000 && entry.at < 120_000); + expect(firstMinute.length).toBeLessThanOrEqual(64); + expect(laterMinute.length).toBeLessThanOrEqual(62); + expect(firstMinute.filter((entry) => entry.kind === "shell").length).toBeLessThanOrEqual(32); + expect(laterMinute.filter((entry) => entry.kind === "shell").length).toBeLessThanOrEqual(30); + poller.close(); + }); +}); diff --git a/test/boundary-convergence.test.ts b/test/boundary-convergence.test.ts new file mode 100644 index 0000000..687ab45 --- /dev/null +++ b/test/boundary-convergence.test.ts @@ -0,0 +1,922 @@ +import { describe, expect, test } from "bun:test"; +import { homedir } from "node:os"; +import { resolve } from "node:path"; + +import { + allocateProjectCreateIdentity, + canonicalizeWorkspaceRoot, + createStockT3Facade, + parseProjectCreateIdentity, +} from "../src/facade"; +import { + createStockT3NativeRuntime, + type StockSpawnInput, + type TurnReceipt, +} from "../src/nativeRuntime"; + +const iso = "2026-08-01T08:00:00.000Z"; +const selection = { instanceId: "claudeAgent", model: "claude-opus-5" }; + +function ids(...values: string[]) { + return () => { + const value = values.shift(); + if (value === undefined) throw new Error("unexpected ID allocation"); + return value; + }; +} + +class BoundaryStock { + readonly projects = new Map>(); + readonly threads = new Map>(); + readonly messages = new Map[]>(); + readonly mutations: Record[] = []; + readonly receipts = new Map(); + sequence = 1; + failNextShell = false; + failNextDispatch: string | null = null; + ambiguousNextDispatch = false; + ambiguousNextDispatchType: string | null = null; + afterNextCommit: (() => void) | null = null; + afterEveryCommit: ((command: Record) => void) | null = null; + afterShellRead: (() => void) | null = null; + projectedMessages: Record[] | null = null; + terminal = false; + requests = 0; + shellReads = 0; + + constructor(readonly projectRoot: string) { + this.projects.set("project-visible", { + id: "project-visible", + title: "project", + workspaceRoot: projectRoot, + defaultModelSelection: selection, + createdAt: iso, + updatedAt: iso, + }); + this.seedThread("thread-existing"); + } + + seedThread(id: string, projectId = "project-visible") { + this.threads.set(id, { + id, + projectId, + title: "worker", + modelSelection: selection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + createdAt: iso, + updatedAt: iso, + }); + this.messages.set(id, []); + } + + private latestTurn() { + return this.terminal + ? { + turnId: "turn-stock", + state: "completed", + requestedAt: iso, + startedAt: iso, + completedAt: iso, + assistantMessageId: "assistant-stock", + error: null, + } + : null; + } + + private shell() { + return { + snapshotSequence: this.sequence, + projects: [...this.projects.values()], + threads: [...this.threads.values()].map((thread) => ({ + ...thread, + latestTurn: this.latestTurn(), + session: null, + latestUserMessageAt: null, + hasPendingApprovals: false, + hasPendingUserInput: false, + })), + updatedAt: iso, + }; + } + + private detail(threadId: string): Response { + const thread = this.threads.get(threadId); + if (thread === undefined) return Response.json({ code: "not_found" }, { status: 404 }); + const messages = [...(this.projectedMessages ?? this.messages.get(threadId) ?? [])]; + if (this.terminal) { + for (const message of messages) { + if (message.role === "user") message.turnId = "turn-stock"; + } + messages.push({ + id: "assistant-stock", + role: "assistant", + text: "done", + attachments: [], + turnId: "turn-stock", + streaming: false, + createdAt: iso, + updatedAt: iso, + }); + } + return Response.json({ + snapshotSequence: this.sequence, + thread: { + ...thread, + latestTurn: this.latestTurn(), + session: null, + messages, + activities: [], + checkpoints: [], + }, + }); + } + + private commit(command: Record): number { + const commandId = command.commandId as string; + const existing = this.receipts.get(commandId); + if (existing !== undefined) return existing; + if (command.type === "project.create") { + this.projects.set(command.projectId as string, { + id: command.projectId, + title: command.title, + workspaceRoot: command.workspaceRoot, + defaultModelSelection: command.defaultModelSelection, + createdAt: command.createdAt, + updatedAt: command.createdAt, + }); + } else if (command.type === "thread.create") { + this.seedThread(command.threadId as string, command.projectId as string); + } else { + const message = command.message as Record; + this.messages.get(command.threadId as string)!.push({ + id: message.messageId, + role: "user", + text: message.text, + attachments: [], + turnId: null, + streaming: false, + createdAt: iso, + updatedAt: iso, + }); + } + this.sequence += 1; + this.receipts.set(commandId, this.sequence); + return this.sequence; + } + + readonly fetch = async (input: string | URL | Request, init?: RequestInit): Promise => { + this.requests += 1; + const request = new Request(input, init); + const path = new URL(request.url).pathname; + if (path === "/.well-known/t3/environment") { + return Response.json({ + environmentId: "env-1", + label: "local", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "stock", + capabilities: { repositoryIdentity: false }, + }); + } + if (path === "/api/orchestration/shell") { + this.shellReads += 1; + this.afterShellRead?.(); + if (this.failNextShell) { + this.failNextShell = false; + return Response.json( + { code: "internal_error", reason: "orchestration_dispatch_failed" }, + { status: 500 }, + ); + } + return Response.json(this.shell()); + } + if (path.startsWith("/api/orchestration/threads/")) { + return this.detail(decodeURIComponent(path.slice("/api/orchestration/threads/".length))); + } + if (path === "/api/orchestration/dispatch") { + const command = JSON.parse(await request.text()) as Record; + this.mutations.push(command); + if (this.failNextDispatch === command.type) { + this.failNextDispatch = null; + return Response.json( + { code: "internal_error", reason: "orchestration_dispatch_failed" }, + { status: 500 }, + ); + } + const sequence = this.commit(command); + this.afterEveryCommit?.(command); + this.afterNextCommit?.(); + this.afterNextCommit = null; + if (this.ambiguousNextDispatch || this.ambiguousNextDispatchType === command.type) { + this.ambiguousNextDispatch = false; + this.ambiguousNextDispatchType = null; + throw new TypeError("response lost after stock acceptance"); + } + return Response.json({ sequence }); + } + throw new Error(`unexpected route ${request.method} ${path}`); + }; +} + +function runtimeFor( + stock: BoundaryStock, + allocated: string[] = [], + extra: Readonly> = {}, +) { + return createStockT3NativeRuntime({ + baseUrl: "http://stock.invalid", + fetch: stock.fetch, + id: ids(...allocated), + now: () => iso, + ...extra, + }); +} + +function input(workspaceRoot: string): StockSpawnInput { + return { + workspaceRoot, + projectId: "project-visible", + title: "worker", + message: "initial", + modelSelection: selection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }; +} + +describe("phase 3 boundary convergence", () => { + test.each([ + ["trailing separator", "/tmp/boundary-project/", "/tmp/boundary-project"], + ["surrounding whitespace", " /tmp/boundary-project/ ", "/tmp/boundary-project"], + ["relative path", "./boundary-project/", resolve("boundary-project")], + ["home path", "~/boundary-project/", resolve(homedir(), "boundary-project")], + ])("canonicalizes %s before visible-project lookup and dispatch", async (_name, supplied, canonical) => { + const stock = new BoundaryStock(canonical); + const runtime = runtimeFor(stock, [ + "thread-command-1", "thread-1", "turn-1", "message-1", "lease-1", + ]); + const result = await runtime.spawn(input(supplied), { maxReconciliationReads: 1 }); + expect(result.kind).toBe("spawned"); + expect(stock.mutations.find((entry) => entry.type === "thread.create")?.projectId) + .toBe("project-visible"); + expect(canonicalizeWorkspaceRoot(supplied)).toBe(canonical); + if (result.kind === "spawned") runtime.releaseReceipt(result.turnReceipt); + runtime.close(); + }); + + test("public allocator/parser round-trips through plain JSON with canonical replay fields", () => { + const identity = allocateProjectCreateIdentity( + { + workspaceRoot: " /tmp/boundary-project/ ", + title: "project", + defaultModelSelection: selection, + }, + { id: ids("project-public", "command-public"), now: () => iso }, + ); + expect(parseProjectCreateIdentity(JSON.parse(JSON.stringify(identity)), { + workspaceRoot: "/tmp/boundary-project", + projectId: "project-public", + })).toEqual({ + projectId: "project-public", + commandId: "command-public", + createdAt: iso, + workspaceRoot: "/tmp/boundary-project", + title: "project", + defaultModelSelection: selection, + }); + expect(Object.isFrozen(identity)).toBe(true); + expect(Object.isFrozen(identity.defaultModelSelection)).toBe(true); + }); + + test("project.create receives only the canonical root stored in caller identity", async () => { + const stock = new BoundaryStock("/tmp/new-boundary"); + stock.projects.clear(); + const identity = allocateProjectCreateIdentity({ + workspaceRoot: " /tmp/new-boundary/ ", + title: "project", + defaultModelSelection: selection, + }, { id: ids("project-new", "project-command"), now: () => iso }); + const runtime = runtimeFor(stock, [ + "thread-command", "thread-new", "turn-command", "message-new", "lease-new", + ]); + const result = await runtime.spawn({ + ...input(" /tmp/new-boundary/ "), + projectId: "project-new", + projectCreateIdentity: identity, + }, { maxReconciliationReads: 1 }); + expect(result.kind).toBe("spawned"); + expect(stock.mutations.find((entry) => entry.type === "project.create")).toMatchObject({ + projectId: "project-new", + commandId: "project-command", + workspaceRoot: "/tmp/new-boundary", + }); + if (result.kind === "spawned") runtime.releaseReceipt(result.turnReceipt); + runtime.close(); + }); + + test.each([ + ["missing model", { defaultModelSelection: { instanceId: "provider" } }], + ["whitespace ID", { projectId: " " }], + ["malformed nested object", { defaultModelSelection: [] }], + ])("rejects %s as typed invalid identity before mutation", async (_name, override) => { + const stock = new BoundaryStock("/tmp/new-boundary"); + stock.projects.clear(); + const runtime = runtimeFor(stock); + const valid = allocateProjectCreateIdentity({ + workspaceRoot: "/tmp/new-boundary", + title: "project", + defaultModelSelection: selection, + }, { id: ids("project-new", "command-new"), now: () => iso }); + await expect(runtime.spawn({ + ...input("/tmp/new-boundary"), + projectId: "project-new", + projectCreateIdentity: { ...valid, ...override } as never, + })).rejects.toMatchObject({ + code: "identity_conflict", + evidence: { reason: "invalid_project_create_identity" }, + }); + expect(stock.mutations).toHaveLength(0); + runtime.close(); + }); + + test("a received project.create failure retains full caller identity evidence", async () => { + const stock = new BoundaryStock("/tmp/new-boundary"); + stock.projects.clear(); + stock.failNextDispatch = "project.create"; + const runtime = runtimeFor(stock); + const identity = allocateProjectCreateIdentity({ + workspaceRoot: "/tmp/new-boundary/", + title: "project", + defaultModelSelection: selection, + }, { id: ids("project-new", "command-new"), now: () => iso }); + await expect(runtime.spawn({ + ...input("/tmp/new-boundary/"), + projectId: "project-new", + projectCreateIdentity: identity, + })).rejects.toMatchObject({ + code: "server_internal", + evidence: { + reason: "project_create_received_error", + projectAttempt: { + projectId: "project-new", + commandId: "command-new", + workspaceRoot: "/tmp/new-boundary", + defaultModelSelection: selection, + }, + }, + }); + runtime.close(); + }); + + test("shell 500 is observational: same receipt retries and duplicate send remains blocked", async () => { + const stock = new BoundaryStock("/tmp/boundary-project"); + const runtime = runtimeFor(stock, [ + "command-1", "message-1", "lease-1", + "command-2", "message-2", "lease-2", + ]); + const ref = { environmentId: "env-1", threadId: "thread-existing" }; + const receipt = await runtime.send(ref, "hello"); + stock.failNextShell = true; + await expect(runtime.wait(receipt, { timeoutMs: 1_000 })).rejects.toMatchObject({ + code: "server_internal", + }); + await expect(runtime.send(ref, "duplicate")).rejects.toMatchObject({ code: "send_in_progress" }); + stock.terminal = true; + const completed = await runtime.wait(receipt, { timeoutMs: 2_000 }); + expect(completed).toMatchObject({ kind: "completed", receipt: { leaseState: "released" } }); + const next = await runtime.send(ref, "next"); + expect(next.leaseState).toBe("active"); + runtime.releaseReceipt(next); + runtime.close(); + }); + + test("ninth wait capacity failure preserves its lease for retry and blocks duplicate send", async () => { + const stock = new BoundaryStock("/tmp/boundary-project"); + const allocated = Array.from({ length: 10 }, (_, index) => [ + `command-${index + 1}`, + `message-${index + 1}`, + `lease-${index + 1}`, + ]).flat(); + const runtime = runtimeFor(stock, allocated); + const receipts: TurnReceipt[] = []; + for (let index = 0; index < 9; index += 1) { + const threadId = `thread-capacity-${index + 1}`; + stock.seedThread(threadId); + receipts.push(await runtime.send({ environmentId: "env-1", threadId }, `message ${index + 1}`)); + } + const controllers = Array.from({ length: 8 }, () => new AbortController()); + const active = receipts.slice(0, 8).map((receipt, index) => + runtime.wait(receipt, { signal: controllers[index]!.signal, timeoutMs: 5_000 }) + .catch((error) => error), + ); + for (let spin = 0; spin < 50 && runtime.pollMetrics().activeWaits < 8; spin += 1) { + await new Promise((resolve) => setTimeout(resolve, 5)); + } + expect(runtime.pollMetrics().activeWaits).toBe(8); + await expect(runtime.wait(receipts[8]!, { timeoutMs: 1_000 })).rejects.toMatchObject({ + code: "transport_unavailable", + evidence: { reason: "capacity" }, + }); + await expect(runtime.send(receipts[8]!.agentRef, "duplicate")).rejects.toMatchObject({ + code: "send_in_progress", + }); + for (const controller of controllers) controller.abort(); + await Promise.all(active); + stock.terminal = true; + const completed = await runtime.wait(receipts[8]!, { timeoutMs: 2_000 }); + expect(completed.receipt).toMatchObject({ + leaseId: "lease-9", + leaseState: "released", + }); + runtime.close(); + }); + + test("received send rejection exposes a complete released receipt and unblocks later send", async () => { + const stock = new BoundaryStock("/tmp/boundary-project"); + stock.failNextDispatch = "thread.turn.start"; + const runtime = runtimeFor(stock, [ + "command-1", "message-1", "lease-1", + "command-2", "message-2", "lease-2", + ]); + const ref = { environmentId: "env-1", threadId: "thread-existing" }; + const failure = await runtime.send(ref, "rejected").catch((error) => error); + expect(failure).toMatchObject({ + code: "server_internal", + evidence: { + receipt: { + agentRef: ref, + commandId: "command-1", + messageId: "message-1", + acceptedSequence: null, + leaseState: "released", + }, + }, + }); + await expect(runtime.wait(failure.evidence.receipt as TurnReceipt)).rejects.toMatchObject({ + code: "receipt_expired", + }); + const next = await runtime.send(ref, "next"); + runtime.releaseReceipt(next); + runtime.close(); + }); + + test("received initial-turn rejection returns a complete released receipt", async () => { + const stock = new BoundaryStock("/tmp/boundary-project"); + stock.failNextDispatch = "thread.turn.start"; + const runtime = runtimeFor(stock, [ + "thread-command", "thread-new", "turn-command", "message-new", "lease-new", + ]); + const result = await runtime.spawn(input("/tmp/boundary-project"), { + maxReconciliationReads: 1, + }); + expect(result).toMatchObject({ + kind: "partial", + initialTurn: { + state: "initial_turn_rejected", + turnReceipt: { + commandId: "turn-command", + messageId: "message-new", + acceptedSequence: null, + leaseState: "released", + }, + }, + }); + runtime.close(); + }); + + test.each([ + ["superseded", [{ id: "foreign", text: "foreign" }]], + ["concurrent_writer", [ + { id: "message-1", text: "target" }, + { id: "foreign", text: "foreign" }, + ]], + ["causality_unverifiable", [{ id: "message-1", text: "rewritten" }]], + ])("send %s terminal evidence contains a released receipt", async (classification, rows) => { + const stock = new BoundaryStock("/tmp/boundary-project"); + stock.ambiguousNextDispatch = true; + stock.afterNextCommit = () => { + stock.projectedMessages = rows.map((row) => ({ + id: row.id, + role: "user", + text: row.text, + attachments: [], + turnId: null, + streaming: false, + createdAt: iso, + updatedAt: iso, + })); + }; + const runtime = runtimeFor(stock, ["command-1", "message-1", "lease-1"]); + const failure = await runtime.send( + { environmentId: "env-1", threadId: "thread-existing" }, + "target", + ).catch((error) => error); + expect(failure).toMatchObject({ + code: classification, + evidence: { + receipt: { + commandId: "command-1", + messageId: "message-1", + acceptedSequence: null, + leaseState: "released", + }, + }, + }); + runtime.close(); + }); + + test("cancellation after possible acceptance releases a complete send receipt", async () => { + const stock = new BoundaryStock("/tmp/boundary-project"); + const controller = new AbortController(); + stock.afterNextCommit = () => controller.abort(); + stock.ambiguousNextDispatch = true; + const runtime = runtimeFor(stock, ["command-1", "message-1", "lease-1"]); + const failure = await runtime.send( + { environmentId: "env-1", threadId: "thread-existing" }, + "possibly accepted", + { signal: controller.signal }, + ).catch((error) => error); + expect(failure).toMatchObject({ + code: "cancelled", + evidence: { + receipt: { + commandId: "command-1", + messageId: "message-1", + acceptedSequence: null, + leaseState: "released", + }, + }, + }); + runtime.close(); + }); + + test("deadline after possible acceptance releases a complete send receipt", async () => { + const stock = new BoundaryStock("/tmp/boundary-project"); + let clock = 100; + stock.afterNextCommit = () => { clock = 200; }; + stock.ambiguousNextDispatch = true; + const runtime = runtimeFor( + stock, + ["command-1", "message-1", "lease-1"], + { clock: () => clock }, + ); + const failure = await runtime.send( + { environmentId: "env-1", threadId: "thread-existing" }, + "possibly accepted", + { deadlineMs: 200 }, + ).catch((error) => error); + expect(failure).toMatchObject({ + code: "timeout", + evidence: { + receipt: { + commandId: "command-1", + messageId: "message-1", + acceptedSequence: null, + leaseState: "released", + }, + }, + }); + runtime.close(); + }); + + test("facade publishes identity allocation and parsing without a private helper", () => { + const facade = createStockT3Facade({} as never); + expect(facade).toBeDefined(); + expect(typeof allocateProjectCreateIdentity).toBe("function"); + expect(typeof parseProjectCreateIdentity).toBe("function"); + }); +}); + +describe("post-ship medium closure", () => { + test("MEDIUM-1 wait with a caller-mutated receipt still releases the admitted lease", async () => { + const stock = new BoundaryStock("/tmp/boundary-project"); + const runtime = runtimeFor(stock, [ + "command-1", "message-1", "lease-1", + "command-2", "message-2", "lease-2", + ]); + const receipt = await runtime.send( + { environmentId: "env-1", threadId: "thread-existing" }, + "mutated receipt", + { timeoutMs: 30_000 }, + ); + const mutatedReceipt = { ...receipt, commandId: "caller-mutated-command" }; + stock.terminal = true; + + const completed = await runtime.wait(mutatedReceipt, { timeoutMs: 30_000 }); + expect(completed.kind).toBe("completed"); + expect(completed.receipt).toMatchObject({ + commandId: "caller-mutated-command", + leaseId: "lease-1", + leaseState: "released", + }); + + await expect( + runtime.send( + { environmentId: "env-1", threadId: "thread-existing" }, + "lease is reusable", + { timeoutMs: 30_000 }, + ), + ).resolves.toMatchObject({ + commandId: "command-2", + messageId: "message-2", + leaseId: "lease-2", + leaseState: "active", + }); + runtime.close(); + }); + + test("MEDIUM-1 honest receipt control continues to release the lease", async () => { + const stock = new BoundaryStock("/tmp/boundary-project"); + const runtime = runtimeFor(stock, [ + "command-1", "message-1", "lease-1", + "command-2", "message-2", "lease-2", + ]); + const receipt = await runtime.send( + { environmentId: "env-1", threadId: "thread-existing" }, + "honest receipt", + { timeoutMs: 30_000 }, + ); + stock.terminal = true; + + const completed = await runtime.wait(receipt, { timeoutMs: 30_000 }); + expect(completed.kind).toBe("completed"); + expect(completed.receipt.leaseState).toBe("released"); + + await expect( + runtime.send( + { environmentId: "env-1", threadId: "thread-existing" }, + "lease is reusable", + { timeoutMs: 30_000 }, + ), + ).resolves.toMatchObject({ + commandId: "command-2", + messageId: "message-2", + leaseId: "lease-2", + leaseState: "active", + }); + runtime.close(); + }); + + test("MEDIUM-A wait without a lease rejects a receipt missing commandId as receipt_expired", async () => { + const stock = new BoundaryStock("/tmp/boundary-project"); + const runtime = runtimeFor(stock); + const malformedReceipt = { + agentRef: { environmentId: "env-1", threadId: "thread-existing" }, + leaseId: "ghost-lease", + messageId: "ghost-message", + acceptedSequence: null, + observedSequence: 0, + leaseExpiresAt: Date.now() + 60_000, + leaseState: "active", + } as unknown as TurnReceipt; + + const failure = await runtime.wait(malformedReceipt, { timeoutMs: 30_000 }) + .catch((error) => error); + expect(failure).not.toBeInstanceOf(TypeError); + expect(failure).toMatchObject({ + name: "StockRuntimeError", + code: "receipt_expired", + evidence: { + receipt: { + leaseId: "ghost-lease", + messageId: "ghost-message", + leaseState: "released", + }, + }, + }); + runtime.close(); + }); + + test("MEDIUM-A stale receipt missing commandId rejects as receipt_expired without releasing the live lease", async () => { + const stock = new BoundaryStock("/tmp/boundary-project"); + const runtime = runtimeFor(stock, ["command-1", "message-1", "lease-1"]); + const receipt = await runtime.send( + { environmentId: "env-1", threadId: "thread-existing" }, + "live lease", + { timeoutMs: 30_000 }, + ); + const staleReceipt = { ...receipt, leaseId: "stale-lease" } as unknown as Record; + delete staleReceipt.commandId; + + const failure = await runtime.wait(staleReceipt as unknown as TurnReceipt, { timeoutMs: 30_000 }) + .catch((error) => error); + expect(failure).not.toBeInstanceOf(TypeError); + expect(failure).toMatchObject({ + name: "StockRuntimeError", + code: "receipt_expired", + evidence: { + receipt: { + leaseId: "stale-lease", + messageId: "message-1", + leaseState: "released", + }, + }, + }); + await expect( + runtime.send( + { environmentId: "env-1", threadId: "thread-existing" }, + "live lease remains held", + { timeoutMs: 30_000 }, + ), + ).rejects.toMatchObject({ name: "StockRuntimeError", code: "send_in_progress" }); + runtime.close(); + }); + + test("already-expired active wait entry returns receipt_expired without network access", async () => { + const stock = new BoundaryStock("/tmp/boundary-project"); + let clock = 100; + const runtime = runtimeFor( + stock, + ["command-1", "message-1", "lease-1"], + { clock: () => clock }, + ); + const receipt = await runtime.send( + { environmentId: "env-1", threadId: "thread-existing" }, + "already expired", + { deadlineMs: 200 }, + ); + const requestsBeforeWait = stock.requests; + clock = 200; + + const failure = await runtime.wait(receipt).catch((error) => error); + expect(failure).toMatchObject({ + code: "receipt_expired", + evidence: { + receipt: { + leaseId: "lease-1", + commandId: "command-1", + messageId: "message-1", + acceptedSequence: receipt.acceptedSequence, + leaseState: "released", + }, + }, + }); + expect(stock.requests).toBe(requestsBeforeWait); + runtime.close(); + }); + + test("already-expired released wait entry returns receipt_expired without network access", async () => { + const stock = new BoundaryStock("/tmp/boundary-project"); + let clock = 100; + const runtime = runtimeFor( + stock, + ["command-1", "message-1", "lease-1"], + { clock: () => clock }, + ); + const receipt = await runtime.send( + { environmentId: "env-1", threadId: "thread-existing" }, + "released then held", + { deadlineMs: 200 }, + ); + runtime.releaseReceipt(receipt); + const requestsBeforeWait = stock.requests; + clock = 200; + + const failure = await runtime.wait(receipt).catch((error) => error); + expect(failure).toMatchObject({ + code: "receipt_expired", + evidence: { + receipt: { + leaseId: "lease-1", + commandId: "command-1", + messageId: "message-1", + acceptedSequence: receipt.acceptedSequence, + leaseState: "released", + }, + }, + }); + expect(stock.requests).toBe(requestsBeforeWait); + runtime.close(); + }); + + test("mid-wait expiry remains receipt_expired when the shared clock advances", async () => { + const stock = new BoundaryStock("/tmp/boundary-project"); + let clock = 100; + const runtime = runtimeFor( + stock, + ["command-1", "message-1", "lease-1"], + { clock: () => clock }, + ); + const receipt = await runtime.send( + { environmentId: "env-1", threadId: "thread-existing" }, + "expires while polling", + { deadlineMs: 1_000 }, + ); + stock.afterShellRead = () => { clock = 1_000; }; + + const failure = await runtime.wait(receipt).catch((error) => error); + expect(failure).toMatchObject({ + code: "receipt_expired", + evidence: { + receipt: { + leaseId: "lease-1", + acceptedSequence: receipt.acceptedSequence, + leaseState: "released", + }, + }, + }); + expect(stock.shellReads).toBe(1); + runtime.close(); + }); + + test("P3.1 accepted initial turn cancelled after response preserves exact sequence", async () => { + const stock = new BoundaryStock("/tmp/boundary-project"); + const controller = new AbortController(); + stock.afterEveryCommit = (command) => { + if (command.type === "thread.turn.start") controller.abort(); + }; + const runtime = runtimeFor(stock, [ + "thread-command", "thread-new", "turn-command", "message-new", "lease-new", + ]); + + const result = await runtime.spawn(input("/tmp/boundary-project"), { + signal: controller.signal, + maxReconciliationReads: 1, + }); + expect(result).toMatchObject({ + kind: "partial", + initialTurn: { + state: "initial_turn_accepted_projection_pending", + turnReceipt: { + commandId: "turn-command", + messageId: "message-new", + acceptedSequence: 3, + leaseState: "released", + }, + }, + }); + runtime.close(); + }); + + test("P3.2 accepted initial turn past deadline preserves exact sequence", async () => { + const stock = new BoundaryStock("/tmp/boundary-project"); + let clock = 100; + stock.afterEveryCommit = (command) => { + if (command.type === "thread.turn.start") clock = 200; + }; + const runtime = runtimeFor( + stock, + ["thread-command", "thread-new", "turn-command", "message-new", "lease-new"], + { clock: () => clock }, + ); + + const result = await runtime.spawn(input("/tmp/boundary-project"), { + deadlineMs: 200, + maxReconciliationReads: 1, + }); + expect(result).toMatchObject({ + kind: "partial", + initialTurn: { + state: "initial_turn_accepted_projection_pending", + turnReceipt: { + commandId: "turn-command", + messageId: "message-new", + acceptedSequence: 3, + leaseState: "released", + }, + }, + }); + runtime.close(); + }); + + test("P6.7 accepted identical retry preserves the original exact sequence", async () => { + const stock = new BoundaryStock("/tmp/boundary-project"); + let clock = 100; + let turnAttempts = 0; + stock.ambiguousNextDispatchType = "thread.turn.start"; + stock.afterEveryCommit = (command) => { + if (command.type !== "thread.turn.start") return; + turnAttempts += 1; + if (turnAttempts === 1) stock.projectedMessages = []; + if (turnAttempts === 2) clock = 200; + }; + const runtime = runtimeFor( + stock, + ["thread-command", "thread-new", "turn-command", "message-new", "lease-new"], + { clock: () => clock }, + ); + + const result = await runtime.spawn(input("/tmp/boundary-project"), { + deadlineMs: 200, + maxReconciliationReads: 1, + }); + expect(turnAttempts).toBe(2); + expect(result).toMatchObject({ + kind: "partial", + initialTurn: { + state: "initial_turn_accepted_projection_pending", + turnReceipt: { + commandId: "turn-command", + messageId: "message-new", + acceptedSequence: 3, + leaseState: "released", + }, + }, + }); + runtime.close(); + }); +}); diff --git a/test/facade.contract.test.ts b/test/facade.contract.test.ts deleted file mode 100644 index 22c79ad..0000000 --- a/test/facade.contract.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { createConfig } from "../src/config"; -import { createT3Facade } from "../src/facade"; - -const FACADE_CONFIG = createConfig({ - baseUrl: "http://127.0.0.1:3773", - provider: "claudeAgent", - model: "claude-opus-5", - effort: "high", - contextWindow: "1m", - runtimeMode: "full-access", - interactionMode: "default", -}); - -function makeRuntime( - startThreadInputs: unknown[], - startTurnInputs: unknown[] = [], -) { - return { - async listProjects() { - return [{ projectId: "project-1", workspaceRoot: "/work/app" }]; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread(input: unknown) { - startThreadInputs.push(input); - return { sequence: 2 }; - }, - async startTurn(input: unknown) { - startTurnInputs.push(input); - return { sequence: 3 }; - }, - async getThread(threadId: string) { - return { - threadId, - projectId: "project-1", - snapshotSequence: 2, - session: { status: "ready", activeTurnId: null }, - latestTurn: - startThreadInputs.length === 0 - ? null - : { - turnId: "turn-1", - status: "completed", - userMessageId: "message-1", - assistantMessage: { - content: "complete", - streaming: false, - }, - }, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; -} - -describe("canonical pre-adapter contracts", () => { - test("accepts omitted model options and records an evidence count of zero", async () => { - const startThreadInputs: unknown[] = []; - const evidence: unknown[] = []; - const ids = ["thread-1", "command-1", "message-1"]; - const facade = createT3Facade(makeRuntime(startThreadInputs), { - ...FACADE_CONFIG, - id: () => ids.shift()!, - now: () => "2026-07-31T10:00:00.000Z", - evidence: (record) => evidence.push(record), - }); - - await facade.spawn({ - workspaceRoot: "/work/app", - title: "worker", - message: "task", - modelSelection: { - instanceId: "codex", - model: "gpt-5.6-sol", - }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - }); - - expect(startThreadInputs[0]).toMatchObject({ - modelSelection: { - instanceId: "codex", - model: "gpt-5.6-sol", - }, - }); - expect( - ( - startThreadInputs[0] as { - readonly modelSelection: Record; - } - ).modelSelection, - ).not.toHaveProperty("options"); - expect(evidence[0]).toMatchObject({ - modelSelection: { - instanceId: "codex", - model: "gpt-5.6-sol", - optionCount: 0, - }, - }); - }); - - test("accepts exact boolean model option values without exposing them", async () => { - const startThreadInputs: unknown[] = []; - const evidence: unknown[] = []; - const ids = ["thread-1", "command-1", "message-1"]; - const facade = createT3Facade(makeRuntime(startThreadInputs), { - ...FACADE_CONFIG, - id: () => ids.shift()!, - now: () => "2026-07-31T10:00:00.000Z", - evidence: (record) => evidence.push(record), - }); - - await facade.spawn({ - workspaceRoot: "/work/app", - title: "worker", - message: "task", - modelSelection: { - instanceId: "codex", - model: "gpt-5.6-sol", - options: [{ id: "fastMode", value: true }], - }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - }); - - expect(startThreadInputs[0]).toMatchObject({ - modelSelection: { - options: [{ id: "fastMode", value: true }], - }, - }); - expect(evidence[0]).toMatchObject({ - modelSelection: { optionCount: 1 }, - }); - expect(JSON.stringify(evidence)).not.toContain("fastMode"); - expect( - ( - evidence[0] as { - readonly modelSelection: Record; - } - ).modelSelection, - ).not.toHaveProperty("options"); - }); - - test("includes the configured modes in follow-up dispatch and evidence", async () => { - const startTurnInputs: unknown[] = []; - const evidence: unknown[] = []; - const ids = ["command-2", "message-2"]; - const facade = createT3Facade(makeRuntime([], startTurnInputs), { - ...FACADE_CONFIG, - id: () => ids.shift()!, - now: () => "2026-07-31T10:05:00.000Z", - evidence: (record) => evidence.push(record), - }); - - await facade.send("thread-1", "follow-up"); - - expect(startTurnInputs).toEqual([ - { - commandId: "command-2", - threadId: "thread-1", - messageId: "message-2", - message: "follow-up", - runtimeMode: "full-access", - interactionMode: "default", - createdAt: "2026-07-31T10:05:00.000Z", - attachments: [], - }, - ]); - expect(evidence).toEqual([ - { - operation: "send", - commandId: "command-2", - threadId: "thread-1", - messageId: "message-2", - runtimeMode: "full-access", - interactionMode: "default", - createdAt: "2026-07-31T10:05:00.000Z", - attachments: 0, - messageBytes: 9, - }, - ]); - }); -}); diff --git a/test/facade.send.test.ts b/test/facade.send.test.ts deleted file mode 100644 index 4c840e7..0000000 --- a/test/facade.send.test.ts +++ /dev/null @@ -1,867 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { AmbiguousDispatchError, createT3Facade } from "../src/facade"; - -const DISPATCH_MODES = { - runtimeMode: "full-access", - interactionMode: "default", -} as const; - -describe("send", () => { - test("reuses the thread with fresh IDs and no bootstrap payload", async () => { - const calls: unknown[] = []; - const evidence: unknown[] = []; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn(input: unknown) { - calls.push(input); - return { sequence: 21 }; - }, - async getThread(threadId: string) { - return { - threadId, - projectId: "project-1", - snapshotSequence: 20, - session: { status: "ready", activeTurnId: null }, - latestTurn: null, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const ids = ["command-2", "message-2"]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-30T18:05:00.000Z", - evidence: (record) => evidence.push(record), - }); - - const receipt = await facade.send("thread-1", "secret follow-up"); - - expect(calls).toEqual([ - { - commandId: "command-2", - threadId: "thread-1", - messageId: "message-2", - message: "secret follow-up", - runtimeMode: "full-access", - interactionMode: "default", - createdAt: "2026-07-30T18:05:00.000Z", - attachments: [], - }, - ]); - expect(receipt).toEqual({ - agentId: "thread-1", - commandId: "command-2", - messageId: "message-2", - sequence: 21, - sequenceSource: "dispatch", - recovered: false, - }); - expect(evidence).toEqual([ - { - operation: "send", - commandId: "command-2", - threadId: "thread-1", - messageId: "message-2", - runtimeMode: "full-access", - interactionMode: "default", - createdAt: "2026-07-30T18:05:00.000Z", - attachments: 0, - messageBytes: 16, - }, - ]); - expect(JSON.stringify(evidence)).not.toContain("secret follow-up"); - expect(JSON.stringify(calls)).not.toContain("bootstrap"); - }); - - test("counts multibyte send evidence by encoded byte length", async () => { - const evidence: unknown[] = []; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 21 }; - }, - async getThread(threadId: string) { - return { - threadId, - projectId: "project-1", - snapshotSequence: 20, - session: { status: "ready", activeTurnId: null }, - latestTurn: null, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const ids = ["command-multibyte", "message-multibyte"]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-31T00:00:00.000Z", - evidence: (record) => evidence.push(record), - }); - - await facade.send("thread-1", "é🙂"); - - expect("é🙂").toHaveLength(3); - expect(evidence).toEqual([ - { - operation: "send", - commandId: "command-multibyte", - threadId: "thread-1", - messageId: "message-multibyte", - runtimeMode: "full-access", - interactionMode: "default", - createdAt: "2026-07-31T00:00:00.000Z", - attachments: 0, - messageBytes: 6, - }, - ]); - expect(JSON.stringify(evidence)).not.toContain("é🙂"); - }); - - test("fails closed before dispatch when preflight returns a different thread", async () => { - const credential = "hostile-preflight-credential"; - let attempts = 0; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - attempts += 1; - return { sequence: 21 }; - }, - async getThread() { - return { - threadId: "thread-other", - projectId: "project-1", - snapshotSequence: 20, - session: { - status: "ready", - activeTurnId: null, - authorization: credential, - }, - latestTurn: null, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - - const result = facade.send("thread-requested", "follow-up"); - const error = await result.catch((reason: unknown) => reason); - - expect(error).toMatchObject({ - code: "transport_unavailable", - sequence: 20, - }); - expect(attempts).toBe(0); - expect(String(error)).not.toContain(credential); - expect(JSON.stringify(error)).not.toContain(credential); - }); - - test("returns a structural facade error when the preflight thread is unavailable", async () => { - let attempts = 0; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - attempts += 1; - return { sequence: 21 }; - }, - async getThread() { - return undefined; - }, - async *subscribeThread() { - return; - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - - const result = facade.send("thread-requested", "follow-up"); - - await expect(result).rejects.toMatchObject({ - code: "transport_unavailable", - sequence: 0, - structuralSnapshot: { - threadId: "thread-requested", - }, - }); - expect(attempts).toBe(0); - }); - - test("recovers an ambiguous receipt from the chosen thread and does not duplicate the turn", async () => { - let attempts = 0; - let queryCount = 0; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - attempts += 1; - throw new AmbiguousDispatchError(); - }, - async getThread(threadId: string) { - queryCount += 1; - if (queryCount === 1) { - return { - threadId, - projectId: "project-1", - snapshotSequence: 20, - session: { status: "ready", activeTurnId: null }, - latestTurn: null, - pendingApproval: null, - pendingInput: null, - }; - } - return { - threadId, - projectId: "project-1", - snapshotSequence: 22, - session: { status: "running", activeTurnId: "turn-2" }, - latestUserMessageId: "message-2", - latestTurn: { - turnId: "turn-2", - status: "running", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const ids = ["command-2", "message-2"]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-30T18:05:00.000Z", - }); - - const receipt = await facade.send("thread-1", "follow-up"); - - expect(attempts).toBe(1); - expect(receipt).toEqual({ - agentId: "thread-1", - commandId: "command-2", - messageId: "message-2", - sequence: 22, - sequenceSource: "projection", - recovered: true, - }); - }); - - test("retries an absent ambiguous send with the identical preallocated payload", async () => { - const attempts: unknown[] = []; - let queryCount = 0; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn(input: unknown) { - attempts.push(input); - if (attempts.length === 1) { - throw new AmbiguousDispatchError(); - } - return { sequence: 23 }; - }, - async getThread(threadId: string) { - queryCount += 1; - return { - threadId, - projectId: "project-1", - snapshotSequence: queryCount === 1 ? 20 : 22, - session: { status: "ready", activeTurnId: null }, - latestTurn: null, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const ids = ["command-stable", "message-stable"]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-31T00:00:00.000Z", - }); - - const receipt = await facade.send("thread-1", "follow-up"); - - expect(attempts).toHaveLength(2); - expect(attempts[1]).toBe(attempts[0]); - expect(attempts[1]).toEqual({ - commandId: "command-stable", - threadId: "thread-1", - messageId: "message-stable", - message: "follow-up", - runtimeMode: "full-access", - interactionMode: "default", - createdAt: "2026-07-31T00:00:00.000Z", - attachments: [], - }); - expect(ids).toHaveLength(0); - expect(receipt).toEqual({ - agentId: "thread-1", - commandId: "command-stable", - messageId: "message-stable", - sequence: 23, - sequenceSource: "dispatch", - recovered: false, - }); - }); - - test("reconciles the stable message after an ambiguous identical retry lands", async () => { - let attempts = 0; - let queryCount = 0; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - attempts += 1; - throw new AmbiguousDispatchError(); - }, - async getThread(threadId: string) { - queryCount += 1; - return { - threadId, - projectId: "project-1", - snapshotSequence: 20 + queryCount, - session: - queryCount === 3 - ? { status: "running", activeTurnId: "turn-2" } - : { status: "ready", activeTurnId: null }, - ...(queryCount === 3 - ? { latestUserMessageId: "message-stable" } - : {}), - latestTurn: - queryCount === 3 - ? { - turnId: "turn-2", - status: "running", - assistantMessage: null, - } - : null, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const ids = ["command-stable", "message-stable"]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-31T00:00:00.000Z", - }); - - const receipt = await facade.send("thread-1", "follow-up"); - - expect(attempts).toBe(2); - expect(queryCount).toBe(3); - expect(ids).toHaveLength(0); - expect(receipt).toEqual({ - agentId: "thread-1", - commandId: "command-stable", - messageId: "message-stable", - sequence: 23, - sequenceSource: "projection", - recovered: true, - }); - }); - - test("fails closed when ambiguous send identity sources conflict", async () => { - let attempts = 0; - let queryCount = 0; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - attempts += 1; - throw new AmbiguousDispatchError(); - }, - async getThread(threadId: string) { - queryCount += 1; - if (queryCount === 1) { - return { - threadId, - projectId: "project-1", - snapshotSequence: 20, - session: { status: "ready", activeTurnId: null }, - latestTurn: null, - pendingApproval: null, - pendingInput: null, - }; - } - return { - threadId, - projectId: "project-1", - snapshotSequence: 22, - session: { status: "running", activeTurnId: "turn-2" }, - latestUserMessageId: "message-stable", - latestTurn: { - turnId: "turn-2", - status: "running", - userMessageId: "message-stale", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const ids = ["command-stable", "message-stable"]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-31T00:00:00.000Z", - }); - - await expect(facade.send("thread-1", "follow-up")).rejects.toMatchObject({ - code: "turn_error", - sequence: 22, - }); - expect(attempts).toBe(1); - expect(queryCount).toBe(2); - expect(ids).toHaveLength(0); - }); - - test("fails closed without retry when the first ambiguous-send reconciliation returns a different thread", async () => { - let attempts = 0; - let queryCount = 0; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - attempts += 1; - throw new AmbiguousDispatchError(); - }, - async getThread(threadId: string) { - queryCount += 1; - return { - threadId: queryCount === 2 ? "thread-colliding" : threadId, - projectId: "project-1", - snapshotSequence: 20 + queryCount, - session: { status: "ready", activeTurnId: null }, - latestTurn: null, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const ids = ["command-stable", "message-stable"]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-31T00:00:00.000Z", - }); - - await expect(facade.send("thread-1", "follow-up")).rejects.toMatchObject({ - code: "transport_unavailable", - sequence: 22, - }); - expect(attempts).toBe(1); - expect(queryCount).toBe(2); - expect(ids).toHaveLength(0); - }); - - test("rejects a different thread after the final ambiguous-send reconciliation", async () => { - let attempts = 0; - let queryCount = 0; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - attempts += 1; - throw new AmbiguousDispatchError(); - }, - async getThread(threadId: string) { - queryCount += 1; - const colliding = queryCount === 3; - return { - threadId: colliding ? "thread-colliding" : threadId, - projectId: "project-1", - snapshotSequence: 20 + queryCount, - session: { status: "ready", activeTurnId: null }, - latestTurn: colliding - ? { - turnId: "turn-colliding", - status: "running", - userMessageId: "message-stable", - assistantMessage: null, - } - : null, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const ids = ["command-stable", "message-stable"]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-31T00:00:00.000Z", - }); - - await expect(facade.send("thread-1", "follow-up")).rejects.toBeInstanceOf( - AmbiguousDispatchError, - ); - expect(attempts).toBe(2); - expect(queryCount).toBe(3); - expect(ids).toHaveLength(0); - }); - - for (const blocked of [ - { - label: "an active turn", - session: { status: "ready", activeTurnId: "turn-other" }, - latestTurn: null, - pendingApproval: null, - pendingInput: null, - }, - { - label: "a starting session", - session: { status: "starting", activeTurnId: null }, - latestTurn: null, - pendingApproval: null, - pendingInput: null, - }, - { - label: "a running session", - session: { status: "running", activeTurnId: null }, - latestTurn: null, - pendingApproval: null, - pendingInput: null, - }, - { - label: "a running latest turn", - session: { status: "ready", activeTurnId: null }, - latestTurn: { - turnId: "turn-other", - status: "running", - userMessageId: "message-other", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }, - { - label: "pending approval", - session: { status: "ready", activeTurnId: null }, - latestTurn: null, - pendingApproval: { requestId: "approval-other" }, - pendingInput: null, - }, - { - label: "pending input", - session: { status: "ready", activeTurnId: null }, - latestTurn: null, - pendingApproval: null, - pendingInput: { requestId: "input-other" }, - }, - ] as const) { - test(`does not retry an ambiguous send after reconciliation finds ${blocked.label}`, async () => { - let attempts = 0; - let queryCount = 0; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - attempts += 1; - throw new AmbiguousDispatchError(); - }, - async getThread(threadId: string) { - queryCount += 1; - if (queryCount === 1) { - return { - threadId, - projectId: "project-1", - snapshotSequence: 20, - session: { status: "ready", activeTurnId: null }, - latestTurn: null, - pendingApproval: null, - pendingInput: null, - }; - } - return { - threadId, - projectId: "project-1", - snapshotSequence: 22, - session: blocked.session, - latestTurn: blocked.latestTurn, - pendingApproval: blocked.pendingApproval, - pendingInput: blocked.pendingInput, - }; - }, - async *subscribeThread() { - return; - }, - }; - const ids = ["command-stable", "message-stable"]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-31T00:00:00.000Z", - }); - - await expect(facade.send("thread-1", "follow-up")).rejects.toMatchObject({ - code: "turn_error", - sequence: 22, - }); - expect(attempts).toBe(1); - expect(queryCount).toBe(2); - expect(ids).toHaveLength(0); - }); - } - - test("fails after final reconciliation without a third dispatch or new IDs", async () => { - let attempts = 0; - let queryCount = 0; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - attempts += 1; - throw new AmbiguousDispatchError(); - }, - async getThread(threadId: string) { - queryCount += 1; - return { - threadId, - projectId: "project-1", - snapshotSequence: 20 + queryCount, - session: { status: "ready", activeTurnId: null }, - latestTurn: null, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const ids = ["command-stable", "message-stable"]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-31T00:00:00.000Z", - }); - - await expect(facade.send("thread-1", "follow-up")).rejects.toBeInstanceOf( - AmbiguousDispatchError, - ); - expect(attempts).toBe(2); - expect(queryCount).toBe(3); - expect(ids).toHaveLength(0); - }); - - for (const pendingKind of ["pendingApproval", "pendingInput"] as const) { - test(`refuses a new turn while native state reports ${pendingKind}`, async () => { - let attempts = 0; - const evidence: unknown[] = []; - const ids = ["command-unused", "message-unused"]; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - attempts += 1; - return { sequence: 3 }; - }, - async getThread(threadId: string) { - return { - threadId, - projectId: "project-1", - snapshotSequence: 20, - session: { status: "ready", activeTurnId: null }, - latestTurn: null, - pendingApproval: - pendingKind === "pendingApproval" - ? { requestId: "approval-1" } - : null, - pendingInput: - pendingKind === "pendingInput" ? { requestId: "input-1" } : null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - evidence: (record) => evidence.push(record), - }); - - await expect( - facade.send("thread-1", "must not dispatch"), - ).rejects.toMatchObject({ - code: "turn_error", - sequence: 20, - }); - expect(attempts).toBe(0); - expect(ids).toEqual(["command-unused", "message-unused"]); - expect(evidence).toEqual([]); - }); - } - - test("refuses a second turn while native state reports one in flight", async () => { - let attempts = 0; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - attempts += 1; - return { sequence: 3 }; - }, - async getThread(threadId: string) { - return { - threadId, - projectId: "project-1", - snapshotSequence: 20, - session: { status: "running", activeTurnId: "turn-1" }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-1", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - - await expect(facade.send("thread-1", "duplicate")).rejects.toMatchObject({ - code: "turn_error", - sequence: 20, - }); - expect(attempts).toBe(0); - }); -}); diff --git a/test/facade.spawn.test.ts b/test/facade.spawn.test.ts deleted file mode 100644 index 5d89d6b..0000000 --- a/test/facade.spawn.test.ts +++ /dev/null @@ -1,918 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { AmbiguousDispatchError, createT3Facade } from "../src/facade"; - -const DISPATCH_MODES = { - runtimeMode: "full-access", - interactionMode: "default", -} as const; - -describe("spawn", () => { - test("discovers the project by exact workspace root and starts one explicit atomic turn", async () => { - const calls: Array<{ - readonly operation: string; - readonly input: unknown; - }> = []; - const evidence: unknown[] = []; - const runtime = { - async listProjects() { - return [ - { projectId: "project-other", workspaceRoot: "/work/app-copy" }, - { projectId: "project-exact", workspaceRoot: "/work/app" }, - ]; - }, - async createProject(input: unknown) { - calls.push({ operation: "createProject", input }); - return { sequence: 1 }; - }, - async startThread(input: unknown) { - calls.push({ operation: "startThread", input }); - return { sequence: 12 }; - }, - async startTurn(input: unknown) { - calls.push({ operation: "startTurn", input }); - return { sequence: 13 }; - }, - async getThread(threadId: string) { - return { - threadId, - projectId: "project-exact", - snapshotSequence: 12, - session: { status: "running", activeTurnId: "turn-1" }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-1", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const ids = ["thread-1", "command-1", "message-1"]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-30T18:00:00.000Z", - evidence: (record) => evidence.push(record), - }); - - const result = await facade.spawn({ - workspaceRoot: "/work/app", - title: "reviewer", - message: "secret task body", - modelSelection: { - instanceId: "claudeAgent", - model: "claude-opus-5", - options: [ - { id: "effort", value: "high" }, - { id: "contextWindow", value: "1m" }, - ], - }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - }); - - expect(calls).toEqual([ - { - operation: "startThread", - input: { - commandId: "command-1", - projectId: "project-exact", - threadId: "thread-1", - messageId: "message-1", - title: "reviewer", - message: "secret task body", - modelSelection: { - instanceId: "claudeAgent", - model: "claude-opus-5", - options: [ - { id: "effort", value: "high" }, - { id: "contextWindow", value: "1m" }, - ], - }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - createdAt: "2026-07-30T18:00:00.000Z", - attachments: [], - }, - }, - ]); - expect(result.agentId).toBe("thread-1"); - expect(evidence).toEqual([ - { - operation: "spawn", - commandId: "command-1", - projectId: "project-exact", - threadId: "thread-1", - messageId: "message-1", - workspaceRoot: "/work/app", - modelSelection: { - instanceId: "claudeAgent", - model: "claude-opus-5", - optionCount: 2, - }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - createdAt: "2026-07-30T18:00:00.000Z", - attachments: 0, - messageBytes: 16, - }, - ]); - expect(JSON.stringify(evidence)).not.toContain("secret task body"); - }); - - test("counts multibyte spawn evidence by encoded byte length", async () => { - const evidence: unknown[] = []; - const runtime = { - async listProjects() { - return [{ projectId: "project-1", workspaceRoot: "/work/app" }]; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread(threadId: string) { - return { - threadId, - projectId: "project-1", - snapshotSequence: 2, - session: { status: "running", activeTurnId: "turn-1" }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-multibyte", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const ids = ["thread-multibyte", "command-multibyte", "message-multibyte"]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-31T00:00:00.000Z", - evidence: (record) => evidence.push(record), - }); - - await facade.spawn({ - workspaceRoot: "/work/app", - title: "worker", - message: "é🙂", - modelSelection: { - instanceId: "codex", - model: "gpt-5.6-sol", - options: [], - }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - }); - - expect("é🙂").toHaveLength(3); - expect(evidence).toEqual([ - { - operation: "spawn", - commandId: "command-multibyte", - projectId: "project-1", - threadId: "thread-multibyte", - messageId: "message-multibyte", - workspaceRoot: "/work/app", - modelSelection: { - instanceId: "codex", - model: "gpt-5.6-sol", - optionCount: 0, - }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - createdAt: "2026-07-31T00:00:00.000Z", - attachments: 0, - messageBytes: 6, - }, - ]); - expect(JSON.stringify(evidence)).not.toContain("é🙂"); - }); - - test("allowlists model-selection evidence without option values", async () => { - const credential = "hostile-model-option-credential"; - const evidence: unknown[] = []; - const runtime = { - async listProjects() { - return [{ projectId: "project-1", workspaceRoot: "/work/app" }]; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread(threadId: string) { - return { - threadId, - projectId: "project-1", - snapshotSequence: 2, - session: { status: "running", activeTurnId: "turn-1" }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-1", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const ids = ["thread-1", "command-1", "message-1"]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-31T00:00:00.000Z", - evidence: (record) => evidence.push(record), - }); - - await facade.spawn({ - workspaceRoot: "/work/app", - title: "worker", - message: "run once", - modelSelection: { - instanceId: "codex", - model: "gpt-5.6-sol", - options: [{ id: "credential", value: credential }], - }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - }); - - expect(evidence).toHaveLength(1); - expect(evidence[0]).toMatchObject({ - modelSelection: { - instanceId: "codex", - model: "gpt-5.6-sol", - optionCount: 1, - }, - }); - expect(JSON.stringify(evidence)).not.toContain(credential); - expect(JSON.stringify(evidence)).not.toContain("credential"); - }); - - test("creates a missing project without creating the workspace root", async () => { - const calls: Array<{ - readonly operation: string; - readonly input: unknown; - }> = []; - const runtime = { - async listProjects() { - return []; - }, - async createProject(input: unknown) { - calls.push({ operation: "createProject", input }); - return { sequence: 4 }; - }, - async startThread(input: unknown) { - calls.push({ operation: "startThread", input }); - return { sequence: 5 }; - }, - async startTurn() { - return { sequence: 6 }; - }, - async getThread(threadId: string) { - return { - threadId, - projectId: "project-1", - snapshotSequence: 5, - session: { status: "running", activeTurnId: "turn-1" }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-1", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const ids = [ - "project-1", - "project-command-1", - "thread-1", - "thread-command-1", - "message-1", - ]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-30T18:00:00.000Z", - }); - - await facade.spawn({ - workspaceRoot: "/work/new-app", - title: "worker", - message: "do work", - modelSelection: { - instanceId: "codex", - model: "gpt-5.6-sol", - options: [{ id: "effort", value: "high" }], - }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - }); - - expect(calls[0]).toEqual({ - operation: "createProject", - input: { - commandId: "project-command-1", - projectId: "project-1", - title: "worker", - workspaceRoot: "/work/new-app", - createWorkspaceRootIfMissing: false, - defaultModelSelection: { - instanceId: "codex", - model: "gpt-5.6-sol", - options: [{ id: "effort", value: "high" }], - }, - createdAt: "2026-07-30T18:00:00.000Z", - }, - }); - expect(calls[1]).toMatchObject({ - operation: "startThread", - input: { - projectId: "project-1", - threadId: "thread-1", - commandId: "thread-command-1", - messageId: "message-1", - }, - }); - }); - - test("reconciles an ambiguously created project after one identical retry", async () => { - const projectAttempts: unknown[] = []; - const threadAttempts: unknown[] = []; - let listCount = 0; - const runtime = { - async listProjects() { - listCount += 1; - return listCount === 3 - ? [{ projectId: "project-stable", workspaceRoot: "/work/app" }] - : []; - }, - async createProject(input: unknown) { - projectAttempts.push(input); - throw new AmbiguousDispatchError(); - }, - async startThread(input: unknown) { - threadAttempts.push(input); - return { sequence: 5 }; - }, - async startTurn() { - return { sequence: 6 }; - }, - async getThread(threadId: string) { - return { - threadId, - projectId: "project-stable", - snapshotSequence: 5, - session: { status: "running", activeTurnId: "turn-1" }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-stable", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const ids = [ - "project-stable", - "project-command-stable", - "thread-stable", - "thread-command-stable", - "message-stable", - ]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-31T00:00:00.000Z", - }); - - const snapshot = await facade.spawn({ - workspaceRoot: "/work/app", - title: "worker", - message: "run once", - modelSelection: { - instanceId: "codex", - model: "gpt-5.6-sol", - options: [], - }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - }); - - expect(projectAttempts).toHaveLength(2); - expect(projectAttempts[1]).toBe(projectAttempts[0]); - expect(listCount).toBe(3); - expect(threadAttempts).toHaveLength(1); - expect(threadAttempts[0]).toMatchObject({ - projectId: "project-stable", - threadId: "thread-stable", - messageId: "message-stable", - }); - expect(ids).toHaveLength(0); - expect(snapshot).toMatchObject({ - agentId: "thread-stable", - projectId: "project-stable", - }); - }); - - test("retries an ambiguous spawn with the identical preallocated payload", async () => { - const attempts: unknown[] = []; - let queryCount = 0; - const runtime = { - async listProjects() { - return [{ projectId: "project-1", workspaceRoot: "/work/app" }]; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread(input: unknown) { - attempts.push(input); - if (attempts.length === 1) { - throw new AmbiguousDispatchError(); - } - return { sequence: 9 }; - }, - async startTurn() { - return { sequence: 10 }; - }, - async getThread(threadId: string) { - queryCount += 1; - if (queryCount === 1) return undefined; - return { - threadId, - projectId: "project-1", - snapshotSequence: 9, - session: { status: "running", activeTurnId: "turn-stable" }, - latestTurn: { - turnId: "turn-stable", - status: "running", - userMessageId: "message-stable", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const ids = ["thread-stable", "command-stable", "message-stable"]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-30T18:00:00.000Z", - }); - - await facade.spawn({ - workspaceRoot: "/work/app", - title: "worker", - message: "run once", - modelSelection: { - instanceId: "codex", - model: "gpt-5.6-sol", - options: [], - }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - }); - - expect(attempts).toHaveLength(2); - expect(attempts[1]).toEqual(attempts[0]); - expect(ids).toHaveLength(0); - }); - - test("reconciles the same thread after an ambiguous identical retry lands", async () => { - const attempts: unknown[] = []; - let queryCount = 0; - const runtime = { - async listProjects() { - return [{ projectId: "project-selected", workspaceRoot: "/work/app" }]; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread(input: unknown) { - attempts.push(input); - throw new AmbiguousDispatchError(); - }, - async startTurn() { - return { sequence: 10 }; - }, - async getThread(threadId: string) { - queryCount += 1; - if (queryCount === 1) return undefined; - return { - threadId, - projectId: "project-selected", - snapshotSequence: 9, - session: { status: "running", activeTurnId: "turn-stable" }, - latestTurn: { - turnId: "turn-stable", - status: "running", - userMessageId: "message-stable", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const ids = ["thread-stable", "command-stable", "message-stable"]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-31T00:00:00.000Z", - }); - - const snapshot = await facade.spawn({ - workspaceRoot: "/work/app", - title: "worker", - message: "run once", - modelSelection: { - instanceId: "codex", - model: "gpt-5.6-sol", - options: [], - }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - }); - - expect(attempts).toHaveLength(2); - expect(attempts[1]).toBe(attempts[0]); - expect(queryCount).toBe(2); - expect(ids).toHaveLength(0); - expect(snapshot).toMatchObject({ - agentId: "thread-stable", - projectId: "project-selected", - sequence: 9, - }); - }); - - test("returns a structural facade error when an accepted spawn snapshot is unavailable", async () => { - const runtime = { - async listProjects() { - return [{ projectId: "project-selected", workspaceRoot: "/work/app" }]; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 9 }; - }, - async startTurn() { - return { sequence: 10 }; - }, - async getThread() { - return undefined; - }, - async *subscribeThread() { - return; - }, - }; - const ids = ["thread-stable", "command-stable", "message-stable"]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-31T00:00:00.000Z", - }); - - const result = facade.spawn({ - workspaceRoot: "/work/app", - title: "worker", - message: "run once", - modelSelection: { - instanceId: "codex", - model: "gpt-5.6-sol", - options: [], - }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - }); - - await expect(result).rejects.toMatchObject({ - code: "transport_unavailable", - sequence: 0, - structuralSnapshot: { - threadId: "thread-stable", - projectId: "project-selected", - }, - }); - expect(ids).toHaveLength(0); - }); - - test("accepts an exact initial message before the provider assigns a turn id", async () => { - const runtime = { - async listProjects() { - return [{ projectId: "project-selected", workspaceRoot: "/work/app" }]; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 9 }; - }, - async startTurn() { - return { sequence: 10 }; - }, - async getThread() { - return { - threadId: "thread-stable", - projectId: "project-selected", - snapshotSequence: 9, - session: { status: "starting", activeTurnId: null }, - latestUserMessageId: "message-stable", - latestTurn: null, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const ids = ["thread-stable", "command-stable", "message-stable"]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-31T00:00:00.000Z", - }); - - const snapshot = await facade.spawn({ - workspaceRoot: "/work/app", - title: "worker", - message: "run once", - modelSelection: { - instanceId: "codex", - model: "gpt-5.6-sol", - options: [], - }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - }); - - expect(snapshot).toMatchObject({ - agentId: "thread-stable", - projectId: "project-selected", - sequence: 9, - native: { - latestUserMessageId: "message-stable", - latestTurn: null, - }, - }); - expect(ids).toHaveLength(0); - }); - - for (const mismatch of [ - { - label: "thread", - threadId: "thread-other", - projectId: "project-selected", - userMessageId: "message-stable", - }, - { - label: "project", - threadId: "thread-stable", - projectId: "project-other", - userMessageId: "message-stable", - }, - { - label: "initial message", - threadId: "thread-stable", - projectId: "project-selected", - userMessageId: "message-other", - }, - { - label: "inconsistent message identity", - threadId: "thread-stable", - projectId: "project-selected", - latestUserMessageId: "message-stable", - userMessageId: "message-other", - }, - ]) { - test(`fails closed when an accepted spawn lookup returns a mismatched ${mismatch.label}`, async () => { - const runtime = { - async listProjects() { - return [ - { projectId: "project-selected", workspaceRoot: "/work/app" }, - ]; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 9 }; - }, - async startTurn() { - return { sequence: 10 }; - }, - async getThread() { - return { - threadId: mismatch.threadId, - projectId: mismatch.projectId, - snapshotSequence: 9, - session: { status: "running", activeTurnId: "turn-1" }, - ...("latestUserMessageId" in mismatch - ? { latestUserMessageId: mismatch.latestUserMessageId } - : {}), - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: mismatch.userMessageId, - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const ids = ["thread-stable", "command-stable", "message-stable"]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-31T00:00:00.000Z", - }); - - const result = facade.spawn({ - workspaceRoot: "/work/app", - title: "worker", - message: "run once", - modelSelection: { - instanceId: "codex", - model: "gpt-5.6-sol", - options: [], - }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - }); - - await expect(result).rejects.toMatchObject({ - code: "transport_unavailable", - sequence: 9, - structuralSnapshot: { - threadId: mismatch.threadId, - projectId: mismatch.projectId, - }, - }); - expect(ids).toHaveLength(0); - }); - } - - for (const collision of [ - { - label: "project", - threadId: "thread-stable", - projectId: "project-colliding", - userMessageId: "message-stable", - }, - { - label: "initial message", - threadId: "thread-stable", - projectId: "project-selected", - userMessageId: "message-colliding", - }, - { - label: "thread", - threadId: "thread-colliding", - projectId: "project-selected", - userMessageId: "message-stable", - }, - ]) { - test(`fails closed when ambiguous recovery finds a colliding ${collision.label} identity`, async () => { - let attempts = 0; - let queryCount = 0; - const runtime = { - async listProjects() { - return [ - { projectId: "project-selected", workspaceRoot: "/work/app" }, - ]; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - attempts += 1; - throw new AmbiguousDispatchError(); - }, - async startTurn() { - return { sequence: 10 }; - }, - async getThread() { - queryCount += 1; - return { - threadId: collision.threadId, - projectId: collision.projectId, - snapshotSequence: 9, - session: { status: "running", activeTurnId: "turn-colliding" }, - latestTurn: { - turnId: "turn-colliding", - status: "running", - userMessageId: collision.userMessageId, - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - }, - async *subscribeThread() { - return; - }, - }; - const ids = ["thread-stable", "command-stable", "message-stable"]; - const facade = createT3Facade(runtime, { - ...DISPATCH_MODES, - id: () => ids.shift()!, - now: () => "2026-07-31T00:00:00.000Z", - }); - - await expect( - facade.spawn({ - workspaceRoot: "/work/app", - title: "worker", - message: "run once", - modelSelection: { - instanceId: "codex", - model: "gpt-5.6-sol", - options: [], - }, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - }), - ).rejects.toBeInstanceOf(AmbiguousDispatchError); - - expect(attempts).toBe(1); - expect(queryCount).toBe(1); - expect(ids).toHaveLength(0); - }); - } -}); diff --git a/test/facade.stock-http.test.ts b/test/facade.stock-http.test.ts new file mode 100644 index 0000000..27dd234 --- /dev/null +++ b/test/facade.stock-http.test.ts @@ -0,0 +1,891 @@ +import { describe, expect, test } from "bun:test"; + +import { + StockRuntimeError, + createStockT3NativeRuntime, + digestStockSpawnInput, + type StockT3RuntimeClient, +} from "../src/nativeRuntime"; +import type { + ShellSnapshot, + StockMessage, + StockThreadDetail, + StockThreadShell, + ThreadDetailSnapshot, +} from "../src/stockT3Contracts"; +import { StockT3HttpError } from "../src/stockT3HttpClient"; +import { createStockT3Facade } from "../src/facade"; + +const iso = "2026-07-31T18:00:00.000Z"; +const selection = { instanceId: "claudeAgent", model: "claude-opus-5" }; +const project = { + id: "project-1", + title: "project", + workspaceRoot: "/tmp/project", + defaultModelSelection: selection, + createdAt: iso, + updatedAt: iso, +}; + +function threadIdentity(id = "thread-1"): StockThreadShell { + return { + id, + projectId: project.id, + title: "worker", + modelSelection: selection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + latestTurn: null, + createdAt: iso, + updatedAt: iso, + session: null, + latestUserMessageAt: null, + hasPendingApprovals: false, + hasPendingUserInput: false, + }; +} + +function shell(sequence: number, threads: StockThreadShell[] = []): ShellSnapshot { + return { snapshotSequence: sequence, projects: [project], threads, updatedAt: iso }; +} + +function detail( + sequence: number, + messages: StockMessage[] = [], + latestTurn: StockThreadDetail["latestTurn"] = null, +): ThreadDetailSnapshot { + const identity = threadIdentity(); + return { + snapshotSequence: sequence, + thread: { + id: identity.id, + projectId: identity.projectId, + title: identity.title, + modelSelection: identity.modelSelection, + runtimeMode: identity.runtimeMode, + interactionMode: identity.interactionMode, + branch: identity.branch, + worktreePath: identity.worktreePath, + latestTurn, + createdAt: identity.createdAt, + updatedAt: identity.updatedAt, + session: null, + messages, + activities: [], + checkpoints: [], + }, + }; +} + +function message(id: string, text: string, createdAt = iso): StockMessage { + return { + id, + role: "user", + text, + attachments: [], + turnId: null, + streaming: false, + createdAt, + updatedAt: createdAt, + }; +} + +function client(overrides: Partial = {}): StockT3RuntimeClient { + return { + getDescriptor: async () => ({ + environmentId: "env-1", + label: "local", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "stock", + capabilities: { repositoryIdentity: false }, + }), + getShell: async () => shell(1), + getThread: async () => undefined, + dispatch: async () => ({ sequence: 1 }), + ...overrides, + }; +} + +const spawnInput = { + workspaceRoot: project.workspaceRoot, + title: "worker", + message: "initial", + modelSelection: selection, + runtimeMode: "full-access" as const, + interactionMode: "default" as const, + branch: null, + worktreePath: null, +}; + +describe("stock HTTP runtime state machine", () => { + test("exposes the stock runtime through the public facade seam", async () => { + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => shell(1, [threadIdentity()]), + getThread: async () => detail(1), + }), + }); + const facade = createStockT3Facade(runtime); + + await expect( + facade.observe({ environmentId: "env-1", threadId: "thread-1" }), + ).resolves.toMatchObject({ snapshotSequence: 1 }); + expect(runtime.pollMetrics()).toMatchObject({ shellStarts: 0, activeWaits: 0 }); + }); + + test("spawns with reconciled thread.create then bootstrap-free thread.turn.start", async () => { + const commands: Array> = []; + const details = [ + detail(2), + detail(2), + detail(4, [message("message-1", "initial")]), + ]; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => shell(4, [threadIdentity()]), + getThread: async () => details.shift(), + dispatch: async (command) => { + commands.push(command); + return { sequence: commands.length === 1 ? 2 : 4 }; + }, + }), + id: (() => { + const ids = ["thread-create-1", "thread-1", "turn-command-1", "message-1", "lease-1"]; + return () => ids.shift()!; + })(), + now: () => iso, + }); + + const result = await runtime.spawn(spawnInput); + + expect(result.kind).toBe("spawned"); + expect(commands.map((command) => command.type)).toEqual([ + "thread.create", + "thread.turn.start", + ]); + expect(commands[0]?.commandId).not.toBe(commands[1]?.commandId); + expect(commands[1]).not.toHaveProperty("bootstrap"); + if (result.kind === "spawned") { + expect(result.agentRef).toEqual({ environmentId: "env-1", threadId: "thread-1" }); + expect(result.turnReceipt.messageId).toBe("message-1"); + } + }); + + test("preserves an accepted create as reconciliation-pending after projection absence", async () => { + const commands: Array> = []; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => shell(1), + getThread: async () => undefined, + dispatch: async (command) => { + commands.push(command); + return { sequence: 7 }; + }, + }), + id: (() => { + const ids = ["create-1", "thread-1", "turn-1", "message-1"]; + return () => ids.shift()!; + })(), + now: () => iso, + }); + + const result = await runtime.spawn(spawnInput, { maxReconciliationReads: 1 }); + + expect(result).toMatchObject({ + kind: "create_reconciliation_pending", + provisionalRef: { environmentId: "env-1", threadId: "thread-1" }, + createAttempt: { + commandId: "create-1", + threadId: "thread-1", + acceptedSequence: 7, + dispatchState: "accepted", + retryState: "not_applicable", + }, + reconciliation: { reason: "projection_pending" }, + initialTurnContinuation: { commandId: "turn-1", messageId: "message-1" }, + safeAction: "resume_create_reconciliation", + }); + expect(commands).toHaveLength(1); + }); + + test.each([ + [400, "command_rejected", "invalid_request", "invalid_command"], + [401, "authentication_failed", "auth_invalid", null], + [403, "permission_denied", "insufficient_scope", null], + [500, "server_internal", "internal_error", "orchestration_dispatch_failed"], + ] as const)( + "keeps ambiguous original create pending when identical retry returns %i", + async (status, errorClass, code, reason) => { + let dispatches = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => shell(0), + getThread: async () => undefined, + dispatch: async () => { + dispatches += 1; + if (dispatches === 1) { + throw new StockT3HttpError("transport_unavailable", null); + } + throw new StockT3HttpError(errorClass, status, { code, reason }); + }, + }), + id: (() => { + const ids = ["create-1", "thread-1", "turn-1", "message-1"]; + return () => ids.shift()!; + })(), + now: () => iso, + }); + + const result = await runtime.spawn(spawnInput, { maxReconciliationReads: 1 }); + + expect(result).toMatchObject({ + kind: "create_reconciliation_pending", + createAttempt: { + dispatchState: "outcome_unknown", + acceptedSequence: null, + retryState: "identical_retry_received_error", + retryError: { status, class: errorClass, code, reason }, + }, + reconciliation: { reason: "retry_error_after_ambiguous_original" }, + safeAction: "resume_create_reconciliation", + }); + expect(dispatches).toBe(2); + }, + ); + + test("resume performs reads only before reconciliation and preserves initial-turn IDs", async () => { + const commands: Array> = []; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => shell(9, [threadIdentity()]), + getThread: async () => + commands.length === 0 + ? detail(9) + : detail(10, [message("message-1", "initial")]), + dispatch: async (command) => { + commands.push(command); + return { sequence: 10 }; + }, + }), + id: () => "lease-1", + now: () => iso, + }); + const pending = { + kind: "create_reconciliation_pending" as const, + provisionalRef: { environmentId: "env-1", threadId: "thread-1" }, + createAttempt: { + commandId: "create-1", + threadId: "thread-1", + projectId: "project-1", + acceptedSequence: 9, + dispatchState: "accepted" as const, + retryState: "not_applicable" as const, + retryError: null, + }, + reconciliation: { + reason: "projection_pending" as const, + projectionState: "unobserved" as const, + highestShellSequence: null, + highestDetailSequence: null, + deadlineMs: Date.now() + 1_000, + evidence: [], + }, + initialTurnContinuation: { + commandId: "turn-1", + messageId: "message-1", + inputDigest: await digestStockSpawnInput(spawnInput), + }, + safeAction: "resume_create_reconciliation" as const, + }; + + const result = await runtime.resumeCreateReconciliation(pending, spawnInput); + + expect(result.kind).toBe("spawned"); + expect(commands).toHaveLength(1); + expect(commands[0]).toMatchObject({ + type: "thread.turn.start", + commandId: "turn-1", + threadId: "thread-1", + message: { messageId: "message-1", text: "initial" }, + }); + }); + + test("serializes sends with an expiring per-thread lease", async () => { + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => shell(2, [threadIdentity()]), + getThread: async () => detail(2), + dispatch: async () => ({ sequence: 3 }), + }), + id: (() => { + const ids = ["command-1", "message-1", "lease-1", "command-2", "message-2", "lease-2"]; + return () => ids.shift()!; + })(), + now: () => iso, + }); + + const first = await runtime.send({ environmentId: "env-1", threadId: "thread-1" }, "one"); + expect(first.messageId).toBe("message-1"); + await expect( + runtime.send({ environmentId: "env-1", threadId: "thread-1" }, "two"), + ).rejects.toMatchObject({ code: "send_in_progress" }); + runtime.releaseReceipt(first); + await expect( + runtime.send({ environmentId: "env-1", threadId: "thread-1" }, "two"), + ).resolves.toMatchObject({ messageId: "message-2" }); + }); + + test("fails causal wait when a distinct-ID writer appears before the target", async () => { + let detailReads = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => shell(4, [threadIdentity()]), + getThread: async () => { + detailReads += 1; + return detailReads === 1 + ? detail(2) + : detail(4, [message("foreign", "foreign")]); + }, + dispatch: async () => ({ sequence: 3 }), + }), + id: (() => { + const ids = ["command-1", "target", "lease-1"]; + return () => ids.shift()!; + })(), + now: () => iso, + }); + const receipt = await runtime.send( + { environmentId: "env-1", threadId: "thread-1" }, + "target text", + ); + + const error = await runtime.wait(receipt, { timeoutMs: 1_000 }).catch((cause) => cause); + expect(error).toBeInstanceOf(StockRuntimeError); + expect(error.code).toBe("superseded"); + }); + + test("retries an ambiguous initial turn exactly once with byte-identical command identity", async () => { + const commands: Array> = []; + const details = [ + detail(2), + detail(2), + detail(2), + detail(4, [message("message-1", "initial")]), + ]; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => shell(4, [threadIdentity()]), + getThread: async () => details.shift(), + dispatch: async (command) => { + commands.push(command); + if (commands.length === 1) return { sequence: 2 }; + if (commands.length === 2) { + throw new StockT3HttpError("transport_unavailable", null, { + reason: "request_failed", + }); + } + return { sequence: 4 }; + }, + }), + id: (() => { + const ids = ["create-1", "thread-1", "turn-1", "message-1", "lease-1"]; + return () => ids.shift()!; + })(), + now: () => iso, + }); + + const result = await runtime.spawn(spawnInput); + + expect(result.kind).toBe("spawned"); + expect(commands.map((entry) => entry.type)).toEqual([ + "thread.create", + "thread.turn.start", + "thread.turn.start", + ]); + expect(commands[2]).toEqual(commands[1]); + }); + + test.each([ + [400, "command_rejected"], + [401, "authentication_failed"], + [403, "permission_denied"], + [500, "server_internal"], + ] as const)( + "preserves ambiguous initial-turn identity when its identical retry returns %i", + async (status, errorClass) => { + const commands: Array> = []; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => shell(4, [threadIdentity()]), + getThread: async () => detail(4), + dispatch: async (command) => { + commands.push(command); + if (commands.length === 1) return { sequence: 2 }; + if (commands.length === 2) throw new StockT3HttpError("transport_unavailable", null); + throw new StockT3HttpError(errorClass, status); + }, + }), + id: (() => { + const ids = ["create-1", "thread-1", "turn-1", "message-1", "lease-1"]; + return () => ids.shift()!; + })(), + now: () => iso, + }); + + const result = await runtime.spawn(spawnInput, { maxReconciliationReads: 1 }); + + expect(result).toMatchObject({ + kind: "partial", + initialTurn: { + commandId: "turn-1", + messageId: "message-1", + state: "initial_turn_outcome_unknown", + safeAction: "wait", + turnReceipt: { + commandId: "turn-1", + messageId: "message-1", + leaseId: "lease-1", + }, + evidence: [{ retryClass: errorClass }], + }, + }); + expect(commands).toHaveLength(3); + expect(commands[2]).toEqual(commands[1]); + }, + ); + + test.each([ + [400, "command_rejected"], + [401, "authentication_failed"], + [403, "permission_denied"], + [500, "server_internal"], + ] as const)( + "preserves an ambiguous send receipt when its identical retry returns %i", + async (status, errorClass) => { + const commands: Array> = []; + const runtime = createStockT3NativeRuntime({ + client: client({ + getThread: async () => detail(2), + dispatch: async (command) => { + commands.push(command); + if (commands.length === 1) throw new StockT3HttpError("transport_unavailable", null); + throw new StockT3HttpError(errorClass, status); + }, + }), + id: (() => { + const ids = ["command-1", "message-1", "lease-1"]; + return () => ids.shift()!; + })(), + now: () => iso, + }); + + const receipt = await runtime.send( + { environmentId: "env-1", threadId: "thread-1" }, + "follow-up", + { maxReconciliationReads: 1 }, + ); + + expect(receipt).toMatchObject({ commandId: "command-1", messageId: "message-1", acceptedSequence: null }); + expect(commands).toHaveLength(2); + expect(commands[1]).toEqual(commands[0]); + }, + ); + + test("does not retry an ambiguous send after cancellation wins", async () => { + const controller = new AbortController(); + let detailReads = 0; + let dispatches = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getThread: async () => { + detailReads += 1; + if (detailReads === 2) controller.abort(); + return detail(2); + }, + dispatch: async () => { + dispatches += 1; + throw new StockT3HttpError("transport_unavailable", null, { + reason: "request_failed", + }); + }, + }), + id: (() => { + const ids = ["command-1", "message-1", "lease-1"]; + return () => ids.shift()!; + })(), + now: () => iso, + }); + + await expect( + runtime.send( + { environmentId: "env-1", threadId: "thread-1" }, + "follow-up", + { signal: controller.signal }, + ), + ).rejects.toMatchObject({ code: "cancelled" }); + expect(dispatches).toBe(1); + }); + + test("uses one absolute timeout and never starts the initial turn at its inclusive deadline", async () => { + let current = 0; + let shellReads = 0; + const commands: Array> = []; + const runtime = createStockT3NativeRuntime({ + client: client({ + getDescriptor: async () => { + current = 10; + return { + environmentId: "env-1", + label: "local", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "stock", + capabilities: { repositoryIdentity: false }, + }; + }, + getShell: async () => { + shellReads += 1; + current = shellReads === 1 ? 90 : 100; + return shell(2, shellReads === 1 ? [] : [threadIdentity()]); + }, + getThread: async () => detail(2), + dispatch: async (command) => { + commands.push(command); + return { sequence: 2 }; + }, + }), + id: (() => { + const ids = ["create-1", "thread-1", "turn-1", "message-1"]; + return () => ids.shift()!; + })(), + now: () => iso, + clock: () => current, + }); + + const result = await runtime.spawn(spawnInput, { timeoutMs: 100 }); + + expect(result).toMatchObject({ + kind: "create_reconciliation_pending", + reconciliation: { + reason: "deadline_exhausted", + projectionState: "shell_only", + }, + safeAction: "resume_create_reconciliation", + }); + expect(commands.map((entry) => entry.type)).toEqual(["thread.create"]); + }); + + test("never creates a missing project when resolution reaches the inclusive deadline", async () => { + let current = 0; + const commands: Array> = []; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => { + current = 100; + return { ...shell(1), projects: [] }; + }, + dispatch: async (command) => { + commands.push(command); + return { sequence: 1 }; + }, + }), + clock: () => current, + }); + + await expect(runtime.spawn(spawnInput, { timeoutMs: 100 })).rejects.toMatchObject({ + code: "timeout", + }); + expect(commands).toHaveLength(0); + }); + + test("accepts valid detail-only resume evidence without manufacturing an identity conflict", async () => { + let dispatches = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => shell(9), + getThread: async () => detail(9), + dispatch: async () => { + dispatches += 1; + return { sequence: 10 }; + }, + }), + id: () => "unused", + now: () => iso, + }); + const pending = { + kind: "create_reconciliation_pending" as const, + provisionalRef: { environmentId: "env-1", threadId: "thread-1" }, + createAttempt: { + commandId: "create-1", + threadId: "thread-1", + projectId: "project-1", + acceptedSequence: 9, + dispatchState: "accepted" as const, + retryState: "not_applicable" as const, + retryError: null, + }, + reconciliation: { + reason: "projection_pending" as const, + projectionState: "unobserved" as const, + highestShellSequence: null, + highestDetailSequence: null, + deadlineMs: Date.now() + 1_000, + evidence: [], + }, + initialTurnContinuation: { + commandId: "turn-1", + messageId: "message-1", + inputDigest: await digestStockSpawnInput(spawnInput), + }, + safeAction: "resume_create_reconciliation" as const, + }; + + const result = await runtime.resumeCreateReconciliation(pending, spawnInput, { + maxReconciliationReads: 1, + }); + + expect(result).toMatchObject({ + kind: "create_reconciliation_pending", + provisionalRef: pending.provisionalRef, + reconciliation: { projectionState: "detail_only" }, + }); + expect(dispatches).toBe(0); + }); + + test("fails closed when terminal shell and detail bind different turns", async () => { + const targetTurn = { + turnId: "turn-target", + state: "completed" as const, + requestedAt: iso, + startedAt: iso, + completedAt: iso, + assistantMessageId: "assistant-target", + }; + const foreignTurn = { ...targetTurn, turnId: "turn-foreign" }; + let detailReads = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => + shell(3, [{ ...threadIdentity(), latestTurn: foreignTurn }]), + getThread: async () => { + detailReads += 1; + if (detailReads === 1) return detail(1); + return detail( + 3, + [ + message("message-target", "follow-up"), + { + id: "assistant-target", + role: "assistant", + text: "foreign content must not complete", + attachments: [], + turnId: "turn-target", + streaming: false, + createdAt: iso, + updatedAt: iso, + }, + ], + targetTurn, + ); + }, + dispatch: async () => ({ sequence: 2 }), + }), + id: (() => { + const ids = ["command-target", "message-target", "lease-target"]; + return () => ids.shift()!; + })(), + now: () => iso, + }); + const receipt = await runtime.send( + { environmentId: "env-1", threadId: "thread-1" }, + "follow-up", + ); + + await expect(runtime.wait(receipt, { timeoutMs: 1_000 })).rejects.toMatchObject({ + code: "concurrent_writer", + }); + }); + + test.each([ + ["same-ID payload mutation", [message("message-target", "mutated")], "causality_unverifiable"], + ["writer after target", [message("message-target", "follow-up"), message("foreign", "foreign")], "concurrent_writer"], + ] as const)("fails closed on detectable %s", async (_label, messages, expectedCode) => { + const terminal = { + turnId: "turn-target", + state: "completed" as const, + requestedAt: iso, + startedAt: iso, + completedAt: iso, + assistantMessageId: "assistant-target", + }; + let reads = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => shell(3, [{ ...threadIdentity(), latestTurn: terminal }]), + getThread: async () => { + reads += 1; + return reads === 1 ? detail(1) : detail(3, [...messages], terminal); + }, + dispatch: async () => ({ sequence: 2 }), + }), + id: (() => { + const ids = ["command-target", "message-target", "lease-target"]; + return () => ids.shift()!; + })(), + now: () => iso, + }); + const receipt = await runtime.send( + { environmentId: "env-1", threadId: "thread-1" }, + "follow-up", + ); + + await expect(runtime.wait(receipt, { timeoutMs: 1_000 })).rejects.toMatchObject({ + code: expectedCode, + }); + }); + + test("documents the indistinguishable exact-ID/payload reuse boundary", async () => { + const terminal = { + turnId: "turn-target", + state: "completed" as const, + requestedAt: iso, + startedAt: iso, + completedAt: iso, + assistantMessageId: "assistant-target", + }; + let reads = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => shell(3, [{ ...threadIdentity(), latestTurn: terminal }]), + getThread: async () => { + reads += 1; + return reads === 1 + ? detail(1) + : detail(3, [ + message("message-target", "follow-up"), + { + id: "assistant-target", + role: "assistant", + text: "completed", + attachments: [], + turnId: "turn-target", + streaming: false, + createdAt: iso, + updatedAt: iso, + }, + ], terminal); + }, + dispatch: async () => ({ sequence: 2 }), + }), + id: (() => { + const ids = ["command-target", "message-target", "lease-target"]; + return () => ids.shift()!; + })(), + now: () => iso, + }); + const receipt = await runtime.send( + { environmentId: "env-1", threadId: "thread-1" }, + "follow-up", + ); + + await expect(runtime.wait(receipt, { timeoutMs: 1_000 })).resolves.toMatchObject({ + kind: "completed", + assistantContent: "completed", + }); + }); + + test("caps terminal assistant evidence on a valid UTF-8 boundary and reports truncation", async () => { + const targetTurn = { + turnId: "turn-target", + state: "completed" as const, + requestedAt: iso, + startedAt: iso, + completedAt: iso, + assistantMessageId: "assistant-target", + }; + const oversized = `${"a".repeat(256 * 1024 - 1)}🙂tail`; + let detailReads = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => + shell(3, [{ ...threadIdentity(), latestTurn: targetTurn }]), + getThread: async () => { + detailReads += 1; + if (detailReads === 1) return detail(1); + return detail( + 3, + [ + message("message-target", "follow-up"), + { + id: "assistant-target", + role: "assistant", + text: oversized, + attachments: [], + turnId: "turn-target", + streaming: false, + createdAt: iso, + updatedAt: iso, + }, + ], + targetTurn, + ); + }, + dispatch: async () => ({ sequence: 2 }), + }), + id: (() => { + const ids = ["command-target", "message-target", "lease-target"]; + return () => ids.shift()!; + })(), + now: () => iso, + }); + const receipt = await runtime.send( + { environmentId: "env-1", threadId: "thread-1" }, + "follow-up", + ); + + const result = await runtime.wait(receipt, { timeoutMs: 1_000 }); + const encoded = new TextEncoder().encode(result.assistantContent); + expect(encoded.byteLength).toBeLessThanOrEqual(256 * 1024); + expect(new TextDecoder("utf-8", { fatal: true }).decode(encoded)).toBe( + result.assistantContent, + ); + expect(result).toMatchObject({ + evidence: { + truncated: true, + originalBytes: new TextEncoder().encode(oversized).byteLength, + retainedBytes: encoded.byteLength, + }, + }); + }); + + test("expires a receipt and its send lease at the inclusive boundary", async () => { + let current = 0; + let idIndex = 0; + const ids = [ + "command-first", + "message-first", + "lease-first", + "command-second", + "message-second", + "lease-second", + ]; + const runtime = createStockT3NativeRuntime({ + client: client({ + getThread: async () => detail(1), + dispatch: async () => ({ sequence: 2 }), + }), + id: () => ids[idIndex++]!, + now: () => iso, + clock: () => current, + }); + const ref = { environmentId: "env-1", threadId: "thread-1" }; + const receipt = await runtime.send(ref, "first", { deadlineMs: 100 }); + current = 100; + + await expect(runtime.wait(receipt)).rejects.toMatchObject({ code: "receipt_expired" }); + await expect(runtime.send(ref, "second", { deadlineMs: 200 })).resolves.toMatchObject({ + messageId: "message-second", + }); + }); +}); diff --git a/test/facade.wait.test.ts b/test/facade.wait.test.ts deleted file mode 100644 index b591cc0..0000000 --- a/test/facade.wait.test.ts +++ /dev/null @@ -1,1259 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { FacadeError, createT3Facade, type AgentEvent } from "../src/facade"; - -const DISPATCH_MODES = { - runtimeMode: "full-access", - interactionMode: "default", -} as const; - -async function collect( - iterable: AsyncIterable, -): Promise { - const events: AgentEvent[] = []; - for await (const event of iterable) events.push(event); - return events; -} - -describe("FacadeError", () => { - test("allowlists structural session fields and drops unexpected secrets", () => { - const bearer = "Bearer hostile-runtime-secret"; - const hostileSession = { - status: "running", - activeTurnId: "turn-1", - authorization: bearer, - }; - const error = new FacadeError("timeout", { - threadId: "thread-1", - projectId: "project-1", - snapshotSequence: 9, - session: hostileSession, - latestTurn: null, - pendingApproval: null, - pendingInput: null, - }); - - expect(error.structuralSnapshot.session).toEqual({ - status: "running", - activeTurnId: "turn-1", - }); - expect(JSON.stringify(error.structuralSnapshot)).not.toContain(bearer); - expect(JSON.stringify(error.structuralSnapshot)).not.toContain( - "authorization", - ); - }); -}); - -describe("wait", () => { - test("waits past receipt and streaming content until structural completion", async () => { - const subscriptions: unknown[] = []; - const initial = { - threadId: "thread-1", - projectId: "project-1", - snapshotSequence: 21, - session: { status: "running", activeTurnId: "turn-1" }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-1", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - const streaming = { - ...initial, - snapshotSequence: 22, - latestTurn: { - ...initial.latestTurn, - assistantMessage: { content: "partial", streaming: true }, - }, - }; - const completed = { - ...initial, - snapshotSequence: 23, - session: { status: "ready", activeTurnId: null }, - latestTurn: { - ...initial.latestTurn, - status: "completed", - assistantMessage: { content: "final answer", streaming: false }, - }, - }; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 21 }; - }, - async getThread() { - return initial; - }, - async *subscribeThread(threadId: string, input: unknown) { - subscriptions.push({ threadId, input }); - yield { sequence: 22, snapshot: streaming }; - yield { sequence: 23, snapshot: completed }; - throw new Error("wait consumed past completion"); - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - - const events = []; - for await (const event of facade.wait("thread-1", { - kind: "terminal", - timeoutMs: 1_000, - maxEvidenceBytes: 10_000, - })) { - events.push(event); - } - - expect(subscriptions).toEqual([ - { threadId: "thread-1", input: { afterSequence: 21 } }, - ]); - expect(events.map((event) => event.lifecycle)).toEqual([ - "running", - "running", - "completed", - ]); - expect(events.at(-1)).toMatchObject({ - agentId: "thread-1", - sequence: 23, - lifecycle: "completed", - assistantContent: "final answer", - }); - }); - - for (const violation of [ - { - label: "duplicates the initial sequence", - yieldsAdvancingObservation: false, - expectedLastSequence: 70, - }, - { - label: "regresses after an advancing observation", - yieldsAdvancingObservation: true, - expectedLastSequence: 71, - }, - ] as const) { - test(`fails closed before retaining or yielding an observation that ${violation.label}`, async () => { - const initial = { - threadId: "thread-1", - projectId: "project-1", - snapshotSequence: 70, - session: { status: "running", activeTurnId: "turn-1" }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-1", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - const advancing = { - ...initial, - snapshotSequence: 71, - }; - const invalid = { - ...initial, - pendingApproval: { - requestId: "approval-invalid", - payload: "x".repeat(20_000), - }, - }; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread() { - return initial; - }, - async *subscribeThread() { - if (violation.yieldsAdvancingObservation) { - yield { sequence: 71, snapshot: advancing }; - } - yield { sequence: 70, snapshot: invalid }; - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - const iterator = facade - .wait("thread-1", { - kind: "terminal", - timeoutMs: 1_000, - maxEvidenceBytes: 10_000, - }) - [Symbol.asyncIterator](); - - await expect(iterator.next()).resolves.toMatchObject({ - value: { sequence: 70 }, - done: false, - }); - if (violation.yieldsAdvancingObservation) { - await expect(iterator.next()).resolves.toMatchObject({ - value: { sequence: 71 }, - done: false, - }); - } - await expect(iterator.next()).rejects.toMatchObject({ - code: "transport_unavailable", - sequence: violation.expectedLastSequence, - }); - }); - } - - for (const divergence of [ - { - label: "duplicates the initial snapshot sequence", - yieldsAdvancingObservation: false, - invalidEnvelopeSequence: 71, - invalidSnapshotSequence: 70, - expectedLastSequence: 70, - }, - { - label: "regresses the snapshot after an accepted observation", - yieldsAdvancingObservation: true, - invalidEnvelopeSequence: 72, - invalidSnapshotSequence: 70, - expectedLastSequence: 71, - }, - ] as const) { - test(`fails closed when an advancing envelope ${divergence.label}`, async () => { - const initial = { - threadId: "thread-1", - projectId: "project-1", - snapshotSequence: 70, - session: { status: "running", activeTurnId: "turn-1" }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-1", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - const advancing = { - ...initial, - snapshotSequence: 71, - }; - const invalid = { - ...initial, - snapshotSequence: divergence.invalidSnapshotSequence, - pendingApproval: { - requestId: "approval-invalid", - payload: "x".repeat(20_000), - }, - }; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread() { - return initial; - }, - async *subscribeThread() { - if (divergence.yieldsAdvancingObservation) { - yield { sequence: 71, snapshot: advancing }; - } - yield { - sequence: divergence.invalidEnvelopeSequence, - snapshot: invalid, - }; - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - const iterator = facade - .wait("thread-1", { - kind: "terminal", - timeoutMs: 1_000, - maxEvidenceBytes: 10_000, - }) - [Symbol.asyncIterator](); - - await expect(iterator.next()).resolves.toMatchObject({ - value: { sequence: 70 }, - done: false, - }); - if (divergence.yieldsAdvancingObservation) { - await expect(iterator.next()).resolves.toMatchObject({ - value: { sequence: 71 }, - done: false, - }); - } - await expect(iterator.next()).rejects.toMatchObject({ - code: "transport_unavailable", - sequence: divergence.expectedLastSequence, - }); - }); - } - - test("fails closed when the initial lookup returns a different thread", async () => { - const divergent = { - threadId: "thread-other", - projectId: "project-1", - snapshotSequence: 24, - session: { status: "stopped", activeTurnId: null }, - latestTurn: null, - pendingApproval: null, - pendingInput: null, - }; - let subscribed = false; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread() { - return divergent; - }, - async *subscribeThread() { - subscribed = true; - return; - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - - const result = collect( - facade.wait("thread-requested", { - kind: "terminal", - timeoutMs: 1_000, - maxEvidenceBytes: 10_000, - }), - ); - - await expect(result).rejects.toMatchObject({ - code: "transport_unavailable", - sequence: 24, - }); - expect(subscribed).toBe(false); - }); - - test("returns a structural facade error when the initial thread is unavailable", async () => { - let subscribed = false; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread() { - return undefined; - }, - async *subscribeThread() { - subscribed = true; - return; - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - - const result = collect( - facade.wait("thread-requested", { - kind: "terminal", - timeoutMs: 1_000, - maxEvidenceBytes: 10_000, - }), - ); - - await expect(result).rejects.toMatchObject({ - code: "transport_unavailable", - sequence: 0, - structuralSnapshot: { - threadId: "thread-requested", - }, - }); - expect(subscribed).toBe(false); - }); - - test("fails closed when a subscription observation returns a different thread", async () => { - const initial = { - threadId: "thread-requested", - projectId: "project-1", - snapshotSequence: 25, - session: { status: "running", activeTurnId: "turn-1" }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-1", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - const divergent = { - ...initial, - threadId: "thread-other", - snapshotSequence: 26, - session: { status: "stopped", activeTurnId: null }, - }; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread() { - return initial; - }, - async *subscribeThread() { - yield { sequence: 26, snapshot: divergent }; - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - - const result = collect( - facade.wait("thread-requested", { - kind: "terminal", - timeoutMs: 1_000, - maxEvidenceBytes: 10_000, - }), - ); - - await expect(result).rejects.toMatchObject({ - code: "transport_unavailable", - sequence: 26, - }); - }); - - test("classifies empty terminal assistant content as a structural error", async () => { - const emptyTerminal = { - threadId: "thread-1", - projectId: "project-1", - snapshotSequence: 30, - session: { status: "ready", activeTurnId: null }, - latestTurn: { - turnId: "turn-1", - status: "completed", - userMessageId: "message-1", - assistantMessage: { content: " ", streaming: false }, - }, - pendingApproval: null, - pendingInput: null, - }; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread() { - return emptyTerminal; - }, - async *subscribeThread() { - return; - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - - const result = collect( - facade.wait("thread-1", { - kind: "terminal", - timeoutMs: 1_000, - maxEvidenceBytes: 10_000, - }), - ); - - await expect(result).rejects.toBeInstanceOf(FacadeError); - await expect(result).rejects.toMatchObject({ - code: "empty_assistant_response", - sequence: 30, - }); - }); - - test("fails closed immediately when a completed turn has no assistant message", async () => { - const missingAssistant = { - threadId: "thread-1", - projectId: "project-1", - snapshotSequence: 31, - session: { status: "ready", activeTurnId: null }, - latestTurn: { - turnId: "turn-1", - status: "completed", - userMessageId: "message-1", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - let subscribed = false; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread() { - return missingAssistant; - }, - async *subscribeThread() { - subscribed = true; - throw new Error("missing assistant must fail before subscription"); - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - - const result = collect( - facade.wait("thread-1", { - kind: "terminal", - timeoutMs: 1, - maxEvidenceBytes: 10_000, - }), - ); - - await expect(result).rejects.toMatchObject({ - code: "empty_assistant_response", - sequence: 31, - }); - expect(subscribed).toBe(false); - }); - - for (const pendingKind of ["pendingApproval", "pendingInput"] as const) { - test(`yields awaiting_input when a completed empty assistant has ${pendingKind}`, async () => { - const pending = { - threadId: "thread-1", - projectId: "project-1", - snapshotSequence: 31, - session: { status: "ready", activeTurnId: null }, - latestTurn: { - turnId: "turn-1", - status: "completed", - userMessageId: "message-1", - assistantMessage: { content: " ", streaming: false }, - }, - pendingApproval: - pendingKind === "pendingApproval" - ? { requestId: "approval-pending" } - : null, - pendingInput: - pendingKind === "pendingInput" - ? { requestId: "input-pending" } - : null, - }; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread() { - return pending; - }, - async *subscribeThread() { - throw new Error("pending state must stop before subscription"); - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - - const events = await collect( - facade.wait("thread-1", { - kind: "terminal", - timeoutMs: 1_000, - maxEvidenceBytes: 10_000, - }), - ); - - expect(events).toHaveLength(1); - expect(events[0]?.lifecycle).toBe("awaiting_input"); - }); - } - - test("stops on structured pending approval without inspecting provider text", async () => { - const awaitingApproval = { - threadId: "thread-1", - projectId: "project-1", - snapshotSequence: 31, - session: { status: "ready", activeTurnId: null }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-1", - assistantMessage: null, - }, - pendingApproval: { requestId: "approval-1", status: "pending" }, - pendingInput: null, - }; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread() { - return awaitingApproval; - }, - async *subscribeThread() { - throw new Error("pending approval must stop before subscription"); - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - - const events = await collect( - facade.wait("thread-1", { - kind: "terminal", - timeoutMs: 1_000, - maxEvidenceBytes: 10_000, - }), - ); - - expect(events.map((event) => event.lifecycle)).toEqual(["awaiting_input"]); - }); - - for (const failedState of ["interrupted", "error"] as const) { - test(`keeps ${failedState} ahead of pending state and omits whitespace-only assistant content`, async () => { - const failed = { - threadId: "thread-1", - projectId: "project-1", - snapshotSequence: 32, - session: { status: failedState, activeTurnId: null }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-1", - assistantMessage: { content: " \n\t ", streaming: false }, - }, - pendingApproval: - failedState === "interrupted" - ? { requestId: "approval-pending" } - : null, - pendingInput: - failedState === "error" ? { requestId: "input-pending" } : null, - }; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread() { - return failed; - }, - async *subscribeThread() { - throw new Error("failed state must stop before subscription"); - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - - const events = await collect( - facade.wait("thread-1", { - kind: "terminal", - timeoutMs: 1_000, - maxEvidenceBytes: 10_000, - }), - ); - - expect(events).toHaveLength(1); - expect(events[0]?.lifecycle).toBe(failedState); - expect(events[0]).not.toHaveProperty("assistantContent"); - }); - } - - test("decodes omitted pending fields as no pending input", async () => { - const initial = { - threadId: "thread-1", - projectId: "project-1", - snapshotSequence: 32, - session: { status: "ready", activeTurnId: null }, - latestTurn: null, - }; - const stopped = { - ...initial, - snapshotSequence: 33, - session: { status: "stopped", activeTurnId: null }, - pendingApproval: null, - pendingInput: null, - }; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread() { - return initial; - }, - async *subscribeThread() { - yield { sequence: 33, snapshot: stopped }; - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - - const events = await collect( - facade.wait("thread-1", { - kind: "terminal", - timeoutMs: 1_000, - maxEvidenceBytes: 10_000, - }), - ); - - expect(events.map((event) => event.lifecycle)).toEqual([ - "ready", - "stopped", - ]); - }); - - test("fails with timeout while native state remains nonterminal", async () => { - const running = { - threadId: "thread-1", - projectId: "project-1", - snapshotSequence: 40, - session: { status: "running", activeTurnId: "turn-1" }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-1", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - const completed = { - ...running, - snapshotSequence: 41, - session: { status: "ready", activeTurnId: null }, - latestTurn: { - ...running.latestTurn, - status: "completed", - assistantMessage: { content: "late", streaming: false }, - }, - }; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread() { - return running; - }, - async *subscribeThread() { - await Bun.sleep(30); - yield { sequence: 41, snapshot: completed }; - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - - const result = collect( - facade.wait("thread-1", { - kind: "terminal", - timeoutMs: 5, - maxEvidenceBytes: 10_000, - }), - ); - - await expect(result).rejects.toMatchObject({ - code: "timeout", - sequence: 40, - }); - }); - - test("bounds the initial native lookup by the wait timeout", async () => { - const completed = { - threadId: "thread-1", - projectId: "project-1", - snapshotSequence: 41, - session: { status: "ready", activeTurnId: null }, - latestTurn: { - turnId: "turn-1", - status: "completed", - userMessageId: "message-1", - assistantMessage: { content: "too late", streaming: false }, - }, - pendingApproval: null, - pendingInput: null, - }; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread() { - await Bun.sleep(100); - return completed; - }, - async *subscribeThread() { - throw new Error("late terminal lookup must not subscribe"); - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - const startedAt = performance.now(); - - const result = collect( - facade.wait("thread-1", { - kind: "terminal", - timeoutMs: 5, - maxEvidenceBytes: 10_000, - }), - ); - const error = await result.catch((reason: unknown) => reason); - - expect(error).toMatchObject({ code: "timeout" }); - expect(performance.now() - startedAt).toBeLessThan(50); - }); - - test("does not await stalled iterator teardown before rejecting at timeout", async () => { - const running = { - threadId: "thread-1", - projectId: "project-1", - snapshotSequence: 42, - session: { status: "running", activeTurnId: "turn-1" }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-1", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread() { - return running; - }, - async *subscribeThread() { - await Bun.sleep(150); - yield { sequence: 43, snapshot: running }; - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - const startedAt = performance.now(); - - const result = collect( - facade.wait("thread-1", { - kind: "terminal", - timeoutMs: 5, - maxEvidenceBytes: 10_000, - }), - ); - - await expect(result).rejects.toMatchObject({ - code: "timeout", - sequence: 42, - }); - expect(performance.now() - startedAt).toBeLessThan(75); - }); - - test("sanitizes credential-bearing subscription errors", async () => { - const credential = "Bearer hostile-subscription-credential"; - const running = { - threadId: "thread-1", - projectId: "project-1", - snapshotSequence: 44, - session: { status: "running", activeTurnId: "turn-1" }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-1", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread() { - return running; - }, - async *subscribeThread() { - throw new Error(`transport rejected ${credential}`); - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - - const result = collect( - facade.wait("thread-1", { - kind: "terminal", - timeoutMs: 1_000, - maxEvidenceBytes: 10_000, - }), - ); - const error = await result.catch((reason: unknown) => reason); - - expect(error).toMatchObject({ - code: "transport_unavailable", - sequence: 44, - }); - expect(String(error)).not.toContain(credential); - expect(JSON.stringify(error)).not.toContain(credential); - }); - - test("fails closed when the subscription ends before a terminal state", async () => { - const running = { - threadId: "thread-1", - projectId: "project-1", - snapshotSequence: 45, - session: { status: "running", activeTurnId: "turn-1" }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-1", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread() { - return running; - }, - async *subscribeThread() { - return; - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - - const result = collect( - facade.wait("thread-1", { - kind: "terminal", - timeoutMs: 1_000, - maxEvidenceBytes: 10_000, - }), - ); - - await expect(result).rejects.toMatchObject({ - code: "transport_unavailable", - sequence: 45, - }); - }); - - test("fails before retaining evidence beyond the configured byte cap", async () => { - const running = { - threadId: "thread-1", - projectId: "project-1", - snapshotSequence: 50, - session: { status: "running", activeTurnId: "turn-1" }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-1", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread() { - return running; - }, - async *subscribeThread() { - return; - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - - const result = collect( - facade.wait("thread-1", { - kind: "terminal", - timeoutMs: 1_000, - maxEvidenceBytes: 1, - }), - ); - - await expect(result).rejects.toMatchObject({ - code: "buffer_exhausted", - sequence: 50, - }); - }); - - test("counts a hostile pending payload before returning the native event", async () => { - const secretMarker = "hostile-pending-payload"; - const awaitingApproval = { - threadId: "thread-1", - projectId: "project-1", - snapshotSequence: 51, - session: { status: "ready", activeTurnId: null }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-1", - assistantMessage: null, - }, - pendingApproval: { - requestId: "approval-1", - payload: `${secretMarker}:${"x".repeat(20_000)}`, - }, - pendingInput: null, - }; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread() { - return awaitingApproval; - }, - async *subscribeThread() { - throw new Error( - "oversized pending state must fail before subscription", - ); - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - - const result = collect( - facade.wait("thread-1", { - kind: "terminal", - timeoutMs: 1_000, - maxEvidenceBytes: 500, - }), - ); - - await expect(result).rejects.toMatchObject({ - code: "buffer_exhausted", - sequence: 51, - }); - const error = await result.catch((reason: unknown) => reason); - expect(JSON.stringify(error)).not.toContain(secretMarker); - }); - - test("stops when the native session is structurally interrupted", async () => { - const interrupted = { - threadId: "thread-1", - projectId: "project-1", - snapshotSequence: 60, - session: { status: "interrupted", activeTurnId: null }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-1", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread() { - return interrupted; - }, - async *subscribeThread() { - throw new Error("interrupted state must stop before subscription"); - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - - const events = await collect( - facade.wait("thread-1", { - kind: "terminal", - timeoutMs: 1_000, - maxEvidenceBytes: 10_000, - }), - ); - - expect(events.map((event) => event.lifecycle)).toEqual(["interrupted"]); - }); - - test("stops when the native session is structurally stopped", async () => { - const stopped = { - threadId: "thread-1", - projectId: "project-1", - snapshotSequence: 61, - session: { status: "stopped", activeTurnId: null }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-1", - assistantMessage: null, - }, - pendingApproval: null, - pendingInput: null, - }; - const runtime = { - async listProjects() { - return []; - }, - async createProject() { - return { sequence: 1 }; - }, - async startThread() { - return { sequence: 2 }; - }, - async startTurn() { - return { sequence: 3 }; - }, - async getThread() { - return stopped; - }, - async *subscribeThread() { - throw new Error("stopped state must stop before subscription"); - }, - }; - const facade = createT3Facade(runtime, DISPATCH_MODES); - - const events = await collect( - facade.wait("thread-1", { - kind: "terminal", - timeoutMs: 1_000, - maxEvidenceBytes: 10_000, - }), - ); - - expect(events.map((event) => event.lifecycle)).toEqual(["stopped"]); - }); -}); diff --git a/test/native-runtime-adapter.test.ts b/test/native-runtime-adapter.test.ts index e09ab26..958db6b 100644 --- a/test/native-runtime-adapter.test.ts +++ b/test/native-runtime-adapter.test.ts @@ -1,1602 +1,465 @@ import { describe, expect, test } from "bun:test"; -import type { - ClientOrchestrationCommand, - OrchestrationEvent, - OrchestrationProjectShell, - OrchestrationShellSnapshot, - OrchestrationShellStreamItem, - OrchestrationThread, - OrchestrationThreadShell, - OrchestrationThreadStreamItem, - RuntimeClientRpcSessionFactory, -} from "@t3tools/runtime-client"; -import * as Effect from "effect/Effect"; + import { - createDefaultSessionFactory, - createT3NativeRuntime, - type RuntimeClientSession, - type RuntimeClientSessionFactory, + createStockT3NativeRuntime, + digestStockSpawnInput, + type CreateReconciliationPending, + type StockSpawnInput, + type StockT3RuntimeClient, } from "../src/nativeRuntime"; - -const NOW = "2026-07-31T00:00:00.000Z"; -const MODEL = { - instanceId: "codex", - model: "gpt-5.6-sol", - options: [{ id: "reasoningEffort", value: "high" }], -} as const; -const BOOLEAN_MODEL = { - instanceId: "codex", - model: "gpt-5.6-sol", - options: [{ id: "fastMode", value: true }], -} as const; - -function project( - id = "project-1", - workspaceRoot = "/repo", -): OrchestrationProjectShell { - return { - id, - title: "Project", - workspaceRoot, - defaultModelSelection: MODEL, - scripts: [], - createdAt: NOW, - updatedAt: NOW, - } as unknown as OrchestrationProjectShell; -} - -function thread( - sequence: number, - overrides: Partial = {}, -): OrchestrationThread { +import type { + ShellSnapshot, + StockMessage, + StockThreadShell, + ThreadDetailSnapshot, +} from "../src/stockT3Contracts"; +import { StockT3HttpError } from "../src/stockT3HttpClient"; + +const iso = "2026-07-31T18:00:00.000Z"; +const modelSelection = { instanceId: "claudeAgent", model: "claude-opus-5" }; +const project = { + id: "project-1", + title: "project", + workspaceRoot: "/tmp/project", + defaultModelSelection: modelSelection, + createdAt: iso, + updatedAt: iso, +}; +const spawnInput: StockSpawnInput = { + workspaceRoot: project.workspaceRoot, + title: "worker", + message: "initial", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, +}; + +function shellThread(overrides: Partial = {}): StockThreadShell { return { id: "thread-1", - projectId: "project-1", - title: "Worker", - modelSelection: MODEL, + projectId: project.id, + title: "worker", + modelSelection, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, - latestTurn: { - turnId: "turn-1", - state: "running", - requestedAt: NOW, - startedAt: NOW, - completedAt: null, - assistantMessageId: null, - }, - createdAt: NOW, - updatedAt: `${NOW.slice(0, -5)}${String(sequence).padStart(3, "0")}Z`, - archivedAt: null, - settledOverride: null, - settledAt: null, - deletedAt: null, - messages: [ - { - id: "message-user", - role: "user", - text: "work", - turnId: "turn-1", - streaming: false, - createdAt: NOW, - updatedAt: NOW, - }, - ], - proposedPlans: [], - activities: [], - checkpoints: [], - session: { - threadId: "thread-1", - status: "running", - providerName: "codex", - providerInstanceId: "codex", - runtimeMode: "full-access", - activeTurnId: "turn-1", - lastError: null, - updatedAt: NOW, - }, + latestTurn: null, + createdAt: iso, + updatedAt: iso, + session: null, + latestUserMessageAt: null, + hasPendingApprovals: false, + hasPendingUserInput: false, ...overrides, - } as unknown as OrchestrationThread; + }; } -function shellThread( - sequence: number, - options: { - readonly pendingApproval?: boolean; - readonly pendingInput?: boolean; - readonly status?: "running" | "ready"; - } = {}, -): OrchestrationThreadShell { - const status = options.status ?? "running"; - return { - id: "thread-1", - projectId: "project-1", - title: "Worker", - modelSelection: MODEL, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - latestTurn: { - turnId: "turn-1", - state: status === "ready" ? "completed" : "running", - requestedAt: NOW, - startedAt: NOW, - completedAt: status === "ready" ? NOW : null, - assistantMessageId: status === "ready" ? "message-assistant" : null, - }, - createdAt: NOW, - updatedAt: `${NOW.slice(0, -5)}${String(sequence).padStart(3, "0")}Z`, - archivedAt: null, - settledOverride: null, - settledAt: null, - session: { - threadId: "thread-1", - status, - providerName: "codex", - providerInstanceId: "codex", - runtimeMode: "full-access", - activeTurnId: status === "running" ? "turn-1" : null, - lastError: null, - updatedAt: NOW, - }, - latestUserMessageAt: NOW, - hasPendingApprovals: options.pendingApproval ?? false, - hasPendingUserInput: options.pendingInput ?? false, - hasActionableProposedPlan: false, - } as unknown as OrchestrationThreadShell; +function shell(sequence: number, threads: readonly StockThreadShell[] = []): ShellSnapshot { + return { snapshotSequence: sequence, projects: [project], threads, updatedAt: iso }; } -function shellSnapshot( - sequence: number, - threads: readonly OrchestrationThreadShell[] = [], -): OrchestrationShellSnapshot { +function userMessage(id = "message-1"): StockMessage { return { - snapshotSequence: sequence, - projects: [project()], - threads: [...threads], - updatedAt: NOW, + id, + role: "user", + text: "initial", + attachments: [], + turnId: null, + streaming: false, + createdAt: iso, + updatedAt: iso, }; } -function stream( - items: readonly T[], - onReturn?: () => void, -): AsyncIterable { +function detail( + sequence: number, + messages: readonly StockMessage[] = [], + overrides: Partial = {}, +): ThreadDetailSnapshot { + const identity = shellThread(); return { - [Symbol.asyncIterator]() { - let index = 0; - return { - async next() { - if (index >= items.length) return { done: true, value: undefined }; - return { done: false, value: items[index++]! }; - }, - async return() { - onReturn?.(); - return { done: true, value: undefined }; - }, - }; + snapshotSequence: sequence, + thread: { + id: identity.id, + projectId: identity.projectId, + title: identity.title, + modelSelection: identity.modelSelection, + runtimeMode: identity.runtimeMode, + interactionMode: identity.interactionMode, + branch: identity.branch, + worktreePath: identity.worktreePath, + latestTurn: null, + createdAt: identity.createdAt, + updatedAt: identity.updatedAt, + session: null, + messages, + activities: [], + checkpoints: [], + ...overrides, }, }; } -function scriptedStream( - script: ReadonlyArray<{ - readonly delayMs: number; - readonly item: T; - }>, - onReturn?: () => void, -): AsyncIterable { +function baseClient(overrides: Partial = {}): StockT3RuntimeClient { return { - [Symbol.asyncIterator]() { - let index = 0; - return { - async next() { - const entry = script[index++]; - if (entry === undefined) return { done: true, value: undefined }; - if (entry.delayMs > 0) { - await new Promise((resolve) => setTimeout(resolve, entry.delayMs)); - } - return { done: false, value: entry.item }; - }, - async return() { - onReturn?.(); - return { done: true, value: undefined }; - }, - }; - }, + getDescriptor: async () => ({ + environmentId: "env-1", + label: "local", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "stock", + capabilities: { repositoryIdentity: false }, + }), + getShell: async () => shell(1), + getThread: async () => undefined, + dispatch: async () => ({ sequence: 1 }), + ...overrides, }; } -type PromiseOutcome = - | { readonly kind: "resolved"; readonly value: T } - | { readonly kind: "rejected"; readonly error: unknown } - | { readonly kind: "pending" }; - -async function outcomeWithin( - promise: Promise, - timeoutMs = 100, -): Promise> { - return Promise.race([ - promise.then( - (value): PromiseOutcome => ({ kind: "resolved", value }), - (error): PromiseOutcome => ({ kind: "rejected", error }), - ), - new Promise>((resolve) => { - setTimeout(() => resolve({ kind: "pending" }), timeoutMs); - }), - ]); +function ids() { + const values = ["create-1", "thread-1", "turn-1", "message-1", "lease-1"]; + return () => values.shift()!; } -describe("T3 native runtime adapter", () => { - test("maps project and turn operations onto canonical RPC commands", async () => { - const commands: unknown[] = []; - const connections: Array<{ - readonly environmentId: string; - readonly label: string; - readonly socketUrl: string; - }> = []; - let closes = 0; - const sessionFactory: RuntimeClientSessionFactory = { - async connect(connection) { - connections.push(connection); - return { - async dispatchCommand(command) { - commands.push(command); - return { sequence: commands.length }; - }, - subscribeShell: () => - stream([ - { kind: "snapshot", snapshot: shellSnapshot(4) }, - { kind: "synchronized" }, - ]), - subscribeThread: () => stream([]), - async close() { - closes += 1; - }, - }; - }, - }; - let socketAcquisitions = 0; - const runtime = createT3NativeRuntime({ - environmentId: "environment-1", - label: "MacBook Pro", - acquireSocketUrl: async () => - `ws://127.0.0.1/socket?token=${++socketAcquisitions}`, - sessionFactory, - }); - - expect(await runtime.listProjects()).toEqual([ - { projectId: "project-1", workspaceRoot: "/repo" }, - ]); - expect( - await runtime.createProject({ - commandId: "command-project", - projectId: "project-new", - title: "New Project", - workspaceRoot: "/new", - createWorkspaceRootIfMissing: false, - defaultModelSelection: MODEL, - createdAt: NOW, - }), - ).toEqual({ sequence: 1 }); - expect( - await runtime.startThread({ - commandId: "command-spawn", - projectId: "project-1", - threadId: "thread-1", - messageId: "message-user", - title: "Worker", - message: "Do the work", - modelSelection: BOOLEAN_MODEL, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - createdAt: NOW, - attachments: [], - }), - ).toEqual({ sequence: 2 }); - expect( - await runtime.startTurn({ - commandId: "command-send", - threadId: "thread-1", - messageId: "message-follow-up", - message: "Continue", - runtimeMode: "full-access", - interactionMode: "default", - createdAt: NOW, - attachments: [], - }), - ).toEqual({ sequence: 3 }); - - expect(commands).toEqual([ - { - type: "project.create", - commandId: "command-project", - projectId: "project-new", - title: "New Project", - workspaceRoot: "/new", - createWorkspaceRootIfMissing: false, - defaultModelSelection: MODEL, - createdAt: NOW, - }, - { - type: "thread.turn.start", - commandId: "command-spawn", - threadId: "thread-1", - message: { - messageId: "message-user", - role: "user", - text: "Do the work", - attachments: [], - }, - modelSelection: BOOLEAN_MODEL, - runtimeMode: "full-access", - interactionMode: "default", - bootstrap: { - createThread: { - projectId: "project-1", - title: "Worker", - modelSelection: BOOLEAN_MODEL, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - createdAt: NOW, - }, - }, - createdAt: NOW, - }, - { - type: "thread.turn.start", - commandId: "command-send", - threadId: "thread-1", - message: { - messageId: "message-follow-up", - role: "user", - text: "Continue", - attachments: [], - }, - runtimeMode: "full-access", - interactionMode: "default", - createdAt: NOW, - }, - ]); - expect(connections).toHaveLength(4); - expect(socketAcquisitions).toBe(4); - expect(closes).toBe(4); - }); - - test("sanitizes unsent command projection failures without opening a session", async () => { - const secret = "projection-super-secret"; - const invalidCreatedAt = "not-an-iso-date"; - let connects = 0; - const runtime = createT3NativeRuntime({ - environmentId: "environment-1", - label: "MacBook Pro", - acquireSocketUrl: async () => "ws://127.0.0.1/ephemeral", - sessionFactory: { - async connect() { - connects += 1; - throw new Error("must not connect"); - }, - }, - }); - const invalidCalls = [ - () => - runtime.createProject({ - commandId: "", - projectId: "project-new", - title: secret, - workspaceRoot: "/new", - createWorkspaceRootIfMissing: false, - defaultModelSelection: MODEL, - createdAt: invalidCreatedAt, - }), - () => - runtime.startThread({ - commandId: "", - projectId: "project-1", - threadId: "thread-1", - messageId: "message-user", - title: "Worker", - message: secret, - modelSelection: MODEL, - runtimeMode: "full-access", - interactionMode: "default", - branch: null, - worktreePath: null, - createdAt: invalidCreatedAt, - attachments: [], - }), - () => - runtime.startTurn({ - commandId: "", - threadId: "thread-1", - messageId: "message-follow-up", - message: secret, - runtimeMode: "full-access", - interactionMode: "default", - createdAt: invalidCreatedAt, - attachments: [], - }), - ]; - - for (const call of invalidCalls) { - const outcome = await outcomeWithin(Promise.resolve().then(call)); - expect(outcome).toMatchObject({ - kind: "rejected", - error: { - name: "NativeRuntimeAdapterError", - code: "command_rejected", - }, - }); - if (outcome.kind !== "rejected") { - throw new Error("expected rejected command projection"); - } - expect(String(outcome.error)).toBe( - "NativeRuntimeAdapterError: command_rejected", - ); - expect((outcome.error as Error).message).toBe("command_rejected"); - expect(String(outcome.error)).not.toContain(secret); - expect(String(outcome.error)).not.toContain("ParseError"); - expect((outcome.error as Error).name).not.toBe("AmbiguousDispatchError"); - } - expect(connects).toBe(0); - }); - - test("reconciles a pending-only shell snapshot ahead of a synchronized detail snapshot", async () => { - const subscriptionInputs: unknown[] = []; - let closes = 0; - const session: RuntimeClientSession = { - async dispatchCommand() { - return { sequence: 1 }; - }, - subscribeShell(input) { - subscriptionInputs.push(["shell", input]); - return stream([ - { - kind: "snapshot", - snapshot: shellSnapshot(11, [ - shellThread(11, { pendingApproval: true }), - ]), - }, - { kind: "synchronized" }, - ]); - }, - subscribeThread(input) { - subscriptionInputs.push(["thread", input]); - return stream([ - { - kind: "snapshot", - snapshot: { snapshotSequence: 10, thread: thread(10) }, - }, - { kind: "synchronized" }, - ]); - }, - async close() { - closes += 1; - }, - }; - const runtime = createT3NativeRuntime({ - environmentId: "environment-1", - label: "MacBook Pro", - acquireSocketUrl: async () => "ws://127.0.0.1/ephemeral", - sessionFactory: { connect: async () => session }, - }); - - const snapshot = await runtime.getThread("thread-1"); - - expect(snapshot).toMatchObject({ +async function pendingReceipt( + runtime: ReturnType, + overrides: Partial = {}, +): Promise { + return { + kind: "create_reconciliation_pending", + provisionalRef: { environmentId: "env-1", threadId: "thread-1" }, + createAttempt: { + commandId: "create-1", threadId: "thread-1", projectId: "project-1", - snapshotSequence: 11, - session: { status: "running", activeTurnId: "turn-1" }, - latestTurn: { - turnId: "turn-1", - status: "running", - userMessageId: "message-user", - }, - pendingApproval: true, - pendingInput: null, - }); - expect(subscriptionInputs).toHaveLength(3); - expect(subscriptionInputs).toContainEqual([ - "shell", - { requestCompletionMarker: true }, - ]); - expect(subscriptionInputs).toContainEqual([ - "thread", - { threadId: "thread-1", requestCompletionMarker: true }, - ]); - expect(subscriptionInputs).toContainEqual([ - "thread", - { - threadId: "thread-1", - afterSequence: 10, - requestCompletionMarker: true, - }, - ]); - expect(closes).toBe(1); - }); - - test("preserves initial user-message identity before a provider turn is assigned", async () => { - const unassignedThread = thread(4, { - latestTurn: null, - messages: [ - { - id: "message-pending", - role: "user", - text: "start", - turnId: null, - streaming: false, - createdAt: NOW, - updatedAt: NOW, - }, - ], - session: { - threadId: "thread-1", - status: "starting", - providerName: "codex", - providerInstanceId: "codex", - runtimeMode: "full-access", - activeTurnId: null, - lastError: null, - updatedAt: NOW, - }, - } as unknown as Partial); - const unassignedShell = { - ...shellThread(4), - latestTurn: null, - session: unassignedThread.session, - } as unknown as OrchestrationThreadShell; - const runtime = createT3NativeRuntime({ - environmentId: "environment-1", - label: "MacBook Pro", - acquireSocketUrl: async () => "ws://127.0.0.1/ephemeral", - sessionFactory: { - async connect() { - return { - async dispatchCommand() { - return { sequence: 1 }; - }, - subscribeShell() { - return stream([ - { - kind: "snapshot", - snapshot: shellSnapshot(4, [unassignedShell]), - }, - { kind: "synchronized" }, - ]); - }, - subscribeThread() { - return stream([ - { - kind: "snapshot", - snapshot: { - snapshotSequence: 4, - thread: unassignedThread, - }, - }, - { kind: "synchronized" }, - ]); - }, - async close() {}, - }; - }, - }, - }); - - const snapshot = await runtime.getThread("thread-1"); + acceptedSequence: 9, + dispatchState: "accepted", + retryState: "not_applicable", + retryError: null, + ...overrides, + }, + reconciliation: { + reason: "projection_pending", + projectionState: "unobserved", + highestShellSequence: null, + highestDetailSequence: null, + deadlineMs: 1_000, + evidence: [], + }, + initialTurnContinuation: { + commandId: "turn-1", + messageId: "message-1", + inputDigest: await digestStockSpawnInput(spawnInput), + }, + safeAction: "resume_create_reconciliation", + }; +} - expect(snapshot).toMatchObject({ - threadId: "thread-1", - projectId: "project-1", - snapshotSequence: 4, - session: { status: "starting", activeTurnId: null }, - latestUserMessageId: "message-pending", - latestTurn: null, - }); +describe("stock native runtime create state machine", () => { + test("requires a public stock HTTP client or base URL", () => { + expect(() => createStockT3NativeRuntime({})).toThrow("client or baseUrl is required"); }); - test("reconciles a synchronized detail snapshot ahead of a compatible shell snapshot", async () => { - const runtime = createT3NativeRuntime({ - environmentId: "environment-1", - label: "MacBook Pro", - acquireSocketUrl: async () => "ws://127.0.0.1/ephemeral", - sessionFactory: { - async connect() { - return { - async dispatchCommand() { - return { sequence: 1 }; - }, - subscribeShell: () => - stream([ - { - kind: "snapshot", - snapshot: shellSnapshot(10, [shellThread(10)]), - }, - { kind: "synchronized" }, - ]), - subscribeThread: () => - stream([ - { - kind: "snapshot", - snapshot: { snapshotSequence: 11, thread: thread(11) }, - }, - { kind: "synchronized" }, - ]), - async close() {}, - }; + test("preserves an accepted create when cancellation follows its response", async () => { + const controller = new AbortController(); + const commands: Array> = []; + const runtime = createStockT3NativeRuntime({ + client: baseClient({ + dispatch: async (command) => { + commands.push(command); + controller.abort(); + return { sequence: 2 }; }, - }, + }), + id: ids(), + now: () => iso, }); - expect(await runtime.getThread("thread-1")).toMatchObject({ - threadId: "thread-1", - snapshotSequence: 11, - session: { status: "running", activeTurnId: "turn-1" }, - pendingApproval: null, - pendingInput: null, + const result = await runtime.spawn(spawnInput, { signal: controller.signal }); + expect(result).toMatchObject({ + kind: "create_reconciliation_pending", + createAttempt: { acceptedSequence: 2, retryState: "not_applicable" }, + reconciliation: { reason: "cancelled" }, }); + expect(commands.map((entry) => entry.type)).toEqual(["thread.create"]); }); - test("returns undefined for a synchronized missing shell thread even if detail lookup rejects", async () => { - let closes = 0; - const missingDetail: AsyncIterable = { - [Symbol.asyncIterator]() { - return { - async next(): Promise> { - throw { _tag: "OrchestrationGetSnapshotError" }; - }, - }; - }, - }; - const session: RuntimeClientSession = { - async dispatchCommand() { - return { sequence: 1 }; - }, - subscribeShell() { - return scriptedStream([ - { - delayMs: 5, - item: { kind: "snapshot", snapshot: shellSnapshot(10) }, - }, - { delayMs: 0, item: { kind: "synchronized" } }, - ]); - }, - subscribeThread() { - return missingDetail; - }, - async close() { - closes += 1; - }, - }; - const runtime = createT3NativeRuntime({ - environmentId: "environment-1", - label: "MacBook Pro", - acquireSocketUrl: async () => "ws://127.0.0.1/ephemeral", - sessionFactory: { connect: async () => session }, - }); - - expect(await runtime.getThread("missing-thread")).toBeUndefined(); - expect(closes).toBe(1); - }); - - test("does not swallow a detail failure when shell absence was never synchronized", async () => { - const failingDetail: AsyncIterable = { - [Symbol.asyncIterator]() { - return { - async next(): Promise> { - throw { _tag: "OrchestrationGetSnapshotError" }; - }, - }; - }, - }; - const runtime = createT3NativeRuntime({ - environmentId: "environment-1", - label: "MacBook Pro", - acquireSocketUrl: async () => "ws://127.0.0.1/ephemeral", - sessionFactory: { - async connect() { - return { - async dispatchCommand() { - return { sequence: 1 }; - }, - subscribeShell: () => stream([]), - subscribeThread: () => failingDetail, - async close() {}, - }; + test("does not retry an ambiguous create when cancellation wins during first reconciliation", async () => { + const controller = new AbortController(); + let dispatches = 0; + let shellReads = 0; + const runtime = createStockT3NativeRuntime({ + client: baseClient({ + getShell: async () => { + shellReads += 1; + if (shellReads > 1) controller.abort(); + return shell(1); }, - }, - }); - - await expect(runtime.getThread("thread-1")).rejects.toMatchObject({ - name: "NativeRuntimeAdapterError", - code: "transport_unavailable", - }); - }); - - test("waits for a same-sequence detail update when a detail-relevant shell update arrives first", async () => { - const readyEvent = { - ...eventBase(11, "thread.session-set"), - payload: { - threadId: "thread-1", - session: { - ...thread(11).session!, - status: "ready", - activeTurnId: null, - updatedAt: NOW, + dispatch: async () => { + dispatches += 1; + throw new StockT3HttpError("transport_unavailable", null); }, - }, - } as OrchestrationEvent; - const session: RuntimeClientSession = { - async dispatchCommand() { - return { sequence: 1 }; - }, - subscribeShell() { - return scriptedStream([ - { - delayMs: 0, - item: { - kind: "snapshot", - snapshot: shellSnapshot(10, [shellThread(10)]), - }, - }, - { delayMs: 0, item: { kind: "synchronized" } }, - { - delayMs: 1, - item: { - kind: "thread-upserted", - sequence: 11, - thread: shellThread(11, { status: "ready" }), - }, - }, - ]); - }, - subscribeThread() { - return scriptedStream([ - { - delayMs: 0, - item: { - kind: "snapshot", - snapshot: { snapshotSequence: 10, thread: thread(10) }, - }, - }, - { delayMs: 0, item: { kind: "synchronized" } }, - { delayMs: 15, item: { kind: "event", event: readyEvent } }, - ]); - }, - async close() {}, - }; - const runtime = createT3NativeRuntime({ - environmentId: "environment-1", - label: "MacBook Pro", - acquireSocketUrl: async () => "ws://127.0.0.1/ephemeral", - sessionFactory: { connect: async () => session }, - }); - - const observations = []; - for await (const observation of runtime.subscribeThread("thread-1", {})) { - observations.push(observation); - if (observation.sequence === 11) break; - } - - expect(observations.map(({ sequence }) => sequence)).toEqual([10, 11]); - expect(observations[1]?.snapshot.session).toEqual({ - status: "ready", - activeTurnId: null, + }), + id: ids(), + now: () => iso, }); - }); - test("does not let a shell-first same-sequence upsert suppress a detail-only message update", async () => { - const assistantEvent = { - ...eventBase(11, "thread.message-sent"), - payload: { - threadId: "thread-1", - messageId: "message-assistant", - role: "assistant", - text: "AB", - turnId: "turn-1", - streaming: true, - createdAt: NOW, - updatedAt: NOW, - }, - } as OrchestrationEvent; - const session: RuntimeClientSession = { - async dispatchCommand() { - return { sequence: 1 }; - }, - subscribeShell: () => - scriptedStream([ - { - delayMs: 0, - item: { - kind: "snapshot", - snapshot: shellSnapshot(10, [shellThread(10)]), - }, - }, - { delayMs: 0, item: { kind: "synchronized" } }, - { - delayMs: 1, - item: { - kind: "thread-upserted", - sequence: 11, - thread: shellThread(11), - }, - }, - ]), - subscribeThread: () => - scriptedStream([ - { - delayMs: 0, - item: { - kind: "snapshot", - snapshot: { snapshotSequence: 10, thread: thread(10) }, - }, - }, - { delayMs: 0, item: { kind: "synchronized" } }, - { delayMs: 15, item: { kind: "event", event: assistantEvent } }, - ]), - async close() {}, - }; - const runtime = createT3NativeRuntime({ - environmentId: "environment-1", - label: "MacBook Pro", - acquireSocketUrl: async () => "ws://127.0.0.1/ephemeral", - sessionFactory: { connect: async () => session }, + const result = await runtime.spawn(spawnInput, { + signal: controller.signal, + maxReconciliationReads: 1, }); - - const observations = []; - for await (const observation of runtime.subscribeThread("thread-1", {})) { - observations.push(observation); - if (observation.sequence === 11) break; - } - - expect(observations.map(({ sequence }) => sequence)).toEqual([10, 11]); - expect( - observations[1]?.snapshot.latestTurn?.assistantMessage, - ).toMatchObject({ - content: "AB", - streaming: true, + expect(result).toMatchObject({ + kind: "create_reconciliation_pending", + createAttempt: { retryState: "eligible_not_sent" }, + reconciliation: { reason: "cancelled" }, }); + expect(dispatches).toBe(1); }); - test("replays an initially lagging detail stream before emitting a newer shell snapshot sequence", async () => { - const assistantEvent = { - ...eventBase(11, "thread.message-sent"), - payload: { - threadId: "thread-1", - messageId: "message-assistant", - role: "assistant", - text: "AB", - turnId: "turn-1", - streaming: true, - createdAt: NOW, - updatedAt: NOW, - }, - } as OrchestrationEvent; - const detailInputs: unknown[] = []; - const session: RuntimeClientSession = { - async dispatchCommand() { - return { sequence: 1 }; - }, - subscribeShell: () => - stream([ - { - kind: "snapshot", - snapshot: shellSnapshot(11, [shellThread(11)]), - }, - { kind: "synchronized" }, - ]), - subscribeThread(input) { - detailInputs.push(input); - if (input.afterSequence === 10) { - return stream([ - { kind: "event", event: assistantEvent }, - { kind: "synchronized" }, - ]); - } - return scriptedStream([ - { - delayMs: 0, - item: { - kind: "snapshot", - snapshot: { snapshotSequence: 10, thread: thread(10) }, - }, - }, - { delayMs: 0, item: { kind: "synchronized" } }, - { delayMs: 25, item: { kind: "event", event: assistantEvent } }, - ]); - }, - async close() {}, - }; - const runtime = createT3NativeRuntime({ - environmentId: "environment-1", - label: "MacBook Pro", - acquireSocketUrl: async () => "ws://127.0.0.1/ephemeral", - sessionFactory: { connect: async () => session }, + test("records a no-response retry and never mutates again when cancellation follows it", async () => { + const controller = new AbortController(); + let dispatches = 0; + const runtime = createStockT3NativeRuntime({ + client: baseClient({ + getShell: async () => shell(1), + getThread: async () => undefined, + dispatch: async () => { + dispatches += 1; + if (dispatches === 2) controller.abort(); + throw new StockT3HttpError("transport_unavailable", null); + }, + }), + id: ids(), + now: () => iso, }); - const snapshot = await runtime.getThread("thread-1"); - - expect(snapshot?.snapshotSequence).toBe(11); - expect(snapshot?.latestTurn?.assistantMessage).toMatchObject({ - content: "AB", - streaming: true, + const result = await runtime.spawn(spawnInput, { + signal: controller.signal, + maxReconciliationReads: 1, }); - expect(detailInputs).toContainEqual({ - threadId: "thread-1", - afterSequence: 10, - requestCompletionMarker: true, + expect(result).toMatchObject({ + kind: "create_reconciliation_pending", + createAttempt: { retryState: "identical_retry_sent_no_response" }, + reconciliation: { reason: "cancelled" }, }); + expect(dispatches).toBe(2); }); - test("times out a hanging initial alignment replay without awaiting a hanging iterator return", async () => { - let replayReturns = 0; - let detailReturns = 0; - let shellReturns = 0; - const hangingReplay: AsyncIterable = { - [Symbol.asyncIterator]() { - return { - next: () => - new Promise>( - () => undefined, - ), - return() { - replayReturns += 1; - return new Promise>( - () => undefined, - ); - }, - }; - }, - }; - const session: RuntimeClientSession = { - async dispatchCommand() { - return { sequence: 1 }; - }, - subscribeShell: () => - stream( - [ - { - kind: "snapshot", - snapshot: shellSnapshot(11, [shellThread(11)]), - }, - { kind: "synchronized" }, - ], - () => { - shellReturns += 1; - }, - ), - subscribeThread(input) { - if (input.afterSequence === 10) return hangingReplay; - return stream( - [ - { - kind: "snapshot", - snapshot: { snapshotSequence: 10, thread: thread(10) }, - }, - { kind: "synchronized" }, - ], - () => { - detailReturns += 1; - }, - ); - }, - async close() {}, - }; - const runtime = createT3NativeRuntime({ - environmentId: "environment-1", - label: "MacBook Pro", - alignmentTimeoutMs: 10, - acquireSocketUrl: async () => "ws://127.0.0.1/ephemeral", - sessionFactory: { connect: async () => session }, + test("preserves retry acceptance when reconciliation is interrupted", async () => { + const controller = new AbortController(); + let dispatches = 0; + const runtime = createStockT3NativeRuntime({ + client: baseClient({ + getShell: async () => shell(1), + getThread: async () => undefined, + dispatch: async () => { + dispatches += 1; + if (dispatches === 1) throw new StockT3HttpError("transport_unavailable", null); + controller.abort(); + return { sequence: 7 }; + }, + }), + id: ids(), + now: () => iso, }); - const outcome = await outcomeWithin(runtime.getThread("thread-1")); - - expect(outcome).toMatchObject({ - kind: "rejected", - error: { - name: "NativeRuntimeAdapterError", - code: "transport_unavailable", - }, + const result = await runtime.spawn(spawnInput, { + signal: controller.signal, + maxReconciliationReads: 1, }); - expect(replayReturns).toBe(1); - expect(detailReturns).toBe(1); - expect(shellReturns).toBe(1); - }); - - test("returns missing when aligned shell replay removes the target thread", async () => { - const session: RuntimeClientSession = { - async dispatchCommand() { - return { sequence: 1 }; - }, - subscribeShell(input) { - if (input.afterSequence === 10) { - return stream([ - { - kind: "snapshot", - snapshot: shellSnapshot(12, []), - }, - { kind: "synchronized" }, - ]); - } - return stream([ - { - kind: "snapshot", - snapshot: shellSnapshot(10, [shellThread(10)]), - }, - { kind: "synchronized" }, - ]); - }, - subscribeThread(input) { - if (input.afterSequence === 11) { - return stream([{ kind: "synchronized" }]); - } - return stream([ - { - kind: "snapshot", - snapshot: { snapshotSequence: 11, thread: thread(11) }, - }, - { kind: "synchronized" }, - ]); + expect(result).toMatchObject({ + kind: "create_reconciliation_pending", + createAttempt: { + dispatchState: "accepted", + acceptedSequence: 7, + retryState: "identical_retry_accepted", }, - async close() {}, - }; - const runtime = createT3NativeRuntime({ - environmentId: "environment-1", - label: "MacBook Pro", - acquireSocketUrl: async () => "ws://127.0.0.1/ephemeral", - sessionFactory: { connect: async () => session }, + reconciliation: { reason: "cancelled" }, }); - - expect(await runtime.getThread("thread-1")).toBeUndefined(); + expect(dispatches).toBe(2); }); - test("terminates fallback alignment after one shell catch-up and one detail catch-up", async () => { - let shellSubscriptions = 0; - let detailSubscriptions = 0; - let iteratorReturns = 0; - const session: RuntimeClientSession = { - async dispatchCommand() { - return { sequence: 1 }; - }, - subscribeShell(input) { - shellSubscriptions += 1; - if (input.afterSequence === 10) { - return stream( - [ - { - kind: "snapshot", - snapshot: shellSnapshot(20, [shellThread(20)]), - }, - { kind: "synchronized" }, - ], - () => { - iteratorReturns += 1; - }, - ); - } - return stream( - [ - { - kind: "snapshot", - snapshot: shellSnapshot(10, [shellThread(10)]), - }, - { kind: "synchronized" }, - ], - () => { - iteratorReturns += 1; - }, - ); - }, - subscribeThread(input) { - detailSubscriptions += 1; - if (input.afterSequence === 11) { - return stream([{ kind: "synchronized" }], () => { - iteratorReturns += 1; - }); - } - return stream( - [ - { - kind: "snapshot", - snapshot: { snapshotSequence: 11, thread: thread(11) }, - }, - { kind: "synchronized" }, - ], - () => { - iteratorReturns += 1; + test.each([ + [400, "command_rejected", "invalid_request", "invalid_command"], + [401, "authentication_failed", "auth_invalid", null], + [403, "permission_denied", "insufficient_scope", null], + [500, "server_internal", "internal_error", "orchestration_dispatch_failed"], + ] as const)( + "recovers a durable original read-only after its identical retry returns %i", + async (status, errorClass, code, reason) => { + let visible = false; + let turnAccepted = false; + const commands: Array> = []; + const runtime = createStockT3NativeRuntime({ + client: baseClient({ + getShell: async () => + shell(9, visible ? [shellThread()] : []), + getThread: async () => + visible + ? detail(9, turnAccepted ? [userMessage()] : []) + : undefined, + dispatch: async (command) => { + commands.push(command); + if (commands.length === 1) { + throw new StockT3HttpError("transport_unavailable", null); + } + if (commands.length === 2) { + throw new StockT3HttpError(errorClass, status, { code, reason }); + } + turnAccepted = true; + return { sequence: 10 }; }, - ); - }, - async close() {}, - }; - const runtime = createT3NativeRuntime({ - environmentId: "environment-1", - label: "MacBook Pro", - acquireSocketUrl: async () => "ws://127.0.0.1/ephemeral", - sessionFactory: { connect: async () => session }, - }); - - expect(await runtime.getThread("thread-1")).toMatchObject({ - threadId: "thread-1", - snapshotSequence: 20, - }); - expect(shellSubscriptions).toBe(2); - expect(detailSubscriptions).toBe(2); - expect(iteratorReturns).toBe(4); - }); + }), + id: ids(), + now: () => iso, + }); - test("reconciles interleaved reducers at a common monotonic watermark and preserves pending precedence", async () => { - let shellReturns = 0; - let detailReturns = 0; - let closes = 0; - const assistantEvent = { - ...eventBase(12, "thread.message-sent"), - payload: { - threadId: "thread-1", - messageId: "message-assistant", - role: "assistant", - text: "Done", - turnId: "turn-1", - streaming: false, - createdAt: NOW, - updatedAt: NOW, - }, - } as OrchestrationEvent; - const readyEvent = { - ...eventBase(13, "thread.session-set"), - payload: { - threadId: "thread-1", - session: { - threadId: "thread-1", - status: "ready", - providerName: "codex", - providerInstanceId: "codex", - runtimeMode: "full-access", - activeTurnId: null, - lastError: null, - updatedAt: NOW, + const first = await runtime.spawn(spawnInput, { maxReconciliationReads: 1 }); + expect(first).toMatchObject({ + kind: "create_reconciliation_pending", + createAttempt: { + projectId: "project-1", + dispatchState: "outcome_unknown", + acceptedSequence: null, + retryState: "identical_retry_received_error", + retryError: { status, class: errorClass, code, reason }, }, - }, - } as OrchestrationEvent; - const session: RuntimeClientSession = { - async dispatchCommand() { - return { sequence: 1 }; - }, - subscribeShell() { - return scriptedStream( - [ - { - delayMs: 5, - item: { - kind: "snapshot", - snapshot: shellSnapshot(10, [shellThread(10)]), - }, - }, - { delayMs: 0, item: { kind: "synchronized" } }, - { - delayMs: 5, - item: { - kind: "thread-upserted", - sequence: 11, - thread: shellThread(11, { pendingApproval: true }), - }, - }, - { - delayMs: 5, - item: { - kind: "thread-upserted", - sequence: 11, - thread: shellThread(11, { pendingApproval: true }), - }, - }, - { - delayMs: 40, - item: { - kind: "thread-upserted", - sequence: 13, - thread: shellThread(13, { status: "ready" }), - }, - }, - ], - () => { - shellReturns += 1; - }, - ); - }, - subscribeThread() { - return scriptedStream( - [ - { - delayMs: 0, - item: { - kind: "snapshot", - snapshot: { snapshotSequence: 10, thread: thread(10) }, - }, - }, - { delayMs: 0, item: { kind: "synchronized" } }, - { delayMs: 20, item: { kind: "event", event: assistantEvent } }, - { delayMs: 20, item: { kind: "event", event: readyEvent } }, - ], - () => { - detailReturns += 1; - }, - ); - }, - async close() { - closes += 1; - }, - }; - const runtime = createT3NativeRuntime({ - environmentId: "environment-1", - label: "MacBook Pro", - acquireSocketUrl: async () => "ws://127.0.0.1/ephemeral", - sessionFactory: { connect: async () => session }, - }); - - const observations = []; - for await (const observation of runtime.subscribeThread("thread-1", {})) { - observations.push(observation); - if (observation.sequence === 13) break; - } + }); + if (first.kind !== "create_reconciliation_pending") { + throw new Error("expected pending create"); + } + expect(commands).toHaveLength(2); + expect(commands[1]).toEqual(commands[0]); - expect(observations.map(({ sequence }) => sequence)).toEqual([10, 11, 13]); - expect(observations[1]?.snapshot).toMatchObject({ - snapshotSequence: 11, - pendingApproval: true, - pendingInput: null, - latestTurn: { status: "running", assistantMessage: null }, - }); - expect(observations[2]?.snapshot).toMatchObject({ - snapshotSequence: 13, - pendingApproval: null, - pendingInput: null, - session: { status: "ready", activeTurnId: null }, - latestTurn: { - status: "completed", - assistantMessage: { content: "Done", streaming: false }, - }, - }); - expect(shellReturns).toBe(1); - expect(detailReturns).toBe(1); - expect(closes).toBe(1); - }); + visible = true; + const resumed = await runtime.resumeCreateReconciliation(first, spawnInput, { + maxReconciliationReads: 1, + }); + expect(resumed.kind).toBe("spawned"); + expect(commands.map((entry) => entry.type)).toEqual([ + "thread.create", + "thread.create", + "thread.turn.start", + ]); + expect(commands).not.toContainEqual(expect.objectContaining({ type: "thread.delete" })); + }, + ); +}); - test("requests synchronized subscription markers, filters the resume boundary, and sanitizes connection failures", async () => { - const inputs: unknown[] = []; - let closes = 0; - const healthy: RuntimeClientSession = { - async dispatchCommand() { - return { sequence: 1 }; - }, - subscribeShell(input) { - inputs.push(["shell", input]); - if (input.afterSequence === 8) { - return stream([ - { - kind: "thread-upserted", - sequence: 9, - thread: shellThread(9), - }, - { kind: "synchronized" }, - ]); - } - return stream([ - { kind: "snapshot", snapshot: shellSnapshot(8, [shellThread(8)]) }, - { kind: "synchronized" }, - ]); - }, - subscribeThread(input) { - inputs.push(["thread", input]); - if (input.afterSequence === 8) { - return stream([ - { - kind: "event", - event: { - ...eventBase(9, "thread.session-set"), - payload: { - threadId: "thread-1", - session: { - ...thread(9).session!, - updatedAt: NOW, - }, - }, - } as OrchestrationEvent, - }, - { kind: "synchronized" }, - ]); - } - return stream([ - { - kind: "snapshot", - snapshot: { snapshotSequence: 8, thread: thread(8) }, +describe("stock native runtime read-only resume", () => { + test.each([ + ["shell_only", shell(9, [shellThread()]), undefined], + ["detail_only", shell(9), detail(9)], + ["below_required_sequence", shell(8, [shellThread()]), detail(8)], + ] as const)( + "keeps valid %s evidence pending without mutation", + async (projectionState, shellValue, detailValue) => { + let dispatches = 0; + const runtime = createStockT3NativeRuntime({ + client: baseClient({ + getShell: async () => shellValue, + getThread: async () => detailValue, + dispatch: async () => { + dispatches += 1; + return { sequence: 10 }; }, - { kind: "synchronized" }, - ]); - }, - async close() { - closes += 1; - }, - }; - const runtime = createT3NativeRuntime({ - environmentId: "environment-1", - label: "MacBook Pro", - acquireSocketUrl: async () => "ws://127.0.0.1/ephemeral", - sessionFactory: { connect: async () => healthy }, - }); - - const iterator = runtime - .subscribeThread("thread-1", { afterSequence: 8 }) - [Symbol.asyncIterator](); - expect((await iterator.next()).value?.sequence).toBe(9); - await iterator.return?.(); - - expect(inputs).toHaveLength(4); - expect(inputs).toContainEqual(["shell", { requestCompletionMarker: true }]); - expect(inputs).toContainEqual([ - "thread", - { threadId: "thread-1", requestCompletionMarker: true }, - ]); - expect(inputs).toContainEqual([ - "shell", - { afterSequence: 8, requestCompletionMarker: true }, - ]); - expect(inputs).toContainEqual([ - "thread", - { - threadId: "thread-1", - afterSequence: 8, - requestCompletionMarker: true, - }, - ]); - expect(closes).toBe(1); - - const secret = "ws://127.0.0.1/socket?authorization=super-secret"; - const broken = createT3NativeRuntime({ - environmentId: "environment-1", - label: "MacBook Pro", - acquireSocketUrl: async () => secret, - sessionFactory: { - async connect() { - throw new Error(`could not dial ${secret}`); - }, - }, - }); - let failure: unknown; - try { - await broken.listProjects(); - } catch (error) { - failure = error; - } - expect(failure).toMatchObject({ - name: "NativeRuntimeAdapterError", - code: "transport_unavailable", - }); - expect(String(failure)).not.toContain("super-secret"); - expect(JSON.stringify(failure)).not.toContain("super-secret"); - }); - - test("bounds socket acquisition and injected session connection hangs", async () => { - let socketFactoryConnects = 0; - const socketHang = createT3NativeRuntime({ - environmentId: "environment-1", - label: "MacBook Pro", - connectionTimeoutMs: 10, - acquireSocketUrl: () => new Promise(() => undefined), - sessionFactory: { - async connect() { - socketFactoryConnects += 1; - throw new Error("must not connect"); + }), + id: ids(), + now: () => iso, + }); + const pending = await pendingReceipt(runtime); + const result = await runtime.resumeCreateReconciliation(pending, spawnInput, { + maxReconciliationReads: 1, + }); + expect(result).toMatchObject({ + kind: "create_reconciliation_pending", + reconciliation: { projectionState }, + }); + expect(dispatches).toBe(0); + }, + ); + + test("retains the provisional ref on a true detail identity conflict", async () => { + let dispatches = 0; + const runtime = createStockT3NativeRuntime({ + client: baseClient({ + getShell: async () => shell(9), + getThread: async () => detail(9, [], { projectId: "foreign-project" }), + dispatch: async () => { + dispatches += 1; + return { sequence: 10 }; }, - }, - }); - const socketOutcome = await outcomeWithin(socketHang.listProjects()); - expect(socketOutcome).toMatchObject({ - kind: "rejected", - error: { - name: "NativeRuntimeAdapterError", - code: "transport_unavailable", - }, - }); - expect(socketFactoryConnects).toBe(0); - - const connectHang = createT3NativeRuntime({ - environmentId: "environment-1", - label: "MacBook Pro", - connectionTimeoutMs: 10, - acquireSocketUrl: async () => "ws://127.0.0.1/ephemeral", - sessionFactory: { - connect: () => new Promise(() => undefined), - }, - }); - const connectOutcome = await outcomeWithin(connectHang.listProjects()); - expect(connectOutcome).toMatchObject({ - kind: "rejected", - error: { - name: "NativeRuntimeAdapterError", - code: "transport_unavailable", - }, + }), + id: ids(), + now: () => iso, }); - }); - - test("closes a late injected session exactly once after the caller times out", async () => { - let resolveConnect: ((session: RuntimeClientSession) => void) | undefined; - let closes = 0; - const lateSession: RuntimeClientSession = { - async dispatchCommand() { - return { sequence: 1 }; - }, - subscribeShell: () => stream([]), - subscribeThread: () => stream([]), - async close() { - closes += 1; - }, - }; - const runtime = createT3NativeRuntime({ - environmentId: "environment-1", - label: "MacBook Pro", - connectionTimeoutMs: 10, - acquireSocketUrl: async () => "ws://127.0.0.1/ephemeral", - sessionFactory: { - connect: () => - new Promise((resolve) => { - resolveConnect = resolve; - }), - }, + const pending = await pendingReceipt(runtime); + const result = await runtime.resumeCreateReconciliation(pending, spawnInput, { + maxReconciliationReads: 1, }); - - const outcome = await outcomeWithin(runtime.listProjects()); - expect(outcome).toMatchObject({ - kind: "rejected", - error: { - name: "NativeRuntimeAdapterError", - code: "transport_unavailable", - }, + expect(result).toMatchObject({ + kind: "create_protocol_failure", + provisionalRef: pending.provisionalRef, + conflict: { source: "detail" }, }); - - resolveConnect?.(lateSession); - await new Promise((resolve) => setTimeout(resolve, 10)); - expect(closes).toBe(1); + expect(dispatches).toBe(0); }); +}); - test("rejects timeout values larger than the platform timer limit", () => { - const options = { - environmentId: "environment-1", - label: "MacBook Pro", - acquireSocketUrl: async () => "ws://127.0.0.1/ephemeral", - }; - - for (const oversized of [ - { connectionTimeoutMs: 2_147_483_648 }, - { alignmentTimeoutMs: 2_147_483_648 }, - ]) { - expect(() => createT3NativeRuntime({ ...options, ...oversized })).toThrow( - expect.objectContaining({ - name: "NativeRuntimeAdapterError", - code: "transport_unavailable", - }), - ); - } - }); - - test("bounds hanging default factory effects without awaiting their cleanup finalizers", async () => { - const connection = { - environmentId: "environment-1", - label: "MacBook Pro", - socketUrl: "ws://127.0.0.1/ephemeral", - }; - let connectScopeCloses = 0; - const connectHang = createDefaultSessionFactory( - async () => - ({ - connect: () => - Effect.gen(function* () { - yield* Effect.addFinalizer(() => - Effect.sync(() => { - connectScopeCloses += 1; - }).pipe(Effect.andThen(Effect.never)), - ); - return yield* Effect.never; - }), - }) as unknown as RuntimeClientRpcSessionFactory, - 10, - ); - const connectOutcome = await outcomeWithin(connectHang.connect(connection)); - expect(connectOutcome).toMatchObject({ - kind: "rejected", - error: { - name: "NativeRuntimeAdapterError", - code: "transport_unavailable", - }, - }); - await new Promise((resolve) => setTimeout(resolve, 10)); - expect(connectScopeCloses).toBe(1); - - let readyInterrupts = 0; - let readyScopeCloses = 0; - const readyHang = createDefaultSessionFactory( - async () => - ({ - connect: () => - Effect.gen(function* () { - yield* Effect.addFinalizer(() => - Effect.sync(() => { - readyScopeCloses += 1; - }).pipe(Effect.andThen(Effect.never)), - ); - return { - ready: Effect.never.pipe( - Effect.ensuring( - Effect.sync(() => { - readyInterrupts += 1; - }).pipe(Effect.andThen(Effect.never)), - ), - ), - client: {}, - }; - }), - }) as unknown as RuntimeClientRpcSessionFactory, - 10, - ); - const readyOutcome = await outcomeWithin(readyHang.connect(connection)); - expect(readyOutcome).toMatchObject({ - kind: "rejected", - error: { - name: "NativeRuntimeAdapterError", - code: "transport_unavailable", - }, +describe("stock native runtime inclusive create deadline", () => { + test.each([ + [99, "spawned"], + [100, "partial"], + [101, "create_reconciliation_pending"], + ] as const)("classifies complete create evidence arriving at t=%i", async (arrival, expectedKind) => { + let current = 0; + let shellReads = 0; + let turnAccepted = false; + const commands: Array> = []; + const runtime = createStockT3NativeRuntime({ + client: baseClient({ + getShell: async () => { + shellReads += 1; + if (shellReads > 1) current = 98; + return shell(2, shellReads > 1 ? [shellThread()] : []); + }, + getThread: async () => { + if (!turnAccepted) current = arrival; + return detail(2, turnAccepted ? [userMessage()] : []); + }, + dispatch: async (command) => { + commands.push(command); + if (command.type === "thread.turn.start") turnAccepted = true; + return { sequence: command.type === "thread.create" ? 2 : 3 }; + }, + }), + id: ids(), + now: () => iso, + clock: () => current, }); - await new Promise((resolve) => setTimeout(resolve, 10)); - expect(readyInterrupts).toBe(1); - expect(readyScopeCloses).toBe(1); - }); - test("retries runtime-client factory initialization after a rejected attempt", async () => { - let loads = 0; - const sessionFactory = createDefaultSessionFactory(async () => { - loads += 1; - if (loads === 1) throw new Error("temporary initialization failure"); - return { - connect: () => - Effect.succeed({ - ready: Effect.succeed(undefined), - client: {}, - }), - } as unknown as RuntimeClientRpcSessionFactory; + const result = await runtime.spawn(spawnInput, { + deadlineMs: 100, + maxReconciliationReads: 1, }); - const connection = { - environmentId: "environment-1", - label: "MacBook Pro", - socketUrl: "ws://127.0.0.1/ephemeral", - }; - - await expect(sessionFactory.connect(connection)).rejects.toThrow( - "temporary initialization failure", + expect(result.kind).toBe(expectedKind); + expect(commands.map((entry) => entry.type)).toEqual( + arrival < 100 ? ["thread.create", "thread.turn.start"] : ["thread.create"], ); - const session = await sessionFactory.connect(connection); - await session.close(); - - expect(loads).toBe(2); + if (arrival === 100) { + expect(result).toMatchObject({ + kind: "partial", + initialTurn: { state: "deadline_exhausted" }, + }); + } + if (arrival === 101) { + expect(result).toMatchObject({ + kind: "create_reconciliation_pending", + reconciliation: { reason: "deadline_exhausted" }, + }); + } }); }); - -function eventBase(sequence: number, type: string) { - return { - sequence, - eventId: `event-${sequence}`, - aggregateKind: "thread", - aggregateId: "thread-1", - occurredAt: NOW, - commandId: null, - causationEventId: null, - correlationId: null, - metadata: {}, - type, - }; -} diff --git a/test/protocol.test.ts b/test/protocol.test.ts deleted file mode 100644 index 9549e98..0000000 --- a/test/protocol.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { - ProtocolError, - createAckFrame, - createInterruptFrame, - createRequestFrame, - decodeResponseFrame, -} from "../src/protocol"; - -describe("Effect RPC client frames", () => { - test("creates a Request with a numeric-string ID, tag, payload, and headers", () => { - const frame = createRequestFrame({ - id: "17", - tag: "server.getConfig", - payload: { includeProviders: true }, - headers: [["x-client-version", "t3layer"]], - }); - - expect(frame).toEqual({ - _tag: "Request", - id: "17", - tag: "server.getConfig", - payload: { includeProviders: true }, - headers: [["x-client-version", "t3layer"]], - }); - expect(typeof frame.id).toBe("string"); - }); - - test.each(["17.5", "-1", "request-17", ""])( - "rejects a non-numeric request ID %p", - (id) => { - expect(() => - createRequestFrame({ - id, - tag: "server.getConfig", - payload: {}, - headers: [], - }), - ).toThrow("request ID must be a numeric string"); - }, - ); - - test("creates an Ack using the original request ID", () => { - expect(createAckFrame("17")).toEqual({ - _tag: "Ack", - requestId: "17", - }); - }); - - test("creates an Interrupt using the original request ID", () => { - expect(createInterruptFrame("17")).toEqual({ - _tag: "Interrupt", - requestId: "17", - }); - }); -}); - -describe("Effect RPC server frames", () => { - test("classifies a successful unary Exit and exposes its value", () => { - expect( - decodeResponseFrame({ - _tag: "Exit", - requestId: "17", - exit: { - _tag: "Success", - value: { provider: "claudeAgent", status: "ready" }, - }, - }), - ).toEqual({ - _tag: "Success", - requestId: "17", - value: { provider: "claudeAgent", status: "ready" }, - }); - }); - - test("classifies a failed unary Exit and exposes its cause", () => { - const cause = [ - { - _tag: "Fail", - error: { message: "provider unavailable" }, - }, - ]; - - expect( - decodeResponseFrame({ - _tag: "Exit", - requestId: "18", - exit: { - _tag: "Failure", - cause, - }, - }), - ).toEqual({ - _tag: "Failure", - requestId: "18", - cause, - }); - }); - - test("extracts values from a Chunk", () => { - const values = [ - { type: "thread.snapshot", sequence: 1 }, - { type: "thread.updated", sequence: 2 }, - ] as const; - - expect( - decodeResponseFrame({ - _tag: "Chunk", - requestId: "19", - values, - }), - ).toEqual({ - _tag: "Chunk", - requestId: "19", - values, - }); - }); - - test.each([ - { frame: null }, - { frame: [] }, - { frame: {} }, - { - frame: { - _tag: "Exit", - requestId: 17, - exit: { _tag: "Success", value: null }, - }, - }, - { frame: { _tag: "Exit", requestId: "17", exit: { _tag: "Success" } } }, - { frame: { _tag: "Exit", requestId: "17", exit: { _tag: "Failure" } } }, - { frame: { _tag: "Chunk", requestId: "17", values: [] } }, - ])("rejects malformed server frame %#", ({ frame }) => { - expect(() => decodeResponseFrame(frame)).toThrow(ProtocolError); - }); - - test("rejects an unknown server frame tag", () => { - expect(() => - decodeResponseFrame({ - _tag: "Defect", - defect: "not part of the proven narrow protocol", - }), - ).toThrow("unknown Effect RPC server frame"); - }); - - test("does not expose credentials from a malformed frame in errors", () => { - const bearer = "Bearer secret-bearer-value"; - const ticket = "secret-websocket-ticket"; - - let thrown: unknown; - try { - decodeResponseFrame({ - _tag: "Chunk", - requestId: "not-numeric", - values: [{ authorization: bearer, wsTicket: ticket }], - }); - } catch (error) { - thrown = error; - } - - expect(thrown).toBeInstanceOf(ProtocolError); - const message = String(thrown); - expect(message).not.toContain(bearer); - expect(message).not.toContain(ticket); - expect(message).not.toContain("authorization"); - expect(message).not.toContain("wsTicket"); - }); -}); diff --git a/test/r3-runtime-regressions.test.ts b/test/r3-runtime-regressions.test.ts new file mode 100644 index 0000000..b7272cc --- /dev/null +++ b/test/r3-runtime-regressions.test.ts @@ -0,0 +1,438 @@ +import { describe, expect, test } from "bun:test"; + +import { + StockRuntimeError, + createStockT3NativeRuntime, + type StockSpawnInput, + type StockT3RuntimeClient, +} from "../src/nativeRuntime"; +import type { + ShellSnapshot, + StockMessage, + StockThreadDetail, + StockThreadShell, + ThreadDetailSnapshot, +} from "../src/stockT3Contracts"; +import { StockT3HttpError } from "../src/stockT3HttpClient"; + +const iso = "2026-07-31T18:00:00.000Z"; +const modelSelection = { instanceId: "claudeAgent", model: "claude-opus-5" }; +const project = { + id: "project-1", + title: "project", + workspaceRoot: "/tmp/project", + defaultModelSelection: modelSelection, + createdAt: iso, + updatedAt: iso, +}; +const spawnInput: StockSpawnInput = { + workspaceRoot: project.workspaceRoot, + title: "worker", + message: "initial", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, +}; +const newProjectInput: StockSpawnInput = { + ...spawnInput, + projectCreateIdentity: { + projectId: "project-new", + commandId: "project-command", + createdAt: iso, + workspaceRoot: project.workspaceRoot, + title: "project", + defaultModelSelection: modelSelection, + }, +}; + +function shellThread(overrides: Partial = {}): StockThreadShell { + return { + id: "thread-1", + projectId: project.id, + title: "worker", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + latestTurn: null, + createdAt: iso, + updatedAt: iso, + session: null, + latestUserMessageAt: null, + hasPendingApprovals: false, + hasPendingUserInput: false, + ...overrides, + }; +} + +function shell(sequence: number, threads: readonly StockThreadShell[] = []): ShellSnapshot { + return { snapshotSequence: sequence, projects: [project], threads, updatedAt: iso }; +} + +function message(id: string, text: string, createdAt = iso): StockMessage { + return { + id, + role: "user", + text, + attachments: [], + turnId: null, + streaming: false, + createdAt, + updatedAt: createdAt, + }; +} + +function detail( + sequence: number, + messages: readonly StockMessage[] = [], + latestTurn: StockThreadDetail["latestTurn"] = null, +): ThreadDetailSnapshot { + const identity = shellThread(); + return { + snapshotSequence: sequence, + thread: { + id: identity.id, + projectId: identity.projectId, + title: identity.title, + modelSelection: identity.modelSelection, + runtimeMode: identity.runtimeMode, + interactionMode: identity.interactionMode, + branch: identity.branch, + worktreePath: identity.worktreePath, + latestTurn, + createdAt: identity.createdAt, + updatedAt: identity.updatedAt, + session: null, + messages, + activities: [], + checkpoints: [], + }, + }; +} + +function client(overrides: Partial = {}): StockT3RuntimeClient { + return { + getDescriptor: async () => ({ + environmentId: "env-1", + label: "local", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "stock", + capabilities: { repositoryIdentity: false }, + }), + getShell: async () => shell(1), + getThread: async () => undefined, + dispatch: async () => ({ sequence: 1 }), + ...overrides, + }; +} + +function ids(...values: string[]) { + return () => values.shift()!; +} + +describe("round 3 runtime regressions", () => { + test("reconciles an accepted project.create before dispatching thread.create", async () => { + const createdProject = { ...project, id: "project-new" }; + let shellReads = 0; + let turnStarted = false; + const commands: Record[] = []; + const projectThread = { ...shellThread(), projectId: createdProject.id }; + const projectDetail = (sequence: number, messages: readonly StockMessage[] = []) => { + const value = detail(sequence, messages); + return { ...value, thread: { ...value.thread, projectId: createdProject.id } }; + }; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => { + shellReads += 1; + if (shellReads === 1) return { ...shell(1), projects: [] }; + if (shellReads === 2) return { ...shell(2), projects: [createdProject] }; + return { ...shell(4, [projectThread]), projects: [createdProject] }; + }, + getThread: async () => + projectDetail(4, turnStarted ? [message("message-1", "initial")] : []), + dispatch: async (command) => { + commands.push(command); + if (command.type === "thread.turn.start") turnStarted = true; + return { sequence: commands.length + 1 }; + }, + }), + id: ids( + "create-command", "thread-1", + "turn-command", "message-1", "lease-1", + ), + now: () => iso, + }); + + await expect(runtime.spawn(newProjectInput, { maxReconciliationReads: 1 })).resolves.toMatchObject({ + kind: "spawned", + createReceipt: { threadId: "thread-1" }, + }); + expect(shellReads).toBeGreaterThanOrEqual(3); + expect(commands.map((entry) => entry.type)).toEqual([ + "project.create", "thread.create", "thread.turn.start", + ]); + }); + + test("retries an ambiguous project.create once with byte-identical identity", async () => { + const createdProject = { ...project, id: "project-new" }; + let shellReads = 0; + let projectDispatches = 0; + let turnStarted = false; + const commands: Record[] = []; + const projectThread = { ...shellThread(), projectId: createdProject.id }; + const projectDetail = (sequence: number, messages: readonly StockMessage[] = []) => { + const value = detail(sequence, messages); + return { ...value, thread: { ...value.thread, projectId: createdProject.id } }; + }; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => { + shellReads += 1; + if (shellReads <= 2) return { ...shell(1), projects: [] }; + if (shellReads === 3) return { ...shell(2), projects: [createdProject] }; + return { ...shell(4, [projectThread]), projects: [createdProject] }; + }, + getThread: async () => + projectDetail(4, turnStarted ? [message("message-1", "initial")] : []), + dispatch: async (command) => { + commands.push(command); + if (command.type === "project.create") { + projectDispatches += 1; + if (projectDispatches === 1) throw new StockT3HttpError("transport_unavailable", null); + return { sequence: 2 }; + } + if (command.type === "thread.turn.start") turnStarted = true; + return { sequence: command.type === "thread.create" ? 3 : 4 }; + }, + }), + id: ids( + "create-command", "thread-1", + "turn-command", "message-1", "lease-1", + ), + now: () => iso, + }); + + await expect(runtime.spawn(newProjectInput, { maxReconciliationReads: 1 })).resolves.toMatchObject({ + kind: "spawned", + }); + expect(commands[1]).toEqual(commands[0]); + expect(commands.map((entry) => entry.type)).toEqual([ + "project.create", "project.create", "thread.create", "thread.turn.start", + ]); + }); + + test("a fresh 404 after reconciled create preserves the ref and never starts the turn", async () => { + let detailReads = 0; + const commands: Record[] = []; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => shell(2, [shellThread()]), + getThread: async () => { + detailReads += 1; + return detailReads === 1 ? detail(2) : undefined; + }, + dispatch: async (command) => { + commands.push(command); + return { sequence: 2 }; + }, + }), + id: ids("create-1", "thread-1", "turn-1", "message-1"), + now: () => iso, + }); + + const result = await runtime.spawn(spawnInput, { maxReconciliationReads: 1 }); + expect(result).toMatchObject({ + kind: "partial", + agentRef: { environmentId: "env-1", threadId: "thread-1" }, + initialTurn: { state: "not_attempted", safeAction: "observe" }, + }); + expect(commands.map((entry) => entry.type)).toEqual(["thread.create"]); + }); + + test("a fresh transport failure after reconciled create returns a ref-preserving partial", async () => { + let detailReads = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => shell(2, [shellThread()]), + getThread: async () => { + detailReads += 1; + if (detailReads === 1) return detail(2); + throw new StockT3HttpError("transport_unavailable", null); + }, + dispatch: async () => ({ sequence: 2 }), + }), + id: ids("create-1", "thread-1", "turn-1", "message-1"), + now: () => iso, + }); + + await expect(runtime.spawn(spawnInput, { maxReconciliationReads: 1 })).resolves.toMatchObject({ + kind: "partial", + agentRef: { environmentId: "env-1", threadId: "thread-1" }, + initialTurn: { + state: "not_attempted", + evidence: [{ stage: "fresh_preflight", class: "transport_unavailable" }], + }, + }); + }); + + test("wait revalidates the receipt environment before polling", async () => { + let descriptorReads = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getDescriptor: async () => ({ + environmentId: ++descriptorReads === 1 ? "env-1" : "env-2", + label: "local", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "stock", + capabilities: { repositoryIdentity: false }, + }), + getThread: async () => detail(1), + dispatch: async () => ({ sequence: 2 }), + }), + id: ids("command-1", "message-1", "lease-1"), + now: () => iso, + }); + const receipt = await runtime.send({ environmentId: "env-1", threadId: "thread-1" }, "target"); + + await expect(runtime.wait(receipt, { timeoutMs: 1_000 })).rejects.toMatchObject({ + code: "environment_changed", + }); + expect(descriptorReads).toBe(2); + }); + + test("observe rejects a ref from a changed environment", async () => { + let descriptorReads = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getDescriptor: async () => ({ + environmentId: ++descriptorReads === 1 ? "env-1" : "env-2", + label: "local", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "stock", + capabilities: { repositoryIdentity: false }, + }), + getThread: async () => detail(1), + dispatch: async () => ({ sequence: 2 }), + }), + id: ids("command-1", "message-1", "lease-1"), + now: () => iso, + }); + const receipt = await runtime.send({ environmentId: "env-1", threadId: "thread-1" }, "target"); + runtime.releaseReceipt(receipt); + + await expect(runtime.observe(receipt.agentRef)).rejects.toMatchObject({ code: "environment_changed" }); + }); + + test.each([ + ["pending approval", { hasPendingApprovals: true }], + ["pending input", { hasPendingUserInput: true }], + ] as const)("does not attribute foreign %s before target binding", async (_label, pending) => { + let detailReads = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => shell(3, [{ ...shellThread(), ...pending }]), + getThread: async () => { + detailReads += 1; + return detailReads === 1 ? detail(1) : detail(3); + }, + dispatch: async () => ({ sequence: 2 }), + }), + id: ids("command-1", "message-1", "lease-1"), + now: () => iso, + }); + const receipt = await runtime.send({ environmentId: "env-1", threadId: "thread-1" }, "target"); + + await expect(runtime.wait(receipt, { timeoutMs: 300 })).rejects.toMatchObject({ code: "timeout" }); + }); + + test("does not attribute a pre-existing interrupted turn before target binding", async () => { + const oldTurn = { + turnId: "old-turn", + state: "interrupted" as const, + requestedAt: "2026-07-31T17:59:00.000Z", + startedAt: null, + completedAt: iso, + assistantMessageId: null, + }; + let detailReads = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => shell(3, [{ ...shellThread(), latestTurn: oldTurn }]), + getThread: async () => { + detailReads += 1; + return detailReads === 1 + ? detail(1, [], oldTurn) + : detail(3, [message("message-1", "target")], oldTurn); + }, + dispatch: async () => ({ sequence: 2 }), + }), + id: ids("command-1", "message-1", "lease-1"), + now: () => iso, + }); + const receipt = await runtime.send({ environmentId: "env-1", threadId: "thread-1" }, "target"); + + await expect(runtime.wait(receipt, { timeoutMs: 300 })).rejects.toMatchObject({ code: "timeout" }); + }); + + test("cancellation during the identical send retry releases the lease", async () => { + const controller = new AbortController(); + let dispatches = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getThread: async () => detail(1), + dispatch: async () => { + dispatches += 1; + if (dispatches === 2) controller.abort(); + if (dispatches <= 2) throw new StockT3HttpError("transport_unavailable", null); + return { sequence: 3 }; + }, + }), + id: ids( + "command-1", "message-1", "lease-1", + "command-2", "message-2", "lease-2", + ), + now: () => iso, + }); + const ref = { environmentId: "env-1", threadId: "thread-1" }; + + await expect(runtime.send(ref, "first", { signal: controller.signal })).rejects.toMatchObject({ + code: "cancelled", + }); + await expect(runtime.send(ref, "second")).resolves.toMatchObject({ messageId: "message-2" }); + }); + + test("a malformed successful identical-send response preserves the possibly durable receipt", async () => { + let dispatches = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getThread: async () => detail(1), + dispatch: async () => { + dispatches += 1; + if (dispatches === 1) throw new StockT3HttpError("transport_unavailable", null); + if (dispatches === 2) throw new StockT3HttpError("protocol_mismatch", 200); + return { sequence: 3 }; + }, + }), + id: ids( + "command-1", "message-1", "lease-1", + "command-2", "message-2", "lease-2", + ), + now: () => iso, + }); + const ref = { environmentId: "env-1", threadId: "thread-1" }; + + const first = await runtime.send(ref, "first"); + expect(first).toMatchObject({ + commandId: "command-1", + messageId: "message-1", + acceptedSequence: null, + }); + await expect(runtime.send(ref, "second")).rejects.toMatchObject({ code: "send_in_progress" }); + runtime.releaseReceipt(first); + await expect(runtime.send(ref, "second")).resolves.toMatchObject({ messageId: "message-2" }); + }); +}); diff --git a/test/r4-runtime-regressions.test.ts b/test/r4-runtime-regressions.test.ts new file mode 100644 index 0000000..3f13abc --- /dev/null +++ b/test/r4-runtime-regressions.test.ts @@ -0,0 +1,442 @@ +import { describe, expect, test } from "bun:test"; + +import { + StockRuntimeError, + createStockT3NativeRuntime, + type StockSpawnInput, + type StockT3RuntimeClient, +} from "../src/nativeRuntime"; +import type { + ShellSnapshot, + StockMessage, + StockThreadDetail, + StockThreadShell, + ThreadDetailSnapshot, +} from "../src/stockT3Contracts"; + +const iso = "2026-07-31T18:00:00.000Z"; +const selection = { instanceId: "claudeAgent", model: "claude-opus-5" }; +const project = { + id: "project-1", + title: "project", + workspaceRoot: "/tmp/project", + defaultModelSelection: selection, + createdAt: iso, + updatedAt: iso, +}; +const spawnInput: StockSpawnInput = { + workspaceRoot: project.workspaceRoot, + title: "worker", + message: "initial", + modelSelection: selection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, +}; + +function shellThread(overrides: Partial = {}): StockThreadShell { + return { + id: "thread-1", + projectId: project.id, + title: "worker", + modelSelection: selection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + latestTurn: null, + createdAt: iso, + updatedAt: iso, + session: null, + latestUserMessageAt: null, + hasPendingApprovals: false, + hasPendingUserInput: false, + ...overrides, + }; +} + +function shell(sequence: number, threads: readonly StockThreadShell[] = []): ShellSnapshot { + return { snapshotSequence: sequence, projects: [project], threads: [...threads], updatedAt: iso }; +} + +function message( + id: string, + text: string, + role: StockMessage["role"] = "user", + turnId: string | null = null, +): StockMessage { + return { + id, + role, + text, + attachments: [], + turnId, + streaming: false, + createdAt: iso, + updatedAt: iso, + }; +} + +function detail( + sequence: number, + messages: readonly StockMessage[] = [], + latestTurn: StockThreadDetail["latestTurn"] = null, +): ThreadDetailSnapshot { + const identity = shellThread(); + return { + snapshotSequence: sequence, + thread: { + id: identity.id, + projectId: identity.projectId, + title: identity.title, + modelSelection: identity.modelSelection, + runtimeMode: identity.runtimeMode, + interactionMode: identity.interactionMode, + branch: identity.branch, + worktreePath: identity.worktreePath, + latestTurn, + createdAt: identity.createdAt, + updatedAt: identity.updatedAt, + session: null, + messages: [...messages], + activities: [], + checkpoints: [], + }, + }; +} + +function client(overrides: Partial = {}): StockT3RuntimeClient { + return { + getDescriptor: async () => ({ + environmentId: "env-1", + label: "local", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "stock", + capabilities: { repositoryIdentity: false }, + }), + getShell: async () => shell(1), + getThread: async () => undefined, + dispatch: async () => ({ sequence: 1 }), + ...overrides, + }; +} + +function ids(...values: string[]) { + return () => values.shift()!; +} + +describe("round 4 runtime regressions", () => { + test("atomically admits only one concurrent same-thread send", async () => { + let releasePreflight!: (value: ThreadDetailSnapshot) => void; + const preflight = new Promise((resolve) => { + releasePreflight = resolve; + }); + let detailReads = 0; + let descriptorReads = 0; + let dispatches = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getDescriptor: async () => { + descriptorReads += 1; + return { + environmentId: "env-1", + label: "local", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "stock", + capabilities: { repositoryIdentity: false }, + }; + }, + getThread: async () => { + detailReads += 1; + return preflight; + }, + dispatch: async () => { + dispatches += 1; + return { sequence: 2 }; + }, + }), + id: ids("command-1", "message-1", "lease-1", "command-2", "message-2", "lease-2"), + now: () => iso, + }); + const ref = { environmentId: "env-1", threadId: "thread-1" }; + + const outcomesPromise = Promise.allSettled([ + runtime.send(ref, "first"), + runtime.send(ref, "second"), + ]); + for (let index = 0; index < 20 && detailReads < 1; index += 1) { + await Promise.resolve(); + } + expect(detailReads).toBe(1); + releasePreflight(detail(1)); + const outcomes = await outcomesPromise; + + expect(outcomes.filter((entry) => entry.status === "fulfilled")).toHaveLength(1); + const rejected = outcomes.find((entry) => entry.status === "rejected"); + expect(rejected).toMatchObject({ reason: { code: "send_in_progress" } }); + expect(descriptorReads).toBe(1); + expect(detailReads).toBe(1); + expect(dispatches).toBe(1); + }); + + test("returns an executable receipt for accepted-but-unprojected initial turns", async () => { + let detailReads = 0; + let waiting = false; + const completedTurn = { + turnId: "turn-id-1", + state: "completed" as const, + requestedAt: iso, + startedAt: iso, + completedAt: iso, + assistantMessageId: "assistant-1", + }; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => + waiting + ? shell(5, [shellThread({ latestTurn: completedTurn })]) + : shell(2, [shellThread()]), + getThread: async () => { + detailReads += 1; + if (!waiting) return detail(2); + return detail(5, [ + message("message-1", "initial"), + message("assistant-1", "done", "assistant", "turn-id-1"), + ], completedTurn); + }, + dispatch: async (command) => ({ sequence: command.type === "thread.create" ? 2 : 4 }), + }), + id: ids("create-1", "thread-1", "turn-1", "message-1", "lease-1"), + now: () => iso, + }); + + const result = await runtime.spawn(spawnInput, { maxReconciliationReads: 1 }); + expect(result).toMatchObject({ + kind: "partial", + initialTurn: { + state: "initial_turn_accepted_projection_pending", + safeAction: "wait", + turnReceipt: { commandId: "turn-1", messageId: "message-1", leaseId: "lease-1" }, + }, + }); + if (result.kind !== "partial" || result.initialTurn.turnReceipt === null) { + throw new Error("expected executable partial receipt"); + } + waiting = true; + await expect(runtime.wait(result.initialTurn.turnReceipt, { timeoutMs: 1_000 })).resolves.toMatchObject({ + kind: "completed", + assistantContent: "done", + }); + await expect(runtime.wait(result.initialTurn.turnReceipt, { timeoutMs: 1_000 })).rejects.toMatchObject({ + code: "receipt_expired", + }); + expect(detailReads).toBeGreaterThanOrEqual(4); + }); + + test.each(["release", "expiry"] as const)( + "the %s path clears an accepted-but-unprojected initial-turn receipt", + async (mode) => { + let current = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => shell(2, [shellThread()]), + getThread: async () => detail(2), + dispatch: async (command) => ({ sequence: command.type === "thread.create" ? 2 : 4 }), + }), + id: ids( + "create-1", + "thread-1", + "turn-1", + "message-1", + "lease-1", + "command-2", + "message-2", + "lease-2", + ), + now: () => iso, + clock: () => current, + }); + const result = await runtime.spawn(spawnInput, { + timeoutMs: 100, + maxReconciliationReads: 1, + }); + if (result.kind !== "partial" || result.initialTurn.turnReceipt === null) { + throw new Error("expected executable partial receipt"); + } + if (mode === "release") runtime.releaseReceipt(result.initialTurn.turnReceipt); + else current = 100; + + await expect(runtime.wait(result.initialTurn.turnReceipt, { timeoutMs: 100 })).rejects.toMatchObject({ + code: "receipt_expired", + }); + await expect(runtime.send(result.agentRef, "next")).resolves.toMatchObject({ + commandId: "command-2", + messageId: "message-2", + leaseId: "lease-2", + }); + }, + ); + + test("classifies below-accepted project projection as lag through the real HTTP decoder", async () => { + let shellReads = 0; + let dispatches = 0; + const fetch = async (input: string | URL | Request, init?: RequestInit): Promise => { + const url = new URL(input instanceof Request ? input.url : input.toString()); + if (url.pathname === "/.well-known/t3/environment") { + return Response.json({ + environmentId: "env-1", + label: "local", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "stock", + capabilities: { repositoryIdentity: false }, + }); + } + if (url.pathname === "/api/orchestration/shell") { + shellReads += 1; + return Response.json({ + snapshotSequence: shellReads === 1 ? 1 : 3, + projects: + shellReads === 1 + ? [] + : [{ ...project, id: "project-new", workspaceRoot: "/tmp/new-project" }], + threads: [], + updatedAt: iso, + }); + } + if (url.pathname === "/api/orchestration/dispatch" && init?.method === "POST") { + dispatches += 1; + return Response.json({ sequence: 5 }); + } + throw new Error(`unexpected route ${init?.method ?? "GET"} ${url.pathname}`); + }; + const runtime = createStockT3NativeRuntime({ + baseUrl: "http://127.0.0.1:3773", + fetch, + id: ids(), + now: () => iso, + }); + + const error = await runtime + .spawn({ + ...spawnInput, + workspaceRoot: "/tmp/new-project", + projectCreateIdentity: { + projectId: "project-new", + commandId: "project-command", + createdAt: iso, + workspaceRoot: "/tmp/new-project", + title: "project", + defaultModelSelection: selection, + }, + }, { maxReconciliationReads: 1 }) + .catch((cause) => cause); + + expect(error).toBeInstanceOf(StockRuntimeError); + expect(error).toMatchObject({ + code: "transport_unavailable", + evidence: { + reason: "project_projection_pending", + provisionalProjectId: "project-new", + acceptedSequence: 5, + }, + }); + expect(dispatches).toBe(1); + }); + + test("still fails closed when lagging project observations regress", async () => { + let shellReads = 0; + const fetch = async (input: string | URL | Request, init?: RequestInit): Promise => { + const url = new URL(input instanceof Request ? input.url : input.toString()); + if (url.pathname === "/.well-known/t3/environment") { + return Response.json({ + environmentId: "env-1", + label: "local", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "stock", + capabilities: { repositoryIdentity: false }, + }); + } + if (url.pathname === "/api/orchestration/shell") { + shellReads += 1; + const sequence = shellReads === 1 ? 1 : shellReads === 2 ? 4 : 3; + return Response.json({ + snapshotSequence: sequence, + projects: + shellReads === 1 + ? [] + : [{ ...project, id: "project-new", workspaceRoot: "/tmp/new-project" }], + threads: [], + updatedAt: iso, + }); + } + if (url.pathname === "/api/orchestration/dispatch" && init?.method === "POST") { + return Response.json({ sequence: 5 }); + } + throw new Error(`unexpected route ${init?.method ?? "GET"} ${url.pathname}`); + }; + const runtime = createStockT3NativeRuntime({ + baseUrl: "http://127.0.0.1:3773", + fetch, + id: ids(), + now: () => iso, + }); + + await expect( + runtime.spawn( + { + ...spawnInput, + workspaceRoot: "/tmp/new-project", + projectCreateIdentity: { + projectId: "project-new", + commandId: "project-command", + createdAt: iso, + workspaceRoot: "/tmp/new-project", + title: "project", + defaultModelSelection: selection, + }, + }, + { maxReconciliationReads: 2 }, + ), + ).rejects.toMatchObject({ + code: "protocol_mismatch", + evidence: { reason: "shell_sequence_regression" }, + }); + }); + + test.each([ + ["pending_approval", { hasPendingApprovals: true }], + ["pending_input", { hasPendingUserInput: true }], + ] as const)("reports %s only after target binding and retains its lease", async (code, pending) => { + const boundTurn = { + turnId: "turn-id-1", + state: "running" as const, + requestedAt: iso, + startedAt: iso, + completedAt: null, + assistantMessageId: null, + }; + let preflight = true; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => shell(3, [{ ...shellThread({ latestTurn: boundTurn }), ...pending }]), + getThread: async () => { + if (preflight) { + preflight = false; + return detail(1); + } + return detail(3, [message("message-1", "target")], boundTurn); + }, + dispatch: async () => ({ sequence: 2 }), + }), + id: ids("command-1", "message-1", "lease-1", "command-2", "message-2", "lease-2"), + now: () => iso, + }); + const ref = { environmentId: "env-1", threadId: "thread-1" }; + const receipt = await runtime.send(ref, "target"); + + await expect(runtime.wait(receipt, { timeoutMs: 1_000 })).rejects.toMatchObject({ code }); + await expect(runtime.send(ref, "second")).rejects.toMatchObject({ code: "send_in_progress" }); + runtime.releaseReceipt(receipt); + }); +}); diff --git a/test/r5-runtime-regressions.test.ts b/test/r5-runtime-regressions.test.ts new file mode 100644 index 0000000..716cd9e --- /dev/null +++ b/test/r5-runtime-regressions.test.ts @@ -0,0 +1,338 @@ +import { describe, expect, test } from "bun:test"; + +import { + StockRuntimeError, + createStockT3NativeRuntime, + digestStockSpawnInput, + type CreateReconciliationPending, + type StockSpawnInput, + type StockT3RuntimeClient, +} from "../src/nativeRuntime"; +import type { + ShellSnapshot, + StockMessage, + StockThreadDetail, + StockThreadShell, + ThreadDetailSnapshot, +} from "../src/stockT3Contracts"; +import { StockT3HttpError } from "../src/stockT3HttpClient"; + +const iso = "2026-07-31T18:00:00.000Z"; +const selection = { instanceId: "claudeAgent", model: "claude-opus-5" }; +const project = { + id: "project-1", + title: "project", + workspaceRoot: "/tmp/project", + defaultModelSelection: selection, + createdAt: iso, + updatedAt: iso, +}; +const spawnInput: StockSpawnInput = { + workspaceRoot: project.workspaceRoot, + title: "worker", + message: "initial", + modelSelection: selection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, +}; + +function shellThread(overrides: Partial = {}): StockThreadShell { + return { + id: "thread-1", + projectId: project.id, + title: "worker", + modelSelection: selection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + latestTurn: null, + createdAt: iso, + updatedAt: iso, + session: null, + latestUserMessageAt: null, + hasPendingApprovals: false, + hasPendingUserInput: false, + ...overrides, + }; +} + +function shell(sequence: number, threads: readonly StockThreadShell[] = []): ShellSnapshot { + return { snapshotSequence: sequence, projects: [project], threads: [...threads], updatedAt: iso }; +} + +function message(id: string, text: string): StockMessage { + return { + id, + role: "user", + text, + attachments: [], + turnId: null, + streaming: false, + createdAt: iso, + updatedAt: iso, + }; +} + +function detail( + sequence: number, + messages: readonly StockMessage[] = [], + latestTurn: StockThreadDetail["latestTurn"] = null, +): ThreadDetailSnapshot { + const identity = shellThread(); + return { + snapshotSequence: sequence, + thread: { + id: identity.id, + projectId: identity.projectId, + title: identity.title, + modelSelection: identity.modelSelection, + runtimeMode: identity.runtimeMode, + interactionMode: identity.interactionMode, + branch: identity.branch, + worktreePath: identity.worktreePath, + latestTurn, + createdAt: identity.createdAt, + updatedAt: identity.updatedAt, + session: null, + messages: [...messages], + activities: [], + checkpoints: [], + }, + }; +} + +function client(overrides: Partial = {}): StockT3RuntimeClient { + return { + getDescriptor: async () => ({ + environmentId: "env-1", + label: "local", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "stock", + capabilities: { repositoryIdentity: false }, + }), + getShell: async () => shell(2, [shellThread()]), + getThread: async () => detail(2), + dispatch: async () => ({ sequence: 2 }), + ...overrides, + }; +} + +function ids(...values: string[]) { + return () => values.shift()!; +} + +async function delayedInitialTurnResult( + retry: "received_500" | "accepted_9", +) { + let detailReads = 0; + const commands: Readonly>[] = []; + const runtime = createStockT3NativeRuntime({ + client: client({ + getThread: async () => { + detailReads += 1; + if (detailReads === 4) { + await new Promise((resolve) => setTimeout(resolve, 60)); + } + return detail(2); + }, + dispatch: async (command) => { + commands.push(command); + if (command.type === "thread.create") return { sequence: 2 }; + const turnStarts = commands.filter((entry) => entry.type === "thread.turn.start").length; + if (turnStarts === 1) { + throw new StockT3HttpError("transport_unavailable", null); + } + if (retry === "accepted_9") return { sequence: 9 }; + throw new StockT3HttpError("server_internal", 500, { + code: "internal_error", + reason: "orchestration_dispatch_failed", + }); + }, + }), + id: ids("create-1", "thread-1", "turn-1", "message-1", "lease-1"), + now: () => iso, + }); + + const result = await runtime.spawn(spawnInput, { + timeoutMs: 40, + maxReconciliationReads: 1, + }); + return { result, commands }; +} + +describe("round 5 runtime regressions", () => { + test("preserves ambiguous initial-turn identity when retry 500 projection overruns expiry", async () => { + const { result, commands } = await delayedInitialTurnResult("received_500"); + + expect(result).toMatchObject({ + kind: "partial", + agentRef: { environmentId: "env-1", threadId: "thread-1" }, + createReceipt: { commandId: "create-1", threadId: "thread-1", acceptedSequence: 2 }, + initialTurn: { + commandId: "turn-1", + messageId: "message-1", + state: "initial_turn_outcome_unknown", + turnReceipt: { + commandId: "turn-1", + messageId: "message-1", + acceptedSequence: null, + leaseState: "released", + }, + leaseExpiresAt: null, + safeAction: "observe", + evidence: [{ retryClass: "server_internal" }], + }, + }); + expect(commands.map((entry) => entry.type)).toEqual([ + "thread.create", + "thread.turn.start", + "thread.turn.start", + ]); + }); + + test("preserves accepted retry sequence when projection overruns expiry", async () => { + const { result } = await delayedInitialTurnResult("accepted_9"); + + expect(result).toMatchObject({ + kind: "partial", + agentRef: { environmentId: "env-1", threadId: "thread-1" }, + createReceipt: { commandId: "create-1", threadId: "thread-1", acceptedSequence: 2 }, + initialTurn: { + commandId: "turn-1", + messageId: "message-1", + state: "initial_turn_accepted_projection_pending", + turnReceipt: { + commandId: "turn-1", + messageId: "message-1", + acceptedSequence: 9, + leaseState: "released", + }, + leaseExpiresAt: null, + safeAction: "observe", + evidence: [{ acceptedSequence: 9 }], + }, + }); + }); + + test("returns accepted send identity when post-dispatch promotion occurs after expiry", async () => { + let dispatches = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getThread: async () => detail(1), + dispatch: async () => { + dispatches += 1; + await new Promise((resolve) => setTimeout(resolve, 60)); + return { sequence: 7 }; + }, + }), + id: ids("command-1", "message-1", "lease-1"), + now: () => iso, + }); + + const receipt = await runtime.send( + { environmentId: "env-1", threadId: "thread-1" }, + "target", + { timeoutMs: 40 }, + ); + + expect(receipt).toMatchObject({ + commandId: "command-1", + messageId: "message-1", + leaseId: "lease-1", + acceptedSequence: 7, + }); + expect(dispatches).toBe(1); + await expect(runtime.wait(receipt)).rejects.toMatchObject({ code: "receipt_expired" }); + }); + + test("resume slot contention preserves its earned create receipt and remains retryable", async () => { + let releaseSendPreflight!: (value: ThreadDetailSnapshot) => void; + const sendPreflight = new Promise((resolve) => { + releaseSendPreflight = resolve; + }); + let detailReads = 0; + let turnStarts = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getThread: async () => { + detailReads += 1; + return detailReads === 1 ? sendPreflight : detail(2); + }, + dispatch: async (command) => { + if (command.type === "thread.turn.start") turnStarts += 1; + return { sequence: turnStarts + 2 }; + }, + }), + id: ids("send-command", "other-message", "send-lease", "resume-lease"), + now: () => iso, + }); + const ref = { environmentId: "env-1", threadId: "thread-1" }; + const pending: CreateReconciliationPending = { + kind: "create_reconciliation_pending", + provisionalRef: ref, + createAttempt: { + commandId: "create-1", + threadId: "thread-1", + projectId: "project-1", + acceptedSequence: 2, + dispatchState: "accepted", + retryState: "not_applicable", + retryError: null, + }, + reconciliation: { + reason: "projection_pending", + projectionState: "unobserved", + highestShellSequence: null, + highestDetailSequence: null, + deadlineMs: Date.now() + 1_000, + evidence: [], + }, + initialTurnContinuation: { + commandId: "turn-1", + messageId: "message-1", + inputDigest: await digestStockSpawnInput(spawnInput), + }, + safeAction: "resume_create_reconciliation", + }; + + const activeSend = runtime.send(ref, "other"); + const contended = await runtime + .resumeCreateReconciliation(pending, spawnInput, { maxReconciliationReads: 1 }) + .catch((cause) => cause); + + expect(contended).toMatchObject({ + kind: "partial", + agentRef: ref, + createReceipt: { commandId: "create-1", threadId: "thread-1", acceptedSequence: 2 }, + initialTurn: { + commandId: "turn-1", + messageId: "message-1", + state: "contended_before_start", + turnReceipt: null, + safeAction: "observe", + }, + }); + expect(turnStarts).toBe(0); + + releaseSendPreflight(detail(2)); + const sendReceipt = await activeSend; + expect(turnStarts).toBe(1); + runtime.releaseReceipt(sendReceipt); + + const recovered = await runtime.resumeCreateReconciliation(pending, spawnInput, { + maxReconciliationReads: 1, + }); + expect(recovered).toMatchObject({ + kind: "partial", + createReceipt: { commandId: "create-1", threadId: "thread-1" }, + initialTurn: { + state: "initial_turn_accepted_projection_pending", + turnReceipt: { commandId: "turn-1", messageId: "message-1" }, + }, + }); + expect(turnStarts).toBe(2); + }); +}); diff --git a/test/r6-runtime-regressions.test.ts b/test/r6-runtime-regressions.test.ts new file mode 100644 index 0000000..8f6b862 --- /dev/null +++ b/test/r6-runtime-regressions.test.ts @@ -0,0 +1,399 @@ +import { describe, expect, test } from "bun:test"; +import { join } from "node:path"; + +import { + createStockT3NativeRuntime, + type StockSpawnInput, + type StockT3RuntimeClient, +} from "../src/nativeRuntime"; +import type { + EnvironmentDescriptor, + ShellSnapshot, + StockMessage, + StockThreadDetail, + StockThreadShell, + ThreadDetailSnapshot, +} from "../src/stockT3Contracts"; +import { StockT3HttpError } from "../src/stockT3HttpClient"; + +const iso = "2026-07-31T18:00:00.000Z"; +const selection = { instanceId: "claudeAgent", model: "claude-opus-5" }; +const project = { + id: "project-1", + title: "project", + workspaceRoot: "/tmp/project", + defaultModelSelection: selection, + createdAt: iso, + updatedAt: iso, +}; +const spawnInput: StockSpawnInput = { + workspaceRoot: project.workspaceRoot, + title: "worker", + message: "initial", + modelSelection: selection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, +}; + +function environment(environmentId: string): EnvironmentDescriptor { + return { + environmentId, + label: "local", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "stock", + capabilities: { repositoryIdentity: false }, + }; +} + +function shellThread(overrides: Partial = {}): StockThreadShell { + return { + id: "thread-1", + projectId: project.id, + title: "worker", + modelSelection: selection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + latestTurn: null, + createdAt: iso, + updatedAt: iso, + session: null, + latestUserMessageAt: null, + hasPendingApprovals: false, + hasPendingUserInput: false, + ...overrides, + }; +} + +function shell(sequence: number, threads: readonly StockThreadShell[] = []): ShellSnapshot { + return { snapshotSequence: sequence, projects: [project], threads: [...threads], updatedAt: iso }; +} + +function message(id: string, text: string): StockMessage { + return { + id, + role: "user", + text, + attachments: [], + turnId: null, + streaming: false, + createdAt: iso, + updatedAt: iso, + }; +} + +function detail( + sequence: number, + messages: readonly StockMessage[] = [], + latestTurn: StockThreadDetail["latestTurn"] = null, +): ThreadDetailSnapshot { + const identity = shellThread(); + return { + snapshotSequence: sequence, + thread: { + id: identity.id, + projectId: identity.projectId, + title: identity.title, + modelSelection: identity.modelSelection, + runtimeMode: identity.runtimeMode, + interactionMode: identity.interactionMode, + branch: identity.branch, + worktreePath: identity.worktreePath, + latestTurn, + createdAt: identity.createdAt, + updatedAt: identity.updatedAt, + session: null, + messages: [...messages], + activities: [], + checkpoints: [], + }, + }; +} + +function client(overrides: Partial = {}): StockT3RuntimeClient { + return { + getDescriptor: async () => environment("env-1"), + getShell: async () => shell(2, [shellThread()]), + getThread: async () => detail(2), + dispatch: async () => ({ sequence: 2 }), + ...overrides, + }; +} + +function ids(...values: string[]) { + return () => values.shift()!; +} + +function deferred() { + let resolve!: (value: T) => void; + const promise = new Promise((accept) => { + resolve = accept; + }); + return { promise, resolve }; +} + +function expectAcceptedPending(result: unknown, acceptedSequence = 5) { + expect(result).toMatchObject({ + kind: "partial", + agentRef: { environmentId: "env-1", threadId: "thread-1" }, + createReceipt: { + commandId: "create-1", + threadId: "thread-1", + acceptedSequence: 2, + }, + initialTurn: { + commandId: "turn-1", + messageId: "message-1", + state: "initial_turn_accepted_projection_pending", + turnReceipt: { + commandId: "turn-1", + messageId: "message-1", + leaseState: "released", + }, + leaseExpiresAt: null, + safeAction: "observe", + evidence: [{ acceptedSequence }], + }, + }); +} + +describe("round 6 runtime regressions", () => { + test("P4 preserves an accepted initial turn across concurrent environment invalidation", async () => { + const targetRead = deferred(); + const targetStarted = deferred(); + let descriptorReads = 0; + let detailReads = 0; + const commands: Readonly>[] = []; + const runtime = createStockT3NativeRuntime({ + client: client({ + getDescriptor: async () => environment(++descriptorReads === 1 ? "env-1" : "env-2"), + getThread: async () => { + detailReads += 1; + if (detailReads === 3) { + targetStarted.resolve(); + return targetRead.promise; + } + return detail(2); + }, + dispatch: async (command) => { + commands.push(command); + return { sequence: command.type === "thread.create" ? 2 : 5 }; + }, + }), + id: ids("create-1", "thread-1", "turn-1", "message-1", "lease-1"), + now: () => iso, + }); + + const spawning = runtime.spawn(spawnInput, { maxReconciliationReads: 1 }); + await targetStarted.promise; + await expect( + runtime.observe({ environmentId: "env-1", threadId: "thread-1" }), + ).rejects.toMatchObject({ code: "environment_changed" }); + targetRead.resolve(detail(2)); + + expectAcceptedPending(await spawning); + expect(commands.map((entry) => entry.type)).toEqual([ + "thread.create", + "thread.turn.start", + ]); + }); + + test("P6 preserves an ambiguous send receipt when reconciliation loses its lease", async () => { + const targetRead = deferred(); + const targetStarted = deferred(); + let descriptorReads = 0; + let detailReads = 0; + let dispatches = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getDescriptor: async () => environment(++descriptorReads === 1 ? "env-1" : "env-2"), + getThread: async () => { + detailReads += 1; + if (detailReads === 2) { + targetStarted.resolve(); + return targetRead.promise; + } + return detail(2); + }, + dispatch: async () => { + dispatches += 1; + if (dispatches === 1) throw new StockT3HttpError("transport_unavailable", null); + throw new StockT3HttpError("server_internal", 500, { + code: "internal_error", + reason: "orchestration_dispatch_failed", + }); + }, + }), + id: ids("command-1", "message-1", "lease-1"), + now: () => iso, + }); + const ref = { environmentId: "env-1", threadId: "thread-1" }; + + const sending = runtime.send(ref, "target"); + await targetStarted.promise; + await expect(runtime.observe(ref)).rejects.toMatchObject({ code: "environment_changed" }); + targetRead.resolve(detail(2)); + + expect(await sending).toMatchObject({ + agentRef: ref, + commandId: "command-1", + messageId: "message-1", + leaseId: "lease-1", + acceptedSequence: null, + }); + expect(dispatches).toBe(2); + }); + + test("P2 preserves accepted identity at the exact inclusive deadline boundary", async () => { + let detailReads = 0; + let atBoundary = false; + const commands: Readonly>[] = []; + const runtime = createStockT3NativeRuntime({ + client: client({ + getThread: async () => { + detailReads += 1; + if (detailReads === 3) { + atBoundary = true; + await new Promise((resolve) => setTimeout(resolve, 110)); + } + return detail(2); + }, + dispatch: async (command) => { + commands.push(command); + return { sequence: command.type === "thread.create" ? 2 : 5 }; + }, + }), + id: ids("create-1", "thread-1", "turn-1", "message-1", "lease-1"), + now: () => iso, + clock: () => (atBoundary ? 100 : 0), + }); + + const result = await runtime.spawn(spawnInput, { + deadlineMs: 100, + maxReconciliationReads: 1, + }); + + expectAcceptedPending(result); + expect(commands.map((entry) => entry.type)).toEqual([ + "thread.create", + "thread.turn.start", + ]); + }); + + test("P1-A preserves first-dispatch acceptance when projection reaches the deadline", async () => { + let detailReads = 0; + const commands: Readonly>[] = []; + const runtime = createStockT3NativeRuntime({ + client: client({ + getThread: async () => { + detailReads += 1; + if (detailReads === 3) await new Promise((resolve) => setTimeout(resolve, 60)); + return detail(2); + }, + dispatch: async (command) => { + commands.push(command); + return { sequence: command.type === "thread.create" ? 2 : 5 }; + }, + }), + id: ids("create-1", "thread-1", "turn-1", "message-1", "lease-1"), + now: () => iso, + }); + + expectAcceptedPending(await runtime.spawn(spawnInput, { + timeoutMs: 40, + maxReconciliationReads: 1, + })); + expect(commands.map((entry) => entry.type)).toEqual([ + "thread.create", + "thread.turn.start", + ]); + }); + + test("P1-B preserves first-dispatch acceptance when projection is cancelled", async () => { + const controller = new AbortController(); + let detailReads = 0; + const commands: Readonly>[] = []; + const runtime = createStockT3NativeRuntime({ + client: client({ + getThread: async () => { + detailReads += 1; + if (detailReads === 3) controller.abort(); + return detail(2); + }, + dispatch: async (command) => { + commands.push(command); + return { sequence: command.type === "thread.create" ? 2 : 5 }; + }, + }), + id: ids("create-1", "thread-1", "turn-1", "message-1", "lease-1"), + now: () => iso, + }); + + expectAcceptedPending(await runtime.spawn(spawnInput, { + signal: controller.signal, + maxReconciliationReads: 1, + })); + expect(commands.map((entry) => entry.type)).toEqual([ + "thread.create", + "thread.turn.start", + ]); + }); + + test("preserves the create receipt when the deadline crosses inside slot claim", async () => { + let returnedCreateDetail = false; + let clocksAfterDetail = 0; + const commands: Readonly>[] = []; + const runtime = createStockT3NativeRuntime({ + client: client({ + getThread: async () => { + returnedCreateDetail = true; + return detail(2); + }, + dispatch: async (command) => { + commands.push(command); + return { sequence: 2 }; + }, + }), + id: ids("create-1", "thread-1", "turn-1", "message-1"), + now: () => iso, + clock: () => { + if (!returnedCreateDetail) return 0; + clocksAfterDetail += 1; + return clocksAfterDetail >= 4 ? 100 : 0; + }, + }); + + const result = await runtime.spawn(spawnInput, { + deadlineMs: 100, + maxReconciliationReads: 1, + }); + + expect(result).toMatchObject({ + kind: "partial", + agentRef: { environmentId: "env-1", threadId: "thread-1" }, + createReceipt: { + commandId: "create-1", + threadId: "thread-1", + acceptedSequence: 2, + }, + initialTurn: { + commandId: "turn-1", + messageId: "message-1", + state: "deadline_exhausted", + turnReceipt: null, + leaseExpiresAt: null, + safeAction: "observe", + evidence: [], + }, + }); + expect(commands.map((entry) => entry.type)).toEqual(["thread.create"]); + }); + + test("contains no throwing lease-state accessor", async () => { + const source = await Bun.file(join(import.meta.dir, "../src/nativeRuntime.ts")).text(); + expect(source).not.toMatch(/\bleaseState\s*\(/); + }); +}); diff --git a/test/r7-runtime-regressions.test.ts b/test/r7-runtime-regressions.test.ts new file mode 100644 index 0000000..95c1edc --- /dev/null +++ b/test/r7-runtime-regressions.test.ts @@ -0,0 +1,466 @@ +import { describe, expect, test } from "bun:test"; + +import { + createStockT3NativeRuntime, + type StockSpawnInput, + type StockT3RuntimeClient, +} from "../src/nativeRuntime"; +import type { + EnvironmentDescriptor, + ShellSnapshot, + StockMessage, + StockThreadDetail, + StockThreadShell, + ThreadDetailSnapshot, +} from "../src/stockT3Contracts"; +import { StockT3HttpError } from "../src/stockT3HttpClient"; + +const iso = "2026-07-31T18:00:00.000Z"; +const selection = { instanceId: "claudeAgent", model: "claude-opus-5" }; +const project = { + id: "project-1", + title: "project", + workspaceRoot: "/tmp/project", + defaultModelSelection: selection, + createdAt: iso, + updatedAt: iso, +}; +const spawnInput: StockSpawnInput = { + workspaceRoot: project.workspaceRoot, + title: "worker", + message: "initial", + modelSelection: selection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, +}; + +function environment(environmentId = "env-1"): EnvironmentDescriptor { + return { + environmentId, + label: "local", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "stock", + capabilities: { repositoryIdentity: false }, + }; +} + +function shellThread(overrides: Partial = {}): StockThreadShell { + return { + id: "thread-1", + projectId: project.id, + title: "worker", + modelSelection: selection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + latestTurn: null, + createdAt: iso, + updatedAt: iso, + session: null, + latestUserMessageAt: null, + hasPendingApprovals: false, + hasPendingUserInput: false, + ...overrides, + }; +} + +function shell(sequence: number, threads: readonly StockThreadShell[] = [shellThread()]): ShellSnapshot { + return { snapshotSequence: sequence, projects: [project], threads: [...threads], updatedAt: iso }; +} + +function message(id: string, text: string): StockMessage { + return { + id, + role: "user", + text, + attachments: [], + turnId: null, + streaming: false, + createdAt: iso, + updatedAt: iso, + }; +} + +function detail( + sequence: number, + messages: readonly StockMessage[] = [], + latestTurn: StockThreadDetail["latestTurn"] = null, +): ThreadDetailSnapshot { + const identity = shellThread(); + return { + snapshotSequence: sequence, + thread: { + id: identity.id, + projectId: identity.projectId, + title: identity.title, + modelSelection: identity.modelSelection, + runtimeMode: identity.runtimeMode, + interactionMode: identity.interactionMode, + branch: identity.branch, + worktreePath: identity.worktreePath, + latestTurn, + createdAt: identity.createdAt, + updatedAt: identity.updatedAt, + session: null, + messages: [...messages], + activities: [], + checkpoints: [], + }, + }; +} + +function client(overrides: Partial = {}): StockT3RuntimeClient { + return { + getDescriptor: async () => environment(), + getShell: async () => shell(2), + getThread: async () => detail(2), + dispatch: async () => ({ sequence: 2 }), + ...overrides, + }; +} + +function ids(...values: string[]) { + return () => values.shift()!; +} + +function deferred() { + let resolve!: (value: T) => void; + const promise = new Promise((accept) => { + resolve = accept; + }); + return { promise, resolve }; +} + +function receivedError(code: "server_internal" | "protocol_mismatch") { + return code === "server_internal" + ? new StockT3HttpError("server_internal", 500, { + code: "internal_error", + reason: "orchestration_dispatch_failed", + }) + : new StockT3HttpError("protocol_mismatch", 200); +} + +function expectAcceptedPartial(result: unknown, errorClass: string) { + expect(result).toMatchObject({ + kind: "partial", + agentRef: { environmentId: "env-1", threadId: "thread-1" }, + createReceipt: { + commandId: "create-1", + threadId: "thread-1", + acceptedSequence: 2, + }, + initialTurn: { + commandId: "turn-1", + messageId: "message-1", + state: "initial_turn_accepted_projection_pending", + safeAction: "wait", + turnReceipt: { + commandId: "turn-1", + messageId: "message-1", + acceptedSequence: 5, + }, + evidence: [ + { acceptedSequence: 5 }, + { stage: "target_reconciliation", class: errorClass }, + ], + }, + }); +} + +describe("round 7 post-mutation result invariant", () => { + test.each([ + ["N2-A received detail 500", "server_internal"], + ["N2-C detail protocol mismatch", "protocol_mismatch"], + ] as const)("%s preserves accepted create and initial-turn evidence", async (_label, errorClass) => { + let detailReads = 0; + const commands: Readonly>[] = []; + const runtime = createStockT3NativeRuntime({ + client: client({ + getThread: async () => { + detailReads += 1; + if (detailReads === 3) throw receivedError(errorClass); + return detail(2); + }, + dispatch: async (command) => { + commands.push(command); + return { sequence: command.type === "thread.create" ? 2 : 5 }; + }, + }), + id: ids("create-1", "thread-1", "turn-1", "message-1", "lease-1"), + now: () => iso, + }); + + const result = await runtime.spawn(spawnInput, { maxReconciliationReads: 1 }); + + expectAcceptedPartial(result, errorClass); + expect(commands.map((entry) => entry.type)).toEqual([ + "thread.create", + "thread.turn.start", + ]); + }); + + test("N2-B accepted create plus received shell 500 returns reconciliation pending", async () => { + let shellReads = 0; + const commands: Readonly>[] = []; + const runtime = createStockT3NativeRuntime({ + client: client({ + getShell: async () => { + shellReads += 1; + if (shellReads === 2) throw receivedError("server_internal"); + return shell(2); + }, + dispatch: async (command) => { + commands.push(command); + return { sequence: 2 }; + }, + }), + id: ids("create-1", "thread-1", "turn-1", "message-1"), + now: () => iso, + }); + + await expect(runtime.spawn(spawnInput, { maxReconciliationReads: 1 })).resolves.toMatchObject({ + kind: "create_reconciliation_pending", + provisionalRef: { environmentId: "env-1", threadId: "thread-1" }, + createAttempt: { + commandId: "create-1", + threadId: "thread-1", + acceptedSequence: 2, + }, + reconciliation: { + reason: "transport_exhausted", + evidence: [{ stage: "create_shell_reconciliation", class: "server_internal", status: 500 }], + }, + initialTurnContinuation: { commandId: "turn-1", messageId: "message-1" }, + safeAction: "resume_create_reconciliation", + }); + expect(commands.map((entry) => entry.type)).toEqual(["thread.create"]); + }); + + test("N2-D ambiguous send plus received detail 500 preserves the receipt identity", async () => { + let detailReads = 0; + let dispatches = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getThread: async () => { + detailReads += 1; + if (detailReads === 2) throw receivedError("server_internal"); + return detail(2); + }, + dispatch: async () => { + dispatches += 1; + throw new StockT3HttpError("transport_unavailable", null); + }, + }), + id: ids("command-1", "message-1", "lease-1"), + now: () => iso, + }); + const ref = { environmentId: "env-1", threadId: "thread-1" }; + + await expect(runtime.send(ref, "target", { maxReconciliationReads: 1 })).resolves.toMatchObject({ + agentRef: ref, + commandId: "command-1", + messageId: "message-1", + leaseId: "lease-1", + acceptedSequence: null, + }); + expect(dispatches).toBe(2); + }); + + test("N1-A create-time claim invalidation returns a ref-preserving partial", async () => { + const freshRead = deferred(); + const freshStarted = deferred(); + let descriptorReads = 0; + let detailReads = 0; + const commands: Readonly>[] = []; + const runtime = createStockT3NativeRuntime({ + client: client({ + getDescriptor: async () => environment(++descriptorReads === 1 ? "env-1" : "env-2"), + getThread: async () => { + detailReads += 1; + if (detailReads === 2) { + freshStarted.resolve(); + return freshRead.promise; + } + return detail(2); + }, + dispatch: async (command) => { + commands.push(command); + return { sequence: 2 }; + }, + }), + id: ids("create-1", "thread-1", "turn-1", "message-1"), + now: () => iso, + }); + + const spawning = runtime.spawn(spawnInput, { maxReconciliationReads: 1 }); + await freshStarted.promise; + await expect(runtime.observe({ environmentId: "env-1", threadId: "thread-1" })).rejects.toMatchObject({ + code: "environment_changed", + }); + freshRead.resolve(detail(2)); + + await expect(spawning).resolves.toMatchObject({ + kind: "partial", + agentRef: { environmentId: "env-1", threadId: "thread-1" }, + createReceipt: { commandId: "create-1", threadId: "thread-1", acceptedSequence: 2 }, + initialTurn: { + commandId: "turn-1", + messageId: "message-1", + state: "not_attempted", + safeAction: "observe", + evidence: [{ stage: "lease_promotion", class: "environment_changed" }], + }, + }); + expect(commands.map((entry) => entry.type)).toEqual(["thread.create"]); + }); + + test("N1-B send claim invalidation reports environment_changed without dispatch", async () => { + const preflight = deferred(); + const preflightStarted = deferred(); + let descriptorReads = 0; + let dispatches = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getDescriptor: async () => environment(++descriptorReads === 1 ? "env-1" : "env-2"), + getThread: async () => { + preflightStarted.resolve(); + return preflight.promise; + }, + dispatch: async () => { + dispatches += 1; + return { sequence: 2 }; + }, + }), + id: ids("command-1", "message-1", "lease-1"), + now: () => iso, + }); + const ref = { environmentId: "env-1", threadId: "thread-1" }; + + const sending = runtime.send(ref, "target"); + await preflightStarted.promise; + await expect(runtime.observe(ref)).rejects.toMatchObject({ code: "environment_changed" }); + preflight.resolve(detail(2)); + + await expect(sending).rejects.toMatchObject({ code: "environment_changed" }); + expect(dispatches).toBe(0); + }); + + test("inclusive deadline crossing before lease promotion preserves the create receipt", async () => { + let detailReads = 0; + let freshReturned = false; + let clocksAfterFresh = 0; + const commands: Readonly>[] = []; + const runtime = createStockT3NativeRuntime({ + client: client({ + getThread: async () => { + detailReads += 1; + if (detailReads === 2) freshReturned = true; + return detail(2); + }, + dispatch: async (command) => { + commands.push(command); + return { sequence: 2 }; + }, + }), + id: ids("create-1", "thread-1", "turn-1", "message-1"), + now: () => iso, + clock: () => { + if (!freshReturned) return 0; + clocksAfterFresh += 1; + return clocksAfterFresh >= 2 ? 100 : 0; + }, + }); + + await expect(runtime.spawn(spawnInput, { + deadlineMs: 100, + maxReconciliationReads: 1, + })).resolves.toMatchObject({ + kind: "partial", + agentRef: { environmentId: "env-1", threadId: "thread-1" }, + createReceipt: { commandId: "create-1", threadId: "thread-1", acceptedSequence: 2 }, + initialTurn: { + commandId: "turn-1", + messageId: "message-1", + state: "deadline_exhausted", + safeAction: "observe", + }, + }); + expect(commands.map((entry) => entry.type)).toEqual(["thread.create"]); + }); + + test("resume descriptor failure retains the possibly durable create attempt", async () => { + let descriptorReads = 0; + let shellReads = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getDescriptor: async () => { + descriptorReads += 1; + if (descriptorReads === 2) throw receivedError("server_internal"); + return environment(); + }, + getShell: async () => { + shellReads += 1; + if (shellReads === 2) throw receivedError("server_internal"); + return shell(2); + }, + dispatch: async () => ({ sequence: 2 }), + }), + id: ids("create-1", "thread-1", "turn-1", "message-1"), + now: () => iso, + }); + + const first = await runtime.spawn(spawnInput, { maxReconciliationReads: 1 }); + if (first.kind !== "create_reconciliation_pending") { + throw new Error("expected pending create reconciliation"); + } + + await expect(runtime.resumeCreateReconciliation(first, spawnInput, { + maxReconciliationReads: 1, + })).resolves.toMatchObject({ + kind: "create_reconciliation_pending", + provisionalRef: { environmentId: "env-1", threadId: "thread-1" }, + createAttempt: { commandId: "create-1", threadId: "thread-1", acceptedSequence: 2 }, + reconciliation: { + reason: "transport_exhausted", + evidence: [{ stage: "resume_descriptor", class: "server_internal", status: 500 }], + }, + initialTurnContinuation: { commandId: "turn-1", messageId: "message-1" }, + }); + }); + + test("ambiguous send terminal rejection carries full receipt identity", async () => { + let detailReads = 0; + const runtime = createStockT3NativeRuntime({ + client: client({ + getThread: async () => { + detailReads += 1; + return detail(detailReads === 1 ? 2 : 3, detailReads === 1 ? [] : [message("foreign", "other")]); + }, + dispatch: async () => { + throw new StockT3HttpError("transport_unavailable", null); + }, + }), + id: ids("command-1", "message-1", "lease-1"), + now: () => iso, + }); + const ref = { environmentId: "env-1", threadId: "thread-1" }; + + await expect(runtime.send(ref, "target", { maxReconciliationReads: 1 })).rejects.toMatchObject({ + code: "superseded", + evidence: { + stage: "send_reconciliation", + receipt: { + agentRef: ref, + commandId: "command-1", + messageId: "message-1", + leaseId: "lease-1", + acceptedSequence: null, + }, + }, + }); + }); + +}); diff --git a/test/r8-runtime-regressions.test.ts b/test/r8-runtime-regressions.test.ts new file mode 100644 index 0000000..8c9fbe7 --- /dev/null +++ b/test/r8-runtime-regressions.test.ts @@ -0,0 +1,604 @@ +import { describe, expect, test } from "bun:test"; + +import { + createStockT3NativeRuntime, + type StockSpawnInput, +} from "../src/nativeRuntime"; + +const iso = "2026-07-31T18:00:00.000Z"; +const selection = { instanceId: "claudeAgent", model: "claude-opus-5" }; +const spawnInput: StockSpawnInput = { + workspaceRoot: "/tmp/project", + projectCreateIdentity: { + projectId: "project-1", + commandId: "project-command-1", + createdAt: iso, + workspaceRoot: "/tmp/project", + title: "project", + defaultModelSelection: selection, + }, + title: "worker", + message: "initial", + modelSelection: selection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, +}; + +type MutationType = "project.create" | "thread.create" | "thread.turn.start"; + +interface DispatchDecision { + readonly commit?: boolean; + readonly response?: Response; + readonly error?: Error; +} + +interface FixtureOptions { + readonly projects?: readonly string[]; + readonly environmentForRead?: (read: number) => string; + readonly onShell?: (read: number, fixture: StockFixture) => Response | undefined; + readonly onThread?: ( + read: number, + threadId: string, + fixture: StockFixture, + ) => Response | undefined; + readonly onDispatch?: ( + command: Readonly>, + attempt: number, + fixture: StockFixture, + ) => DispatchDecision | undefined; +} + +function descriptor(environmentId: string) { + return { + environmentId, + label: "local", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "stock", + capabilities: { repositoryIdentity: false }, + }; +} + +function readFailure(status = 500): Response { + return Response.json( + { code: "internal_error", reason: "orchestration_dispatch_failed", secret: "not-retained" }, + { status }, + ); +} + +function dispatchFailure(status: 400 | 401 | 403 | 500): Response { + const bodies = { + 400: { code: "invalid_request", reason: "invalid_command" }, + 401: { code: "auth_invalid", reason: "invalid_credential" }, + 403: { code: "insufficient_scope", reason: null }, + 500: { code: "internal_error", reason: "orchestration_dispatch_failed" }, + } as const; + return Response.json(bodies[status], { status }); +} + +class StockFixture { + readonly projects = new Map>(); + readonly threads = new Map>(); + readonly messages = new Map[]>(); + readonly commands: Readonly>[] = []; + readonly dispatchAttempts = new Map(); + descriptorReads = 0; + shellReads = 0; + threadReads = 0; + sequence = 0; + + constructor(readonly options: FixtureOptions = {}) { + for (const id of options.projects ?? []) { + this.projects.set(id, this.project(id)); + } + } + + private project(id: string): Record { + return { + id, + title: "project", + workspaceRoot: spawnInput.workspaceRoot, + defaultModelSelection: selection, + createdAt: iso, + updatedAt: iso, + }; + } + + private shellThread(thread: Record): Record { + return { + ...thread, + latestTurn: null, + session: null, + latestUserMessageAt: null, + hasPendingApprovals: false, + hasPendingUserInput: false, + }; + } + + shellResponse( + projects = [...this.projects.values()], + threads = [...this.threads.values()], + ): Response { + return Response.json({ + snapshotSequence: this.sequence, + projects, + threads: threads.map((entry) => this.shellThread(entry)), + updatedAt: iso, + }); + } + + detailResponse(threadId: string, includeMessages = true): Response { + const thread = this.threads.get(threadId); + if (thread === undefined) return Response.json({ code: "not_found" }, { status: 404 }); + return Response.json({ + snapshotSequence: this.sequence, + thread: { + ...thread, + latestTurn: null, + session: null, + messages: includeMessages ? (this.messages.get(threadId) ?? []) : [], + activities: [], + checkpoints: [], + }, + }); + } + + private commit(command: Readonly>): void { + const type = command.type as MutationType; + if (type === "project.create") { + const projectId = command.projectId as string; + this.projects.set(projectId, { + ...this.project(projectId), + title: command.title, + workspaceRoot: command.workspaceRoot, + defaultModelSelection: command.defaultModelSelection, + createdAt: command.createdAt, + updatedAt: command.createdAt, + }); + } else if (type === "thread.create") { + const threadId = command.threadId as string; + this.threads.set(threadId, { + id: threadId, + projectId: command.projectId, + title: command.title, + modelSelection: command.modelSelection, + runtimeMode: command.runtimeMode, + interactionMode: command.interactionMode, + branch: command.branch, + worktreePath: command.worktreePath, + createdAt: command.createdAt, + updatedAt: command.createdAt, + }); + this.messages.set(threadId, []); + } else { + const threadId = command.threadId as string; + const input = command.message as Record; + const rows = this.messages.get(threadId) ?? []; + if (!rows.some((entry) => entry.id === input.messageId)) { + rows.push({ + id: input.messageId, + role: "user", + text: input.text, + attachments: input.attachments ?? [], + turnId: null, + streaming: false, + createdAt: command.createdAt, + updatedAt: command.createdAt, + }); + } + this.messages.set(threadId, rows); + } + this.sequence += 1; + } + + readonly fetch = async (input: string | URL | Request, init?: RequestInit): Promise => { + const request = new Request(input, init); + const path = new URL(request.url).pathname; + if (path === "/.well-known/t3/environment") { + this.descriptorReads += 1; + return Response.json( + descriptor(this.options.environmentForRead?.(this.descriptorReads) ?? "env-1"), + ); + } + if (path === "/api/orchestration/shell") { + this.shellReads += 1; + return this.options.onShell?.(this.shellReads, this) ?? this.shellResponse(); + } + if (path.startsWith("/api/orchestration/threads/")) { + this.threadReads += 1; + const threadId = decodeURIComponent(path.slice("/api/orchestration/threads/".length)); + return ( + this.options.onThread?.(this.threadReads, threadId, this) ?? + this.detailResponse(threadId) + ); + } + if (path === "/api/orchestration/dispatch") { + const command = JSON.parse(await request.text()) as Readonly>; + const type = command.type as MutationType; + const attempt = (this.dispatchAttempts.get(type) ?? 0) + 1; + this.dispatchAttempts.set(type, attempt); + (this.commands as Readonly>[]).push(command); + const decision = this.options.onDispatch?.(command, attempt, this); + if (decision?.commit ?? true) this.commit(command); + if (decision?.error !== undefined) throw decision.error; + return decision?.response ?? Response.json({ sequence: this.sequence }); + } + throw new Error(`unexpected route ${request.method} ${path}`); + }; +} + +function ids(...values: string[]) { + return () => values.shift()!; +} + +function projectAttemptEvidence(retryState: string, acceptedSequence: number | null) { + return { + provisionalProjectId: "project-1", + projectAttempt: { + commandId: "project-command-1", + projectId: "project-1", + acceptedSequence, + retryState, + }, + readEvidence: [ + { stage: "project_create_reconciliation", class: "server_internal", status: 500 }, + ], + }; +} + +describe("round 8 all-mutation result invariant over stock HTTP", () => { + test("E2 accepted project.create plus reconciliation 500 preserves project identity and evidence", async () => { + const fixture = new StockFixture({ + onShell: (read) => (read === 2 ? readFailure() : undefined), + }); + const runtime = createStockT3NativeRuntime({ + baseUrl: "http://stock.invalid", + fetch: fixture.fetch, + id: ids("project-1", "project-command-1"), + now: () => iso, + }); + + const error = await runtime.spawn(spawnInput, { maxReconciliationReads: 1 }).catch((cause) => cause); + expect(error).toMatchObject({ + code: "transport_unavailable", + evidence: { + reason: "project_projection_pending", + ...projectAttemptEvidence("not_applicable", 1), + }, + }); + expect(fixture.dispatchAttempts.get("project.create")).toBe(1); + expect(JSON.stringify(error)).not.toContain("not-retained"); + runtime.close(); + }); + + test("E2b ambiguous project.create plus accepted identical retry and reconciliation 500 preserves one attempt", async () => { + const fixture = new StockFixture({ + onShell: (read, stock) => { + if (read === 2) return stock.shellResponse([], []); + if (read === 3) return readFailure(); + return undefined; + }, + onDispatch: (command, attempt) => + command.type === "project.create" && attempt === 1 + ? { commit: true, error: new Error("response lost") } + : undefined, + }); + const runtime = createStockT3NativeRuntime({ + baseUrl: "http://stock.invalid", + fetch: fixture.fetch, + id: ids("project-1", "project-command-1"), + now: () => iso, + }); + + await expect(runtime.spawn(spawnInput, { maxReconciliationReads: 1 })).rejects.toMatchObject({ + code: "transport_unavailable", + evidence: { + reason: "project_create_outcome_unknown", + ...projectAttemptEvidence("identical_retry_accepted", 2), + }, + }); + const projectCommands = fixture.commands.filter((entry) => entry.type === "project.create"); + expect(projectCommands).toHaveLength(2); + expect(projectCommands[1]).toEqual(projectCommands[0]); + runtime.close(); + }); + + test("C1 caller retries reuse the caller-held project identity and never create a second project row", async () => { + const fixture = new StockFixture({ + onShell: (read, stock) => { + if (read === 2) return readFailure(); + if (read === 3) return stock.shellResponse([], []); + return undefined; + }, + }); + const runtime = createStockT3NativeRuntime({ + baseUrl: "http://stock.invalid", + fetch: fixture.fetch, + id: ids( + "create-1", "thread-1", "turn-1", "message-1", "lease-1", + "create-2", "thread-2", "turn-2", "message-2", "lease-2", + ), + now: () => iso, + }); + + await expect(runtime.spawn(spawnInput, { maxReconciliationReads: 1 })).rejects.toMatchObject({ + evidence: { provisionalProjectId: "project-1" }, + }); + const second = await runtime.spawn(spawnInput, { maxReconciliationReads: 1 }); + expect(second).toMatchObject({ kind: "spawned", agentRef: { threadId: "thread-1" } }); + if (second.kind === "spawned") runtime.releaseReceipt(second.turnReceipt); + const third = await runtime.spawn(spawnInput, { maxReconciliationReads: 1 }); + expect(third).toMatchObject({ kind: "spawned", agentRef: { threadId: "thread-2" } }); + if (third.kind === "spawned") runtime.releaseReceipt(third.turnReceipt); + const projectCommands = fixture.commands.filter((entry) => entry.type === "project.create"); + expect(projectCommands).toHaveLength(2); + expect(projectCommands[1]).toEqual(projectCommands[0]); + expect([...fixture.projects]).toHaveLength(1); + runtime.close(); + }); + + test("an ambiguous project attempt cancelled before retry reuses the exact caller identity", async () => { + const controller = new AbortController(); + const fixture = new StockFixture({ + onShell: (read, stock) => { + if (read === 2) { + controller.abort(); + return stock.shellResponse([], []); + } + if (read === 3) return stock.shellResponse([], []); + return undefined; + }, + onDispatch: (command, attempt) => + command.type === "project.create" && attempt === 1 + ? { commit: true, error: new Error("response lost") } + : undefined, + }); + const runtime = createStockT3NativeRuntime({ + baseUrl: "http://stock.invalid", + fetch: fixture.fetch, + id: ids( + "create-1", "thread-1", "turn-1", "message-1", "lease-1", + ), + now: () => iso, + }); + + await expect(runtime.spawn(spawnInput, { + maxReconciliationReads: 1, + signal: controller.signal, + })).rejects.toMatchObject({ + code: "cancelled", + evidence: { + provisionalProjectId: "project-1", + projectAttempt: { retryState: "eligible_not_sent" }, + }, + }); + await expect(runtime.spawn(spawnInput, { maxReconciliationReads: 1 })).resolves.toMatchObject({ + kind: "spawned", + agentRef: { threadId: "thread-1" }, + }); + const projectCommands = fixture.commands.filter((entry) => entry.type === "project.create"); + expect(projectCommands).toHaveLength(2); + expect(projectCommands[1]).toEqual(projectCommands[0]); + runtime.close(); + }); + + test("E4 one received target-read 500 does not consume the remaining four-read budget", async () => { + let targetReads = 0; + const fixture = new StockFixture({ + projects: ["project-1"], + onThread: (read, threadId, stock) => { + if (read <= 2) return undefined; + targetReads += 1; + if (targetReads === 1) return readFailure(); + if (targetReads < 4) return stock.detailResponse(threadId, false); + return undefined; + }, + }); + const runtime = createStockT3NativeRuntime({ + baseUrl: "http://stock.invalid", + fetch: fixture.fetch, + id: ids("create-1", "thread-1", "turn-1", "message-1", "lease-1"), + now: () => iso, + }); + + await expect(runtime.spawn(spawnInput, { maxReconciliationReads: 4 })).resolves.toMatchObject({ + kind: "spawned", + turnReceipt: { + messageId: "message-1", + acceptedSequence: 2, + reconciliationEvidence: [ + { stage: "target_reconciliation", class: "server_internal", status: 500 }, + ], + }, + }); + expect(targetReads).toBe(4); + expect(runtime.httpObservations().endpointStatusTrace).toContainEqual({ + method: "GET", + path: "/api/orchestration/threads/thread-1", + status: 500, + }); + runtime.close(); + }); + + test("E3 a stable environment roll invalidates old refs once and admits new work after re-pin", async () => { + const fixture = new StockFixture({ + projects: ["project-1"], + environmentForRead: (read) => (read === 1 ? "env-1" : "env-2"), + }); + fixture.threads.set("old-thread", { + id: "old-thread", + projectId: "project-1", + title: "worker", + modelSelection: selection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + createdAt: iso, + updatedAt: iso, + }); + fixture.messages.set("old-thread", []); + const runtime = createStockT3NativeRuntime({ + baseUrl: "http://stock.invalid", + fetch: fixture.fetch, + id: ids("create-1", "thread-1", "turn-1", "message-1", "lease-1"), + now: () => iso, + }); + const oldRef = { environmentId: "env-1", threadId: "old-thread" }; + + await expect(runtime.observe(oldRef)).resolves.toMatchObject({ snapshotSequence: 0 }); + await expect(runtime.observe(oldRef)).rejects.toMatchObject({ code: "environment_changed" }); + await expect(runtime.observe(oldRef)).rejects.toMatchObject({ code: "environment_changed" }); + await expect(runtime.spawn(spawnInput, { maxReconciliationReads: 1 })).resolves.toMatchObject({ + kind: "spawned", + agentRef: { environmentId: "env-2", threadId: "thread-1" }, + }); + runtime.close(); + }); + + test.each([ + "project.create", + "thread.create", + "thread.turn.start", + ] as const)("malformed accepted %s response reconciles exact identity without a duplicate", async (stage) => { + const fixture = new StockFixture({ + projects: stage === "project.create" ? [] : ["project-1"], + onDispatch: (command) => + command.type === stage + ? { commit: true, response: Response.json({ sequence: "malformed" }) } + : undefined, + }); + const runtime = createStockT3NativeRuntime({ + baseUrl: "http://stock.invalid", + fetch: fixture.fetch, + id: + stage === "project.create" + ? ids("create-1", "thread-1", "turn-1", "message-1", "lease-1") + : ids("create-1", "thread-1", "turn-1", "message-1", "lease-1"), + now: () => iso, + }); + + await expect(runtime.spawn(spawnInput, { maxReconciliationReads: 1 })).resolves.toMatchObject({ + kind: "spawned", + agentRef: { threadId: "thread-1" }, + }); + expect(fixture.dispatchAttempts.get(stage)).toBe(1); + runtime.close(); + }); + + test.each([ + ["thread shell", "shell"], + ["thread detail", "detail"], + ] as const)("received %s reconciliation 500 is bounded and the next read recovers", async (_label, boundary) => { + const fixture = new StockFixture({ + projects: ["project-1"], + onShell: (read) => (boundary === "shell" && read === 2 ? readFailure() : undefined), + onThread: (read) => (boundary === "detail" && read === 1 ? readFailure() : undefined), + }); + const runtime = createStockT3NativeRuntime({ + baseUrl: "http://stock.invalid", + fetch: fixture.fetch, + id: ids("create-1", "thread-1", "turn-1", "message-1", "lease-1"), + now: () => iso, + }); + + await expect(runtime.spawn(spawnInput, { maxReconciliationReads: 2 })).resolves.toMatchObject({ + kind: "spawned", + agentRef: { threadId: "thread-1" }, + }); + runtime.close(); + }); + + test.each([400, 401, 403, 500] as const)( + "a trustworthy original project.create %i permits a new caller-held logical identity", + async (status) => { + let first = true; + const fixture = new StockFixture({ + onDispatch: (command) => { + if (command.type === "project.create" && first) { + first = false; + return { commit: false, response: dispatchFailure(status) }; + } + return undefined; + }, + }); + const runtime = createStockT3NativeRuntime({ + baseUrl: "http://stock.invalid", + fetch: fixture.fetch, + id: ids( + "create-1", "thread-1", "turn-1", "message-1", "lease-1", + ), + now: () => iso, + }); + + await expect(runtime.spawn(spawnInput, { maxReconciliationReads: 1 })).rejects.toMatchObject({ + code: status === 400 ? "command_rejected" : status === 401 ? "authentication_failed" : status === 403 ? "permission_denied" : "server_internal", + }); + await expect(runtime.spawn({ + ...spawnInput, + projectCreateIdentity: { + projectId: "project-2", + commandId: "project-command-2", + createdAt: iso, + workspaceRoot: "/tmp/project", + title: "project", + defaultModelSelection: selection, + }, + }, { maxReconciliationReads: 1 })).resolves.toMatchObject({ kind: "spawned" }); + expect(fixture.dispatchAttempts.get("project.create")).toBe(2); + expect([...fixture.projects.keys()]).toEqual(["project-2"]); + runtime.close(); + }, + ); + + test.each([400, 401, 403, 500] as const)( + "an ambiguous accepted original survives a trustworthy identical project retry %i", + async (status) => { + const fixture = new StockFixture({ + onShell: (read, stock) => + read === 2 || read === 3 ? stock.shellResponse([], []) : undefined, + onDispatch: (command, attempt) => { + if (command.type !== "project.create") return undefined; + if (attempt === 1) return { commit: true, error: new Error("response lost") }; + return { commit: false, response: dispatchFailure(status) }; + }, + }); + const runtime = createStockT3NativeRuntime({ + baseUrl: "http://stock.invalid", + fetch: fixture.fetch, + id: ids( + "create-1", "thread-1", "turn-1", "message-1", "lease-1", + ), + now: () => iso, + }); + + await expect(runtime.spawn(spawnInput, { maxReconciliationReads: 1 })).rejects.toMatchObject({ + code: "transport_unavailable", + evidence: { + provisionalProjectId: "project-1", + projectAttempt: { + commandId: "project-command-1", + projectId: "project-1", + acceptedSequence: null, + dispatchState: "outcome_unknown", + retryState: "identical_retry_received_error", + retryClass: + status === 400 + ? "command_rejected" + : status === 401 + ? "authentication_failed" + : status === 403 + ? "permission_denied" + : "server_internal", + }, + }, + }); + await expect(runtime.spawn(spawnInput, { maxReconciliationReads: 1 })).resolves.toMatchObject({ + kind: "spawned", + }); + expect(fixture.dispatchAttempts.get("project.create")).toBe(2); + runtime.close(); + }, + ); +}); diff --git a/test/r9-runtime-regressions.test.ts b/test/r9-runtime-regressions.test.ts new file mode 100644 index 0000000..4d81e44 --- /dev/null +++ b/test/r9-runtime-regressions.test.ts @@ -0,0 +1,664 @@ +import { describe, expect, test } from "bun:test"; + +import { + createStockT3NativeRuntime, + type StockSpawnInput, + type TurnReceipt, +} from "../src/nativeRuntime"; + +const iso = "2026-08-01T04:00:00.000Z"; +const selection = { instanceId: "claudeAgent", model: "claude-opus-5" }; +const projectIdentity = { + projectId: "project-shared", + commandId: "project-command-shared", + createdAt: iso, + workspaceRoot: "/tmp/project", + title: "project", + defaultModelSelection: selection, +}; +const spawnInput = { + workspaceRoot: "/tmp/project", + projectCreateIdentity: projectIdentity, + title: "worker", + message: "initial", + modelSelection: selection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, +} as StockSpawnInput & { + readonly projectCreateIdentity: typeof projectIdentity & { + readonly environmentId?: string; + }; +}; +const existingProjectInput = { + ...spawnInput, + projectId: "project-existing", + projectCreateIdentity: undefined, +}; + +type MutationType = "project.create" | "thread.create" | "thread.turn.start"; + +interface DispatchDecision { + readonly commit?: boolean; + readonly response?: Response; + readonly error?: Error; +} + +interface FixtureOptions { + readonly environmentForRead?: (read: number) => string; + readonly onShell?: ( + read: number, + fixture: StockFixture, + ) => Response | Promise | undefined; + readonly onThread?: ( + read: number, + threadId: string, + fixture: StockFixture, + ) => Response | Promise | undefined; + readonly onDispatch?: ( + command: Readonly>, + attempt: number, + fixture: StockFixture, + ) => DispatchDecision | undefined; +} + +function descriptor(environmentId: string) { + return { + environmentId, + label: "local", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "stock", + capabilities: { repositoryIdentity: false }, + }; +} + +function ids(...values: string[]) { + return () => { + const value = values.shift(); + if (value === undefined) throw new Error("unexpected ID allocation"); + return value; + }; +} + +function deferred() { + let resolve!: (value: T) => void; + const promise = new Promise((done) => { + resolve = done; + }); + return { promise, resolve }; +} + +class StockFixture { + readonly projects = new Map>(); + readonly threads = new Map>(); + readonly messages = new Map[]>(); + readonly commands: Readonly>[] = []; + readonly dispatchAttempts = new Map(); + readonly acceptedCommands = new Map(); + descriptorReads = 0; + shellReads = 0; + threadReads = 0; + sequence = 0; + + constructor(readonly options: FixtureOptions = {}) {} + + project(id: string): Record { + return { + id, + title: "project", + workspaceRoot: spawnInput.workspaceRoot, + defaultModelSelection: selection, + createdAt: iso, + updatedAt: iso, + }; + } + + seedProject(id = "project-existing"): void { + this.projects.set(id, this.project(id)); + } + + seedThread(threadId: string, projectId = "project-existing"): void { + this.threads.set(threadId, { + id: threadId, + projectId, + title: "worker", + modelSelection: selection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + createdAt: iso, + updatedAt: iso, + }); + this.messages.set(threadId, []); + } + + shellResponse( + projects = [...this.projects.values()], + threads = [...this.threads.values()], + ): Response { + return Response.json({ + snapshotSequence: this.sequence, + projects, + threads: threads.map((thread) => ({ + ...thread, + latestTurn: null, + session: null, + latestUserMessageAt: null, + hasPendingApprovals: false, + hasPendingUserInput: false, + })), + updatedAt: iso, + }); + } + + detailResponse( + threadId: string, + input: { + readonly messages?: readonly Record[]; + readonly latestTurn?: Record | null; + } = {}, + ): Response { + const thread = this.threads.get(threadId); + if (thread === undefined) return Response.json({ code: "not_found" }, { status: 404 }); + return Response.json({ + snapshotSequence: this.sequence, + thread: { + ...thread, + latestTurn: input.latestTurn ?? null, + session: null, + messages: input.messages ?? this.messages.get(threadId) ?? [], + activities: [], + checkpoints: [], + }, + }); + } + + userMessage(id: string, text: string): Record { + return { + id, + role: "user", + text, + attachments: [], + turnId: null, + streaming: false, + createdAt: iso, + updatedAt: iso, + }; + } + + private commit(command: Readonly>): number { + const commandId = command.commandId as string; + const prior = this.acceptedCommands.get(commandId); + if (prior !== undefined) return prior; + const type = command.type as MutationType; + if (type === "project.create") { + const projectId = command.projectId as string; + this.projects.set(projectId, { + ...this.project(projectId), + title: command.title, + workspaceRoot: command.workspaceRoot, + defaultModelSelection: command.defaultModelSelection, + createdAt: command.createdAt, + updatedAt: command.createdAt, + }); + } else if (type === "thread.create") { + const threadId = command.threadId as string; + this.threads.set(threadId, { + id: threadId, + projectId: command.projectId, + title: command.title, + modelSelection: command.modelSelection, + runtimeMode: command.runtimeMode, + interactionMode: command.interactionMode, + branch: command.branch, + worktreePath: command.worktreePath, + createdAt: command.createdAt, + updatedAt: command.createdAt, + }); + this.messages.set(threadId, []); + } else { + const threadId = command.threadId as string; + const message = command.message as Record; + const rows = this.messages.get(threadId) ?? []; + if (!rows.some((entry) => entry.id === message.messageId)) { + rows.push(this.userMessage(message.messageId as string, message.text as string)); + } + this.messages.set(threadId, rows); + } + this.sequence += 1; + this.acceptedCommands.set(commandId, this.sequence); + return this.sequence; + } + + readonly fetch = async ( + input: string | URL | Request, + init?: RequestInit, + ): Promise => { + const request = new Request(input, init); + const path = new URL(request.url).pathname; + if (path === "/.well-known/t3/environment") { + this.descriptorReads += 1; + return Response.json( + descriptor(this.options.environmentForRead?.(this.descriptorReads) ?? "env-1"), + ); + } + if (path === "/api/orchestration/shell") { + this.shellReads += 1; + return (await this.options.onShell?.(this.shellReads, this)) ?? this.shellResponse(); + } + if (path.startsWith("/api/orchestration/threads/")) { + this.threadReads += 1; + const threadId = decodeURIComponent(path.slice("/api/orchestration/threads/".length)); + return ( + (await this.options.onThread?.(this.threadReads, threadId, this)) ?? + this.detailResponse(threadId) + ); + } + if (path === "/api/orchestration/dispatch") { + const command = JSON.parse(await request.text()) as Readonly>; + const type = command.type as MutationType; + const attempt = (this.dispatchAttempts.get(type) ?? 0) + 1; + this.dispatchAttempts.set(type, attempt); + (this.commands as Readonly>[]).push(command); + const decision = this.options.onDispatch?.(command, attempt, this); + const sequence = decision?.commit === false + ? this.sequence + : this.commit(command); + if (decision?.error !== undefined) throw decision.error; + return decision?.response ?? Response.json({ sequence }); + } + throw new Error(`unexpected route ${request.method} ${path}`); + }; +} + +function runtimeFor( + fixture: StockFixture, + allocatedIds: readonly string[] = [], + extra: Readonly> = {}, +) { + return createStockT3NativeRuntime({ + baseUrl: "http://stock.invalid", + fetch: fixture.fetch, + id: ids(...allocatedIds), + now: () => iso, + ...extra, + }); +} + +describe("round 9 caller-held project creation identity", () => { + test("a mismatched caller-held project payload fails before mutation", async () => { + const fixture = new StockFixture(); + const runtime = runtimeFor(fixture); + await expect(runtime.spawn({ + ...spawnInput, + projectCreateIdentity: { + ...projectIdentity, + workspaceRoot: "/tmp/different-project", + }, + }, { maxReconciliationReads: 1 })).rejects.toMatchObject({ + code: "identity_conflict", + evidence: { + reason: "invalid_project_create_identity", + detail: "workspace_root_mismatch", + }, + }); + expect(fixture.dispatchAttempts.get("project.create") ?? 0).toBe(0); + runtime.close(); + }); + + test("a recreated runtime reuses one projection-lagged project identity", async () => { + const fixture = new StockFixture({ + onShell: (read, stock) => + read <= 3 ? stock.shellResponse([], []) : undefined, + }); + const runtimeA = runtimeFor(fixture); + const first = await runtimeA + .spawn(spawnInput, { maxReconciliationReads: 1 }) + .catch((error) => error); + expect(first).toMatchObject({ + evidence: { + provisionalProjectId: projectIdentity.projectId, + projectAttempt: { + environmentId: "env-1", + ...projectIdentity, + acceptedSequence: 1, + }, + }, + }); + runtimeA.close(); + + const runtimeB = runtimeFor(fixture, [ + "create-1", + "thread-1", + "turn-1", + "message-1", + "lease-1", + ]); + const recovered = await runtimeB.spawn(spawnInput, { maxReconciliationReads: 1 }); + expect(recovered).toMatchObject({ kind: "spawned", agentRef: { threadId: "thread-1" } }); + if (recovered.kind === "spawned") runtimeB.releaseReceipt(recovered.turnReceipt); + const commands = fixture.commands.filter((entry) => entry.type === "project.create"); + expect(commands).toHaveLength(2); + expect(commands[1]).toEqual(commands[0]); + expect([...fixture.projects.keys()]).toEqual([projectIdentity.projectId]); + runtimeB.close(); + }); + + test("two identity-free runtimes fail before racing distinct project mutations", async () => { + const bothShellReads = deferred(); + let initialReads = 0; + const fixture = new StockFixture({ + onShell: async (_read, stock) => { + const empty = stock.shellResponse([], []); + initialReads += 1; + if (initialReads === 2) bothShellReads.resolve(); + await bothShellReads.promise; + return empty; + }, + }); + const withoutIdentity = { ...spawnInput, projectCreateIdentity: undefined }; + const left = runtimeFor(fixture, ["project-left", "command-left"]); + const right = runtimeFor(fixture, ["project-right", "command-right"]); + const outcomes = await Promise.all([ + left.spawn(withoutIdentity, { maxReconciliationReads: 1 }).catch((error) => error), + right.spawn(withoutIdentity, { maxReconciliationReads: 1 }).catch((error) => error), + ]); + for (const outcome of outcomes) { + expect(outcome).toMatchObject({ + code: "identity_conflict", + evidence: { reason: "project_create_identity_required" }, + }); + } + expect(fixture.dispatchAttempts.get("project.create") ?? 0).toBe(0); + expect(fixture.projects.size).toBe(0); + left.close(); + right.close(); + }); + + test("two runtimes sharing one caller-held identity converge on one durable project", async () => { + const bothShellReads = deferred(); + let initialReads = 0; + const fixture = new StockFixture({ + onShell: async (read, stock) => { + if (read > 2) return stock.shellResponse(); + const empty = stock.shellResponse([], []); + initialReads += 1; + if (initialReads === 2) bothShellReads.resolve(); + await bothShellReads.promise; + return empty; + }, + }); + const left = runtimeFor(fixture, [ + "create-left", + "thread-left", + "turn-left", + "message-left", + "lease-left", + ]); + const right = runtimeFor(fixture, [ + "create-right", + "thread-right", + "turn-right", + "message-right", + "lease-right", + ]); + const results = await Promise.all([ + left.spawn(spawnInput, { maxReconciliationReads: 2 }), + right.spawn(spawnInput, { maxReconciliationReads: 2 }), + ]); + expect(results.every((entry) => entry.kind === "spawned")).toBe(true); + const commands = fixture.commands.filter((entry) => entry.type === "project.create"); + expect(commands).toHaveLength(2); + expect(commands[1]).toEqual(commands[0]); + expect([...fixture.projects.keys()]).toEqual([projectIdentity.projectId]); + for (const result of results) { + if (result.kind === "spawned") { + (result.agentRef.threadId === "thread-left" ? left : right).releaseReceipt( + result.turnReceipt, + ); + } + } + left.close(); + right.close(); + }); + + test("environment roll rejects scoped old evidence and reuses the unscoped caller identity", async () => { + const fixture = new StockFixture({ + environmentForRead: (read) => (read === 1 ? "env-1" : "env-2"), + onShell: (read, stock) => + read <= 3 ? stock.shellResponse([], []) : undefined, + }); + const runtimeA = runtimeFor(fixture); + const first = await runtimeA + .spawn(spawnInput, { maxReconciliationReads: 1 }) + .catch((error) => error); + expect(first).toMatchObject({ + evidence: { projectAttempt: { environmentId: "env-1", ...projectIdentity } }, + }); + runtimeA.close(); + + const runtimeB = runtimeFor(fixture, [ + "create-1", + "thread-1", + "turn-1", + "message-1", + "lease-1", + ]); + const oldScopedInput = { + ...spawnInput, + projectCreateIdentity: { ...projectIdentity, environmentId: "env-1" }, + }; + await expect(runtimeB.spawn(oldScopedInput, { maxReconciliationReads: 1 })).rejects.toMatchObject({ + code: "environment_changed", + evidence: { expectedEnvironmentId: "env-1", actualEnvironmentId: "env-2" }, + }); + expect(fixture.dispatchAttempts.get("project.create")).toBe(1); + + const recovered = await runtimeB.spawn(spawnInput, { maxReconciliationReads: 1 }); + expect(recovered).toMatchObject({ + kind: "spawned", + agentRef: { environmentId: "env-2", threadId: "thread-1" }, + }); + if (recovered.kind === "spawned") runtimeB.releaseReceipt(recovered.turnReceipt); + const commands = fixture.commands.filter((entry) => entry.type === "project.create"); + expect(commands).toHaveLength(2); + expect(commands[1]).toEqual(commands[0]); + expect([...fixture.projects.keys()]).toEqual([projectIdentity.projectId]); + runtimeB.close(); + }); +}); + +describe("round 9 terminal initial-turn evidence", () => { + test.each([ + ["superseded", (stock: StockFixture) => [stock.userMessage("foreign", "foreign")]], + [ + "concurrent_writer", + (stock: StockFixture) => [ + stock.userMessage("message-1", "initial"), + stock.userMessage("foreign", "foreign"), + ], + ], + ["causality_unverifiable", (stock: StockFixture) => [stock.userMessage("message-1", "rewritten")]], + ] as const)( + "accepted initial turn retains a released receipt for %s", + async (classification, projectedMessages) => { + const fixture = new StockFixture({ + onThread: (read, threadId, stock) => + read >= 3 + ? stock.detailResponse(threadId, { messages: projectedMessages(stock) }) + : undefined, + }); + fixture.seedProject(); + const runtime = runtimeFor(fixture, [ + "create-1", + "thread-1", + "turn-1", + "message-1", + "lease-1", + ]); + const result = await runtime.spawn( + existingProjectInput, + { maxReconciliationReads: 1 }, + ); + expect(result).toMatchObject({ + kind: "partial", + initialTurn: { + state: classification, + leaseExpiresAt: null, + safeAction: "observe", + turnReceipt: { + commandId: "turn-1", + messageId: "message-1", + acceptedSequence: 2, + leaseState: "released", + }, + }, + }); + if (result.kind !== "partial" || result.initialTurn.turnReceipt === null) { + throw new Error("expected evidence-bearing partial"); + } + await expect(runtime.wait(result.initialTurn.turnReceipt)).rejects.toMatchObject({ + code: "receipt_expired", + }); + runtime.close(); + }, + ); + + test.each([ + ["superseded", (stock: StockFixture) => [stock.userMessage("foreign", "foreign")]], + [ + "concurrent_writer", + (stock: StockFixture) => [ + stock.userMessage("message-1", "initial"), + stock.userMessage("foreign", "foreign"), + ], + ], + ["causality_unverifiable", (stock: StockFixture) => [stock.userMessage("message-1", "rewritten")]], + ] as const)( + "ambiguous initial turn retains a released unknown-sequence receipt for %s", + async (classification, projectedMessages) => { + const fixture = new StockFixture({ + onThread: (read, threadId, stock) => + read >= 3 + ? stock.detailResponse(threadId, { messages: projectedMessages(stock) }) + : undefined, + onDispatch: (command) => + command.type === "thread.turn.start" + ? { commit: true, error: new Error("response lost") } + : undefined, + }); + fixture.seedProject(); + const runtime = runtimeFor(fixture, [ + "create-1", + "thread-1", + "turn-1", + "message-1", + "lease-1", + ]); + const result = await runtime.spawn( + existingProjectInput, + { maxReconciliationReads: 1 }, + ); + expect(result).toMatchObject({ + kind: "partial", + initialTurn: { + state: classification, + turnReceipt: { + commandId: "turn-1", + messageId: "message-1", + acceptedSequence: null, + leaseState: "released", + }, + }, + }); + expect(fixture.dispatchAttempts.get("thread.turn.start")).toBe(1); + runtime.close(); + }, + ); +}); + +describe("round 9 environment-scoped turn admission", () => { + test("a stale colliding ref cannot reserve or clear the stable environment slot", async () => { + const oldDescriptorStarted = deferred(); + const releaseOldDescriptor = deferred(); + const fixture = new StockFixture({ + environmentForRead: () => "env-2", + }); + fixture.seedProject(); + fixture.seedThread("shared-thread"); + const originalFetch = fixture.fetch; + let oldDescriptorGated = false; + const gatedFetch = async (input: string | URL | Request, init?: RequestInit) => { + const request = new Request(input, init); + const path = new URL(request.url).pathname; + if ( + path === "/.well-known/t3/environment" && + fixture.descriptorReads === 1 && + !oldDescriptorGated + ) { + oldDescriptorGated = true; + oldDescriptorStarted.resolve(); + await releaseOldDescriptor.promise; + } + return originalFetch(request); + }; + const runtime = createStockT3NativeRuntime({ + baseUrl: "http://stock.invalid", + fetch: gatedFetch, + id: ids("command-fresh", "message-fresh", "lease-fresh"), + now: () => iso, + }); + const freshRef = { environmentId: "env-2", threadId: "shared-thread" }; + const staleRef = { environmentId: "env-1", threadId: "shared-thread" }; + await runtime.observe(freshRef); + + const stale = runtime.send(staleRef, "stale").then( + (receipt) => ({ kind: "fulfilled" as const, receipt }), + (error) => ({ kind: "rejected" as const, error }), + ); + await oldDescriptorStarted.promise; + const fresh = await runtime.send(freshRef, "fresh").then( + (receipt) => ({ kind: "fulfilled" as const, receipt }), + (error) => ({ kind: "rejected" as const, error }), + ); + releaseOldDescriptor.resolve(); + const staleOutcome = await stale; + + expect(fresh).toMatchObject({ + kind: "fulfilled", + receipt: { + agentRef: freshRef, + messageId: "message-fresh", + acceptedSequence: 1, + leaseState: "active", + }, + }); + expect(staleOutcome).toMatchObject({ + kind: "rejected", + error: { code: "environment_changed" }, + }); + expect(fixture.commands.filter((entry) => entry.type === "thread.turn.start")).toHaveLength(1); + if (fresh.kind === "fulfilled") runtime.releaseReceipt(fresh.receipt as TurnReceipt); + runtime.close(); + }); + + test("the injected runtime clock governs the internally constructed HTTP client", async () => { + const fixture = new StockFixture(); + fixture.seedProject(); + const runtime = runtimeFor( + fixture, + ["create-1", "thread-1", "turn-1", "message-1", "lease-1"], + { clock: () => 100 }, + ); + const result = await runtime.spawn( + existingProjectInput, + { deadlineMs: 200, maxReconciliationReads: 1 }, + ); + expect(result.kind).toBe("spawned"); + expect(fixture.descriptorReads).toBe(1); + if (result.kind === "spawned") runtime.releaseReceipt(result.turnReceipt); + runtime.close(); + }); +}); diff --git a/test/stock-only-gate.test.ts b/test/stock-only-gate.test.ts new file mode 100644 index 0000000..c78d9cd --- /dev/null +++ b/test/stock-only-gate.test.ts @@ -0,0 +1,93 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const roots: string[] = []; +afterEach(async () => { + await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); +}); + +async function run(command: string[], cwd: string, env: Record = {}) { + const process = Bun.spawn(command, { + cwd, + env: { ...Bun.env, ...env }, + stdout: "pipe", + stderr: "pipe", + }); + return { + exitCode: await process.exited, + stdout: await new Response(process.stdout).text(), + stderr: await new Response(process.stderr).text(), + }; +} + +async function fixtureRepo(files: Record) { + const root = await mkdtemp(join(tmpdir(), "t3layer-stock-gate.")); + roots.push(root); + await run(["git", "init", "-q"], root); + for (const [path, content] of Object.entries(files)) { + await Bun.write(join(root, path), content); + } + await run(["git", "add", "."], root); + return root; +} + +describe("stock-only gate", () => { + test("passes a clean tracked candidate when optional scripts is absent", async () => { + const root = await fixtureRepo({ + "package.json": "{}\n", + "src/index.ts": "export const transport = 'stock-http-v1';\n", + "README.md": "stock HTTP\n", + "historical.test.ts": "historical evidence\n", + }); + const historical = join(root, "historical.test.ts"); + const digest = new Bun.CryptoHasher("sha256").update(await Bun.file(historical).arrayBuffer()).digest("hex"); + const result = await run( + ["bash", join(import.meta.dir, "../scripts/check-stock-only.sh")], + root, + { STOCK_ONLY_HISTORICAL_PATH: historical, STOCK_ONLY_HISTORICAL_SHA256: digest }, + ); + + expect(result).toMatchObject({ exitCode: 0 }); + expect(result.stdout).toContain("STOCK_ONLY_CHECK: PASS"); + }); + + test("fails on an injected tracked private reference", async () => { + const forbidden = ["@t3tools", "runtime-client"].join("/"); + const root = await fixtureRepo({ + "package.json": "{}\n", + "src/index.ts": `export const forbidden = ${JSON.stringify(forbidden)};\n`, + "historical.test.ts": "historical evidence\n", + }); + const historical = join(root, "historical.test.ts"); + const digest = new Bun.CryptoHasher("sha256").update(await Bun.file(historical).arrayBuffer()).digest("hex"); + const result = await run( + ["bash", join(import.meta.dir, "../scripts/check-stock-only.sh")], + root, + { STOCK_ONLY_HISTORICAL_PATH: historical, STOCK_ONLY_HISTORICAL_SHA256: digest }, + ); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("forbidden candidate reference"); + }); + + test("fails on an injected untracked private reference in the intended artifact set", async () => { + const forbidden = ["@t3tools", "runtime-client"].join("/"); + const root = await fixtureRepo({ + "package.json": "{}\n", + "historical.test.ts": "historical evidence\n", + }); + await Bun.write(join(root, "src/untracked.ts"), `export const forbidden = ${JSON.stringify(forbidden)};\n`); + const historical = join(root, "historical.test.ts"); + const digest = new Bun.CryptoHasher("sha256").update(await Bun.file(historical).arrayBuffer()).digest("hex"); + const result = await run( + ["bash", join(import.meta.dir, "../scripts/check-stock-only.sh")], + root, + { STOCK_ONLY_HISTORICAL_PATH: historical, STOCK_ONLY_HISTORICAL_SHA256: digest }, + ); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("forbidden candidate reference"); + }); +}); diff --git a/test/stock-t3-contracts.test.ts b/test/stock-t3-contracts.test.ts new file mode 100644 index 0000000..b45c6f8 --- /dev/null +++ b/test/stock-t3-contracts.test.ts @@ -0,0 +1,172 @@ +import { describe, expect, test } from "bun:test"; + +import { + ProtocolMismatchError, + decodeDescriptor, + decodeDispatchError, + decodeDispatchResult, + decodeShellSnapshot, + decodeThreadDetailSnapshot, +} from "../src/stockT3Contracts"; + +const iso = "2026-07-31T18:00:00.000Z"; +const modelSelection = { instanceId: "claudeAgent", model: "claude-opus-5" }; +const session = { + threadId: "thread-1", + status: "ready", + providerName: "claudeAgent", + activeTurnId: null, + lastError: null, + updatedAt: iso, +}; +const latestTurn = { + turnId: "turn-1", + state: "completed", + requestedAt: iso, + startedAt: iso, + completedAt: iso, + assistantMessageId: "assistant-1", +}; +const threadBase = { + id: "thread-1", + projectId: "project-1", + title: "proof", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + latestTurn, + createdAt: iso, + updatedAt: iso, + archivedAt: null, + settledOverride: null, + settledAt: null, + deletedAt: null, + session, +}; + +describe("stock T3 narrow contracts", () => { + test("decodes the pinned descriptor and tolerates additive fields", () => { + expect( + decodeDescriptor({ + environmentId: "env-1", + label: "local", + platform: { os: "darwin", arch: "arm64", future: true }, + serverVersion: "0.0.0-stock", + capabilities: { repositoryIdentity: true, future: "ok" }, + additive: { accepted: true }, + }), + ).toEqual({ + environmentId: "env-1", + label: "local", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "0.0.0-stock", + capabilities: { repositoryIdentity: true }, + }); + }); + + test("fails closed on missing or wrong required descriptor fields", () => { + expect(() => + decodeDescriptor({ + environmentId: "env-1", + label: "local", + platform: { os: "darwin", arch: 64 }, + serverVersion: "stock", + capabilities: {}, + }), + ).toThrow(ProtocolMismatchError); + }); + + test("decodes shell pending state and detail terminal evidence", () => { + const shell = decodeShellSnapshot({ + snapshotSequence: 11, + projects: [ + { + id: "project-1", + title: "project", + workspaceRoot: "/tmp/workspace", + defaultModelSelection: modelSelection, + scripts: [], + createdAt: iso, + updatedAt: iso, + }, + ], + threads: [ + { + ...threadBase, + latestUserMessageAt: iso, + hasPendingApprovals: false, + hasPendingUserInput: true, + hasActionableProposedPlan: false, + }, + ], + updatedAt: iso, + future: true, + }); + const detail = decodeThreadDetailSnapshot({ + snapshotSequence: 11, + thread: { + ...threadBase, + messages: [ + { + id: "message-1", + role: "user", + text: "hello", + attachments: [], + turnId: null, + streaming: false, + createdAt: iso, + updatedAt: iso, + }, + { + id: "assistant-1", + role: "assistant", + text: "done", + attachments: [], + turnId: "turn-1", + streaming: false, + createdAt: iso, + updatedAt: iso, + }, + ], + proposedPlans: [], + activities: [], + checkpoints: [], + }, + }); + + expect(shell.threads[0]?.hasPendingUserInput).toBe(true); + expect(detail.thread.messages[1]?.turnId).toBe("turn-1"); + }); + + test("rejects negative or regressing sequence anchors", () => { + expect(() => decodeDispatchResult({ sequence: -1 })).toThrow(ProtocolMismatchError); + expect(() => + decodeShellSnapshot( + { snapshotSequence: 4, projects: [], threads: [], updatedAt: iso }, + { minimumSequence: 5 }, + ), + ).toThrow(ProtocolMismatchError); + }); + + test.each([ + [400, { code: "invalid_request", reason: "invalid_command" }, "command_rejected"], + [401, { code: "auth_invalid", reason: "invalid_credential" }, "authentication_failed"], + [403, { code: "insufficient_scope", requiredScope: "orchestration:operate" }, "permission_denied"], + [500, { code: "internal_error", reason: "orchestration_dispatch_failed" }, "server_internal"], + ] as const)("decodes exact stock dispatch error %i", (status, body, errorClass) => { + expect(decodeDispatchError(status, { ...body, traceId: "trace-secret" })).toMatchObject({ + status, + class: errorClass, + code: body.code, + reason: "reason" in body ? body.reason : null, + }); + }); + + test("does not invent a 409 dispatch outcome", () => { + expect(() => decodeDispatchError(409, { code: "conflict" })).toThrow( + ProtocolMismatchError, + ); + }); +}); diff --git a/test/stock-t3-exact-stock-negative.test.ts b/test/stock-t3-exact-stock-negative.test.ts new file mode 100644 index 0000000..da5f339 --- /dev/null +++ b/test/stock-t3-exact-stock-negative.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, test } from "bun:test"; +import { join } from "node:path"; + +const exactTree = Bun.env.T3_STOCK_EXACT_TREE; +const exactToolchain = Bun.env.T3_STOCK_EXACT_TOOLCHAIN; + +describe("exact-stock characterization driver", () => { + test("pins the adopted SHA, generates only inside the worktree, and invokes the literal stock runner", async () => { + const source = await Bun.file( + join(import.meta.dir, "../scripts/stock-t3-exact-characterization.sh"), + ).text(); + expect(source).toContain("d3037064e61a9f059eafbd4f9869679779bd2a7c"); + expect(source).toContain( + "corepack pnpm --filter t3 exec vp test run src/orchestration/Layers/T3LayerStockProjectionCharacterization.generated.test.ts", + ); + expect(source).toContain("trap cleanup EXIT INT TERM"); + expect(source).toContain('rm -f -- "$generated_path"'); + }); + + test.skipIf(exactTree === undefined || exactToolchain === undefined)( + "executes the generated fixture at the pinned stock SHA (set T3_STOCK_EXACT_TREE and T3_STOCK_EXACT_TOOLCHAIN)", + async () => { + const child = Bun.spawn( + ["bash", "scripts/stock-t3-exact-characterization.sh", exactTree!], + { + cwd: join(import.meta.dir, ".."), + env: { + ...Bun.env, + PATH: `${exactToolchain}:${Bun.env.PATH ?? ""}`, + }, + stdout: "pipe", + stderr: "pipe", + }, + ); + const [exitCode, stdout, stderr] = await Promise.all([ + child.exited, + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]); + expect(exitCode, `${stdout}\n${stderr}`).toBe(0); + }, + 120_000, + ); +}); diff --git a/test/stock-t3-http-client.test.ts b/test/stock-t3-http-client.test.ts new file mode 100644 index 0000000..8770a9a --- /dev/null +++ b/test/stock-t3-http-client.test.ts @@ -0,0 +1,243 @@ +import { describe, expect, test } from "bun:test"; + +import { + StockT3HttpError, + createStockT3HttpClient, +} from "../src/stockT3HttpClient"; + +const descriptor = { + environmentId: "env-1", + label: "local", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "stock", + capabilities: { repositoryIdentity: false }, +}; + +describe("stock T3 HTTP client", () => { + test("records redacted endpoint/status traces and actual request counters", async () => { + const client = createStockT3HttpClient({ + baseUrl: "http://stock.invalid", + bearerToken: "must-not-appear", + fetch: async (input, init) => { + expect(new Headers(init?.headers).get("authorization")).toBeNull(); + return Response.json(descriptor, { status: 200 }); + }, + }); + + await client.getDescriptor(); + + expect(client.observations()).toEqual({ + requestCount: 1, + inFlight: 0, + peakInFlight: 1, + endpointStatusTrace: [ + { method: "GET", path: "/.well-known/t3/environment", status: 200 }, + ], + }); + expect(JSON.stringify(client.observations())).not.toContain("must-not-appear"); + }); + test("uses only descriptor, shell, detail, dispatch, and token HTTP routes", async () => { + const requests: Request[] = []; + const fetch = async (input: string | URL | Request, init?: RequestInit) => { + const request = new Request(input, init); + requests.push(request); + if (request.url.endsWith("/.well-known/t3/environment")) { + return Response.json(descriptor); + } + if (request.url.endsWith("/api/orchestration/shell")) { + return Response.json({ + snapshotSequence: 0, + projects: [], + threads: [], + updatedAt: "2026-07-31T18:00:00.000Z", + }); + } + if (request.url.endsWith("/api/orchestration/threads/thread%2Fone")) { + return new Response( + JSON.stringify({ code: "not_found", reason: "thread_not_found", traceId: "x" }), + { status: 404, headers: { "content-type": "application/json" } }, + ); + } + if (request.url.endsWith("/api/orchestration/dispatch")) { + return Response.json({ sequence: 9 }); + } + if (request.url.endsWith("/oauth/token")) { + return Response.json({ accessToken: "short-lived", tokenType: "Bearer", expiresIn: 60 }); + } + throw new Error(`unexpected route ${request.url}`); + }; + const client = createStockT3HttpClient({ + baseUrl: "http://127.0.0.1:3774///", + bearerToken: "bearer-secret", + fetch, + }); + + await client.getDescriptor(); + await client.getShell(); + expect(await client.getThread("thread/one")).toBeUndefined(); + expect(await client.dispatch({ type: "thread.delete", commandId: "c", threadId: "t" })).toEqual({ sequence: 9 }); + expect(await client.exchangeToken({ grantType: "pairing", credential: "bootstrap-secret" })).toEqual({ + accessToken: "short-lived", + tokenType: "Bearer", + expiresIn: 60, + }); + + expect(requests.map((request) => new URL(request.url).pathname)).toEqual([ + "/.well-known/t3/environment", + "/api/orchestration/shell", + "/api/orchestration/threads/thread%2Fone", + "/api/orchestration/dispatch", + "/oauth/token", + ]); + expect(requests[0]?.headers.get("authorization")).toBeNull(); + expect(requests[1]?.headers.get("authorization")).toBe("Bearer bearer-secret"); + }); + + test("sanitizes bearer values and response bodies from typed failures", async () => { + const fetch = async () => + new Response( + JSON.stringify({ + code: "internal_error", + reason: "orchestration_dispatch_failed", + traceId: "bearer-secret response-secret", + }), + { status: 500, headers: { "content-type": "application/json" } }, + ); + const client = createStockT3HttpClient({ + baseUrl: "http://127.0.0.1:3774", + bearerToken: "bearer-secret", + fetch, + }); + + const error = await client.dispatch({ type: "thread.delete", commandId: "c", threadId: "t" }).catch((cause) => cause); + expect(error).toBeInstanceOf(StockT3HttpError); + expect(error.code).toBe("server_internal"); + expect(JSON.stringify(error)).not.toContain("bearer-secret"); + expect(JSON.stringify(error)).not.toContain("response-secret"); + }); + + test.each([ + [401, "authentication_failed"], + [403, "permission_denied"], + [500, "server_internal"], + ] as const)("preserves received snapshot HTTP %i as %s", async (status, code) => { + const client = createStockT3HttpClient({ + baseUrl: "http://127.0.0.1:3774", + bearerToken: "bearer-secret", + fetch: async () => + Response.json( + { code: "redacted", reason: "redacted", traceId: "response-secret" }, + { status }, + ), + }); + + const error = await client.getShell().catch((cause) => cause); + expect(error).toBeInstanceOf(StockT3HttpError); + expect(error).toMatchObject({ code, status }); + expect(JSON.stringify(error)).not.toContain("response-secret"); + }); + + test("preserves Retry-After on typed transient snapshot failures", async () => { + const client = createStockT3HttpClient({ + baseUrl: "http://127.0.0.1:3774", + bearerToken: "bearer-secret", + fetch: async () => + new Response("temporarily unavailable", { + status: 503, + headers: { "retry-after": "3" }, + }), + }); + + const error = await client.getShell().catch((cause) => cause); + expect(error).toBeInstanceOf(StockT3HttpError); + expect(error).toMatchObject({ + code: "transport_unavailable", + status: 503, + detail: { retryAfterMs: 3_000, transient: true }, + }); + }); + + test("fails closed on a successful response with malformed JSON", async () => { + const client = createStockT3HttpClient({ + baseUrl: "http://127.0.0.1:3774", + bearerToken: "bearer-secret", + fetch: async () => new Response("not-json", { status: 200 }), + }); + + await expect(client.getShell()).rejects.toMatchObject({ + code: "protocol_mismatch", + status: 200, + }); + }); + + test("queues the ninth direct request behind the global in-flight cap", async () => { + let active = 0; + let peak = 0; + const client = createStockT3HttpClient({ + baseUrl: "http://127.0.0.1:3774", + fetch: async () => { + active += 1; + peak = Math.max(peak, active); + await new Promise((resolve) => setTimeout(resolve, 10)); + active -= 1; + return Response.json(descriptor); + }, + }); + + await Promise.all(Array.from({ length: 9 }, () => client.getDescriptor())); + expect(peak).toBe(8); + expect(client.observations()).toMatchObject({ peakInFlight: 8, inFlight: 0 }); + }); + + test.each([ + ["local", 251, true], + ["local", 4_999, true], + ["local", 5_001, false], + ["relay", 10_000, true], + ["tunnel", 10_000, true], + ] as const)( + "fake-clock %s profile classifies an actual %ims request", + async (connectionProfile, actualRequestDuration, succeeds) => { + let current = 0; + let scheduledBudget = 0; + let timeoutCallback: (() => void) | undefined; + let active = 0; + let peak = 0; + const client = createStockT3HttpClient({ + baseUrl: "http://127.0.0.1:3774", + bearerToken: "secret", + connectionProfile, + clock: () => current, + setTimer: (callback, milliseconds) => { + timeoutCallback = callback; + scheduledBudget = milliseconds; + return Symbol("fake-timer"); + }, + clearTimer: () => {}, + fetch: async (_input, init) => { + active += 1; + peak = Math.max(peak, active); + current += actualRequestDuration; + if (actualRequestDuration > scheduledBudget) { + timeoutCallback?.(); + active -= 1; + throw init?.signal?.reason ?? new DOMException("aborted", "AbortError"); + } + active -= 1; + return Response.json({ + snapshotSequence: 1, + projects: [], + threads: [], + updatedAt: "2026-07-31T18:00:00.000Z", + }); + }, + }); + + const outcome = await client.getShell({ deadlineMs: 20_000 }).catch((error) => error); + if (succeeds) expect(outcome).toMatchObject({ snapshotSequence: 1 }); + else expect(outcome).toMatchObject({ code: "transport_unavailable" }); + expect(current).toBe(actualRequestDuration); + expect(peak).toBe(1); + }, + ); +}); diff --git a/test/stock-t3-live-harness.test.ts b/test/stock-t3-live-harness.test.ts new file mode 100644 index 0000000..6653791 --- /dev/null +++ b/test/stock-t3-live-harness.test.ts @@ -0,0 +1,446 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { chmod, mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; + +import { + ProofReceiptError, + canonicalProofBody, + canonicalProofJson, + proofChecksum, + validateProofEnvelope, + validateProofReceipt, +} from "../src/stockProof"; + +async function run(command: string[], env: Record = {}) { + const process = Bun.spawn(command, { + cwd: join(import.meta.dir, ".."), + env: { ...Bun.env, ...env }, + stdout: "pipe", + stderr: "pipe", + }); + return { + exitCode: await process.exited, + stdout: await new Response(process.stdout).text(), + stderr: await new Response(process.stderr).text(), + }; +} + +import { join } from "node:path"; + +const temporaryRoots: string[] = []; +afterEach(async () => { + await Promise.all(temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); +}); + +async function canaryFixture() { + const root = await mkdtemp(join(tmpdir(), "t3layer-canary.")); + temporaryRoots.push(root); + const log = join(root, "commands.log"); + const paths: Record = {}; + for (const name of ["off", "canary", "promote", "prior", "readiness", "descriptor", "thread", "cancel"]) { + const path = join(root, name); + const output = name === "descriptor" + ? `echo '{"environmentId":"env-canary","serverVersion":"stock"}'` + : name === "thread" + ? `echo '{"threadId":"existing-thread","readable":true}'` + : name === "cancel" + ? `printf '%s\\n' 'cancel' >> '${log}'\necho '{"cancelled":2,"replayed":0}'` + : `printf '%s\\n' '${name}' >> '${log}'`; + await Bun.write(path, `#!/usr/bin/env bash\nset -euo pipefail\n${output}\n`); + await chmod(path, 0o700); + paths[name] = path; + } + const artifact = join(root, "artifact"); + const config = join(root, "config.json"); + await Bun.write(artifact, "immutable artifact\n"); + await Bun.write(config, '{"schema":"stock-http-v1","acceleration":"off"}\n'); + return { + root, + log, + receipt: join(root, "receipt.json"), + paths, + artifact, + env: { + T3_STOCK_ROUTE_OFF_COMMAND: paths.off!, + T3_STOCK_ROUTE_CANARY_COMMAND: paths.canary!, + T3_STOCK_ROUTE_PROMOTE_COMMAND: paths.promote!, + T3_STOCK_ROUTE_PRIOR_CONFIG_COMMAND: paths.prior!, + T3_STOCK_READINESS_COMMAND: paths.readiness!, + T3_STOCK_DESCRIPTOR_COMMAND: paths.descriptor!, + T3_STOCK_THREAD_READ_COMMAND: paths.thread!, + T3_STOCK_CANCEL_WAITS_COMMAND: paths.cancel!, + T3_STOCK_ARTIFACT_PATH: artifact, + T3_STOCK_CONFIG_PATH: config, + T3_STOCK_DRILL_RECEIPT_PATH: join(root, "receipt.json"), + }, + }; +} + +describe("stock live harness lifecycle", () => { + const completeBody = () => ({ + runId: "current-run", + candidateSha: "a".repeat(40), + stockSha: "d3037064e61a9f059eafbd4f9869679779bd2a7c", + success: true as const, + cleanBeforeBuild: true as const, + artifactDigest: "b".repeat(64), + privateResolution: false as const, + provenance: { + stockInstall: { command: "corepack pnpm install --frozen-lockfile", status: 0 }, + stockBuild: { command: "corepack pnpm --filter t3 build:bundle", status: 0 }, + candidateInstall: { command: "bun install --frozen-lockfile", status: 0 }, + exactCharacterization: { + command: "corepack pnpm --filter t3 exec vp test run src/orchestration/Layers/T3LayerStockProjectionCharacterization.generated.test.ts", + status: 0, + }, + isolatedBasenames: ["stock-tree", "t3layer-clean", "server-home", "workspace"], + }, + exactHttpNegative: { + status: 500, + shellStatus: 200, + detailStatus: 404, + code: "internal_error", + reason: "orchestration_dispatch_failed", + threadAbsent: true as const, + }, + live: { + environmentId: "environment-fixture", + serverVersion: "0.1.0", + endpointStatusTrace: [ + { method: "GET", path: "/.well-known/t3/environment", status: 200 }, + { method: "GET", path: "/api/orchestration/shell", status: 200 }, + { method: "GET", path: "/api/orchestration/shell", status: 200 }, + { method: "GET", path: "/api/orchestration/shell", status: 200 }, + { method: "GET", path: "/api/orchestration/threads/thread-id", status: 200 }, + { method: "GET", path: "/api/orchestration/threads/thread-id", status: 200 }, + { method: "POST", path: "/api/orchestration/dispatch", status: 200 }, + { method: "POST", path: "/api/orchestration/dispatch", status: 200 }, + { method: "POST", path: "/api/orchestration/dispatch", status: 200 }, + ], + ids: { + projectId: "project-id", + threadId: "thread-id", + createCommandId: "create-command-id", + initialCommandId: "initial-command-id", + initialMessageId: "initial-message-id", + followupCommandId: "followup-command-id", + followupMessageId: "followup-message-id", + }, + sequences: { create: 1, initial: 2, followup: 3 }, + counters: { requests: 9, shellPolls: 3, detailPolls: 2, peakInFlight: 1 }, + terminalKinds: ["completed", "completed"], + timestamps: { startedAt: "2026-07-31T00:00:00.000Z", completedAt: "2026-07-31T00:01:00.000Z" }, + }, + teardown: { pidStopped: true as const, worktreeRemoved: true as const, rootRemoved: true as const }, + }); + + test("arms cleanup immediately after the proof root is allocated", async () => { + const source = await Bun.file(join(import.meta.dir, "../scripts/stock-t3-live-harness.sh")).text(); + const lines = source.split("\n").map((line) => line.trim()).filter(Boolean); + const allocation = lines.findIndex((line) => line.startsWith("proof_root=$(mktemp -d")); + expect(allocation).toBeGreaterThan(0); + expect(lines[allocation + 1]).toBe("trap cleanup EXIT INT TERM"); + expect(source).toContain("set -euo pipefail"); + expect(source).not.toContain("pkill"); + expect(source).not.toContain("killall"); + expect(source).toContain('worktree list --porcelain'); + expect(source).toContain('current_cwd=$(/usr/sbin/lsof'); + expect(source).toContain('validate-provisional'); + expect(source).toContain('validate-envelope'); + expect(source).toContain("stat -f '%Lp'"); + expect(source).toContain('staging_bytes=$(shasum -a 256'); + }); + + test("declares fault seams across setup, live execution, and atomic finalization", async () => { + const source = await Bun.file(join(import.meta.dir, "../scripts/stock-t3-live-harness.sh")).text(); + for (const seam of [ + "after-proof-root", "after-worktree-add", "after-stock-install", "after-stock-build", + "after-archive-extract", "after-candidate-install", "after-exact-characterization", + "after-generated-fixture", + "after-bearer-issue", "after-secret-read", "after-server-launch", "after-readiness", + "after-http-negative", "after-live-test", "after-provisional-validation", "before-normal-exit", + "before-final-body-validation", "after-final-body-validation", "after-final-rename", + ]) expect(source).toContain(seam); + }); + + test("test-mode failure after allocation cleans the exact proof root", async () => { + const result = await run(["bash", "scripts/stock-t3-live-harness.sh"], { + T3_STOCK_PROVIDER_SECRET_REF: "op://fixture/provider/key", + T3_STOCK_HARNESS_TEST_MODE: "1", + T3_STOCK_HARNESS_COMMAND_RUNNER: "/usr/bin/true", + T3_STOCK_FAIL_AT: "after-proof-root", + }); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("injected failure: after-proof-root"); + expect(result.stderr).toContain("cleanup root_removed=true"); + expect(result.stderr).not.toContain("op://fixture/provider/key"); + }); + + test("all setup/live fault seams preserve secret redaction and clean the proof root", async () => { + const seams = [ + "after-worktree-add", "after-stock-install", "after-stock-build", "after-archive-extract", + "after-candidate-install", "after-exact-characterization", "after-bearer-issue", + "after-secret-read", "after-server-launch", "after-readiness", "after-http-negative", + "after-live-test", "after-provisional-validation", "before-normal-exit", + ]; + for (const seam of seams) { + const result = await run(["bash", "scripts/stock-t3-live-harness.sh"], { + T3_STOCK_PROVIDER_SECRET_REF: "op://fixture/provider/key", + T3_STOCK_HARNESS_TEST_MODE: "1", + T3_STOCK_HARNESS_COMMAND_RUNNER: "/usr/bin/true", + T3_STOCK_FAIL_AT: seam, + }); + expect(result.exitCode, seam).not.toBe(0); + expect(result.stderr, seam).toContain(`injected failure: ${seam}`); + expect(result.stderr, seam).toContain("cleanup root_removed=true"); + expect(result.stderr, seam).not.toContain("op://fixture/provider/key"); + } + }); + + test("real-path command seam reaches finalization failures without a passing receipt", async () => { + for (const seam of [ + "before-final-body-validation", + "after-final-body-validation", + "after-final-rename", + ]) { + const root = await mkdtemp(join(tmpdir(), "t3layer-harness-finalize.")); + temporaryRoots.push(root); + const target = join(root, "proof.json"); + const result = await run(["bash", "scripts/stock-t3-live-harness.sh"], { + T3_STOCK_PROVIDER_SECRET_REF: "op://fixture/provider/key", + T3_STOCK_HARNESS_TEST_MODE: "1", + T3_STOCK_HARNESS_COMMAND_RUNNER: "/usr/bin/true", + T3_STOCK_PROOF_TARGET: target, + T3_STOCK_FAIL_AT: seam, + }); + expect(result.exitCode, seam).not.toBe(0); + expect(await Bun.file(target).exists(), seam).toBe(false); + expect(result.stderr, seam).not.toContain("op://fixture/provider/key"); + } + }); + + test("requires caller-held runId and candidateSha instead of trusting a stale path", async () => { + const prior = canonicalProofBody({ ...completeBody(), runId: "prior-run" }); + expect(() => + validateProofReceipt(prior, { runId: "current-run", candidateSha: "b".repeat(40) }), + ).toThrow(ProofReceiptError); + expect( + validateProofReceipt(prior, { runId: "prior-run", candidateSha: "a".repeat(40) }), + ).toEqual(prior); + }); + + test("requires every behavioral/provenance field and verifies canonical envelope bytes", async () => { + const body = completeBody(); + expect(() => canonicalProofBody({ ...body, live: { ...body.live, endpointStatusTrace: [] } })).toThrow(ProofReceiptError); + expect(() => canonicalProofBody({ ...body, provenance: undefined })).toThrow(ProofReceiptError); + const checksum = await proofChecksum(body); + const envelope = { ...body, checksum }; + expect(await validateProofEnvelope(envelope, { runId: body.runId, candidateSha: body.candidateSha })).toEqual(canonicalProofBody(body)); + expect(canonicalProofJson(body)).toEndWith("\n"); + await expect(validateProofEnvelope({ ...envelope, checksum: "0".repeat(64) }, { runId: body.runId, candidateSha: body.candidateSha })).rejects.toThrow(ProofReceiptError); + }); + + test("rejects all 18 proof-forgery classes while accepting the valid control", () => { + const body = completeBody(); + const withoutOneDispatch = body.live.endpointStatusTrace.filter( + (_, index) => index !== body.live.endpointStatusTrace.length - 1, + ); + const forgeries = [ + () => canonicalProofBody({ ...body, stockSha: "f".repeat(40) }), + () => canonicalProofBody({ + ...body, + provenance: { + ...body.provenance, + stockInstall: { command: "echo forged", status: 0 }, + }, + }), + () => canonicalProofBody({ + ...body, + provenance: { + ...body.provenance, + isolatedBasenames: ["t3layer-clean", "stock-tree", "server-home", "workspace"], + }, + }), + () => canonicalProofBody({ + ...body, + live: { + ...body.live, + endpointStatusTrace: [body.live.endpointStatusTrace[0]], + counters: { ...body.live.counters, requests: 1 }, + }, + }), + () => canonicalProofBody({ + ...body, + live: { + ...body.live, + endpointStatusTrace: withoutOneDispatch, + counters: { ...body.live.counters, requests: withoutOneDispatch.length }, + }, + }), + () => canonicalProofBody({ + ...body, + live: { ...body.live, counters: { ...body.live.counters, shellPolls: 0 } }, + }), + () => canonicalProofBody({ + ...body, + live: { ...body.live, counters: { ...body.live.counters, shellPolls: 4 } }, + }), + () => canonicalProofBody({ + ...body, + live: { ...body.live, counters: { ...body.live.counters, requests: 8 } }, + }), + () => canonicalProofBody({ + ...body, + live: { ...body.live, counters: { ...body.live.counters, peakInFlight: 9 } }, + }), + () => canonicalProofBody({ + ...body, + live: { + ...body.live, + ids: { ...body.live.ids, followupMessageId: body.live.ids.initialMessageId }, + }, + }), + () => canonicalProofBody({ + ...body, + live: { + ...body.live, + endpointStatusTrace: body.live.endpointStatusTrace.map((entry, index) => + index === 0 ? { ...entry, path: "/private/orchestration" } : entry, + ), + }, + }), + () => canonicalProofBody({ ...body, success: false }), + () => canonicalProofBody({ ...body, cleanBeforeBuild: false }), + () => canonicalProofBody({ + ...body, + teardown: { ...body.teardown, rootRemoved: false }, + }), + () => canonicalProofBody({ + ...body, + live: { ...body.live, sequences: { create: 1, initial: 1, followup: 3 } }, + }), + () => canonicalProofBody({ + ...body, + live: { ...body.live, serverVersion: "op://fixture/provider/key" }, + }), + () => canonicalProofBody({ + ...body, + exactHttpNegative: { ...body.exactHttpNegative, status: 400 }, + }), + () => validateProofReceipt(body, { + runId: body.runId, + candidateSha: "f".repeat(40), + }), + ]; + expect(validateProofReceipt(body, { + runId: body.runId, + candidateSha: body.candidateSha, + })).toEqual(canonicalProofBody(body)); + expect(forgeries).toHaveLength(18); + for (const forge of forgeries) expect(forge).toThrow(ProofReceiptError); + }); + + test("proof CLI publishes and rereads a canonical current-run envelope", async () => { + const root = await mkdtemp(join(tmpdir(), "t3layer-proof-cli.")); + temporaryRoots.push(root); + const body = completeBody(); + const draft = join(root, "draft.json"); + const receipt = join(root, "receipt.json"); + await Bun.write(draft, JSON.stringify(body)); + await Bun.write(receipt, ""); + await chmod(receipt, 0o600); + const published = await run([ + "bun", "scripts/stock-proof-cli.ts", "publish", draft, receipt, + body.runId, body.candidateSha, + ]); + expect(published.exitCode).toBe(0); + const validated = await run([ + "bun", "scripts/stock-proof-cli.ts", "validate-envelope", receipt, + body.runId, body.candidateSha, + ]); + expect(validated.exitCode).toBe(0); + expect((await Bun.file(receipt).text()).endsWith("\n")).toBe(true); + }); + + test("deploy drill dry-run records the required immutable-artifact transitions", async () => { + const result = await run(["bash", "scripts/stock-t3-canary-drill.sh", "--dry-run"]); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain("off -> canary -> promoted -> canary (prior config) -> off"); + expect(result.stdout).toContain("acceleration=off"); + }); + + test("execute mode records digests, statuses, descriptor health, and thread readability", async () => { + const fixture = await canaryFixture(); + const result = await run(["bash", "scripts/stock-t3-canary-drill.sh", "--execute"], fixture.env); + expect(result.exitCode).toBe(0); + const receipt = await Bun.file(fixture.receipt).json(); + expect(receipt.success).toBe(true); + expect(receipt.configDigestAfter).toBe(receipt.configDigestBefore); + expect(receipt.commandStatuses.length).toBe(16); + expect(receipt.descriptors).toHaveLength(3); + expect(receipt.threadReadability).toHaveLength(3); + expect(receipt.threadReadability.every((entry: { readable: boolean }) => entry.readable)).toBe(true); + expect(receipt.artifactChecks.length).toBeGreaterThan(0); + expect( + receipt.artifactChecks.every( + (entry: { digest: string }) => entry.digest === receipt.artifactDigest, + ), + ).toBe(true); + expect(receipt.cancellation).toEqual({ cancelled: 2, replayed: 0 }); + expect(receipt.checksum).toMatch(/^[0-9a-f]{64}$/); + expect((await Bun.file(fixture.receipt).stat()).mode & 0o777).toBe(0o600); + }); + + test("execute mode rejects artifact drift and environment identity drift", async () => { + const artifactFixture = await canaryFixture(); + await Bun.write( + artifactFixture.paths.readiness!, + `#!/usr/bin/env bash\nprintf '%s\\n' mutation >> '${artifactFixture.artifact}'\n`, + ); + await chmod(artifactFixture.paths.readiness!, 0o700); + const artifactResult = await run( + ["bash", "scripts/stock-t3-canary-drill.sh", "--execute"], + artifactFixture.env, + ); + expect(artifactResult.exitCode).not.toBe(0); + expect(artifactResult.stderr).toContain("artifact drift detected"); + + const identityFixture = await canaryFixture(); + const counter = join(identityFixture.root, "descriptor-count"); + await Bun.write(counter, "0\n"); + await Bun.write( + identityFixture.paths.descriptor!, + `#!/usr/bin/env bash\ncount=$(($(cat '${counter}') + 1))\nprintf '%s\\n' "$count" > '${counter}'\necho "{\\"environmentId\\":\\"env-$count\\",\\"serverVersion\\":\\"stock\\"}"\n`, + ); + await chmod(identityFixture.paths.descriptor!, 0o700); + const identityResult = await run( + ["bash", "scripts/stock-t3-canary-drill.sh", "--execute"], + identityFixture.env, + ); + expect(identityResult.exitCode).not.toBe(0); + expect(identityResult.stderr).toContain("environment identity changed"); + }); + + test("every injected canary transition failure restores prior config and routes off", async () => { + const seams = [ + "route-off", "route-canary", "canary-readiness", "canary-descriptor", "canary-thread", + "route-promote", "promoted-readiness", "promoted-descriptor", "promoted-thread", + "restore-prior-config", "route-prior-canary", "prior-canary-readiness", + "prior-canary-descriptor", "prior-canary-thread", "final-route-off", + "cancel-waits", + ]; + for (const seam of seams) { + const fixture = await canaryFixture(); + const result = await run(["bash", "scripts/stock-t3-canary-drill.sh", "--execute"], { + ...fixture.env, + T3_STOCK_FAIL_AT: seam, + }); + expect(result.exitCode, seam).not.toBe(0); + expect(result.stderr, seam).toContain("CANARY_RECOVERY:"); + const commands = (await Bun.file(fixture.log).text()).trim().split("\n"); + expect(commands.at(-3), seam).toBe("prior"); + expect(commands.at(-2), seam).toBe("off"); + expect(commands.at(-1), seam).toBe("cancel"); + } + }, 60_000); +}); diff --git a/test/stock-t3-live.test.ts b/test/stock-t3-live.test.ts new file mode 100644 index 0000000..fc4d83b --- /dev/null +++ b/test/stock-t3-live.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, test } from "bun:test"; + +import { createStockT3Facade } from "../src/facade"; +import { createStockT3NativeRuntime } from "../src/nativeRuntime"; +import { canonicalProvisionalProof } from "../src/stockProof"; + +const live = Bun.env.T3_STOCK_LIVE === "1"; + +describe.skipIf(!live)("isolated exact-stock live proof", () => { + test("performs receipt-targeted spawn -> wait -> send -> wait", async () => { + const startedAt = new Date().toISOString(); + const baseUrl = Bun.env.T3_STOCK_BASE_URL; + const bearerToken = Bun.env.T3_STOCK_HTTP_TOKEN; + const workspaceRoot = Bun.env.T3_STOCK_WORKSPACE_ROOT; + const receiptPath = Bun.env.T3_STOCK_RECEIPT_PATH; + const runId = Bun.env.T3_STOCK_RUN_ID; + if (!baseUrl || !bearerToken || !workspaceRoot || !receiptPath || !runId) { + throw new Error("live harness contract is incomplete"); + } + const runtime = createStockT3NativeRuntime({ + baseUrl, + bearerToken, + connectionProfile: "local", + }); + const facade = createStockT3Facade(runtime); + const modelSelection = { instanceId: "claudeAgent", model: "claude-sonnet-4-5" }; + const title = `t3layer-stock-proof-${runId.slice(0, 8)}`; + const spawned = await facade.spawn({ + workspaceRoot, + projectCreateIdentity: { + projectId: crypto.randomUUID(), + commandId: crypto.randomUUID(), + createdAt: startedAt, + workspaceRoot, + title, + defaultModelSelection: modelSelection, + }, + title, + message: "Reply with exactly T3LAYER_STOCK_PROOF_OK.", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }); + expect(spawned.kind).toBe("spawned"); + if (spawned.kind !== "spawned") throw new Error("live spawn was partial"); + const first = await facade.wait(spawned.turnReceipt, { timeoutMs: 120_000 }); + const sent = await facade.send( + spawned.agentRef, + "Reply with exactly T3LAYER_STOCK_PROOF_FOLLOWUP_OK.", + { timeoutMs: 120_000 }, + ); + const second = await facade.wait(sent, { timeoutMs: 120_000 }); + const descriptor = await runtime.client.getDescriptor(); + const detail = await runtime.observe(spawned.agentRef, { timeoutMs: 30_000 }); + if (detail === undefined) throw new Error("live thread disappeared before receipt capture"); + const http = runtime.httpObservations(); + const polls = runtime.pollMetrics(); + + const provisional = canonicalProvisionalProof({ + provisional: true, + success: false, + runId, + environmentId: descriptor.environmentId, + serverVersion: descriptor.serverVersion, + endpointStatusTrace: http.endpointStatusTrace, + ids: { + projectId: detail.thread.projectId, + threadId: spawned.agentRef.threadId, + createCommandId: spawned.createReceipt.commandId, + initialCommandId: spawned.turnReceipt.commandId, + initialMessageId: spawned.turnReceipt.messageId, + followupCommandId: sent.commandId, + followupMessageId: sent.messageId, + }, + sequences: { + create: spawned.createReceipt.acceptedSequence, + initial: spawned.turnReceipt.acceptedSequence, + followup: sent.acceptedSequence, + }, + terminalKinds: [first.kind, second.kind], + counters: { + requests: http.requestCount, + shellPolls: polls.shellStarts, + detailPolls: polls.detailStarts, + peakInFlight: Math.max(http.peakInFlight, polls.peakHttpInFlight), + }, + timestamps: { startedAt, completedAt: new Date().toISOString() }, + }, runId); + await Bun.write(receiptPath, `${JSON.stringify(provisional)}\n`); + expect(first.kind).toBe("completed"); + expect(second.kind).toBe("completed"); + }, 120_000); +}); diff --git a/test/stock-t3-sequence.test.ts b/test/stock-t3-sequence.test.ts new file mode 100644 index 0000000..3f13b1f --- /dev/null +++ b/test/stock-t3-sequence.test.ts @@ -0,0 +1,200 @@ +import { afterAll, describe, expect, test } from "bun:test"; + +import { createStockT3Facade } from "../src/facade"; +import { createStockT3NativeRuntime } from "../src/nativeRuntime"; + +const auth = "proof-token"; +const iso = "2026-07-31T18:00:00.000Z"; +const modelSelection = { instanceId: "claudeAgent", model: "claude-opus-5" }; +let sequence = 1; +let turnNumber = 0; +let thread: Record | undefined; +const commandTypes: string[] = []; + +const project = { + id: "project-1", + title: "fixture", + workspaceRoot: "/tmp/t3layer-stock-fixture", + defaultModelSelection: modelSelection, + scripts: [], + createdAt: iso, + updatedAt: iso, +}; + +function shellThread() { + if (thread === undefined) return []; + return [ + { + ...thread, + messages: undefined, + activities: undefined, + checkpoints: undefined, + proposedPlans: undefined, + deletedAt: undefined, + latestUserMessageAt: + thread.messages.filter((entry: any) => entry.role === "user").at(-1)?.createdAt ?? null, + hasPendingApprovals: false, + hasPendingUserInput: false, + hasActionableProposedPlan: false, + }, + ]; +} + +const server = Bun.serve({ + port: 0, + async fetch(request) { + const url = new URL(request.url); + if (url.pathname === "/.well-known/t3/environment") { + return Response.json({ + environmentId: "environment-fixture", + label: "fixture", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "d3037064-fixture", + capabilities: { repositoryIdentity: false }, + }); + } + if (request.headers.get("authorization") !== `Bearer ${auth}`) { + return Response.json( + { code: "auth_invalid", reason: "missing_credential", traceId: "redacted" }, + { status: 401 }, + ); + } + if (url.pathname === "/api/orchestration/shell") { + return Response.json({ + snapshotSequence: sequence, + projects: [project], + threads: shellThread(), + updatedAt: iso, + }); + } + if (url.pathname.startsWith("/api/orchestration/threads/")) { + if (thread === undefined) { + return Response.json( + { code: "not_found", reason: "thread_not_found", traceId: "redacted" }, + { status: 404 }, + ); + } + return Response.json({ snapshotSequence: sequence, thread }); + } + if (url.pathname === "/api/orchestration/dispatch") { + const command = (await request.json()) as Record; + commandTypes.push(command.type); + if (command.type === "thread.create") { + sequence += 1; + thread = { + id: command.threadId, + projectId: command.projectId, + title: command.title, + modelSelection: command.modelSelection, + runtimeMode: command.runtimeMode, + interactionMode: command.interactionMode, + branch: command.branch, + worktreePath: command.worktreePath, + latestTurn: null, + createdAt: iso, + updatedAt: iso, + archivedAt: null, + settledOverride: null, + settledAt: null, + deletedAt: null, + messages: [], + proposedPlans: [], + activities: [], + checkpoints: [], + session: null, + }; + return Response.json({ sequence }); + } + if (command.type === "thread.turn.start" && thread !== undefined) { + turnNumber += 1; + sequence += 2; + const requestedAt = iso.replace("00.000Z", `0${turnNumber}.000Z`); + const turnId = `turn-${turnNumber}`; + const assistantId = `assistant-${turnNumber}`; + thread.messages.push({ + id: command.message.messageId, + role: "user", + text: command.message.text, + attachments: [], + turnId: null, + streaming: false, + createdAt: requestedAt, + updatedAt: requestedAt, + }); + thread.messages.push({ + id: assistantId, + role: "assistant", + text: `completed-${turnNumber}`, + attachments: [], + turnId, + streaming: false, + createdAt: requestedAt, + updatedAt: requestedAt, + }); + thread.latestTurn = { + turnId, + state: "completed", + requestedAt, + startedAt: requestedAt, + completedAt: requestedAt, + assistantMessageId: assistantId, + }; + thread.session = { + threadId: thread.id, + status: "ready", + providerName: "fixture", + activeTurnId: null, + lastError: null, + updatedAt: requestedAt, + }; + return Response.json({ sequence }); + } + return Response.json( + { code: "invalid_request", reason: "invalid_command", traceId: "redacted" }, + { status: 400 }, + ); + } + return new Response("not found", { status: 404 }); + }, +}); + +afterAll(() => server.stop(true)); + +describe("stock HTTP sequence", () => { + test("performs receipt-targeted spawn -> wait -> send -> wait", async () => { + const runtime = createStockT3NativeRuntime({ + baseUrl: `http://127.0.0.1:${server.port}`, + bearerToken: auth, + }); + const facade = createStockT3Facade(runtime); + + const spawned = await facade.spawn({ + workspaceRoot: project.workspaceRoot, + title: "fixture worker", + message: "initial", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + }); + expect(spawned.kind).toBe("spawned"); + if (spawned.kind !== "spawned") throw new Error("spawn did not produce a turn receipt"); + expect(await facade.wait(spawned.turnReceipt)).toMatchObject({ + kind: "completed", + assistantContent: "completed-1", + }); + + const sent = await facade.send(spawned.agentRef, "follow-up"); + expect(await facade.wait(sent)).toMatchObject({ + kind: "completed", + assistantContent: "completed-2", + }); + expect(commandTypes).toEqual([ + "thread.create", + "thread.turn.start", + "thread.turn.start", + ]); + expect(new Set([spawned.createReceipt.commandId, spawned.turnReceipt.commandId, sent.commandId]).size).toBe(3); + }); +}); From ceb8ad6790dc5bba7ecd771ef578e5b76e2b38e1 Mon Sep 17 00:00:00 2001 From: Etan Joseph Heyman Date: Sat, 1 Aug 2026 20:00:50 +0300 Subject: [PATCH 2/5] fix: address PR review findings Co-Authored-By: Claude Opus 5 (1M context) --- scripts/stock-t3-canary-drill.sh | 6 +- scripts/stock-t3-exact-characterization.sh | 5 ++ scripts/stock-t3-live-harness.sh | 31 ++++++++--- src/adaptivePoller.ts | 64 ++++++++++++++++++---- src/nativeRuntime.ts | 10 +++- src/stockT3HttpClient.ts | 31 +++++++---- test/adaptive-poller.test.ts | 6 +- test/boundary-convergence.test.ts | 20 +++++++ test/native-runtime-adapter.test.ts | 28 ++++++++++ test/stock-t3-exact-stock-negative.test.ts | 2 + test/stock-t3-http-client.test.ts | 38 +++++++++++++ test/stock-t3-live-harness.test.ts | 34 +++++++++++- 12 files changed, 236 insertions(+), 39 deletions(-) diff --git a/scripts/stock-t3-canary-drill.sh b/scripts/stock-t3-canary-drill.sh index 9b78367..d074ca0 100644 --- a/scripts/stock-t3-canary-drill.sh +++ b/scripts/stock-t3-canary-drill.sh @@ -117,7 +117,7 @@ trap recover EXIT INT TERM verify_health() { stage=$1 run_step "$stage-readiness" "$T3_STOCK_READINESS_COMMAND" - descriptor=$($T3_STOCK_DESCRIPTOR_COMMAND) + descriptor=$("$T3_STOCK_DESCRIPTOR_COMMAND") if ! /usr/bin/jq -e 'type == "object" and (.environmentId|type == "string" and length > 0) and (.serverVersion|type == "string" and length > 0)' <<<"$descriptor" >/dev/null; then echo "ERROR: invalid descriptor evidence" >&2 return 2 @@ -132,7 +132,7 @@ verify_health() { fi record_status "$stage-descriptor" 0 if [[ ${T3_STOCK_FAIL_AT:-} == "$stage-descriptor" ]]; then return 91; fi - thread=$($T3_STOCK_THREAD_READ_COMMAND) + thread=$("$T3_STOCK_THREAD_READ_COMMAND") if ! /usr/bin/jq -e 'type == "object" and (.threadId|type == "string" and length > 0) and .readable == true' <<<"$thread" >/dev/null; then echo "ERROR: existing thread is not readable" >&2 return 2 @@ -165,7 +165,7 @@ run_step route-prior-canary "$T3_STOCK_ROUTE_CANARY_COMMAND" verify_health prior-canary run_step final-route-off "$T3_STOCK_ROUTE_OFF_COMMAND" cancellation_status=0 -cancellation_evidence=$($T3_STOCK_CANCEL_WAITS_COMMAND) || cancellation_status=$? +cancellation_evidence=$("$T3_STOCK_CANCEL_WAITS_COMMAND") || cancellation_status=$? record_status cancel-waits "$cancellation_status" if [[ "$cancellation_status" -ne 0 ]] || ! /usr/bin/jq -e 'type == "object" and (.cancelled|type == "number" and . >= 0) and .replayed == 0' <<<"$cancellation_evidence" >/dev/null; then echo "ERROR: invalid cancellation/no-replay evidence" >&2 diff --git a/scripts/stock-t3-exact-characterization.sh b/scripts/stock-t3-exact-characterization.sh index d446cb1..bdcbe07 100755 --- a/scripts/stock-t3-exact-characterization.sh +++ b/scripts/stock-t3-exact-characterization.sh @@ -12,6 +12,11 @@ if [[ "$actual_sha" != "$expected_sha" ]]; then exit 2 fi +if [[ -e "$generated_path" ]]; then + echo "ERROR: generated characterization path already exists" >&2 + exit 3 +fi + cleanup() { rm -f -- "$generated_path" } diff --git a/scripts/stock-t3-live-harness.sh b/scripts/stock-t3-live-harness.sh index bab8ea3..8398c63 100755 --- a/scripts/stock-t3-live-harness.sh +++ b/scripts/stock-t3-live-harness.sh @@ -19,8 +19,17 @@ candidate_sha='' artifact_digest='' actual_stock_sha='' run_id='' +finalizer_source='' test_mode=${T3_STOCK_HARNESS_TEST_MODE:-0} +run_finalizer() { + printf '%s' "$finalizer_source" | bun run - "$@" +} + +authenticated_curl() { + printf 'Authorization: Bearer %s\n' "$http_token" | /usr/bin/curl --header @- "$@" +} + run_stage_seam() { stage=$1 if [[ "$test_mode" == 1 ]]; then @@ -72,7 +81,7 @@ cleanup() { fi if [[ "$cleanup_root_valid" == true && -n "$proof_root" && "$proof_root" != / && "$proof_root" != "$HOME" && ! -L "$proof_root" ]]; then rm -rf -- "$proof_root" - root_removed=true + if [[ ! -e "$proof_root" ]]; then root_removed=true; fi fi if [[ "$cleanup_status" -eq 0 && "$proof_ready" == true && "$pid_stopped" == true && "$worktree_removed" == true && "$root_removed" == true ]]; then proof_dir=$(dirname "$proof_target") @@ -95,7 +104,7 @@ cleanup() { echo "ERROR: injected failure: before-final-body-validation" >&2 exit 91 fi - if ! bun "$t3layer_clean/scripts/stock-proof-cli.ts" publish "$final_body_staging" "$final_staging" "$run_id" "$candidate_sha"; then + if ! run_finalizer publish "$final_body_staging" "$final_staging" "$run_id" "$candidate_sha"; then rm -f -- "$final_body_staging" "$final_staging" exit 2 fi @@ -118,7 +127,7 @@ cleanup() { fi chmod 600 "$proof_target" final_bytes=$(shasum -a 256 "$proof_target" | /usr/bin/awk '{print $1}') - if [[ $(/usr/bin/stat -f '%Lp' "$proof_target") != 600 || "$final_bytes" != "$staging_bytes" ]] || ! bun "$t3layer_clean/scripts/stock-proof-cli.ts" validate-envelope "$proof_target" "$run_id" "$candidate_sha"; then + if [[ $(/usr/bin/stat -f '%Lp' "$proof_target") != 600 || "$final_bytes" != "$staging_bytes" ]] || ! run_finalizer validate-envelope "$proof_target" "$run_id" "$candidate_sha"; then rm -f -- "$proof_target" echo "ERROR: final proof bytes, mode, checksum, identity, or teardown mismatch" >&2 exit 2 @@ -191,6 +200,13 @@ if [[ "$test_mode" != 1 ]]; then (cd "$t3layer_clean" && bun install --frozen-lockfile) fi run_stage_seam after-candidate-install +finalizer_bundle="$proof_root/stock-proof-finalizer.mjs" +(cd "$t3layer_clean" && bun build scripts/stock-proof-cli.ts --target=bun --format=esm --outfile "$finalizer_bundle" >/dev/null) +finalizer_source=$(<"$finalizer_bundle") +if [[ -z "$finalizer_source" ]]; then + echo "ERROR: empty stock proof finalizer bundle" >&2 + exit 2 +fi legacy_scope='@t3tools' legacy_name='runtime''-client' @@ -270,10 +286,9 @@ else --arg createdAt "$(date -u '+%Y-%m-%dT%H:%M:%S.000Z')" \ --arg workspace "$workspace" \ '{type:"thread.turn.start",commandId:$commandId,threadId:$threadId,message:{messageId:$messageId,role:"user",text:"negative",attachments:[]},runtimeMode:"full-access",interactionMode:"default",bootstrap:{createThread:{projectId:"00000000-0000-4000-8000-000000000000",title:"negative",modelSelection:{instanceId:"claudeAgent",model:"claude-sonnet-4-5"},runtimeMode:"full-access",interactionMode:"default",branch:null,worktreePath:null,createdAt:$createdAt}},createdAt:$createdAt}') -negative_status=$(/usr/bin/curl --silent --show-error --output "$negative_body" --write-out '%{http_code}' \ +negative_status=$(authenticated_curl --silent --show-error --output "$negative_body" --write-out '%{http_code}' \ --max-time 5 \ --request POST \ - --header "Authorization: Bearer $http_token" \ --header 'Content-Type: application/json' \ --data "$negative_payload" \ http://127.0.0.1:3774/api/orchestration/dispatch) @@ -282,14 +297,12 @@ negative_status=$(/usr/bin/curl --silent --show-error --output "$negative_body" exit 2 fi negative_shell_body="$proof_root/negative-shell.json" -negative_shell_status=$(/usr/bin/curl --silent --show-error --output "$negative_shell_body" --write-out '%{http_code}' \ +negative_shell_status=$(authenticated_curl --silent --show-error --output "$negative_shell_body" --write-out '%{http_code}' \ --max-time 5 \ - --header "Authorization: Bearer $http_token" \ http://127.0.0.1:3774/api/orchestration/shell) negative_detail_body="$proof_root/negative-detail.json" -negative_detail_status=$(/usr/bin/curl --silent --show-error --output "$negative_detail_body" --write-out '%{http_code}' \ +negative_detail_status=$(authenticated_curl --silent --show-error --output "$negative_detail_body" --write-out '%{http_code}' \ --max-time 5 \ - --header "Authorization: Bearer $http_token" \ "http://127.0.0.1:3774/api/orchestration/threads/$negative_thread_id") if [[ "$negative_shell_status" != 200 || "$negative_detail_status" != 404 ]] || /usr/bin/jq -e --arg id "$negative_thread_id" '.threads[]? | select(.id == $id)' "$negative_shell_body" >/dev/null; then echo "ERROR: direct HTTP bootstrap unexpectedly created a thread" >&2 diff --git a/src/adaptivePoller.ts b/src/adaptivePoller.ts index 5f29d8c..25b9997 100644 --- a/src/adaptivePoller.ts +++ b/src/adaptivePoller.ts @@ -7,8 +7,6 @@ const POLICY = Object.freeze({ detailStartsPerWaitMinute: 4, maxActiveWaits: 8, maxHttpInFlight: 8, - firstMinuteAggregateCeiling: 64, - laterMinuteAggregateCeiling: 62, intervalMs(attempt: number): number { return [250, 500, 1_000, 2_000][Math.min(Math.max(0, attempt), 3)] ?? 2_000; }, @@ -162,6 +160,8 @@ export function createAdaptivePoller(options: AdaptivePollerOptions) { let shellStarts = 0; let detailStarts = 0; let throttledCycles = 0; + let aggregateShellStartTimes: number[] = []; + let aggregateFirstStartAt: number | null = null; const slotWaiters: SlotWaiter[] = []; function pumpSlots(): void { @@ -237,14 +237,53 @@ export function createAdaptivePoller(options: AdaptivePollerOptions) { state.shellStartTimes = state.shellStartTimes.filter( (start) => instant - start < 60_000, ); - const firstMinute = + aggregateShellStartTimes = aggregateShellStartTimes.filter( + (start) => instant - start < 60_000, + ); + const stateFirstMinute = state.firstStartAt === null || instant - state.firstStartAt < 60_000; - const cap = firstMinute + const aggregateFirstMinute = + aggregateFirstStartAt === null || instant - aggregateFirstStartAt < 60_000; + const stateCap = stateFirstMinute ? POLICY.firstMinuteShellStarts : POLICY.laterMinuteShellStarts; - if (state.shellStartTimes.length < cap) return 0; + const aggregateCap = aggregateFirstMinute + ? POLICY.firstMinuteShellStarts + : POLICY.laterMinuteShellStarts; + const stateDelay = state.shellStartTimes.length < stateCap + ? 0 + : Math.max(0, 60_000 - (instant - state.shellStartTimes[0]!)); + const aggregateDelay = aggregateShellStartTimes.length < aggregateCap + ? 0 + : Math.max(0, 60_000 - (instant - aggregateShellStartTimes[0]!)); + const delay = Math.max(stateDelay, aggregateDelay); + if (delay === 0) return 0; throttledCycles += 1; - return Math.max(0, 60_000 - (instant - state.shellStartTimes[0]!)); + return delay; + } + + async function reserveShellStart( + state: EnvironmentState, + deadlineMs: number, + ): Promise { + while (!closed && state.subscribers.size > 0) { + const instant = now(); + if (instant >= deadlineMs) return null; + const delay = rateDelay(state, instant); + if (delay === 0) { + if (state.firstStartAt === null) state.firstStartAt = instant; + if (aggregateFirstStartAt === null) aggregateFirstStartAt = instant; + state.shellStartTimes.push(instant); + aggregateShellStartTimes.push(instant); + return instant; + } + await sleep( + Math.min(delay, Math.max(0, deadlineMs - instant)), + state.controller.signal, + ); + expireSubscribers(state); + } + return null; } async function tracked( @@ -401,7 +440,7 @@ export function createAdaptivePoller(options: AdaptivePollerOptions) { const earliestDeadline = Math.min( ...[...state.subscribers.values()].map((entry) => entry.deadlineMs), ); - const desiredDelay = Math.max(nextDelay(state), rateDelay(state, now())); + const desiredDelay = nextDelay(state); const delay = Math.min(desiredDelay, Math.max(0, earliestDeadline - now())); try { await sleep(delay, state.controller.signal); @@ -415,10 +454,15 @@ export function createAdaptivePoller(options: AdaptivePollerOptions) { ...[...state.subscribers.values()].map((entry) => entry.deadlineMs), ); if (now() >= requestDeadline) continue; - const startedAt = now(); - if (state.firstStartAt === null) state.firstStartAt = startedAt; + let startedAt: number | null = null; + try { + startedAt = await reserveShellStart(state, requestDeadline); + } catch { + if (state.subscribers.size === 0 || closed) break; + continue; + } + if (startedAt === null || state.subscribers.size === 0 || closed) continue; state.lastScheduledStart = startedAt; - state.shellStartTimes.push(startedAt); shellStarts += 1; try { const shell = await tracked( diff --git a/src/nativeRuntime.ts b/src/nativeRuntime.ts index e2337cc..d37838b 100644 --- a/src/nativeRuntime.ts +++ b/src/nativeRuntime.ts @@ -258,7 +258,12 @@ function jsonValue(value: unknown, field: string): unknown { const result: Record = {}; for (const [key, entry] of Object.entries(value as Record)) { if (entry === undefined) identityConflict(`${field}.${key}_must_be_json`); - result[key] = jsonValue(entry, `${field}.${key}`); + Object.defineProperty(result, key, { + value: jsonValue(entry, `${field}.${key}`), + enumerable: true, + configurable: true, + writable: true, + }); } return result; } @@ -599,7 +604,8 @@ function sameThreadIdentity( thread.branch === input.branch && thread.worktreePath === input.worktreePath && thread.modelSelection.instanceId === input.modelSelection.instanceId && - thread.modelSelection.model === input.modelSelection.model + thread.modelSelection.model === input.modelSelection.model && + canonical(thread.modelSelection.options) === canonical(input.modelSelection.options) ); } diff --git a/src/stockT3HttpClient.ts b/src/stockT3HttpClient.ts index 8a6c2fb..7d58fd1 100644 --- a/src/stockT3HttpClient.ts +++ b/src/stockT3HttpClient.ts @@ -114,16 +114,26 @@ export function createStockT3HttpClient(options: StockT3HttpClientOptions) { let peakInFlight = 0; const capacityWaiters: Array<() => void> = []; + function capacityFailure(signal: AbortSignal | undefined): StockT3HttpError { + const reason = signal?.reason; + return new StockT3HttpError("transport_unavailable", null, { + reason: + reason instanceof DOMException && reason.name === "TimeoutError" + ? "deadline" + : "cancelled", + }); + } + async function acquireCapacity(boundary: RequestBoundaryOptions): Promise<() => void> { if (boundary.signal?.aborted) { - throw new StockT3HttpError("transport_unavailable", null, { reason: "cancelled" }); + throw capacityFailure(boundary.signal); } if (inFlight >= MAX_HTTP_IN_FLIGHT) { await new Promise((resolve, reject) => { const onAbort = () => { const index = capacityWaiters.indexOf(resume); if (index >= 0) capacityWaiters.splice(index, 1); - reject(new StockT3HttpError("transport_unavailable", null, { reason: "cancelled" })); + reject(capacityFailure(boundary.signal)); }; const resume = () => { boundary.signal?.removeEventListener("abort", onAbort); @@ -169,14 +179,15 @@ export function createStockT3HttpClient(options: StockT3HttpClientOptions) { const attempt = linkedAttemptSignal(boundary.signal, timeoutMs, setTimer, clearTimer); const method = init.method ?? "GET"; requestCount += 1; - const releaseCapacity = await acquireCapacity(boundary); - const headers = new Headers(init.headers); - headers.set("accept", "application/json"); - if (init.body !== undefined && init.body !== null) headers.set("content-type", "application/json"); - if (authenticated && options.bearerToken !== undefined) { - headers.set("authorization", `Bearer ${options.bearerToken}`); - } + let releaseCapacity: (() => void) | undefined; try { + releaseCapacity = await acquireCapacity({ ...boundary, signal: attempt.signal }); + const headers = new Headers(init.headers); + headers.set("accept", "application/json"); + if (init.body !== undefined && init.body !== null) headers.set("content-type", "application/json"); + if (authenticated && options.bearerToken !== undefined) { + headers.set("authorization", `Bearer ${options.bearerToken}`); + } const response = await fetchImpl(new URL(path, baseUrl), { ...init, headers, signal: attempt.signal }); endpointStatusTrace.push({ method, path, status: response.status }); let body: unknown; @@ -202,7 +213,7 @@ export function createStockT3HttpClient(options: StockT3HttpClientOptions) { reason: boundary.signal?.aborted ? "cancelled" : "request_failed", }); } finally { - releaseCapacity(); + releaseCapacity?.(); attempt.cleanup(); } } diff --git a/test/adaptive-poller.test.ts b/test/adaptive-poller.test.ts index 8dc60a2..27c9800 100644 --- a/test/adaptive-poller.test.ts +++ b/test/adaptive-poller.test.ts @@ -79,8 +79,8 @@ describe("environment-coalesced adaptive poller", () => { expect(policy.detailStartsPerWaitMinute).toBe(4); expect(policy.maxActiveWaits).toBe(8); expect(policy.maxHttpInFlight).toBe(8); - expect(policy.firstMinuteAggregateCeiling).toBe(64); - expect(policy.laterMinuteAggregateCeiling).toBe(62); + expect("firstMinuteAggregateCeiling" in policy).toBe(false); + expect("laterMinuteAggregateCeiling" in policy).toBe(false); }); test("enforces the global eight-request in-flight cap across environments", async () => { @@ -398,7 +398,7 @@ describe("environment-coalesced adaptive poller", () => { const waits = Array.from({ length: 8 }, (_, index) => poller .waitFor({ - environmentId: "env-1", + environmentId: `env-${index}`, threadId: `thread-${index}`, deadlineMs: 120_000, evaluate: () => ({ done: false, detail: true }), diff --git a/test/boundary-convergence.test.ts b/test/boundary-convergence.test.ts index 687ab45..8d2c584 100644 --- a/test/boundary-convergence.test.ts +++ b/test/boundary-convergence.test.ts @@ -292,6 +292,26 @@ describe("phase 3 boundary convergence", () => { expect(Object.isFrozen(identity.defaultModelSelection)).toBe(true); }); + test("project identity parsing preserves an own __proto__ JSON key", () => { + const replay = JSON.parse(JSON.stringify(allocateProjectCreateIdentity( + { + workspaceRoot: "/tmp/boundary-project", + title: "project", + defaultModelSelection: selection, + }, + { id: ids("project-public", "command-public"), now: () => iso }, + ))); + replay.defaultModelSelection.options = [ + JSON.parse('{"__proto__":{"polluted":true},"safe":1}'), + ]; + + const parsed = parseProjectCreateIdentity(replay); + const option = parsed.defaultModelSelection.options?.[0] as Record; + expect(Object.hasOwn(option, "__proto__")).toBe(true); + expect(option.__proto__).toEqual({ polluted: true }); + expect(Object.getPrototypeOf(option)).toBe(Object.prototype); + }); + test("project.create receives only the canonical root stored in caller identity", async () => { const stock = new BoundaryStock("/tmp/new-boundary"); stock.projects.clear(); diff --git a/test/native-runtime-adapter.test.ts b/test/native-runtime-adapter.test.ts index 958db6b..0d4e56e 100644 --- a/test/native-runtime-adapter.test.ts +++ b/test/native-runtime-adapter.test.ts @@ -407,6 +407,34 @@ describe("stock native runtime read-only resume", () => { }); expect(dispatches).toBe(0); }); + + test("treats different model options as a true thread identity conflict", async () => { + let dispatches = 0; + const runtime = createStockT3NativeRuntime({ + client: baseClient({ + getShell: async () => shell(9), + getThread: async () => detail(9, [], { + modelSelection: { ...modelSelection, options: [{ temperature: 1 }] }, + }), + dispatch: async () => { + dispatches += 1; + return { sequence: 10 }; + }, + }), + id: ids(), + now: () => iso, + }); + const pending = await pendingReceipt(runtime); + const result = await runtime.resumeCreateReconciliation(pending, spawnInput, { + maxReconciliationReads: 1, + }); + expect(result).toMatchObject({ + kind: "create_protocol_failure", + provisionalRef: pending.provisionalRef, + conflict: { source: "detail" }, + }); + expect(dispatches).toBe(0); + }); }); describe("stock native runtime inclusive create deadline", () => { diff --git a/test/stock-t3-exact-stock-negative.test.ts b/test/stock-t3-exact-stock-negative.test.ts index da5f339..3d24ecd 100644 --- a/test/stock-t3-exact-stock-negative.test.ts +++ b/test/stock-t3-exact-stock-negative.test.ts @@ -15,6 +15,8 @@ describe("exact-stock characterization driver", () => { ); expect(source).toContain("trap cleanup EXIT INT TERM"); expect(source).toContain('rm -f -- "$generated_path"'); + expect(source).toContain('if [[ -e "$generated_path" ]]'); + expect(source).toContain("generated characterization path already exists"); }); test.skipIf(exactTree === undefined || exactToolchain === undefined)( diff --git a/test/stock-t3-http-client.test.ts b/test/stock-t3-http-client.test.ts index 8770a9a..08d93f9 100644 --- a/test/stock-t3-http-client.test.ts +++ b/test/stock-t3-http-client.test.ts @@ -189,6 +189,44 @@ describe("stock T3 HTTP client", () => { expect(client.observations()).toMatchObject({ peakInFlight: 8, inFlight: 0 }); }); + test("an attempt deadline aborts while the request is still queued for capacity", async () => { + let releaseHeld!: () => void; + const held = new Promise((resolve) => { + releaseHeld = resolve; + }); + let starts = 0; + const client = createStockT3HttpClient({ + baseUrl: "http://127.0.0.1:3774", + setTimer: (callback, milliseconds) => setTimeout(callback, milliseconds), + clearTimer: (timer) => clearTimeout(timer as ReturnType), + fetch: async (_input, init) => { + starts += 1; + if (init?.signal?.aborted) throw init.signal.reason; + await held; + return Response.json(descriptor); + }, + }); + const occupying = Array.from({ length: 8 }, () => client.getDescriptor()); + while (starts < 8) await Promise.resolve(); + const queued = client.getDescriptor({ deadlineMs: Date.now() + 5 }).catch((error) => error); + try { + const outcome = await Promise.race([ + queued, + new Promise((resolve) => setTimeout(() => resolve("still-queued"), 50)), + ]); + expect(outcome).not.toBe("still-queued"); + expect(outcome).toMatchObject({ + code: "transport_unavailable", + detail: { reason: "deadline" }, + }); + expect(starts).toBe(8); + } finally { + releaseHeld(); + await Promise.allSettled([...occupying, queued]); + } + expect(client.observations()).toMatchObject({ inFlight: 0 }); + }); + test.each([ ["local", 251, true], ["local", 4_999, true], diff --git a/test/stock-t3-live-harness.test.ts b/test/stock-t3-live-harness.test.ts index 6653791..b3e2d22 100644 --- a/test/stock-t3-live-harness.test.ts +++ b/test/stock-t3-live-harness.test.ts @@ -32,8 +32,8 @@ afterEach(async () => { await Promise.all(temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); }); -async function canaryFixture() { - const root = await mkdtemp(join(tmpdir(), "t3layer-canary.")); +async function canaryFixture(prefix = "t3layer-canary.") { + const root = await mkdtemp(join(tmpdir(), prefix)); temporaryRoots.push(root); const log = join(root, "commands.log"); const paths: Record = {}; @@ -149,6 +149,9 @@ describe("stock live harness lifecycle", () => { expect(source).toContain('validate-envelope'); expect(source).toContain("stat -f '%Lp'"); expect(source).toContain('staging_bytes=$(shasum -a 256'); + expect(source).toContain('if [[ ! -e "$proof_root" ]]'); + expect(source).not.toContain('--header "Authorization: Bearer $http_token"'); + expect(source).toContain('--header @-'); }); test("declares fault seams across setup, live execution, and atomic finalization", async () => { @@ -219,6 +222,26 @@ describe("stock live harness lifecycle", () => { } }); + test("test-mode teardown publishes only after deleting its isolated proof root", async () => { + const root = await mkdtemp(join(tmpdir(), "t3layer-harness-success.")); + temporaryRoots.push(root); + const target = join(root, "proof.json"); + const result = await run(["bash", "scripts/stock-t3-live-harness.sh"], { + T3_STOCK_PROVIDER_SECRET_REF: "op://fixture/provider/key", + T3_STOCK_HARNESS_TEST_MODE: "1", + T3_STOCK_HARNESS_COMMAND_RUNNER: "/usr/bin/true", + T3_STOCK_PROOF_TARGET: target, + }); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stderr).toContain("cleanup root_removed=true"); + expect(result.stderr).not.toContain("op://fixture/provider/key"); + expect(await Bun.file(target).exists()).toBe(true); + await expect(Bun.file(target).json()).resolves.toMatchObject({ + success: true, + teardown: { pidStopped: true, worktreeRemoved: true, rootRemoved: true }, + }); + }); + test("requires caller-held runId and candidateSha instead of trusting a stale path", async () => { const prior = canonicalProofBody({ ...completeBody(), runId: "prior-run" }); expect(() => @@ -391,6 +414,13 @@ describe("stock live harness lifecycle", () => { expect((await Bun.file(fixture.receipt).stat()).mode & 0o777).toBe(0o600); }); + test("execute mode supports command paths containing spaces", async () => { + const fixture = await canaryFixture("t3layer canary. "); + const result = await run(["bash", "scripts/stock-t3-canary-drill.sh", "--execute"], fixture.env); + expect(result.exitCode, result.stderr).toBe(0); + await expect(Bun.file(fixture.receipt).json()).resolves.toMatchObject({ success: true }); + }); + test("execute mode rejects artifact drift and environment identity drift", async () => { const artifactFixture = await canaryFixture(); await Bun.write( From 56253ea4ba8e71dcf7d6ca51aefb884a036ed88c Mon Sep 17 00:00:00 2001 From: Etan Joseph Heyman Date: Sat, 1 Aug 2026 20:54:43 +0300 Subject: [PATCH 3/5] fix: address PR 4 round 1b and 1c reviews Co-Authored-By: Claude Opus 5 (1M context) --- docs/operations/stock-t3-first-release.md | 5 +- scripts/stock-proof-cli.ts | 4 +- scripts/stock-t3-canary-drill.sh | 59 +++++-- scripts/stock-t3-exact-characterization.sh | 7 +- scripts/stock-t3-live-harness.sh | 46 +++++- src/adaptivePoller.ts | 67 +++++++- src/facade.ts | 18 +++ src/nativeRuntime.ts | 113 ++++++++++--- src/stockProof.ts | 21 ++- src/stockT3Contracts.ts | 6 +- src/stockT3HttpClient.ts | 114 ++++++++----- test/adaptive-poller.test.ts | 136 ++++++++++++++++ test/pr4-review-regressions.test.ts | 179 +++++++++++++++++++++ test/r8-runtime-regressions.test.ts | 5 +- test/stock-only-gate.test.ts | 16 ++ test/stock-t3-exact-stock-negative.test.ts | 4 +- test/stock-t3-http-client.test.ts | 84 ++++++++++ test/stock-t3-live-harness.test.ts | 77 ++++++++- test/stock-t3-live.test.ts | 10 +- 19 files changed, 862 insertions(+), 109 deletions(-) create mode 100644 test/pr4-review-regressions.test.ts diff --git a/docs/operations/stock-t3-first-release.md b/docs/operations/stock-t3-first-release.md index e8c4505..6c7c29e 100644 --- a/docs/operations/stock-t3-first-release.md +++ b/docs/operations/stock-t3-first-release.md @@ -14,7 +14,10 @@ the stock T3 server. config restoration, readiness, descriptor inspection, and an existing-thread read, plus a cancellation command that returns redacted `{cancelled, replayed: 0}` evidence. Also supply the immutable artifact and - redacted configuration files. + redacted configuration files. Set `T3_STOCK_APPROVED_ARTIFACT_SHA256` and + `T3_STOCK_APPROVED_CONFIG_SHA256` to the independently reviewed 64-character + lowercase SHA-256 values; execute mode rejects mismatched bytes before the + first routing command. The drill refuses shell snippets. Without a real routing controller and these commands, only `--dry-run` is valid diff --git a/scripts/stock-proof-cli.ts b/scripts/stock-proof-cli.ts index c7f483f..38e7858 100644 --- a/scripts/stock-proof-cli.ts +++ b/scripts/stock-proof-cli.ts @@ -5,7 +5,7 @@ import { validateProofEnvelope, } from "../src/stockProof"; -const [command, source, first, second] = process.argv.slice(2); +const [command, source, first, second, third] = process.argv.slice(2); if (!command || !source) throw new TypeError("usage: stock-proof-cli.ts ..."); const value = await Bun.file(source).json(); @@ -14,7 +14,7 @@ if (command === "validate-provisional") { canonicalProvisionalProof(value, first); } else if (command === "publish") { if (!first || !second) throw new TypeError("output path and expected identity are required"); - const candidateSha = process.argv[6]; + const candidateSha = third; if (!candidateSha) throw new TypeError("candidate SHA is required"); const checksum = await proofChecksum(value); await Bun.write(first, canonicalProofEnvelopeJson(value, checksum)); diff --git a/scripts/stock-t3-canary-drill.sh b/scripts/stock-t3-canary-drill.sh index d074ca0..0bb564e 100644 --- a/scripts/stock-t3-canary-drill.sh +++ b/scripts/stock-t3-canary-drill.sh @@ -30,8 +30,19 @@ fi : "${T3_STOCK_CANCEL_WAITS_COMMAND:?required executable path}" : "${T3_STOCK_ARTIFACT_PATH:?required artifact path}" : "${T3_STOCK_CONFIG_PATH:?required redacted config path}" +: "${T3_STOCK_APPROVED_ARTIFACT_SHA256:?required approved artifact SHA-256}" +: "${T3_STOCK_APPROVED_CONFIG_SHA256:?required approved config SHA-256}" : "${T3_STOCK_DRILL_RECEIPT_PATH:?required receipt path}" +sha256_file() { + local path=$1 + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$path" | /usr/bin/awk '{print $1}' + else + shasum -a 256 "$path" | /usr/bin/awk '{print $1}' + fi +} + commands=( "$T3_STOCK_ROUTE_OFF_COMMAND" "$T3_STOCK_ROUTE_CANARY_COMMAND" @@ -53,8 +64,12 @@ if [[ ! -f "$T3_STOCK_ARTIFACT_PATH" || ! -f "$T3_STOCK_CONFIG_PATH" ]]; then exit 2 fi -artifact_digest=$(shasum -a 256 "$T3_STOCK_ARTIFACT_PATH" | /usr/bin/awk '{print $1}') -config_before=$(shasum -a 256 "$T3_STOCK_CONFIG_PATH" | /usr/bin/awk '{print $1}') +artifact_digest=$(sha256_file "$T3_STOCK_ARTIFACT_PATH") +config_before=$(sha256_file "$T3_STOCK_CONFIG_PATH") +if [[ "$artifact_digest" != "$T3_STOCK_APPROVED_ARTIFACT_SHA256" || "$config_before" != "$T3_STOCK_APPROVED_CONFIG_SHA256" ]]; then + echo "ERROR: approved digest mismatch" >&2 + exit 2 +fi command_statuses='[]' descriptor_evidence='[]' thread_evidence='[]' @@ -66,8 +81,9 @@ expected_thread_id='' cancellation_evidence='' verify_artifact() { - artifact_stage=$1 - current_digest=$(shasum -a 256 "$T3_STOCK_ARTIFACT_PATH" | /usr/bin/awk '{print $1}') + local artifact_stage=$1 + local current_digest + current_digest=$(sha256_file "$T3_STOCK_ARTIFACT_PATH") artifact_evidence=$(/usr/bin/jq -c --arg stage "$artifact_stage" --arg digest "$current_digest" '. + [{stage:$stage,digest:$digest}]' <<<"$artifact_evidence") if [[ "$current_digest" != "$artifact_digest" ]]; then echo "ERROR: artifact drift detected at $artifact_stage" >&2 @@ -80,9 +96,9 @@ record_status() { } run_step() { - step_name=$1 - step_command=$2 - step_status=0 + local step_name=$1 + local step_command=$2 + local step_status=0 "$step_command" || step_status=$? record_status "$step_name" "$step_status" if [[ "$step_status" -ne 0 ]]; then return "$step_status"; fi @@ -94,16 +110,17 @@ run_step() { } recover() { - exit_status=$? + local exit_status=$? + if [[ $# -eq 1 ]]; then exit_status=$1; fi trap - EXIT INT TERM if [[ "$drill_complete" != true && "$recovery_armed" == true ]]; then - prior_status=0 + local prior_status=0 "$T3_STOCK_ROUTE_PRIOR_CONFIG_COMMAND" || prior_status=$? record_status recovery-prior-config "$prior_status" - off_status=0 + local off_status=0 "$T3_STOCK_ROUTE_OFF_COMMAND" || off_status=$? record_status recovery-off "$off_status" - cancel_status=0 + local cancel_status=0 "$T3_STOCK_CANCEL_WAITS_COMMAND" >/dev/null || cancel_status=$? record_status recovery-cancel-waits "$cancel_status" echo "CANARY_RECOVERY: prior_config=$prior_status routing_off=$off_status cancel_waits=$cancel_status" >&2 @@ -111,11 +128,18 @@ recover() { exit "$exit_status" } +handle_signal() { + recover "$1" +} + # Recovery is armed before the first routing mutation. -trap recover EXIT INT TERM +trap recover EXIT +trap 'handle_signal 130' INT +trap 'handle_signal 143' TERM verify_health() { - stage=$1 + local stage=$1 + local descriptor current_environment_id thread current_thread_id run_step "$stage-readiness" "$T3_STOCK_READINESS_COMMAND" descriptor=$("$T3_STOCK_DESCRIPTOR_COMMAND") if ! /usr/bin/jq -e 'type == "object" and (.environmentId|type == "string" and length > 0) and (.serverVersion|type == "string" and length > 0)' <<<"$descriptor" >/dev/null; then @@ -156,7 +180,7 @@ verify_health canary run_step route-promote "$T3_STOCK_ROUTE_PROMOTE_COMMAND" verify_health promoted run_step restore-prior-config "$T3_STOCK_ROUTE_PRIOR_CONFIG_COMMAND" -config_after_restore=$(shasum -a 256 "$T3_STOCK_CONFIG_PATH" | /usr/bin/awk '{print $1}') +config_after_restore=$(sha256_file "$T3_STOCK_CONFIG_PATH") if [[ "$config_after_restore" != "$config_before" ]]; then echo "ERROR: prior configuration digest was not restored" >&2 exit 2 @@ -176,7 +200,7 @@ if [[ ${T3_STOCK_FAIL_AT:-} == cancel-waits ]]; then exit 91 fi verify_artifact cancel-waits -config_after=$(shasum -a 256 "$T3_STOCK_CONFIG_PATH" | /usr/bin/awk '{print $1}') +config_after=$(sha256_file "$T3_STOCK_CONFIG_PATH") [[ "$config_after" == "$config_before" ]] receipt_dir=$(dirname "$T3_STOCK_DRILL_RECEIPT_PATH") @@ -195,7 +219,7 @@ chmod 600 "$body_staging" "$staging" --argjson artifacts "$artifact_evidence" \ --argjson cancellation "$cancellation_evidence" \ '{success:true,transitions:$transitions,artifactDigest:$digest,configDigestBefore:$before,configDigestAfter:$after,schema:"stock-http-v1",acceleration:"off",cancellation:$cancellation,commandStatuses:$statuses,descriptors:$descriptors,threadReadability:$threads,artifactChecks:$artifacts}' >"$body_staging" -checksum=$(shasum -a 256 "$body_staging" | /usr/bin/awk '{print $1}') +checksum=$(sha256_file "$body_staging") /usr/bin/jq -cS --arg checksum "$checksum" '. + {checksum:$checksum}' "$body_staging" >"$staging" mv -f -- "$staging" "$T3_STOCK_DRILL_RECEIPT_PATH" chmod 600 "$T3_STOCK_DRILL_RECEIPT_PATH" @@ -206,12 +230,13 @@ if [[ $(/usr/bin/stat -f '%Lp' "$T3_STOCK_DRILL_RECEIPT_PATH") != 600 ]]; then fi reread_body=$(mktemp "$receipt_dir/.stock-t3-drill-reread.XXXXXX") /usr/bin/jq -cS 'del(.checksum)' "$T3_STOCK_DRILL_RECEIPT_PATH" >"$reread_body" -reread_checksum=$(shasum -a 256 "$reread_body" | /usr/bin/awk '{print $1}') +reread_checksum=$(sha256_file "$reread_body") rm -f -- "$reread_body" if [[ "$reread_checksum" != "$checksum" ]] || ! /usr/bin/jq -e --arg checksum "$checksum" '.success == true and .schema == "stock-http-v1" and .acceleration == "off" and .checksum == $checksum and .cancellation.replayed == 0' "$T3_STOCK_DRILL_RECEIPT_PATH" >/dev/null; then echo "ERROR: canary receipt checksum or reread mismatch" >&2 exit 2 fi drill_complete=true +recovery_armed=false trap - EXIT INT TERM echo "STOCK_T3_CANARY_DRILL: PASS" diff --git a/scripts/stock-t3-exact-characterization.sh b/scripts/stock-t3-exact-characterization.sh index bdcbe07..9f5a7a0 100755 --- a/scripts/stock-t3-exact-characterization.sh +++ b/scripts/stock-t3-exact-characterization.sh @@ -20,7 +20,12 @@ fi cleanup() { rm -f -- "$generated_path" } -trap cleanup EXIT INT TERM +handle_signal() { + exit "$1" +} +trap cleanup EXIT +trap 'handle_signal 130' INT +trap 'handle_signal 143' TERM /bin/cat >"$generated_path" <<'CHARACTERIZATION' import { diff --git a/scripts/stock-t3-live-harness.sh b/scripts/stock-t3-live-harness.sh index 8398c63..8ef9b5b 100755 --- a/scripts/stock-t3-live-harness.sh +++ b/scripts/stock-t3-live-harness.sh @@ -22,6 +22,23 @@ run_id='' finalizer_source='' test_mode=${T3_STOCK_HARNESS_TEST_MODE:-0} +sha256_file() { + local path=$1 + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$path" | /usr/bin/awk '{print $1}' + else + shasum -a 256 "$path" | /usr/bin/awk '{print $1}' + fi +} + +sha256_stream() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum | /usr/bin/awk '{print $1}' + else + shasum -a 256 | /usr/bin/awk '{print $1}' + fi +} + run_finalizer() { printf '%s' "$finalizer_source" | bun run - "$@" } @@ -79,10 +96,13 @@ cleanup() { elif [[ -z "$stock_tree" || ! -e "$stock_tree" ]]; then worktree_removed=true fi - if [[ "$cleanup_root_valid" == true && -n "$proof_root" && "$proof_root" != / && "$proof_root" != "$HOME" && ! -L "$proof_root" ]]; then + if [[ ${T3_STOCK_FAIL_TEARDOWN_AT:-} != root && "$cleanup_root_valid" == true && -n "$proof_root" && "$proof_root" != / && "$proof_root" != "$HOME" && ! -L "$proof_root" ]]; then rm -rf -- "$proof_root" if [[ ! -e "$proof_root" ]]; then root_removed=true; fi fi + if [[ "$cleanup_status" -eq 0 && ("$pid_stopped" != true || "$worktree_removed" != true || "$root_removed" != true) ]]; then + cleanup_status=2 + fi if [[ "$cleanup_status" -eq 0 && "$proof_ready" == true && "$pid_stopped" == true && "$worktree_removed" == true && "$root_removed" == true ]]; then proof_dir=$(dirname "$proof_target") mkdir -p -- "$proof_dir" @@ -113,8 +133,16 @@ cleanup() { echo "ERROR: injected failure: after-final-body-validation" >&2 exit 91 fi - [[ $(/usr/bin/stat -f '%Lp' "$final_staging") == 600 ]] - staging_bytes=$(shasum -a 256 "$final_staging" | /usr/bin/awk '{print $1}') + if [[ $(/usr/bin/stat -f '%Lp' "$final_staging") != 600 ]]; then + rm -f -- "$final_body_staging" "$final_staging" + cleanup_status=2 + fi + if [[ "$cleanup_status" -ne 0 ]]; then + echo "ERROR: final proof staging mode mismatch" >&2 + echo "cleanup root_removed=$root_removed worktree_removed=$worktree_removed pid_stopped=$pid_stopped" >&2 + exit "$cleanup_status" + fi + staging_bytes=$(sha256_file "$final_staging") if ! node -e 'const fs=require("node:fs");const [source,target]=process.argv.slice(1);const fd=fs.openSync(source,"r+");fs.fsyncSync(fd);fs.closeSync(fd);fs.renameSync(source,target);const check=fs.openSync(target,"r");fs.fsyncSync(check);fs.closeSync(check)' "$final_staging" "$proof_target"; then rm -f -- "$final_body_staging" "$final_staging" exit 2 @@ -126,7 +154,7 @@ cleanup() { exit 91 fi chmod 600 "$proof_target" - final_bytes=$(shasum -a 256 "$proof_target" | /usr/bin/awk '{print $1}') + final_bytes=$(sha256_file "$proof_target") if [[ $(/usr/bin/stat -f '%Lp' "$proof_target") != 600 || "$final_bytes" != "$staging_bytes" ]] || ! run_finalizer validate-envelope "$proof_target" "$run_id" "$candidate_sha"; then rm -f -- "$proof_target" echo "ERROR: final proof bytes, mode, checksum, identity, or teardown mismatch" >&2 @@ -193,7 +221,7 @@ if [[ "$test_mode" == 1 ]]; then else candidate_sha=$(/usr/bin/git -C "$candidate_repo" rev-parse HEAD) /usr/bin/git -C "$candidate_repo" archive HEAD | /usr/bin/tar -x -C "$t3layer_clean" - artifact_digest=$(/usr/bin/git -C "$candidate_repo" archive HEAD | shasum -a 256 | /usr/bin/awk '{print $1}') + artifact_digest=$(/usr/bin/git -C "$candidate_repo" archive HEAD | sha256_stream) fi run_stage_seam after-archive-extract if [[ "$test_mode" != 1 ]]; then @@ -279,18 +307,20 @@ else negative_body="$proof_root/exact-http-negative.json" : >"$negative_body" chmod 600 "$negative_body" - negative_payload=$(/usr/bin/jq -n \ + negative_request="$proof_root/exact-http-negative-input.json" + /usr/bin/jq -n \ --arg commandId "$negative_command_id" \ --arg threadId "$negative_thread_id" \ --arg messageId "$negative_message_id" \ --arg createdAt "$(date -u '+%Y-%m-%dT%H:%M:%S.000Z')" \ --arg workspace "$workspace" \ - '{type:"thread.turn.start",commandId:$commandId,threadId:$threadId,message:{messageId:$messageId,role:"user",text:"negative",attachments:[]},runtimeMode:"full-access",interactionMode:"default",bootstrap:{createThread:{projectId:"00000000-0000-4000-8000-000000000000",title:"negative",modelSelection:{instanceId:"claudeAgent",model:"claude-sonnet-4-5"},runtimeMode:"full-access",interactionMode:"default",branch:null,worktreePath:null,createdAt:$createdAt}},createdAt:$createdAt}') + '{type:"thread.turn.start",commandId:$commandId,threadId:$threadId,message:{messageId:$messageId,role:"user",text:"negative",attachments:[]},runtimeMode:"full-access",interactionMode:"default",bootstrap:{createThread:{projectId:"00000000-0000-4000-8000-000000000000",title:"negative",modelSelection:{instanceId:"claudeAgent",model:"claude-sonnet-4-5"},runtimeMode:"full-access",interactionMode:"default",branch:null,worktreePath:null,createdAt:$createdAt}},createdAt:$createdAt}' >"$negative_request" + chmod 600 "$negative_request" negative_status=$(authenticated_curl --silent --show-error --output "$negative_body" --write-out '%{http_code}' \ --max-time 5 \ --request POST \ --header 'Content-Type: application/json' \ - --data "$negative_payload" \ + --data-binary "@$negative_request" \ http://127.0.0.1:3774/api/orchestration/dispatch) if [[ "$negative_status" != 500 ]] || ! /usr/bin/jq -e '.code == "internal_error" and .reason == "orchestration_dispatch_failed"' "$negative_body" >/dev/null; then echo "ERROR: exact HTTP bootstrap negative did not match stock" >&2 diff --git a/src/adaptivePoller.ts b/src/adaptivePoller.ts index 25b9997..754fda9 100644 --- a/src/adaptivePoller.ts +++ b/src/adaptivePoller.ts @@ -64,6 +64,8 @@ export interface AdaptivePollerOptions { ) => Promise; readonly now?: () => number; readonly sleep?: (milliseconds: number, signal: AbortSignal) => Promise; + readonly setTimer?: (callback: () => void, milliseconds: number) => unknown; + readonly clearTimer?: (timer: unknown) => void; /** Returns a signed jitter delta. It is clamped to +/-10% of the delay. */ readonly jitter?: (delayMs: number, failureIndex: number) => number; } @@ -100,6 +102,7 @@ interface EnvironmentState { lastScheduledStart: number | null; lastCompletionAt: number; lastShellSequence: number | null; + sleepController: AbortController | null; } interface SlotWaiter { @@ -108,6 +111,7 @@ interface SlotWaiter { readonly resolve: (release: () => void) => void; readonly reject: (error: PollerError) => void; readonly onAbort: () => void; + timer: unknown; } function defaultSleep(milliseconds: number, signal: AbortSignal): Promise { @@ -116,9 +120,14 @@ function defaultSleep(milliseconds: number, signal: AbortSignal): Promise reject(new PollerError("cancelled")); return; } - const timer = setTimeout(resolve, Math.max(0, milliseconds)); + const cleanup = () => signal.removeEventListener("abort", onAbort); + const timer = setTimeout(() => { + cleanup(); + resolve(); + }, Math.max(0, milliseconds)); const onAbort = () => { clearTimeout(timer); + cleanup(); reject(new PollerError("cancelled")); }; signal.addEventListener("abort", onAbort, { once: true }); @@ -147,6 +156,11 @@ function retryMetadata(error: unknown): { retryAfterMs: number } | null { export function createAdaptivePoller(options: AdaptivePollerOptions) { const now = options.now ?? Date.now; const sleep = options.sleep ?? defaultSleep; + const setTimer = + options.setTimer ?? + ((callback: () => void, milliseconds: number) => setTimeout(callback, milliseconds)); + const clearTimer = + options.clearTimer ?? ((timer: unknown) => clearTimeout(timer as ReturnType)); const jitter = options.jitter ?? ((delayMs: number) => delayMs * (Math.random() * 0.2 - 0.1)); @@ -168,6 +182,7 @@ export function createAdaptivePoller(options: AdaptivePollerOptions) { while (httpInFlight < POLICY.maxHttpInFlight && slotWaiters.length > 0) { const waiter = slotWaiters.shift()!; waiter.signal?.removeEventListener("abort", waiter.onAbort); + clearTimer(waiter.timer); if (waiter.signal?.aborted) { waiter.reject(new PollerError("cancelled")); continue; @@ -200,11 +215,22 @@ export function createAdaptivePoller(options: AdaptivePollerOptions) { onAbort: () => { const index = slotWaiters.indexOf(waiter); if (index >= 0) slotWaiters.splice(index, 1); + clearTimer(waiter.timer); reject(new PollerError("cancelled")); + pumpSlots(); }, + timer: undefined, }; slotWaiters.push(waiter); signal?.addEventListener("abort", waiter.onAbort, { once: true }); + waiter.timer = setTimer(() => { + const index = slotWaiters.indexOf(waiter); + if (index < 0) return; + slotWaiters.splice(index, 1); + signal?.removeEventListener("abort", waiter.onAbort); + reject(new PollerError("timeout")); + pumpSlots(); + }, Math.max(0, deadlineMs - now())); pumpSlots(); }); } @@ -299,6 +325,22 @@ export function createAdaptivePoller(options: AdaptivePollerOptions) { } } + async function sleepUntilNextCycle( + state: EnvironmentState, + milliseconds: number, + ): Promise { + const controller = new AbortController(); + const onEnvironmentAbort = () => controller.abort(state.controller.signal.reason); + state.controller.signal.addEventListener("abort", onEnvironmentAbort, { once: true }); + state.sleepController = controller; + try { + await sleep(milliseconds, controller.signal); + } finally { + state.controller.signal.removeEventListener("abort", onEnvironmentAbort); + if (state.sleepController === controller) state.sleepController = null; + } + } + function evaluate( state: EnvironmentState, subscriber: Subscriber, @@ -360,14 +402,15 @@ export function createAdaptivePoller(options: AdaptivePollerOptions) { expireSubscribers(state); return; } - detailStarts += 1; try { const detail = await tracked( - () => - options.getThread(threadId, { + () => { + detailStarts += 1; + return options.getThread(threadId, { deadlineMs, signal: state.controller.signal, - }), + }); + }, deadlineMs, state.controller.signal, ); @@ -381,6 +424,11 @@ export function createAdaptivePoller(options: AdaptivePollerOptions) { } } catch (error) { if (state.controller.signal.aborted) return; + if (error instanceof PollerError && error.code === "timeout") { + state.details.delete(threadId); + expireSubscribers(state); + return; + } if (retryMetadata(error) !== null) { state.details.delete(threadId); return; @@ -443,7 +491,7 @@ export function createAdaptivePoller(options: AdaptivePollerOptions) { const desiredDelay = nextDelay(state); const delay = Math.min(desiredDelay, Math.max(0, earliestDeadline - now())); try { - await sleep(delay, state.controller.signal); + await sleepUntilNextCycle(state, delay); } catch { if (state.subscribers.size === 0 || closed) break; } @@ -482,6 +530,10 @@ export function createAdaptivePoller(options: AdaptivePollerOptions) { } catch (error) { state.lastCompletionAt = now(); if (state.controller.signal.aborted) break; + if (error instanceof PollerError && error.code === "timeout") { + expireSubscribers(state); + continue; + } const retry = retryMetadata(error); if (retry === null) { const failure = @@ -534,6 +586,7 @@ export function createAdaptivePoller(options: AdaptivePollerOptions) { lastScheduledStart: null, lastCompletionAt: now(), lastShellSequence: null, + sleepController: null, }; environments.set(environmentId, state); return state; @@ -576,6 +629,7 @@ export function createAdaptivePoller(options: AdaptivePollerOptions) { state.cadenceIndex = 0; state.failureIndex = 0; state.failureDelayMs = null; + state.sleepController?.abort(new PollerError("cancelled")); } }, @@ -603,6 +657,7 @@ export function createAdaptivePoller(options: AdaptivePollerOptions) { } for (const waiter of slotWaiters.splice(0)) { waiter.signal?.removeEventListener("abort", waiter.onAbort); + clearTimer(waiter.timer); waiter.reject(new PollerError("closed")); } environments.clear(); diff --git a/src/facade.ts b/src/facade.ts index 14230be..cae7d12 100644 --- a/src/facade.ts +++ b/src/facade.ts @@ -11,12 +11,27 @@ export { allocateProjectCreateIdentity, canonicalizeWorkspaceRoot, parseProjectCreateIdentity, + StockRuntimeError, } from "./nativeRuntime"; export type { + AgentRef, + CreateAttemptReceipt, + CreateReconciliationPending, + CreateReconciliationState, ProjectCreateIdentity, ProjectCreateIdentityAllocationOptions, ProjectCreateIdentityExpectation, ProjectCreateIdentityInput, + RetryState, + RuntimeModelSelection, + RuntimeOperationOptions, + SanitizedRetryError, + SpawnResult, + StockRuntimeErrorCode, + StockSpawnInput, + T3NativeRuntime, + ThreadCreateReceipt, + TurnReceipt, WorkspaceCanonicalizationOptions, } from "./nativeRuntime"; @@ -37,5 +52,8 @@ export function createStockT3Facade(runtime: T3NativeRuntime) { observe: (ref: AgentRef, options?: RuntimeOperationOptions) => runtime.observe(ref, options), releaseReceipt: (receipt: TurnReceipt) => runtime.releaseReceipt(receipt), + pollMetrics: () => runtime.pollMetrics(), + httpObservations: () => runtime.httpObservations(), + close: () => runtime.close(), }); } diff --git a/src/nativeRuntime.ts b/src/nativeRuntime.ts index d37838b..029f88d 100644 --- a/src/nativeRuntime.ts +++ b/src/nativeRuntime.ts @@ -5,6 +5,7 @@ import type { StockThreadDetail, StockThreadShell, ThreadDetailSnapshot, + ConnectionProfile, } from "./stockT3Contracts"; import { homedir } from "node:os"; import { posix, win32 } from "node:path"; @@ -199,6 +200,7 @@ export type StockRuntimeErrorCode = | "authentication_failed" | "permission_denied" | "server_internal" + | "internal_error" | "transport_unavailable" | "protocol_mismatch" | "environment_changed" @@ -245,25 +247,43 @@ function identifier(value: unknown, field: string): string { return parsed; } -function jsonValue(value: unknown, field: string): unknown { +function jsonValue( + value: unknown, + field: string, + ancestors: Set = new Set(), +): unknown { if (value === null || typeof value === "string" || typeof value === "boolean") return value; if (typeof value === "number") { if (!Number.isFinite(value)) identityConflict(`${field}_must_be_json`); return value; } - if (Array.isArray(value)) return value.map((entry, index) => jsonValue(entry, `${field}[${index}]`)); + if (Array.isArray(value)) { + if (ancestors.has(value)) identityConflict(`${field}_must_be_acyclic_json`); + ancestors.add(value); + try { + return value.map((entry, index) => jsonValue(entry, `${field}[${index}]`, ancestors)); + } finally { + ancestors.delete(value); + } + } if (typeof value === "object") { const prototype = Object.getPrototypeOf(value); if (prototype !== Object.prototype && prototype !== null) identityConflict(`${field}_must_be_json`); + if (ancestors.has(value)) identityConflict(`${field}_must_be_acyclic_json`); + ancestors.add(value); const result: Record = {}; - for (const [key, entry] of Object.entries(value as Record)) { - if (entry === undefined) identityConflict(`${field}.${key}_must_be_json`); - Object.defineProperty(result, key, { - value: jsonValue(entry, `${field}.${key}`), - enumerable: true, - configurable: true, - writable: true, - }); + try { + for (const [key, entry] of Object.entries(value as Record)) { + if (entry === undefined) identityConflict(`${field}.${key}_must_be_json`); + Object.defineProperty(result, key, { + value: jsonValue(entry, `${field}.${key}`, ancestors), + enumerable: true, + configurable: true, + writable: true, + }); + } + } finally { + ancestors.delete(value); } return result; } @@ -288,7 +308,18 @@ export function canonicalizeWorkspaceRoot( options: WorkspaceCanonicalizationOptions = {}, ): string { const input = nonBlank(value, "workspace_root").trim(); - const path = options.platform === "windows" || options.platform === "win32" ? win32 : posix; + const requestedPlatform = options.platform === "windows" ? "win32" : options.platform; + const localPlatform = process.platform === "win32" ? "win32" : process.platform; + const targetPlatform = requestedPlatform ?? localPlatform; + const path = targetPlatform === "win32" ? win32 : posix; + const crossPlatform = requestedPlatform !== undefined && targetPlatform !== localPlatform; + const usesHome = input === "~" || input.startsWith("~/") || input.startsWith("~\\"); + if (crossPlatform && usesHome && options.homeDirectory === undefined) { + identityConflict("workspace_root_cross_platform_home_required"); + } + if (crossPlatform && !path.isAbsolute(input) && !usesHome && options.cwd === undefined) { + identityConflict("workspace_root_cross_platform_absolute_required"); + } const home = options.homeDirectory ?? homedir(); const expanded = input === "~" ? home @@ -407,7 +438,7 @@ export interface StockT3NativeRuntimeOptions { readonly baseUrl?: string | URL; readonly bearerToken?: string; readonly fetch?: FetchLike; - readonly connectionProfile?: "local" | "relay" | "tunnel"; + readonly connectionProfile?: ConnectionProfile; readonly id?: () => string; readonly now?: () => string; readonly clock?: () => number; @@ -527,7 +558,9 @@ function mapReceivedError(error: unknown): StockRuntimeError { return new StockRuntimeError(error.code, { status: error.status }); } } - return new StockRuntimeError("transport_unavailable"); + return new StockRuntimeError("internal_error", { + errorName: error instanceof Error ? error.name : typeof error, + }); } function readFailureEvidence( @@ -549,16 +582,34 @@ function turnReceiptError( return new StockRuntimeError(code, { ...evidence, receipt }); } -function canonical(value: unknown): string { - if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`; +function canonical(value: unknown, ancestors: Set = new Set()): string { + if (value === undefined) return "null"; + if (Array.isArray(value)) { + if (ancestors.has(value)) identityConflict("digest_input_must_be_acyclic_json"); + ancestors.add(value); + try { + return `[${value.map((entry) => canonical(entry, ancestors)).join(",")}]`; + } finally { + ancestors.delete(value); + } + } if (typeof value === "object" && value !== null) { const record = value as Record; - return `{${Object.keys(record) - .sort() - .map((key) => `${JSON.stringify(key)}:${canonical(record[key])}`) - .join(",")}}`; + if (ancestors.has(record)) identityConflict("digest_input_must_be_acyclic_json"); + ancestors.add(record); + try { + return `{${Object.keys(record) + .filter((key) => record[key] !== undefined) + .sort() + .map((key) => `${JSON.stringify(key)}:${canonical(record[key], ancestors)}`) + .join(",")}}`; + } finally { + ancestors.delete(record); + } } - return JSON.stringify(value); + const encoded = JSON.stringify(value); + if (encoded === undefined) identityConflict("digest_input_must_be_json"); + return encoded; } function digestSync(value: unknown): string { @@ -1564,9 +1615,25 @@ export function createStockT3NativeRuntime(options: StockT3NativeRuntimeOptions) provisionalProjectId: identity.projectId, }); } - const workspaceMatches = (value: string): boolean => - workspaceComparisonKey(value, { platform: platform === "unknown" ? undefined : platform }) === - workspaceComparisonKey(input.workspaceRoot, { platform: platform === "unknown" ? undefined : platform }); + const workspaceMatches = (value: string): boolean => { + try { + const comparisonPlatform = platform === "unknown" ? undefined : platform; + const path = comparisonPlatform === "windows" ? win32 : posix; + if (!path.isAbsolute(value.trim())) return false; + return workspaceComparisonKey( + value, + { platform: comparisonPlatform }, + ) === workspaceComparisonKey( + input.workspaceRoot, + { platform: comparisonPlatform }, + ); + } catch (error) { + if (error instanceof StockRuntimeError && error.code === "identity_conflict") { + return false; + } + throw error; + } + }; let attempt: ProjectCreateAttemptState | undefined = identity === undefined ? undefined diff --git a/src/stockProof.ts b/src/stockProof.ts index 94be86c..4727a68 100644 --- a/src/stockProof.ts +++ b/src/stockProof.ts @@ -102,6 +102,19 @@ const EXPECTED_ISOLATED_BASENAMES = Object.freeze([ "server-home", "workspace", ]); +const PROOF_BODY_KEYS = new Set([ + "runId", + "candidateSha", + "stockSha", + "success", + "cleanBeforeBuild", + "artifactDigest", + "privateResolution", + "provenance", + "exactHttpNegative", + "live", + "teardown", +]); function record(value: unknown, reason = "not_object"): Record { if (typeof value !== "object" || value === null || Array.isArray(value)) { @@ -240,6 +253,9 @@ function commandResult( export function canonicalProofBody(value: unknown): StockProofBody { const input = record(value); + if (Object.keys(input).some((key) => !PROOF_BODY_KEYS.has(key))) { + throw new ProofReceiptError("unknown_top_level_key"); + } if (typeof input.runId !== "string" || input.runId.length < 8) throw new ProofReceiptError("run_id"); if (typeof input.candidateSha !== "string" || !SHA40.test(input.candidateSha)) throw new ProofReceiptError("candidate_sha"); if (input.stockSha !== EXPECTED_STOCK_SHA) throw new ProofReceiptError("stock_sha"); @@ -283,12 +299,15 @@ export function validateProofReceipt(value: unknown, expected: ExpectedProofIden } function canonical(value: unknown): string { + if (value === undefined) throw new ProofReceiptError("canonical_undefined"); if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`; if (typeof value === "object" && value !== null) { const input = value as Record; return `{${Object.keys(input).sort().map((key) => `${JSON.stringify(key)}:${canonical(input[key])}`).join(",")}}`; } - return JSON.stringify(value); + const encoded = JSON.stringify(value); + if (encoded === undefined) throw new ProofReceiptError("canonical_undefined"); + return encoded; } export function canonicalProofEnvelopeJson(bodyValue: unknown, checksum: string): string { diff --git a/src/stockT3Contracts.ts b/src/stockT3Contracts.ts index 431046b..4c989b3 100644 --- a/src/stockT3Contracts.ts +++ b/src/stockT3Contracts.ts @@ -41,7 +41,7 @@ function nullableString(value: unknown, path: string): string | null { return value === null ? null : string(value, path); } -function optionalNullableString(value: unknown, path: string): string | null | undefined { +export function nullableOptional(value: unknown, path: string): string | null | undefined { return value === undefined ? undefined : nullableString(value, path); } @@ -361,7 +361,3 @@ export function decodeTokenResult(value: unknown): { expiresIn: integer(input.expiresIn, "token.expiresIn"), }; } - -export function nullableOptional(value: unknown, path: string): string | null | undefined { - return optionalNullableString(value, path); -} diff --git a/src/stockT3HttpClient.ts b/src/stockT3HttpClient.ts index 7d58fd1..995df2a 100644 --- a/src/stockT3HttpClient.ts +++ b/src/stockT3HttpClient.ts @@ -77,6 +77,14 @@ function requestBudget(profile: ConnectionProfile): number { } const MAX_HTTP_IN_FLIGHT = 8; +const MAX_ENDPOINT_STATUS_TRACE = 2_048; + +interface CapacityWaiter { + readonly boundary: RequestBoundaryOptions; + readonly resolve: (release: () => void) => void; + readonly reject: (error: StockT3HttpError) => void; + readonly onAbort: () => void; +} function linkedAttemptSignal( external: AbortSignal | undefined, @@ -112,45 +120,27 @@ export function createStockT3HttpClient(options: StockT3HttpClientOptions) { let requestCount = 0; let inFlight = 0; let peakInFlight = 0; - const capacityWaiters: Array<() => void> = []; + const capacityWaiters: CapacityWaiter[] = []; - function capacityFailure(signal: AbortSignal | undefined): StockT3HttpError { - const reason = signal?.reason; + function capacityFailure(boundary: RequestBoundaryOptions): StockT3HttpError { + const reason = boundary.signal?.reason; return new StockT3HttpError("transport_unavailable", null, { reason: - reason instanceof DOMException && reason.name === "TimeoutError" + (reason instanceof DOMException && reason.name === "TimeoutError") || + (boundary.deadlineMs !== undefined && clock() >= boundary.deadlineMs) ? "deadline" : "cancelled", }); } - async function acquireCapacity(boundary: RequestBoundaryOptions): Promise<() => void> { - if (boundary.signal?.aborted) { - throw capacityFailure(boundary.signal); - } - if (inFlight >= MAX_HTTP_IN_FLIGHT) { - await new Promise((resolve, reject) => { - const onAbort = () => { - const index = capacityWaiters.indexOf(resume); - if (index >= 0) capacityWaiters.splice(index, 1); - reject(capacityFailure(boundary.signal)); - }; - const resume = () => { - boundary.signal?.removeEventListener("abort", onAbort); - resolve(); - }; - capacityWaiters.push(resume); - boundary.signal?.addEventListener("abort", onAbort, { once: true }); - }); - } - if ( - boundary.signal?.aborted || - (boundary.deadlineMs !== undefined && clock() >= boundary.deadlineMs) - ) { - throw new StockT3HttpError("transport_unavailable", null, { - reason: boundary.signal?.aborted ? "cancelled" : "deadline", - }); + function trace(entry: EndpointStatusTrace): void { + endpointStatusTrace.push(entry); + if (endpointStatusTrace.length > MAX_ENDPOINT_STATUS_TRACE) { + endpointStatusTrace.splice(0, endpointStatusTrace.length - MAX_ENDPOINT_STATUS_TRACE); } + } + + function claimCapacity(): () => void { inFlight += 1; peakInFlight = Math.max(peakInFlight, inFlight); let released = false; @@ -158,10 +148,53 @@ export function createStockT3HttpClient(options: StockT3HttpClientOptions) { if (released) return; released = true; inFlight -= 1; - capacityWaiters.shift()?.(); + pumpCapacity(); }; } + function pumpCapacity(): void { + while (inFlight < MAX_HTTP_IN_FLIGHT && capacityWaiters.length > 0) { + const waiter = capacityWaiters.shift()!; + waiter.boundary.signal?.removeEventListener("abort", waiter.onAbort); + if ( + waiter.boundary.signal?.aborted || + (waiter.boundary.deadlineMs !== undefined && clock() >= waiter.boundary.deadlineMs) + ) { + waiter.reject(capacityFailure(waiter.boundary)); + continue; + } + waiter.resolve(claimCapacity()); + } + } + + async function acquireCapacity(boundary: RequestBoundaryOptions): Promise<() => void> { + if ( + boundary.signal?.aborted || + (boundary.deadlineMs !== undefined && clock() >= boundary.deadlineMs) + ) { + throw capacityFailure(boundary); + } + if (inFlight < MAX_HTTP_IN_FLIGHT && capacityWaiters.length === 0) { + return claimCapacity(); + } + return new Promise<() => void>((resolve, reject) => { + const waiter: CapacityWaiter = { + boundary, + resolve, + reject, + onAbort: () => { + const index = capacityWaiters.indexOf(waiter); + if (index >= 0) capacityWaiters.splice(index, 1); + reject(capacityFailure(boundary)); + pumpCapacity(); + }, + }; + capacityWaiters.push(waiter); + boundary.signal?.addEventListener("abort", waiter.onAbort, { once: true }); + pumpCapacity(); + }); + } + async function requestJson( path: string, init: RequestInit, @@ -178,7 +211,6 @@ export function createStockT3HttpClient(options: StockT3HttpClientOptions) { } const attempt = linkedAttemptSignal(boundary.signal, timeoutMs, setTimer, clearTimer); const method = init.method ?? "GET"; - requestCount += 1; let releaseCapacity: (() => void) | undefined; try { releaseCapacity = await acquireCapacity({ ...boundary, signal: attempt.signal }); @@ -188,8 +220,12 @@ export function createStockT3HttpClient(options: StockT3HttpClientOptions) { if (authenticated && options.bearerToken !== undefined) { headers.set("authorization", `Bearer ${options.bearerToken}`); } - const response = await fetchImpl(new URL(path, baseUrl), { ...init, headers, signal: attempt.signal }); - endpointStatusTrace.push({ method, path, status: response.status }); + requestCount += 1; + const response = await fetchImpl( + new URL(path.replace(/^\/+/, ""), baseUrl), + { ...init, headers, signal: attempt.signal }, + ); + trace({ method, path, status: response.status }); let body: unknown; try { const text = await response.text(); @@ -208,9 +244,15 @@ export function createStockT3HttpClient(options: StockT3HttpClientOptions) { if (error instanceof ProtocolMismatchError) { throw new StockT3HttpError("protocol_mismatch", null, { path: error.path }); } - endpointStatusTrace.push({ method, path, status: null }); + trace({ method, path, status: null }); throw new StockT3HttpError("transport_unavailable", null, { - reason: boundary.signal?.aborted ? "cancelled" : "request_failed", + reason: + attempt.signal.reason instanceof DOMException && + attempt.signal.reason.name === "TimeoutError" + ? "deadline" + : boundary.signal?.aborted + ? "cancelled" + : "request_failed", }); } finally { releaseCapacity?.(); diff --git a/test/adaptive-poller.test.ts b/test/adaptive-poller.test.ts index 27c9800..8c9ee8f 100644 --- a/test/adaptive-poller.test.ts +++ b/test/adaptive-poller.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { getEventListeners } from "node:events"; import { createAdaptivePoller } from "../src/adaptivePoller"; import { StockT3HttpError } from "../src/stockT3HttpClient"; @@ -206,6 +207,141 @@ describe("environment-coalesced adaptive poller", () => { expect(policy.backoffMs(4, 20_000)).toBe(8_000); }); + test("removes each resolved default-sleep abort listener", async () => { + let sequence = 0; + let maximumAbortListeners = 0; + const poller = createAdaptivePoller({ + getShell: async ({ signal }) => { + maximumAbortListeners = Math.max( + maximumAbortListeners, + signal === undefined ? 0 : getEventListeners(signal, "abort").length, + ); + sequence += 1; + return { + snapshotSequence: sequence, + projects: [], + threads: [], + updatedAt: "2026-07-31T18:00:00.000Z", + }; + }, + getThread: async () => undefined, + }); + + await expect( + poller.waitFor({ + environmentId: "env-listeners", + threadId: "thread-listeners", + deadlineMs: Date.now() + 2_000, + evaluate: ({ shell }) => + shell.snapshotSequence >= 2 ? { done: true, value: "done" } : { done: false }, + }), + ).resolves.toBe("done"); + expect(maximumAbortListeners).toBeLessThanOrEqual(1); + poller.close(); + }, 3_000); + + test("dispatch observation interrupts stale failure backoff and resumes fast cadence", async () => { + let sleepCount = 0; + let shellCount = 0; + let backoffStarted!: () => void; + const enteredBackoff = new Promise((resolve) => { + backoffStarted = resolve; + }); + const poller = createAdaptivePoller({ + sleep: (milliseconds, signal) => { + sleepCount += 1; + if (sleepCount === 1 || sleepCount >= 3) return Promise.resolve(); + expect(milliseconds).toBe(500); + backoffStarted(); + return new Promise((_resolve, reject) => { + signal.addEventListener("abort", () => reject(signal.reason), { once: true }); + }); + }, + jitter: () => 0, + getShell: async () => { + shellCount += 1; + if (shellCount === 1) { + throw new StockT3HttpError("transport_unavailable", 503, { transient: true }); + } + return { + snapshotSequence: shellCount, + projects: [], + threads: [], + updatedAt: "2026-07-31T18:00:00.000Z", + }; + }, + getThread: async () => undefined, + }); + const wait = poller.waitFor({ + environmentId: "env-dispatch-wake", + threadId: "thread-dispatch-wake", + deadlineMs: Date.now() + 2_000, + evaluate: ({ shell }) => + shell.snapshotSequence >= 2 ? { done: true, value: "done" } : { done: false }, + }); + await enteredBackoff; + poller.dispatchObserved("env-dispatch-wake"); + + try { + const outcome = await Promise.race([ + wait, + new Promise((resolve) => setTimeout(() => resolve("stalled"), 100)), + ]); + expect(outcome).toBe("done"); + expect(sleepCount).toBe(2); + } finally { + poller.close(); + await Promise.allSettled([wait]); + } + }); + + test("a detail-slot timeout expires only the earliest subscriber", async () => { + let current = 0; + let scriptedNow: number[] = []; + let shellSequence = 0; + const now = () => { + const next = scriptedNow.shift(); + if (next !== undefined) current = next; + return current; + }; + const poller = createAdaptivePoller({ + now, + sleep: async (milliseconds) => { + current += milliseconds; + }, + getShell: async () => ({ + snapshotSequence: ++shellSequence, + projects: [], + threads: [], + updatedAt: "2026-07-31T18:00:00.000Z", + }), + getThread: async () => ({ ...emptyDetail, snapshotSequence: shellSequence }), + }); + const earliest = poller.waitFor({ + environmentId: "env-detail-deadline", + threadId: "thread-1", + deadlineMs: 251, + evaluate: ({ detail }) => + detail === undefined ? { done: false, detail: true } : { done: true, value: "earliest" }, + }).catch((error) => error); + const survivor = poller.waitFor({ + environmentId: "env-detail-deadline", + threadId: "thread-1", + deadlineMs: 2_000, + evaluate: ({ detail }) => { + if (detail === undefined) { + if (shellSequence === 1) scriptedNow = [250, 250, 250, 251]; + return { done: false, detail: true }; + } + return { done: true, value: "survivor" }; + }, + }); + + await expect(earliest).resolves.toMatchObject({ code: "timeout" }); + await expect(survivor).resolves.toBe("survivor"); + poller.close(); + }); + test("coalesces same-thread detail work and fans one observation to every waiter", async () => { const clock = fakeClock(); let detailStarts = 0; diff --git a/test/pr4-review-regressions.test.ts b/test/pr4-review-regressions.test.ts new file mode 100644 index 0000000..15b4206 --- /dev/null +++ b/test/pr4-review-regressions.test.ts @@ -0,0 +1,179 @@ +import { describe, expect, test } from "bun:test"; + +import { + StockRuntimeError, + canonicalizeWorkspaceRoot, + parseProjectCreateIdentity, + type SpawnResult, + type StockSpawnInput, + type ThreadCreateReceipt, + type TurnReceipt, +} from "../src/facade"; +import { + createStockT3NativeRuntime, + digestStockSpawnInput, +} from "../src/nativeRuntime"; +import { createStockT3Facade } from "../src/facade"; + +const modelSelection = { instanceId: "claudeAgent", model: "claude-opus-5" }; +const spawnInput: StockSpawnInput = { + workspaceRoot: "/tmp/project", + title: "worker", + message: "start", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, +}; + +describe("PR #4 review regressions", () => { + test("project identity rejects cycles as a typed identity conflict", () => { + const cyclic: Record = {}; + cyclic.self = cyclic; + + let error: unknown; + try { + parseProjectCreateIdentity({ + projectId: "project-1", + commandId: "command-1", + createdAt: "2026-07-31T18:00:00.000Z", + workspaceRoot: "/tmp/project", + title: "project", + defaultModelSelection: { ...modelSelection, options: [cyclic] }, + }); + } catch (cause) { + error = cause; + } + + expect(error).toBeInstanceOf(StockRuntimeError); + expect(error).not.toBeInstanceOf(RangeError); + expect(error).toMatchObject({ code: "identity_conflict" }); + }); + + test("spawn digests omit undefined object fields but distinguish undefined array entries", async () => { + const omitted = await digestStockSpawnInput(spawnInput); + const explicitObjectUndefined = await digestStockSpawnInput({ ...spawnInput, projectId: undefined }); + const emptyOptions = await digestStockSpawnInput({ + ...spawnInput, + modelSelection: { ...modelSelection, options: [] }, + }); + const undefinedOption = await digestStockSpawnInput({ + ...spawnInput, + modelSelection: { ...modelSelection, options: [undefined] }, + }); + + expect(explicitObjectUndefined).toBe(omitted); + expect(undefinedOption).not.toBe(emptyOptions); + }); + + test("cross-platform workspace expansion requires an unambiguous absolute path", () => { + expect(() => canonicalizeWorkspaceRoot("~/project", { platform: "windows" })).toThrow( + StockRuntimeError, + ); + expect(() => canonicalizeWorkspaceRoot("relative/project", { platform: "windows" })).toThrow( + StockRuntimeError, + ); + expect(canonicalizeWorkspaceRoot("C:\\work\\project", { platform: "windows" })).toBe( + "C:\\work\\project", + ); + }); + + test("an unrelated invalid server workspace root is non-matching", async () => { + const runtime = createStockT3NativeRuntime({ + client: { + getDescriptor: async () => ({ + environmentId: "env-1", + label: "fixture", + platform: { os: "darwin", arch: "arm64" }, + serverVersion: "stock", + capabilities: { repositoryIdentity: false }, + }), + getShell: async () => ({ + snapshotSequence: 1, + projects: [ + { + id: "blank-root", + title: "unrelated", + workspaceRoot: " ", + defaultModelSelection: modelSelection, + scripts: [], + createdAt: "2026-07-31T18:00:00.000Z", + updatedAt: "2026-07-31T18:00:00.000Z", + }, + { + id: "relative-root", + title: "unrelated", + workspaceRoot: "relative/project", + defaultModelSelection: modelSelection, + scripts: [], + createdAt: "2026-07-31T18:00:00.000Z", + updatedAt: "2026-07-31T18:00:00.000Z", + }, + ], + threads: [], + updatedAt: "2026-07-31T18:00:00.000Z", + }), + getThread: async () => undefined, + dispatch: async () => { + throw new Error("dispatch must not run"); + }, + }, + }); + + await expect(runtime.spawn({ ...spawnInput, workspaceRoot: "relative/project" })).rejects.toMatchObject({ + code: "identity_conflict", + evidence: { reason: "project_create_identity_required" }, + }); + runtime.close(); + }); + + test("unknown client defects are not mislabeled as transport failures", async () => { + const runtime = createStockT3NativeRuntime({ + client: { + getDescriptor: async () => { + throw new TypeError("fixture decoder defect"); + }, + getShell: async () => { + throw new Error("unused"); + }, + getThread: async () => undefined, + dispatch: async () => ({ sequence: 1 }), + }, + }); + + await expect(runtime.spawn(spawnInput)).rejects.toMatchObject({ + code: "internal_error", + evidence: { errorName: "TypeError" }, + }); + runtime.close(); + }); + + test("facade exports result types and forwards metrics plus teardown", () => { + const _compileOnly: [SpawnResult?, ThreadCreateReceipt?, TurnReceipt?] = []; + expect(_compileOnly).toEqual([]); + const runtime = createStockT3NativeRuntime({ + client: { + getDescriptor: async () => { + throw new Error("unused"); + }, + getShell: async () => { + throw new Error("unused"); + }, + getThread: async () => undefined, + dispatch: async () => ({ sequence: 1 }), + observations: () => ({ + requestCount: 4, + inFlight: 0, + peakInFlight: 2, + endpointStatusTrace: [], + }), + }, + }); + const facade = createStockT3Facade(runtime); + + expect(facade.pollMetrics()).toMatchObject({ activeWaits: 0 }); + expect(facade.httpObservations()).toMatchObject({ requestCount: 4, peakInFlight: 2 }); + expect(() => facade.close()).not.toThrow(); + }); +}); diff --git a/test/r8-runtime-regressions.test.ts b/test/r8-runtime-regressions.test.ts index 8c9fbe7..2a39fb1 100644 --- a/test/r8-runtime-regressions.test.ts +++ b/test/r8-runtime-regressions.test.ts @@ -472,10 +472,7 @@ describe("round 8 all-mutation result invariant over stock HTTP", () => { const runtime = createStockT3NativeRuntime({ baseUrl: "http://stock.invalid", fetch: fixture.fetch, - id: - stage === "project.create" - ? ids("create-1", "thread-1", "turn-1", "message-1", "lease-1") - : ids("create-1", "thread-1", "turn-1", "message-1", "lease-1"), + id: ids("create-1", "thread-1", "turn-1", "message-1", "lease-1"), now: () => iso, }); diff --git a/test/stock-only-gate.test.ts b/test/stock-only-gate.test.ts index c78d9cd..755c278 100644 --- a/test/stock-only-gate.test.ts +++ b/test/stock-only-gate.test.ts @@ -90,4 +90,20 @@ describe("stock-only gate", () => { expect(result.exitCode).not.toBe(0); expect(result.stderr).toContain("forbidden candidate reference"); }); + + test("fails when the protected historical SHA-256 does not match", async () => { + const root = await fixtureRepo({ + "package.json": "{}\n", + "historical.test.ts": "historical evidence\n", + }); + const historical = join(root, "historical.test.ts"); + const result = await run( + ["bash", join(import.meta.dir, "../scripts/check-stock-only.sh")], + root, + { STOCK_ONLY_HISTORICAL_PATH: historical, STOCK_ONLY_HISTORICAL_SHA256: "0".repeat(64) }, + ); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("historical evidence SHA-256 mismatch"); + }); }); diff --git a/test/stock-t3-exact-stock-negative.test.ts b/test/stock-t3-exact-stock-negative.test.ts index 3d24ecd..52452c1 100644 --- a/test/stock-t3-exact-stock-negative.test.ts +++ b/test/stock-t3-exact-stock-negative.test.ts @@ -13,7 +13,9 @@ describe("exact-stock characterization driver", () => { expect(source).toContain( "corepack pnpm --filter t3 exec vp test run src/orchestration/Layers/T3LayerStockProjectionCharacterization.generated.test.ts", ); - expect(source).toContain("trap cleanup EXIT INT TERM"); + expect(source).toContain("trap cleanup EXIT"); + expect(source).toContain("trap 'handle_signal 130' INT"); + expect(source).toContain("trap 'handle_signal 143' TERM"); expect(source).toContain('rm -f -- "$generated_path"'); expect(source).toContain('if [[ -e "$generated_path" ]]'); expect(source).toContain("generated characterization path already exists"); diff --git a/test/stock-t3-http-client.test.ts b/test/stock-t3-http-client.test.ts index 08d93f9..67e9d0d 100644 --- a/test/stock-t3-http-client.test.ts +++ b/test/stock-t3-http-client.test.ts @@ -93,6 +93,28 @@ describe("stock T3 HTTP client", () => { expect(requests[1]?.headers.get("authorization")).toBe("Bearer bearer-secret"); }); + test("preserves a reverse-proxy path prefix on every stock endpoint", async () => { + const paths: string[] = []; + const client = createStockT3HttpClient({ + baseUrl: "https://relay.invalid/t3/environment-one///", + bearerToken: "secret", + fetch: async (input) => { + const path = new URL(input.toString()).pathname; + paths.push(path); + if (path.endsWith("/.well-known/t3/environment")) return Response.json(descriptor); + return Response.json({ snapshotSequence: 0, projects: [], threads: [], updatedAt: "2026-07-31T18:00:00.000Z" }); + }, + }); + + await client.getDescriptor(); + await client.getShell(); + + expect(paths).toEqual([ + "/t3/environment-one/.well-known/t3/environment", + "/t3/environment-one/api/orchestration/shell", + ]); + }); + test("sanitizes bearer values and response bodies from typed failures", async () => { const fetch = async () => new Response( @@ -227,6 +249,68 @@ describe("stock T3 HTTP client", () => { expect(client.observations()).toMatchObject({ inFlight: 0 }); }); + test("hands capacity off FIFO without stalling after an expired queued request", async () => { + let current = 0; + const starts: string[] = []; + const releases = new Map void>(); + const clearedTimers: unknown[] = []; + const client = createStockT3HttpClient({ + baseUrl: "http://127.0.0.1:3774", + clock: () => current, + setTimer: () => Symbol("timer"), + clearTimer: (timer) => clearedTimers.push(timer), + fetch: async (_input, init) => { + const command = JSON.parse(String(init?.body)) as { id: string }; + starts.push(command.id); + await new Promise((resolve) => releases.set(command.id, resolve)); + return Response.json({ sequence: 1 }); + }, + }); + const occupying = Array.from({ length: 8 }, (_, index) => + client.dispatch({ id: `held-${index}` }), + ); + while (starts.length < 8) await Promise.resolve(); + const expired = client.dispatch({ id: "expired" }, { deadlineMs: 5 }).catch((error) => error); + const first = client.dispatch({ id: "first" }, { deadlineMs: 100 }); + const second = client.dispatch({ id: "second" }, { deadlineMs: 100 }); + + current = 10; + releases.get("held-0")?.(); + for (let spin = 0; spin < 20 && !starts.includes("first"); spin += 1) { + await Promise.resolve(); + } + expect(await expired).toMatchObject({ + code: "transport_unavailable", + detail: { reason: "deadline" }, + }); + expect(starts).toContain("first"); + expect(starts).not.toContain("second"); + expect(client.observations().peakInFlight).toBe(8); + + releases.get("first")?.(); + for (let spin = 0; spin < 20 && !starts.includes("second"); spin += 1) { + await Promise.resolve(); + } + expect(starts).toContain("second"); + + releases.get("second")?.(); + for (let index = 1; index < 8; index += 1) releases.get(`held-${index}`)?.(); + await Promise.all([...occupying, first, second]); + expect(client.observations()).toMatchObject({ inFlight: 0, peakInFlight: 8 }); + expect(clearedTimers).toHaveLength(11); + }); + + test("bounds the retained endpoint trace while preserving the total request count", async () => { + const client = createStockT3HttpClient({ + baseUrl: "http://127.0.0.1:3774", + fetch: async () => Response.json(descriptor), + }); + for (let index = 0; index < 2_050; index += 1) await client.getDescriptor(); + + expect(client.observations()).toMatchObject({ requestCount: 2_050 }); + expect(client.observations().endpointStatusTrace).toHaveLength(2_048); + }); + test.each([ ["local", 251, true], ["local", 4_999, true], diff --git a/test/stock-t3-live-harness.test.ts b/test/stock-t3-live-harness.test.ts index b3e2d22..27dd732 100644 --- a/test/stock-t3-live-harness.test.ts +++ b/test/stock-t3-live-harness.test.ts @@ -54,6 +54,12 @@ async function canaryFixture(prefix = "t3layer-canary.") { const config = join(root, "config.json"); await Bun.write(artifact, "immutable artifact\n"); await Bun.write(config, '{"schema":"stock-http-v1","acceleration":"off"}\n'); + const artifactDigest = new Bun.CryptoHasher("sha256") + .update(await Bun.file(artifact).arrayBuffer()) + .digest("hex"); + const configDigest = new Bun.CryptoHasher("sha256") + .update(await Bun.file(config).arrayBuffer()) + .digest("hex"); return { root, log, @@ -71,6 +77,8 @@ async function canaryFixture(prefix = "t3layer-canary.") { T3_STOCK_CANCEL_WAITS_COMMAND: paths.cancel!, T3_STOCK_ARTIFACT_PATH: artifact, T3_STOCK_CONFIG_PATH: config, + T3_STOCK_APPROVED_ARTIFACT_SHA256: artifactDigest, + T3_STOCK_APPROVED_CONFIG_SHA256: configDigest, T3_STOCK_DRILL_RECEIPT_PATH: join(root, "receipt.json"), }, }; @@ -148,7 +156,7 @@ describe("stock live harness lifecycle", () => { expect(source).toContain('validate-provisional'); expect(source).toContain('validate-envelope'); expect(source).toContain("stat -f '%Lp'"); - expect(source).toContain('staging_bytes=$(shasum -a 256'); + expect(source).toContain('staging_bytes=$(sha256_file "$final_staging")'); expect(source).toContain('if [[ ! -e "$proof_root" ]]'); expect(source).not.toContain('--header "Authorization: Bearer $http_token"'); expect(source).toContain('--header @-'); @@ -242,6 +250,32 @@ describe("stock live harness lifecycle", () => { }); }); + test("test-mode teardown failure cannot exit zero or publish a receipt", async () => { + const root = await mkdtemp(join(tmpdir(), "t3layer-harness-teardown-failure.")); + temporaryRoots.push(root); + const target = join(root, "proof.json"); + const rootRecord = join(root, "proof-root.txt"); + const runner = join(root, "runner"); + await Bun.write( + runner, + `#!/usr/bin/env bash\nset -euo pipefail\nprintf '%s' "$2" > '${rootRecord}'\n`, + ); + await chmod(runner, 0o700); + const result = await run(["bash", "scripts/stock-t3-live-harness.sh"], { + T3_STOCK_PROVIDER_SECRET_REF: "op://fixture/provider/key", + T3_STOCK_HARNESS_TEST_MODE: "1", + T3_STOCK_HARNESS_COMMAND_RUNNER: runner, + T3_STOCK_FAIL_TEARDOWN_AT: "root", + T3_STOCK_PROOF_TARGET: target, + }); + const strandedRoot = await Bun.file(rootRecord).text(); + temporaryRoots.push(strandedRoot); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("cleanup root_removed=false"); + expect(await Bun.file(target).exists()).toBe(false); + }); + test("requires caller-held runId and candidateSha instead of trusting a stale path", async () => { const prior = canonicalProofBody({ ...completeBody(), runId: "prior-run" }); expect(() => @@ -256,6 +290,11 @@ describe("stock live harness lifecycle", () => { const body = completeBody(); expect(() => canonicalProofBody({ ...body, live: { ...body.live, endpointStatusTrace: [] } })).toThrow(ProofReceiptError); expect(() => canonicalProofBody({ ...body, provenance: undefined })).toThrow(ProofReceiptError); + expect(() => canonicalProofBody({ ...body, forgedTopLevel: true })).toThrow(ProofReceiptError); + expect(() => canonicalProofJson({ + ...body, + live: { ...body.live, unexpectedUndefined: undefined }, + })).toThrow(ProofReceiptError); const checksum = await proofChecksum(body); const envelope = { ...body, checksum }; expect(await validateProofEnvelope(envelope, { runId: body.runId, candidateSha: body.candidateSha })).toEqual(canonicalProofBody(body)); @@ -421,6 +460,42 @@ describe("stock live harness lifecycle", () => { await expect(Bun.file(fixture.receipt).json()).resolves.toMatchObject({ success: true }); }); + test("execute mode rejects unapproved artifact or config bytes before routing", async () => { + const fixture = await canaryFixture(); + const result = await run(["bash", "scripts/stock-t3-canary-drill.sh", "--execute"], { + ...fixture.env, + T3_STOCK_APPROVED_ARTIFACT_SHA256: "0".repeat(64), + }); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("approved digest mismatch"); + expect(await Bun.file(fixture.log).exists()).toBe(false); + }); + + test("SIGINT during execute mode recovers and exits 130", async () => { + const fixture = await canaryFixture(); + await Bun.write( + fixture.paths.canary!, + `#!/usr/bin/env bash\nset -euo pipefail\nsleep 0.2\nprintf '%s\\n' canary >> '${fixture.log}'\n`, + ); + await chmod(fixture.paths.canary!, 0o700); + const child = Bun.spawn(["bash", "scripts/stock-t3-canary-drill.sh", "--execute"], { + cwd: join(import.meta.dir, ".."), + env: { ...Bun.env, ...fixture.env }, + stdout: "pipe", + stderr: "pipe", + }); + await new Promise((resolve) => setTimeout(resolve, 50)); + child.kill("SIGINT"); + const [exitCode, stderr] = await Promise.all([ + child.exited, + new Response(child.stderr).text(), + ]); + + expect(exitCode).toBe(130); + expect(stderr).toContain("CANARY_RECOVERY:"); + }); + test("execute mode rejects artifact drift and environment identity drift", async () => { const artifactFixture = await canaryFixture(); await Bun.write( diff --git a/test/stock-t3-live.test.ts b/test/stock-t3-live.test.ts index fc4d83b..d564a42 100644 --- a/test/stock-t3-live.test.ts +++ b/test/stock-t3-live.test.ts @@ -57,6 +57,10 @@ describe.skipIf(!live)("isolated exact-stock live proof", () => { if (detail === undefined) throw new Error("live thread disappeared before receipt capture"); const http = runtime.httpObservations(); const polls = runtime.pollMetrics(); + const requiredSequence = (value: number | null, label: string): number => { + if (value === null) throw new Error(`${label} accepted sequence is required`); + return value; + }; const provisional = canonicalProvisionalProof({ provisional: true, @@ -75,9 +79,9 @@ describe.skipIf(!live)("isolated exact-stock live proof", () => { followupMessageId: sent.messageId, }, sequences: { - create: spawned.createReceipt.acceptedSequence, - initial: spawned.turnReceipt.acceptedSequence, - followup: sent.acceptedSequence, + create: requiredSequence(spawned.createReceipt.acceptedSequence, "create"), + initial: requiredSequence(spawned.turnReceipt.acceptedSequence, "initial"), + followup: requiredSequence(sent.acceptedSequence, "followup"), }, terminalKinds: [first.kind, second.kind], counters: { From d05e9dd032b5a0515c4d154308d1ed35a10dc73a Mon Sep 17 00:00:00 2001 From: Etan Joseph Heyman Date: Sat, 1 Aug 2026 21:18:33 +0300 Subject: [PATCH 4/5] fix: address PR 4 round 2 review Co-Authored-By: Claude Opus 5 (1M context) --- scripts/stock-proof-cli.ts | 5 +- scripts/stock-t3-canary-drill.sh | 17 ++++++- scripts/stock-t3-exact-characterization.sh | 7 ++- test/adaptive-poller.test.ts | 48 ++++++++++++------- test/stock-t3-exact-stock-negative.test.ts | 6 +++ test/stock-t3-http-client.test.ts | 6 ++- test/stock-t3-live-harness.test.ts | 56 +++++++++++++++++++++- 7 files changed, 122 insertions(+), 23 deletions(-) diff --git a/scripts/stock-proof-cli.ts b/scripts/stock-proof-cli.ts index 38e7858..bac385e 100644 --- a/scripts/stock-proof-cli.ts +++ b/scripts/stock-proof-cli.ts @@ -17,8 +17,9 @@ if (command === "validate-provisional") { const candidateSha = third; if (!candidateSha) throw new TypeError("candidate SHA is required"); const checksum = await proofChecksum(value); - await Bun.write(first, canonicalProofEnvelopeJson(value, checksum)); - await validateProofEnvelope(await Bun.file(first).json(), { runId: second, candidateSha }); + const envelope = canonicalProofEnvelopeJson(value, checksum); + await validateProofEnvelope(JSON.parse(envelope), { runId: second, candidateSha }); + await Bun.write(first, envelope); } else if (command === "validate-envelope") { if (!first || !second) throw new TypeError("expected identity is required"); await validateProofEnvelope(value, { runId: first, candidateSha: second }); diff --git a/scripts/stock-t3-canary-drill.sh b/scripts/stock-t3-canary-drill.sh index 0bb564e..9e8a8f7 100644 --- a/scripts/stock-t3-canary-drill.sh +++ b/scripts/stock-t3-canary-drill.sh @@ -43,6 +43,16 @@ sha256_file() { fi } +file_mode() { + local path=$1 + local mode + if mode=$(/usr/bin/stat -c '%a' "$path" 2>/dev/null); then + printf '%s\n' "$mode" + else + /usr/bin/stat -f '%Lp' "$path" + fi +} + commands=( "$T3_STOCK_ROUTE_OFF_COMMAND" "$T3_STOCK_ROUTE_CANARY_COMMAND" @@ -92,6 +102,10 @@ verify_artifact() { } record_status() { + if [[ ${T3_STOCK_FAIL_STATUS_AT:-} == "$1" ]]; then + echo "ERROR: injected status-record failure: $1" >&2 + return 92 + fi command_statuses=$(/usr/bin/jq -c --arg name "$1" --argjson status "$2" '. + [{name:$name,status:$status}]' <<<"$command_statuses") } @@ -113,6 +127,7 @@ recover() { local exit_status=$? if [[ $# -eq 1 ]]; then exit_status=$1; fi trap - EXIT INT TERM + set +e if [[ "$drill_complete" != true && "$recovery_armed" == true ]]; then local prior_status=0 "$T3_STOCK_ROUTE_PRIOR_CONFIG_COMMAND" || prior_status=$? @@ -224,7 +239,7 @@ checksum=$(sha256_file "$body_staging") mv -f -- "$staging" "$T3_STOCK_DRILL_RECEIPT_PATH" chmod 600 "$T3_STOCK_DRILL_RECEIPT_PATH" rm -f -- "$body_staging" -if [[ $(/usr/bin/stat -f '%Lp' "$T3_STOCK_DRILL_RECEIPT_PATH") != 600 ]]; then +if [[ $(file_mode "$T3_STOCK_DRILL_RECEIPT_PATH") != 600 ]]; then echo "ERROR: canary receipt mode mismatch" >&2 exit 2 fi diff --git a/scripts/stock-t3-exact-characterization.sh b/scripts/stock-t3-exact-characterization.sh index 9f5a7a0..f5f68c7 100755 --- a/scripts/stock-t3-exact-characterization.sh +++ b/scripts/stock-t3-exact-characterization.sh @@ -12,9 +12,14 @@ if [[ "$actual_sha" != "$expected_sha" ]]; then exit 2 fi +if [[ -n $(/usr/bin/git -C "$stock_tree" status --porcelain --untracked-files=all) ]]; then + echo "ERROR: exact stock worktree is not clean" >&2 + exit 3 +fi + if [[ -e "$generated_path" ]]; then echo "ERROR: generated characterization path already exists" >&2 - exit 3 + exit 4 fi cleanup() { diff --git a/test/adaptive-poller.test.ts b/test/adaptive-poller.test.ts index 8c9ee8f..90be560 100644 --- a/test/adaptive-poller.test.ts +++ b/test/adaptive-poller.test.ts @@ -209,13 +209,21 @@ describe("environment-coalesced adaptive poller", () => { test("removes each resolved default-sleep abort listener", async () => { let sequence = 0; - let maximumAbortListeners = 0; + const observedSignals = new Set(); + const shellSignals = new Set(); + const originalAddEventListener = AbortSignal.prototype.addEventListener; + AbortSignal.prototype.addEventListener = function ( + this: AbortSignal, + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, + ) { + if (type === "abort") observedSignals.add(this); + return originalAddEventListener.call(this, type, listener, options); + } as typeof originalAddEventListener; const poller = createAdaptivePoller({ getShell: async ({ signal }) => { - maximumAbortListeners = Math.max( - maximumAbortListeners, - signal === undefined ? 0 : getEventListeners(signal, "abort").length, - ); + if (signal !== undefined) shellSignals.add(signal); sequence += 1; return { snapshotSequence: sequence, @@ -227,17 +235,25 @@ describe("environment-coalesced adaptive poller", () => { getThread: async () => undefined, }); - await expect( - poller.waitFor({ - environmentId: "env-listeners", - threadId: "thread-listeners", - deadlineMs: Date.now() + 2_000, - evaluate: ({ shell }) => - shell.snapshotSequence >= 2 ? { done: true, value: "done" } : { done: false }, - }), - ).resolves.toBe("done"); - expect(maximumAbortListeners).toBeLessThanOrEqual(1); - poller.close(); + try { + await expect( + poller.waitFor({ + environmentId: "env-listeners", + threadId: "thread-listeners", + deadlineMs: Date.now() + 2_000, + evaluate: ({ shell }) => + shell.snapshotSequence >= 2 ? { done: true, value: "done" } : { done: false }, + }), + ).resolves.toBe("done"); + const cycleSignals = [...observedSignals].filter((signal) => !shellSignals.has(signal)); + expect(cycleSignals.length).toBeGreaterThan(0); + expect(cycleSignals.every((signal) => getEventListeners(signal, "abort").length === 0)).toBe( + true, + ); + } finally { + AbortSignal.prototype.addEventListener = originalAddEventListener; + poller.close(); + } }, 3_000); test("dispatch observation interrupts stale failure backoff and resumes fast cadence", async () => { diff --git a/test/stock-t3-exact-stock-negative.test.ts b/test/stock-t3-exact-stock-negative.test.ts index 52452c1..59af0c1 100644 --- a/test/stock-t3-exact-stock-negative.test.ts +++ b/test/stock-t3-exact-stock-negative.test.ts @@ -19,6 +19,12 @@ describe("exact-stock characterization driver", () => { expect(source).toContain('rm -f -- "$generated_path"'); expect(source).toContain('if [[ -e "$generated_path" ]]'); expect(source).toContain("generated characterization path already exists"); + const shaCheck = source.indexOf('if [[ "$actual_sha" != "$expected_sha" ]]'); + const cleanCheck = source.indexOf("status --porcelain"); + const generatedCollisionCheck = source.indexOf('if [[ -e "$generated_path" ]]'); + expect(cleanCheck).toBeGreaterThan(shaCheck); + expect(cleanCheck).toBeLessThan(generatedCollisionCheck); + expect(source).toContain("exact stock worktree is not clean"); }); test.skipIf(exactTree === undefined || exactToolchain === undefined)( diff --git a/test/stock-t3-http-client.test.ts b/test/stock-t3-http-client.test.ts index 67e9d0d..920375a 100644 --- a/test/stock-t3-http-client.test.ts +++ b/test/stock-t3-http-client.test.ts @@ -229,7 +229,8 @@ describe("stock T3 HTTP client", () => { }, }); const occupying = Array.from({ length: 8 }, () => client.getDescriptor()); - while (starts < 8) await Promise.resolve(); + for (let spin = 0; spin < 100 && starts < 8; spin += 1) await Promise.resolve(); + expect(starts).toBe(8); const queued = client.getDescriptor({ deadlineMs: Date.now() + 5 }).catch((error) => error); try { const outcome = await Promise.race([ @@ -269,7 +270,8 @@ describe("stock T3 HTTP client", () => { const occupying = Array.from({ length: 8 }, (_, index) => client.dispatch({ id: `held-${index}` }), ); - while (starts.length < 8) await Promise.resolve(); + for (let spin = 0; spin < 100 && starts.length < 8; spin += 1) await Promise.resolve(); + expect(starts).toHaveLength(8); const expired = client.dispatch({ id: "expired" }, { deadlineMs: 5 }).catch((error) => error); const first = client.dispatch({ id: "first" }, { deadlineMs: 100 }); const second = client.dispatch({ id: "second" }, { deadlineMs: 100 }); diff --git a/test/stock-t3-live-harness.test.ts b/test/stock-t3-live-harness.test.ts index 27dd732..df1ad38 100644 --- a/test/stock-t3-live-harness.test.ts +++ b/test/stock-t3-live-harness.test.ts @@ -5,6 +5,7 @@ import { tmpdir } from "node:os"; import { ProofReceiptError, canonicalProofBody, + canonicalProofEnvelopeJson, canonicalProofJson, proofChecksum, validateProofEnvelope, @@ -424,6 +425,28 @@ describe("stock live harness lifecycle", () => { expect((await Bun.file(receipt).text()).endsWith("\n")).toBe(true); }); + test("proof CLI validates expected identity before replacing an existing receipt", async () => { + const root = await mkdtemp(join(tmpdir(), "t3layer-proof-cli-preserve.")); + temporaryRoots.push(root); + const priorBody = { ...completeBody(), runId: "prior-run" }; + const currentBody = completeBody(); + const draft = join(root, "draft.json"); + const receipt = join(root, "receipt.json"); + const priorChecksum = await proofChecksum(priorBody); + const priorEnvelope = canonicalProofEnvelopeJson(priorBody, priorChecksum); + await Bun.write(draft, JSON.stringify(currentBody)); + await Bun.write(receipt, priorEnvelope); + await chmod(receipt, 0o600); + + const published = await run([ + "bun", "scripts/stock-proof-cli.ts", "publish", draft, receipt, + "wrong-current-run", currentBody.candidateSha, + ]); + + expect(published.exitCode).not.toBe(0); + expect(await Bun.file(receipt).text()).toBe(priorEnvelope); + }); + test("deploy drill dry-run records the required immutable-artifact transitions", async () => { const result = await run(["bash", "scripts/stock-t3-canary-drill.sh", "--dry-run"]); expect(result.exitCode).toBe(0); @@ -460,6 +483,17 @@ describe("stock live harness lifecycle", () => { await expect(Bun.file(fixture.receipt).json()).resolves.toMatchObject({ success: true }); }); + test("execute mode uses a portable receipt-mode probe", async () => { + const source = await Bun.file( + join(import.meta.dir, "../scripts/stock-t3-canary-drill.sh"), + ).text(); + + expect(source).toContain("file_mode() {"); + expect(source).toContain("/usr/bin/stat -c '%a'"); + expect(source).toContain("/usr/bin/stat -f '%Lp'"); + expect(source).not.toContain("if [[ $(/usr/bin/stat -f '%Lp'"); + }); + test("execute mode rejects unapproved artifact or config bytes before routing", async () => { const fixture = await canaryFixture(); const result = await run(["bash", "scripts/stock-t3-canary-drill.sh", "--execute"], { @@ -485,7 +519,11 @@ describe("stock live harness lifecycle", () => { stdout: "pipe", stderr: "pipe", }); - await new Promise((resolve) => setTimeout(resolve, 50)); + for (let attempt = 0; attempt < 200; attempt += 1) { + if (await Bun.file(fixture.log).exists()) break; + await new Promise((resolve) => setTimeout(resolve, 10)); + } + expect(await Bun.file(fixture.log).exists()).toBe(true); child.kill("SIGINT"); const [exitCode, stderr] = await Promise.all([ child.exited, @@ -548,4 +586,20 @@ describe("stock live harness lifecycle", () => { expect(commands.at(-1), seam).toBe("cancel"); } }, 60_000); + + test("recovery completes safety commands when status bookkeeping fails", async () => { + const fixture = await canaryFixture(); + const result = await run(["bash", "scripts/stock-t3-canary-drill.sh", "--execute"], { + ...fixture.env, + T3_STOCK_FAIL_AT: "route-canary", + T3_STOCK_FAIL_STATUS_AT: "recovery-prior-config", + }); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain( + "injected status-record failure: recovery-prior-config", + ); + const commands = (await Bun.file(fixture.log).text()).trim().split("\n"); + expect(commands.slice(-3)).toEqual(["prior", "off", "cancel"]); + }); }); From b95c3ee8e5ea8e92369c681ad69e76f893cb3fcb Mon Sep 17 00:00:00 2001 From: Etan Joseph Heyman Date: Sat, 1 Aug 2026 21:34:45 +0300 Subject: [PATCH 5/5] fix: harden PR 4 final ops edge cases Co-Authored-By: Claude Opus 5 (1M context) --- scripts/stock-t3-canary-drill.sh | 4 +- scripts/stock-t3-exact-characterization.sh | 29 ++++- test/stock-t3-exact-stock-negative.test.ts | 139 ++++++++++++++++++++- test/stock-t3-live-harness.test.ts | 13 ++ 4 files changed, 176 insertions(+), 9 deletions(-) diff --git a/scripts/stock-t3-canary-drill.sh b/scripts/stock-t3-canary-drill.sh index 9e8a8f7..dfce1e6 100644 --- a/scripts/stock-t3-canary-drill.sh +++ b/scripts/stock-t3-canary-drill.sh @@ -20,6 +20,9 @@ if [[ "$mode" != "--execute" ]]; then exit 2 fi +: "${T3_STOCK_DRILL_RECEIPT_PATH:?required receipt path}" +rm -f -- "$T3_STOCK_DRILL_RECEIPT_PATH" + : "${T3_STOCK_ROUTE_OFF_COMMAND:?required executable path}" : "${T3_STOCK_ROUTE_CANARY_COMMAND:?required executable path}" : "${T3_STOCK_ROUTE_PROMOTE_COMMAND:?required executable path}" @@ -32,7 +35,6 @@ fi : "${T3_STOCK_CONFIG_PATH:?required redacted config path}" : "${T3_STOCK_APPROVED_ARTIFACT_SHA256:?required approved artifact SHA-256}" : "${T3_STOCK_APPROVED_CONFIG_SHA256:?required approved config SHA-256}" -: "${T3_STOCK_DRILL_RECEIPT_PATH:?required receipt path}" sha256_file() { local path=$1 diff --git a/scripts/stock-t3-exact-characterization.sh b/scripts/stock-t3-exact-characterization.sh index f5f68c7..824b34d 100755 --- a/scripts/stock-t3-exact-characterization.sh +++ b/scripts/stock-t3-exact-characterization.sh @@ -12,7 +12,8 @@ if [[ "$actual_sha" != "$expected_sha" ]]; then exit 2 fi -if [[ -n $(/usr/bin/git -C "$stock_tree" status --porcelain --untracked-files=all) ]]; then +status_output=$(/usr/bin/git -C "$stock_tree" status --porcelain --untracked-files=all) +if [[ -n "$status_output" ]]; then echo "ERROR: exact stock worktree is not clean" >&2 exit 3 fi @@ -22,15 +23,25 @@ if [[ -e "$generated_path" ]]; then exit 4 fi +runner_pid='' +# shellcheck disable=SC2329 # Invoked by the EXIT trap. cleanup() { rm -f -- "$generated_path" } +# shellcheck disable=SC2329 # Invoked by the INT/TERM traps. handle_signal() { - exit "$1" + local signal=$1 + local exit_status=$2 + trap - INT TERM + if [[ -n "$runner_pid" ]]; then + kill -s "$signal" "$runner_pid" 2>/dev/null || true + wait "$runner_pid" 2>/dev/null || true + fi + exit "$exit_status" } trap cleanup EXIT -trap 'handle_signal 130' INT -trap 'handle_signal 143' TERM +trap 'handle_signal INT 130' INT +trap 'handle_signal TERM 143' TERM /bin/cat >"$generated_path" <<'CHARACTERIZATION' import { @@ -213,4 +224,12 @@ if [[ ${T3_STOCK_EXACT_FAIL_AT:-} == after-generated-fixture ]]; then exit 91 fi -(cd "$stock_tree" && corepack pnpm --filter t3 exec vp test run src/orchestration/Layers/T3LayerStockProjectionCharacterization.generated.test.ts) +( + cd "$stock_tree" + exec corepack pnpm --filter t3 exec vp test run src/orchestration/Layers/T3LayerStockProjectionCharacterization.generated.test.ts +) & +runner_pid=$! +runner_status=0 +wait "$runner_pid" || runner_status=$? +runner_pid='' +exit "$runner_status" diff --git a/test/stock-t3-exact-stock-negative.test.ts b/test/stock-t3-exact-stock-negative.test.ts index 59af0c1..08b1536 100644 --- a/test/stock-t3-exact-stock-negative.test.ts +++ b/test/stock-t3-exact-stock-negative.test.ts @@ -1,8 +1,72 @@ -import { describe, expect, test } from "bun:test"; +import { afterEach, describe, expect, test } from "bun:test"; +import { chmod, mkdir, mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; import { join } from "node:path"; const exactTree = Bun.env.T3_STOCK_EXACT_TREE; const exactToolchain = Bun.env.T3_STOCK_EXACT_TOOLCHAIN; +const temporaryRoots: string[] = []; + +afterEach(async () => { + await Promise.all(temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); +}); + +async function runAt(cwd: string, command: string[], env: Record = {}) { + const child = Bun.spawn(command, { + cwd, + env: { ...Bun.env, ...env }, + stdout: "pipe", + stderr: "pipe", + }); + const [exitCode, stdout, stderr] = await Promise.all([ + child.exited, + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]); + return { exitCode, stdout, stderr }; +} + +async function exactDriverFixture(corepackBody: string) { + const root = await mkdtemp(join(tmpdir(), "t3layer-exact-driver.")); + temporaryRoots.push(root); + const stockTree = join(root, "stock"); + const layers = join(stockTree, "apps/server/src/orchestration/Layers"); + const fakeBin = join(root, "bin"); + await mkdir(layers, { recursive: true }); + await mkdir(fakeBin); + await Bun.write(join(layers, ".gitkeep"), ""); + for (const command of [ + ["/usr/bin/git", "init", "-q", stockTree], + ["/usr/bin/git", "-C", stockTree, "config", "user.email", "fixture@example.invalid"], + ["/usr/bin/git", "-C", stockTree, "config", "user.name", "fixture"], + ["/usr/bin/git", "-C", stockTree, "add", "."], + ["/usr/bin/git", "-C", stockTree, "commit", "-q", "-m", "fixture"], + ]) { + const result = await runAt(root, command); + if (result.exitCode !== 0) throw new Error(result.stderr); + } + const head = (await runAt(root, ["/usr/bin/git", "-C", stockTree, "rev-parse", "HEAD"])) + .stdout.trim(); + const source = await Bun.file( + join(import.meta.dir, "../scripts/stock-t3-exact-characterization.sh"), + ).text(); + const driver = join(root, "exact-characterization.sh"); + await Bun.write(driver, source.replace(/expected_sha=[0-9a-f]{40}/, `expected_sha=${head}`)); + await chmod(driver, 0o700); + const corepack = join(fakeBin, "corepack"); + await Bun.write(corepack, corepackBody); + await chmod(corepack, 0o700); + return { + root, + stockTree, + driver, + fakeBin, + generated: join( + layers, + "T3LayerStockProjectionCharacterization.generated.test.ts", + ), + }; +} describe("exact-stock characterization driver", () => { test("pins the adopted SHA, generates only inside the worktree, and invokes the literal stock runner", async () => { @@ -14,8 +78,8 @@ describe("exact-stock characterization driver", () => { "corepack pnpm --filter t3 exec vp test run src/orchestration/Layers/T3LayerStockProjectionCharacterization.generated.test.ts", ); expect(source).toContain("trap cleanup EXIT"); - expect(source).toContain("trap 'handle_signal 130' INT"); - expect(source).toContain("trap 'handle_signal 143' TERM"); + expect(source).toContain("trap 'handle_signal INT 130' INT"); + expect(source).toContain("trap 'handle_signal TERM 143' TERM"); expect(source).toContain('rm -f -- "$generated_path"'); expect(source).toContain('if [[ -e "$generated_path" ]]'); expect(source).toContain("generated characterization path already exists"); @@ -27,6 +91,75 @@ describe("exact-stock characterization driver", () => { expect(source).toContain("exact stock worktree is not clean"); }); + test("fails closed when git cannot verify worktree cleanliness", async () => { + const fixture = await exactDriverFixture("#!/usr/bin/env bash\nexit 0\n"); + await rm(join(fixture.stockTree, ".git/index")); + await mkdir(join(fixture.stockTree, ".git/index")); + + const result = await runAt( + fixture.root, + ["bash", fixture.driver, fixture.stockTree], + { PATH: `${fixture.fakeBin}:${Bun.env.PATH ?? ""}` }, + ); + + expect(result.exitCode).not.toBe(0); + expect(await Bun.file(fixture.generated).exists()).toBe(false); + }); + + test("forwards TERM to the exact-stock runner and cleans the generated fixture", async () => { + const fixture = await exactDriverFixture( + "#!/usr/bin/env bash\n" + + "set -euo pipefail\n" + + 'printf \'%s\\n\' "$$" > "$T3_STOCK_TEST_RUNNER_PID"\n' + + "trap 'echo TERM > \"$T3_STOCK_TEST_RUNNER_SIGNAL\"; exit 143' TERM\n" + + "while :; do sleep 0.05; done\n", + ); + const runnerPidPath = join(fixture.root, "runner.pid"); + const runnerSignalPath = join(fixture.root, "runner.signal"); + const child = Bun.spawn(["bash", fixture.driver, fixture.stockTree], { + cwd: fixture.root, + env: { + ...Bun.env, + PATH: `${fixture.fakeBin}:${Bun.env.PATH ?? ""}`, + T3_STOCK_TEST_RUNNER_PID: runnerPidPath, + T3_STOCK_TEST_RUNNER_SIGNAL: runnerSignalPath, + }, + stdout: "pipe", + stderr: "pipe", + }); + for (let attempt = 0; attempt < 200; attempt += 1) { + if (await Bun.file(runnerPidPath).exists()) break; + await new Promise((resolve) => setTimeout(resolve, 10)); + } + expect(await Bun.file(runnerPidPath).exists()).toBe(true); + child.kill("SIGTERM"); + const outcome = await Promise.race([ + child.exited.then((exitCode) => ({ exitCode })), + new Promise((resolve) => setTimeout(() => resolve(null), 750)), + ]); + if (outcome === null) { + const runnerPid = Number((await Bun.file(runnerPidPath).text()).trim()); + try { + process.kill(runnerPid, "SIGKILL"); + } catch { + // The runner may have exited between the timeout and cleanup. + } + const parentStopped = await Promise.race([ + child.exited.then(() => true), + new Promise((resolve) => setTimeout(() => resolve(false), 500)), + ]); + if (!parentStopped) { + child.kill("SIGKILL"); + await child.exited; + } + throw new Error("exact-stock driver did not forward TERM promptly"); + } + + expect(outcome.exitCode).toBe(143); + expect(await Bun.file(runnerSignalPath).text()).toBe("TERM\n"); + expect(await Bun.file(fixture.generated).exists()).toBe(false); + }); + test.skipIf(exactTree === undefined || exactToolchain === undefined)( "executes the generated fixture at the pinned stock SHA (set T3_STOCK_EXACT_TREE and T3_STOCK_EXACT_TOOLCHAIN)", async () => { diff --git a/test/stock-t3-live-harness.test.ts b/test/stock-t3-live-harness.test.ts index df1ad38..de0b4dd 100644 --- a/test/stock-t3-live-harness.test.ts +++ b/test/stock-t3-live-harness.test.ts @@ -506,6 +506,19 @@ describe("stock live harness lifecycle", () => { expect(await Bun.file(fixture.log).exists()).toBe(false); }); + test("execute mode invalidates a prior receipt before preflight can fail", async () => { + const fixture = await canaryFixture(); + await Bun.write(fixture.receipt, '{"success":true}\n'); + + const result = await run(["bash", "scripts/stock-t3-canary-drill.sh", "--execute"], { + ...fixture.env, + T3_STOCK_ROUTE_CANARY_COMMAND: join(fixture.root, "missing-canary-command"), + }); + + expect(result.exitCode).not.toBe(0); + expect(await Bun.file(fixture.receipt).exists()).toBe(false); + }); + test("SIGINT during execute mode recovers and exits 130", async () => { const fixture = await canaryFixture(); await Bun.write(