@sema-agent/client-core 0.107.0 → 0.108.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +19 -14
  3. package/dist/adapter/activeRunSelfHeal.d.ts +2 -0
  4. package/dist/adapter/activeRunSelfHeal.js +20 -3
  5. package/dist/checkpointRowVerdict.d.ts +5 -0
  6. package/dist/checkpointRowVerdict.js +26 -0
  7. package/dist/cloudConfigWireCaps.d.ts +10 -0
  8. package/dist/cloudConfigWireCaps.js +33 -0
  9. package/dist/coreValuePorts.d.ts +17 -1
  10. package/dist/coreValuePorts.js +5 -0
  11. package/dist/decideReceipt.js +2 -1
  12. package/dist/engineErrorCodes.d.ts +5 -0
  13. package/dist/engineErrorCodes.js +5 -0
  14. package/dist/engineNoticeCodes.d.ts +19 -1
  15. package/dist/engineNoticeCodes.js +65 -3
  16. package/dist/engineTargetSeat.js +1 -0
  17. package/dist/errorSupplementRoles.js +2 -0
  18. package/dist/eventLeg.js +1 -1
  19. package/dist/executionLaneWords.js +2 -1
  20. package/dist/gateOutcome.d.ts +10 -1
  21. package/dist/gateOutcome.js +48 -1
  22. package/dist/generated/engineFactTables.d.ts +1 -0
  23. package/dist/generated/engineFactTables.js +5 -0
  24. package/dist/generated/engineNoticeTables.js +3 -0
  25. package/dist/generated/settingsSchemaFns.d.ts +4 -2
  26. package/dist/generated/settingsSchemaFns.js +9 -3
  27. package/dist/generated/settingsSchemaMcpRequestEntry.d.ts +1 -1
  28. package/dist/generated/settingsSchemaMcpRequestEntry.js +9 -18
  29. package/dist/generated/settingsSchemaReservedMemberName.d.ts +1 -0
  30. package/dist/generated/settingsSchemaReservedMemberName.js +7 -0
  31. package/dist/generated/settingsSchemaTables.d.ts +2 -0
  32. package/dist/generated/settingsSchemaTables.js +10 -0
  33. package/dist/generated/settingsSchemaWireName.d.ts +1 -0
  34. package/dist/generated/settingsSchemaWireName.js +8 -0
  35. package/dist/headlessReconnectWire.js +12 -7
  36. package/dist/hitl/approvalAskers.d.ts +13 -0
  37. package/dist/hitl/approvalAskers.js +79 -0
  38. package/dist/hitl/approvalResolution.d.ts +3 -0
  39. package/dist/hitl/approvalResolution.js +22 -4
  40. package/dist/hitl/approvalsFeed.js +3 -12
  41. package/dist/hitl/askParkRowRouting.js +5 -1
  42. package/dist/hitl/decisionRefusal.d.ts +1 -0
  43. package/dist/hitl/decisionRefusal.js +5 -2
  44. package/dist/hitl/decisionSend.js +2 -6
  45. package/dist/hitl/frameRouter.d.ts +3 -2
  46. package/dist/hitl/frameRouter.js +28 -6
  47. package/dist/hitl/gateLedger.d.ts +2 -0
  48. package/dist/hitl/gateLedger.js +3 -1
  49. package/dist/hitl/hitlBridge.js +2 -3
  50. package/dist/hitl/parkResolver.js +32 -23
  51. package/dist/hitl/parkRowBirthWait.d.ts +2 -0
  52. package/dist/hitl/parkRowBirthWait.js +8 -1
  53. package/dist/hitl/persistedRulesWire.d.ts +5 -1
  54. package/dist/hitl/persistedRulesWire.js +20 -4
  55. package/dist/hitl/planReviewWire.js +12 -16
  56. package/dist/hitl/policyDenyCause.d.ts +3 -0
  57. package/dist/hitl/policyDenyCause.js +16 -0
  58. package/dist/hitl/sessionPolicyDeliverable.js +1 -1
  59. package/dist/hitl/toolApprovalWire.d.ts +8 -2
  60. package/dist/hitl/toolApprovalWire.js +22 -3
  61. package/dist/hooksReaderGenerations.d.ts +1 -0
  62. package/dist/hooksReaderGenerations.js +12 -0
  63. package/dist/hooksWireCaps.js +8 -4
  64. package/dist/i18n/messages.en.d.ts +38 -1
  65. package/dist/i18n/messages.en.js +40 -3
  66. package/dist/i18n/messages.zh.js +45 -3
  67. package/dist/index.d.ts +6 -1
  68. package/dist/index.js +4 -1
  69. package/dist/liveModelCatalog.d.ts +4 -2
  70. package/dist/liveModelCatalog.js +164 -26
  71. package/dist/model/catalogDefault.d.ts +25 -0
  72. package/dist/model/catalogDefault.js +44 -0
  73. package/dist/permissionWireCaps.d.ts +1 -0
  74. package/dist/permissionWireCaps.js +11 -0
  75. package/dist/pluginHooksWire.d.ts +5 -3
  76. package/dist/pluginHooksWire.js +116 -7
  77. package/dist/queuedDelivery.d.ts +1 -0
  78. package/dist/queuedDelivery.js +12 -0
  79. package/dist/resultFrameKeys.js +4 -2
  80. package/dist/resumeRefusalCopy.js +15 -8
  81. package/dist/runPermissionModeSwitch.d.ts +2 -1
  82. package/dist/runPermissionModeSwitch.js +16 -6
  83. package/dist/seatContract.d.ts +63 -10
  84. package/dist/seatContract.js +164 -11
  85. package/dist/sessionEngine.d.ts +2 -1
  86. package/dist/sessionEngine.js +5 -0
  87. package/dist/settingsHooksSalvage.d.ts +5 -0
  88. package/dist/settingsHooksSalvage.js +54 -0
  89. package/dist/subagentContentStore.js +1 -1
  90. package/dist/supportFloor.js +2 -0
  91. package/dist/taskNotificationAdmission.d.ts +19 -0
  92. package/dist/taskNotificationAdmission.js +84 -0
  93. package/dist/toolResult.js +2 -1
  94. package/dist/webSearchWireCaps.js +9 -4
  95. package/dist/wireRefusalCopy.d.ts +2 -1
  96. package/dist/wireRefusalCopy.js +2 -2
  97. package/docs/INTEGRATION-CLIENTS.md +816 -14
  98. package/package.json +4 -4
package/README.md CHANGED
@@ -35,7 +35,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
35
35
 
36
36
  ## Scope
37
37
 
38
- **Version:** 0.107.0
38
+ **Version:** 0.108.0
39
39
 
40
40
  - **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
41
41
  B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
@@ -301,7 +301,7 @@ guard still cross-checks the table by name).
301
301
  | `scripts/run-client-core-pure-test.mjs` | Consumer-view behaviour of every moved-in module; segment floors (B1–B8 + C) that only move up The non-throwing attachments verdict names a retired key, and gives its reason and way out, from the retirement rule itself; a negative control plants a second retired key in a copy of the build and checks that it is reported by its own name. The configuration refusal registry includes the inline-image refusal code, whose one sentence names no codes or identifiers, recognises only that code, and is checked against both the installed engine (minted in exactly one place) and the receiving server's older engine (not minted at all). The background-agent task-output reader hands the state and error lines that directly follow the status line to optional slots, positionally: lines written inside the agent's own result text never become slots; checked on twelve forms minted by the installed engine's own functions and by the receiving server's older engine. It also hands the result label's parenthetical note and the header bytes not mapped to a named slot to optional slots, with a rebuild invariant (status line, state, error, the unmapped header bytes, the result label and the output rebuild the unwrapped text, up to four named normalizations) checked on those same forms and on a seeded random pool of forged headers. A Bash command that is moved to the background mid-flight (by the user, or by the engine on a timeout or a queued message) is registered for the background panel by the same event and the same record as a model-requested background command: the reader recognises only the structured detached result (never the receipt prose, never the request's background flag), at most one registration is minted per card (a card both readers recognise yields one, taken from the structured result), the package keeps no record of what it has already published (the same task id on a second card is registered again, because only the consumer knows whether it still has the row), the receipt reader and the detached reader both stand down when the structured result says the engine failed to register, and the stream is compared byte for byte against the previous published build on inputs without a detached result (including two receipts for one task id), differing only by that one event on inputs that have one. The attachments env lane's product default now also carries the todo reminder (one default table shared by the absent, empty, unknown-word and `default` paths); `off` removes the key and an explicit list that leaves it out does not get it. The network word table matches terminated and other side closed as whole words: a local Unterminated string in JSON parse error is classified as internal instead of a connection failure, while the sentences undici itself produces, the errno codes and the UND_ERR family still classify as transport. The error-triage readers (`classifyTurnWireError`, `isWireTransportError` and the three sibling readers of the same fields) never throw on an error object whose `status`, `errorCode`, `message`, `cause` or `name` is a throwing getter, a revoked proxy or a throwing string conversion; an unreadable `status` is its own state (neither an HTTP answer nor a transport failure), and ordinary shapes answer exactly as a frozen copy of the previous implementation does. |
302
302
  | `scripts/run-client-core-portability-test.mjs` | Kernel / A-layer / index import closures, the runtime-dependency equality gate, barrel reachability, and a real esbuild `--platform=browser` bundle Since 0.98.0 the gate-script family sweep has four forms: a pattern that can enclose a whole block comment (judged by running it on constructed probes, not by how it is spelled), a pattern that covers the line-comment marker, a hand-written quote-pairing lexer (registration only, currently empty) and string-method calls whose first argument is a comment marker (the shared lexer library is the only home), each proved on planted spellings and clean neighbours. |
303
303
  | `scripts/run-client-core-diff-test.mjs` | Differential equivalence against the CLI reference bridge + replay-id invariant + ledger round-trip |
304
- | `scripts/run-seat-contract-keys-test.mjs` | The seat IPC contract: verb list ↔ SPEC ↔ types, element-wise. Since 0.83.4 the seat approval request also carries the four card bits that say whether an approval must be asked and where it came from (`mandated`, `mandate`, `ruleOffersAbsence` — the parked-row form of mandated, so the seat and the card give the same `approvalIsMandated` answer — and `origin`, which the resident-posture predicate reads), and they reach the seat through `toolPermissionRequestAskBits`, which reads them exactly as the card request does — a strict own `true`, one of the closed mandate words byte for byte, a non-empty own string — so for a host that builds the seat request through that crossing an off-list word, an empty string or an inherited key never lands on the seat; the seat validator does not become a second judge that could drop a safety ask over one bit, which leaves a host that builds the request by hand to apply the same narrow read itself. Since 0.96.0 the seat chrome list also carries the two segment-end events (`text_segment_end` and `thinking_segment_end`), so the authoritative replacement of a segment (redaction or rewrite) can reach a seat lane (since 0.98.0 also the six presentation arms the renderer already consumes: `retry_status`, `context_usage`, `last_turn_usage`, `bgshell_register`, `bgshell_settle` and `memory_saved`; `workflow_notification_enqueue` stays with the host process that owns the session's idle auto-submit and its notification queue, so the completion notice has exactly one deliverer — a real adapter-minted one is pinned as rejected by the seat gate; every `required` chrome arm must have a written seat disposition or the gate goes red, and the set of required arms may not shrink: the frozen floor is declared on the chrome arm table's own type, so turning an existing required arm optional fails to compile, and the gate checks the runtime list against that same declaration), and its type is bound to the discriminants of `ChromeEvent` itself (a word that is not a chrome arm no longer compiles); the approval request carries the seat's allow-for-this-session bit; since 0.97.0 that bit is `sessionRow` (replacing `rememberRefused`): present only when the card judge's session-row premise holds and the request id's domain can carry a session answer, absent otherwise, which hides that row — never invented as `false`; the crossing never copies the bit from its input and never mints it for a seat request itself (the seat reads the request's own bit) Since 0.98.0 it also checks that every outbound gate accepts only plain records — a Date, Map, Set, array, class instance or object with a custom prototype carrying otherwise-valid fields is refused (structured cloning would drop those fields in transit), while a plain object from another JavaScript realm is accepted — and that four unused event fields are gone from the contract. 0.98.0: the outbound seat validator lists the turn-failure superset key without ever refusing an error event because of it. Since 0.99.0 the event no longer has the two positions no host ever sent (`request`, `sources`) while `code` stays as the failure event's status-code field; the event list gains `subagent_content`, whose payload is the subagent content event in exactly the package's own projection, checked end to end from a wire frame to the renderer's own content store; and the six presentation arms (retry status, context usage, last-turn usage, background shell register and settle, memory saved) carry closed payloads, with the engine's turn usage reduced to its six declared keys plus a count of the keys this package does not know. Since 0.101.0 the seat chrome list also carries the two compaction events (`compaction_started` and `compaction_outcome`) and the engine notice (`engine_notice`), so a seat lane can show that a compaction is running, learn how one that did not land ended, and show the engine's session notices; every chrome arm now has one written seat disposition (crosses to the seat, or stays with the host process), checked at compile time and by the gate, and the seat list is derived from that table. |
304
+ | `scripts/run-seat-contract-keys-test.mjs` | The seat IPC contract: verb list ↔ SPEC ↔ types, element-wise. Since 0.83.4 the seat approval request also carries the four card bits that say whether an approval must be asked and where it came from (`mandated`, `mandate`, `ruleOffersAbsence` — the parked-row form of mandated, so the seat and the card give the same `approvalIsMandated` answer — and `origin`, which the resident-posture predicate reads), and they reach the seat through `toolPermissionRequestAskBits`, which reads them exactly as the card request does — a strict own `true`, one of the closed mandate words byte for byte, a non-empty own string — so for a host that builds the seat request through that crossing an off-list word, an empty string or an inherited key never lands on the seat; the seat validator does not become a second judge that could drop a safety ask over one bit, which leaves a host that builds the request by hand to apply the same narrow read itself. Since 0.96.0 the seat chrome list also carries the two segment-end events (`text_segment_end` and `thinking_segment_end`), so the authoritative replacement of a segment (redaction or rewrite) can reach a seat lane (since 0.98.0 also the six presentation arms the renderer already consumes: `retry_status`, `context_usage`, `last_turn_usage`, `bgshell_register`, `bgshell_settle` and `memory_saved`; `workflow_notification_enqueue` stays with the host process that owns the session's idle auto-submit and its notification queue, so the completion notice has exactly one deliverer — a real adapter-minted one is pinned as rejected by the seat gate; every `required` chrome arm must have a written seat disposition or the gate goes red, and the set of required arms may not shrink: the frozen floor is declared on the chrome arm table's own type, so turning an existing required arm optional fails to compile, and the gate checks the runtime list against that same declaration), and its type is bound to the discriminants of `ChromeEvent` itself (a word that is not a chrome arm no longer compiles); the approval request carries the seat's allow-for-this-session bit; since 0.97.0 that bit is `sessionRow` (replacing `rememberRefused`): present only when the card judge's session-row premise holds and the request id's domain can carry a session answer, absent otherwise, which hides that row — never invented as `false`; the crossing never copies the bit from its input and never mints it for a seat request itself (the seat reads the request's own bit) Since 0.98.0 it also checks that every outbound gate accepts only plain records — a Date, Map, Set, array, class instance or object with a custom prototype carrying otherwise-valid fields is refused (structured cloning would drop those fields in transit), while a plain object from another JavaScript realm is accepted — and that four unused event fields are gone from the contract. 0.98.0: the outbound seat validator lists the turn-failure superset key without ever refusing an error event because of it. Since 0.99.0 the event no longer has the two positions no host ever sent (`request`, `sources`) while `code` stays as the failure event's status-code field; the event list gains `subagent_content`, whose payload is the subagent content event in exactly the package's own projection, checked end to end from a wire frame to the renderer's own content store; and the six presentation arms (retry status, context usage, last-turn usage, background shell register and settle, memory saved) carry closed payloads, with the engine's turn usage reduced to its six declared keys plus a count of the keys this package does not know. Since 0.101.0 the seat chrome list also carries the two compaction events (`compaction_started` and `compaction_outcome`) and the engine notice (`engine_notice`), so a seat lane can show that a compaction is running, learn how one that did not land ended, and show the engine's session notices; every chrome arm now has one written seat disposition (crosses to the seat, or stays with the host process), checked at compile time and by the gate, and the seat list is derived from that table. The resolution words also include `engine_unavailable` (the host retired its runtime and withdrew the card while the parked row is still on the engine, so it is not an expiry); the outbound gate stays open to unknown words and the `parked` bit stays reserved for `expired`. Since 0.108.0 the seat's resource verbs take an optional session scope: `listModels(scope?)` and `uploadAttachment(req, scope?)` answer for the engine the named session lives on, or — with no scope — for the default lane that new sessions start on, never for the focused session; `checkSeatResourceScope` accepts only `{ sessionId }` and names the offending key otherwise, and `checkStartSessionOptions` accepts the new-session lane intent `engine` only as `local` or `cloud`. Every seat validator treats an object whose prototype is a hand-made prototype-less object as not a plain record, so a field inherited from such a prototype can never slip past the closed-shape check. |
305
305
  | `scripts/run-approval-frame-keys-test.mjs` | The tool-approval frame key mirror, element-wise against the SDK's runtime anchor (one carve-out: AHEAD_OF_ANCHOR entries — keys the server already emits but the SDK anchor has not caught up to — may lead by one generation; the gate turns red the day the SDK catches up, forcing the entry's removal — the register is occupied again — this time by the bit that says a saved allow rule cannot retire a given approval card, carrying both the release that minted it and the byte coordinates that prove it, so the lead is a dated record rather than an exemption; its predecessor left the register the other way, by being retired upstream rather than by the anchor catching up). Beside the key mirror it now guards three further faces of that bit: the closed word table the durable leg reads it through must be **the very array object the SDK exports**, not a same-looking copy — reference identity, because an equal-contents check still permits a second table that diverges the day upstream adds a member; the one predicate a client is meant to call answers over both legs — the live card's bit and the parked row's absence word, which is all the row carries, since the row has no such bit at all — and answers `false` for a malformed value exactly as its presence-only siblings do, a strictness the upstream mint shares; and the one sentence minted for it must never point the reader at writing a rule, since a rule written in answer to a mandated question can never take effect where it was written. The same bit's key also has to reach the card request itself, which the SDK's card anchor does not list — a fact that arrives at the package boundary and stops there is the shape of defect this file's guards exist to catch. Since 0.83.2 the frame also mirrors `mandate` (the word a mandated question stands on) ahead of the SDK anchor, with its own exit condition; the released server carries it from 7.102.0, so the allowance for the server fixture was removed by its own exit condition. The installed engine must declare the member with the six-word closed type, and the key must reach the card request. Since 0.91.0 the frame also mirrors `probeReason` ahead of the development SDK anchor, with the same exit condition, and the probe-tree comparison reads the newest configured SDK first. |
306
306
  | `scripts/run-segment-authority-single-source-test.mjs` | The authoritative-segment replacement verdict, single-sourced. `text_end.content` and the `text_delta` stream stopped being byte-identical the day the engine started redacting the former through the same filter as the result, so every consumer now has to decide six ways what to do with the segment it has half-emitted — and until this release that decision existed **twice**: once here for the transcript lane, once in the shell for the print lane, hot-fixed a version apart. The verdict is now one pure function both lanes call, and the guard pins it on the quantity that actually decides the outcome: whether the authoritative text still *starts with* the bytes that already left, not whether a flush has happened — the latter is a precondition, and anchoring on it withholds a perfectly ordinary answer. Each of the six forms is checked with its counter-case, the prefix length is pinned to UTF-16 code units against a non-ASCII sample whose UTF-8 byte count differs (slicing by bytes leaves the very thing being redacted on screen), and the withheld-segment ledger is compared by normalised equality rather than substring, because a short redaction marker quoted in an unrelated later answer would otherwise suppress that answer entirely. The same file pins the session-level memory-capture declaration to one mint point — the wire value is a single-member closed set, and a consumer that spells it wrong gets a loud refusal rather than a silently dropped privacy request — and pins the SDK URL/health transit to be the **same function reference**, since wrapping it would discard the one guarantee the transit exists for. A last section strips comments with the TypeScript parser and asserts the second expression has not grown back Since 0.98.0 the gate also pins the single reading of engine liveness: no string expression in the package source evaluates, after static folding, to a `/health` path, so a second hand-written probe cannot come back however it is spelled, and the package's one liveness helper answers six real HTTP fixtures exactly as the SDK probe does (a self-reported degraded body, a non-JSON body and a 503 are not alive; 200, 201 and 299 with an ok body are). |
307
307
  | `scripts/run-print-bash-iserror-test.mjs` | The print lane's Bash `is_error` authority (structured over regex). A second section pins where the denial classification word lands on this lane: on the message envelope, never inside the tool-result block, because that block is forwarded verbatim to the provider on compaction and a self-minted key there is the shape of an old, real defect. A word outside the upstream table — or an empty string, a non-string, or nothing at all — mints no key rather than a guess, and the word never moves the error flag, because attribution does not decide anything |
@@ -400,7 +400,7 @@ guard still cross-checks the table by name).
400
400
  | `scripts/run-wiring-manifest-projection-test.mjs` | The two end-user facts carried on the engine's `wiring_manifest` frame (`modelGate`: which tools this run's model gate removed and the verbatim restore hint; `autoMode`: whether auto mode is actually armed and the engine's own reason word). Projection: both sections ride as `_sema_`-prefixed superset keys, verbatim, and no SDK-named key is minted; a frame where neither section is well-formed projects to `none/not_in_slice` (no empty arm); `modelGate` needs all three keys and treats `removed: []` as a bad value rather than a reading; `autoMode` needs a boolean plus a non-empty reason that agrees with it, and the reason word is never mapped onto the capabilities vocabulary; the frame is flat (a nested `manifest:{}` wrapper is not a supply); `eventId` rides like every other arm. Adapter: exactly one chrome event on the main lane, a sub-flow frame (any `parentToolCallId`, `null` included) yields nothing, and an absent `eventId` leaves the key absent. Added at receiving time because the shell-side gate could not see this package's behaviour: two mutations (empty `removed` accepted, sub-flow gate removed) had passed the package suite untouched 0.71.0 adds sections F–I: the fourth/fifth/sixth manifest sections (`tools` via the roster reader, `hooks[]` rows dropped one by one when malformed, `lsp` absent unless `mounted` is a boolean), the `tool_roster_delta` arm (narrowed `delta`, `malformed` when `fromDigest`/`roster` cannot be read, host applies it against its own digest), the `context_usage` arm (finite-gated scalars plus `sections[]` rows dropped one by one), and the `WiringManifestMcpEntryView` rename with `MAX_AGENT_SKILLS` gone from the surface From 0.84.0 the `hands` section (whether this leg was assembled with the engine's built-in file and shell tools) is projected as well: a strict boolean `mounted` plus the engine's reason word passed through as written, absence kept as "not reported" rather than folded to either answer; a three-state reader and one sentence source share their opening and closing with the roster-derived sentence, whose bytes do not change. The sentence source answers the not-reported sentence for anything it does not recognise, including a roster-derived reading, and never throws. From 0.88.0 the `fileHistory` section (whether this leg records file history; `on` / `disabled` today, passed through as written for unknown words) is projected as well: present only as a non-empty own string, absence kept as "not reported" rather than folded to `disabled`, malformed values dropped like the server drops them, the same view on the non-streaming submit receipt, and a leg built and published by an installed server package checked end to end when one is available. A `fileHistory` value that throws when read leaves only that section absent; the other sections of the same frame are still delivered on both the streaming and the submit-receipt paths. The server-minted `instructions` section (which instruction file the leg loaded) is projected verbatim as a frozen copy, an explicit empty selection stays an empty list, absence stays absent, a malformed section is dropped without touching its siblings, and a frame carrying only this section still becomes an event. The `fleet.subagentTranscripts` statement (from server 7.115.0: how long delegated sub-agent conversations survive) is passed through verbatim as an open word in a frozen `fleet` section; absence stays absent and is never read as `none`, and a malformed value drops only that section. |
401
401
  | `scripts/run-submit-wiring-manifest-test.mjs` | The non-streaming submit receipt can carry the run's opening wiring manifest (`TaskResult.wiringManifest`, additive on newer servers). `readSubmitWiringManifest` answers one of three: the key is absent on the receipt itself (older server, or a deployment whose engine never produced that frame) — not the same as unreadable; the key is present but cannot be read (not an object, or none of the twelve sections survive); or a manifest view. The view is the same shape the streaming lane's chrome event carries (minus its two envelope keys) and is assembled by the same code path, so both lanes agree byte for byte on the same object. Liveness fields ride through untouched — this reader never mints a liveness verdict — and an operator-shaped receipt with extra governance sections reads to the same view as a tenant-shaped one. A zero-tool roster is a real reading, not an absence. A receipt carrying only the `instructions` section reads as a manifest, an explicit empty selection is kept, and absence or a malformed section leaves the key out. |
402
402
  | `scripts/run-rule-offers-reader-test.mjs` | The narrowing reader behind the "don't ask again" options, now a public entry point rather than a card-port-only one. Hosts that render the frame themselves (a browser has no three-way terminal card) previously had to rebuild this reader on their side, and what it carries is a **redemption-safety** judgement, not a convenience: the batch arm is redeemed by **index**, so a reader that compacts the array after dropping a malformed entry makes the k-th option a person clicked and the k-th rule the server writes two different rules. So: a bad entry is dropped **on its own** (one bad option must not make a real one disappear) while every surviving entry keeps its **original wire index** — pinned from both ends, with the bad entries leading and trailing. A batch's *members* are the opposite: any malformed member drops the whole batch, because a conjunctive batch is one "yes" to all of them and a batch missing a member is a different grant; its honest-remainder count is a reading, not decoration, so a non-integer or negative value drops the batch rather than rendering a fabricated zero. An empty array, a non-array, an over-cap array and an all-bad array all read as **absence** rather than an empty list, because an empty list renders as "there is an option lane with nothing in it". Only the `ruleOffers` key is read: a `null` value reads as absence so a relaying layer that serialises "missing" as null cannot delete the whole lane, a frame or row that carries only the retired `ruleSuggestions` key yields no options, and the reader's second argument was removed (an extra argument from an older caller is never read). The public entry is finally reconciled against **both** card-port legs on the same material, byte for byte, so the exported reader and the one the card sees can never become two. Two upstream vocabularies used to be **hand-copied** here, and both had fallen behind: a match word outside the copied pair dropped an otherwise valid option outright, and a batch carrying a directory-read member — a member kind the copy did not know — dropped the whole batch. Both tables now come from one place upstream and are re-exported verbatim, pinned in both directions: every word in the table must be accepted (a narrower copy reds on the words it never learned) and a word constructed to be outside it must still be refused (a reader widened to "any string" reds too). A member whose kind is genuinely unknown still drops **the whole batch and only that batch** — never one member, because a conjunctive batch one member short renders "yes to N" as "yes to N−1", and never the card, because the honest single beside it is intact — while a member from before the discriminant existed normalises to the historical kind rather than being refused. The additive per-segment reasons ride through verbatim, drop only the row that is malformed, and stay **absent rather than empty** when nothing survives, since an empty list would read as "confirmed nothing uncovered" while the count remains the only source of truth |
403
- | `scripts/run-resume-refusal-copy-test.mjs` | The **words** a client says when a resume is refused, minted once here instead of three times. The facts behind them already lived in this package; the sentences did not, so each client wrote its own — and those sentences answer a safety question (was my decision consumed, can this token still be redeemed), which is exactly the kind of answer that must not vary by client. Two closed sets meet here and the guard pins their relationship in both directions, because it is a premise rather than a coincidence: one set answers *can waiting help* (the codes the server mints a wait on), the other answers *what should a person be told*, they **intersect in exactly one code**, and each keeps a member the other must not have — a placement mismatch is never waitable no matter what arrives on the response, since its remedy is a changed argument rather than elapsed time, and a full governance window needs no prose because "you can wait" is the whole message. The overlapping code delegates its wait and its disposition to the existing reading rather than judging again: nine shapes of input drive both entry points and the two readings must agree byte for byte, the absent case included, because two judges always diverge somewhere. The wait is narrowed to the domain the server mints it in, which is **stricter than the shell's own copy was** — a zero now reads as no window rather than as "retry now", and the wake-up it would retry is an at-most-once action with real side effects. The third sentence is chosen by the disposition, never by the engine's prose: rewriting the message to either upstream branch's exact wording, with the window untouched, must leave all three sentences unchanged, while adding a window must change the third one and only the third one. The engine's folded resume refusal (`resume_blocked_by_policy`) gets its own reading — the original code as named, absent or unreadable — and a wording that neither claims nothing was consumed nor predicts whether a retry would pass. From engine `7.104` a folded admission refusal always names a code: when the underlying refusal carried none, the server fills in the generic code for its HTTP status. Those generic codes are not a specific check, so the sentence says the refusal did not name a more specific check (and shows the generic code in brackets) instead of presenting it as the check that refused the resume; the generic-code list is compared word for word, in both directions, with the server dispatcher that mints them, item by item and in order with the same-named list the SDK exports from 16.0.0, and word for word with the table in the server's wire contract. The SDK comparison reads the probe trees named by `SEMA_SDK16_ROOT`, `SEMA_SDK15_ROOT` and `SEMA_SDK15_ROOT_OLD` plus the installed SDK; when the full fixture set is required, having no SDK at 16.0.0 or later among them fails the check. Otherwise a comparison is reported as skipped only when its source is not configured or the server source predates the contract section; a configured source that is missing, unreadable or lacks the contract document fails the check. From server 7.115.0 the continue-only-an-existing-conversation refusals carry their own codes (404, and a 422 field conflict) and the reader takes the code first, falling back to the status-derived code plus the trailing marker only for older servers. A parked task the server can no longer continue in its stored form (409 `checkpoint.unsupported_version` with `retriable: true`) has its own reader and sentence, shaped like the reopen family and reading `retriable` the same single way: the sentence says the work is still waiting without promising that resending will continue it, and its way out depends on the parked kind and run id the caller supplies; the plan-review decision and the decide-failure note read this code the same way. For a plan review refused this way, the closing instruction no longer says the plan is still waiting for a decision; it says deciding again will not take effect and points to the way forward, and it comes from the same tail function as every other outcome. |
403
+ | `scripts/run-resume-refusal-copy-test.mjs` | The **words** a client says when a resume is refused, minted once here instead of three times. The facts behind them already lived in this package; the sentences did not, so each client wrote its own — and those sentences answer a safety question (was my decision consumed, can this token still be redeemed), which is exactly the kind of answer that must not vary by client. Two closed sets meet here and the guard pins their relationship in both directions, because it is a premise rather than a coincidence: one set answers *can waiting help* (the codes the server mints a wait on), the other answers *what should a person be told*, they **intersect in exactly one code**, and each keeps a member the other must not have — a placement mismatch is never waitable no matter what arrives on the response, since its remedy is a changed argument rather than elapsed time, and a full governance window needs no prose because "you can wait" is the whole message. The overlapping code delegates its wait and its disposition to the existing reading rather than judging again: nine shapes of input drive both entry points and the two readings must agree byte for byte, the absent case included, because two judges always diverge somewhere. The wait is narrowed to the domain the server mints it in, which is **stricter than the shell's own copy was** — a zero now reads as no window rather than as "retry now", and the wake-up it would retry is an at-most-once action with real side effects. The third sentence is chosen by the disposition, never by the engine's prose: rewriting the message to either upstream branch's exact wording, with the window untouched, must leave all three sentences unchanged, while adding a window must change the third one and only the third one. The engine's folded resume refusal (`resume_blocked_by_policy`) gets its own reading — the original code as named, absent or unreadable — and a wording that neither claims nothing was consumed nor predicts whether a retry would pass. From engine `7.104` a folded admission refusal always names a code: when the underlying refusal carried none, the server fills in the generic code for its HTTP status. Those generic codes are not a specific check, so the sentence says the refusal did not name a more specific check (and shows the generic code in brackets) instead of presenting it as the check that refused the resume; the generic-code list is compared word for word, in both directions, with the server dispatcher that mints them, item by item and in order with the same-named list the SDK exports from 16.0.0, and word for word with the table in the server's wire contract. The SDK comparison reads the probe trees named by `SEMA_SDK16_ROOT`, `SEMA_SDK15_ROOT` and `SEMA_SDK15_ROOT_OLD` plus the installed SDK; when the full fixture set is required, having no SDK at 16.0.0 or later among them fails the check. Otherwise a comparison is reported as skipped only when its source is not configured or the server source predates the contract section; a configured source that is missing, unreadable or lacks the contract document fails the check. From server 7.115.0 the continue-only-an-existing-conversation refusals carry their own codes (404, and a 422 field conflict) and the reader takes the code first, falling back to the status-derived code plus the trailing marker only for older servers. A parked task the server can no longer continue in its stored form (409 `checkpoint.unsupported_version` with `retriable: true`) has its own reader and sentence, shaped like the reopen family and reading `retriable` the same single way: the sentence says the work is still waiting without promising that resending will continue it, and its way out depends on the parked kind and run id the caller supplies; the plan-review decision and the decide-failure note read this code the same way. For a plan review refused this way, the closing instruction no longer says the plan is still waiting for a decision; it says deciding again will not take effect and points to the way forward, and it comes from the same tail function as every other outcome. Five more refusals that the engine declares as rejected before the commit, with the work still waiting — a different principal, a different composition leg, parent policy constraints that are missing, that do not match, or that cannot be re-checked against an edited approval — join the sentence-bearing set with one sentence each; the second part of every such sentence is the same shared line, and the set is derived from the sentence table so a code cannot be in one without the other. A sixth refusal, a lost claim whose own note says not to retry it, deliberately stays outside every set. The guard reads the engine's declaration file and its per-code notes with a syntax tree: a code the engine adds without a decision here, or a note that changes under a decision made here, fails the check. A code outside the set reads as nothing: no added sentence, and a late decision on it is reported as unknown with no line of its own. |
404
404
  | `scripts/run-parked-resume-startup-test.mjs` | The one reading and the one sentence for a parked resume that fails while starting (`422 parked_resume.startup_failed`). The engine has two unrelated reasons for it — the parked agent's session is gone or the engine rejected the decision itself, or the approval is for a tool the agent inherits from the task that started it and this server cannot hand that tool over — and the response carries **no field that tells them apart**; the reason lives only in the engine's prose, which this package does not branch on. So the reading narrows the cause only by what the caller itself sent: a deny can only be the first reason (the inherited-tool refusal is minted for approvals alone, and the guard pins that upstream premise), while an approval, or a caller that does not say, gets one sentence that lays out both ways forward instead of guessing. Either way the verdict is fixed: sending the same decision again is not a recovery path, and deny stays available. The two sentences are minted in one place, never claim the card is still waiting (in one of the two shapes it is not), and avoid every word the package's own "already decided" scan looks for, since they are appended to failure text that scan reads; all five decision exits append exactly that sentence, and any other failure keeps its text byte for byte. The code also takes precedence over the package's "already decided" word scan: the engine's own text for the gone-session shape contains "not found", which that scan used to read as "someone already settled this card" and answer with a silent reconnect; now a failure carrying this code is classified by the code (no reconnect, the sentence reaches the failure text), while failures with no code or another code are scanned exactly as before. The predicate that says which codes classify themselves is public, so a client with its own "already decided" chain can ask it before scanning words. The engine may also name the reason as a word of its own (an open string; it rides the 422 body's `cause` key, and the SDK reads it as `startupCause` from 13.0.0): the reading carries that word exactly as sent, taking the SDK's named slot first and the generic extra-keys slot second (the only place it exists on SDK 12), only from own data properties and never from the platform's standard error `cause`; an empty, non-string or missing value leaves the key out, and any input without a word reads byte for byte as before. The word changes neither the verdict nor the sentences: the set of words belongs to the engine and has not been published, so nothing here guesses at it (servers up to 7.117.0 do not send it yet). The guard checks every SDK generation it can reach against the real error mapping, and compares a 14,000-input pool without a word byte for byte with the previous release's reading and its two sentences word for word. It also pins the premise that none of the servers and engines it can reach mints a cause word today, so the day one does the guard goes red. Since 0.99.0 the start-up cause also accepts the cause words the engine already mints ahead of the SDK vocabulary: the registered ahead words must equal the engine set minus the SDK set in both directions, each is carried through with its own sentence, and a compile-time pin breaks the build when either vocabulary moves without the registration. |
405
405
  | `scripts/run-resume-retry-later-test.mjs` | The two resume refusals that carry a **wait quantity** — the only members of that refusal family that do, which is the whole reason they form a closed set. Carrying a wait is not the same as being the only ones worth waiting on: a sibling refusal in the same family clears on its own and the engine says so in words, it just cannot put a number on it, so *not recognised here* must never be read as *waiting will not help*. One of the two also has a *terminal* upstream branch that arrives under the same code with the distinguishing detail only in prose, so recognition alone is not permission to say "try again": the disposition is decided by **positive evidence** and pinned from both directions — the quota-window code is evidence in itself, the preflight code counts only when the server really supplied a wait (an upstream fact, not a convention: the terminal branch throws with no detail at all, so a wait value cannot reach the client on that path), and a preflight refusal with no wait reads as *undecidable* (say what is true of both branches — nothing was consumed — and leave redeemability to the engine's own line) rather than being rendered as either a retry or an ending. Every other member means waiting will not help (change a setting, relaunch, the retained session is gone), so the recognition is a **closed set of two codes**: widening it to a family prefix would tell half the users to wait and the other half to keep waiting for something that will never arrive, and the negative controls drive exactly those codes through it, plus a same-named code on a different door (the submission-side quota refusal), the two underscore-form siblings, and a code merely quoted inside a message body. The wait value is narrowed to the same domain the server mints it in (a whole number of seconds, at least one): zero, a negative, a fraction and a non-number all read as **no window given** rather than as zero, because a zero tells the caller to retry immediately and the wake-up it would retry is an at-most-once action with real side effects. Reading is structural rather than `instanceof`, since the client is host-injected and the same class name across two bundles is two classes, and a null-prototype plain object must still be recognised. The failure classifier gains this one disposition without any existing one moving, an unknown code still falls to the honest open-set arm and its wait value is **not** believed, and an end-to-end call proves the disposition and the window reach the host while the call itself is still attempted exactly once. The recognised code set is a **frozen array**, not a type-level readonly set: the latter is a plain mutable collection at runtime and the decision reads the same instance, so one `.add` from any consumer would turn a refusal that waiting cannot fix into one that claims it can — the guard proves it by really trying to mutate the exported value and then checking the verdict did not drift |
406
406
  | `scripts/run-websearch-verdict-test.mjs` | The per-request web-search configuration a host puts on the wire. Newer servers refuse the whole request when that section is malformed — and a missing or misspelled search provider now counts as malformed, because the section names where the searches go and an unknown destination is refused rather than silently swapped for the deployment's own backend. The old readers in this package dropped such a section without a word, which let the server swap destinations after all. The guard pins the new three-way verdict (absent, honoured, malformed with the field that is wrong) against the server's own judge, vector by vector, whenever that judge is available next to this package; it pins that a half-configured environment is malformed rather than ignored, that a malformed environment never falls back to the settings file (that would change the destination too), and that neither the sentence shown to the user nor the recorded reason repeats an endpoint, a key, or a search-parameter name or value — an unrecognised provider is never echoed either (the sentence lists the valid words instead), so a URL or key pasted into the wrong field does not come back out, even when it happens to be all letters. A host's key store is plugged in through a callback the package calls only after the provider has been recognised, so the precedence between environment and settings stays inside the package. Since 0.83.0 an endpoint that carries a user name or password is malformed as well (the server judges the same way from 7.101.0), the reason sentences match the server's own word for word, and the three older readers that dropped a misspelled provider are gone. Since 0.93.0 the search key is checked in the same pass: a key that cannot be carried as an HTTP header value — a line break, a NUL or another control character, or a character above U+00FF anywhere but at the very ends — is malformed, with the server's own sentence (the server refuses such a key from 7.118.0 on, and a deployment's own start-up configuration is refused too), while a trailing or leading newline, as secret files often carry, is accepted just as the runtime accepts it. The rule is a pure re-implementation of the runtime's two-step header check, so the package stays free of Node built-ins; the guard pins it character by character against the real Node header pipeline, sends the whole Latin-1 range through a real loopback request, compares a large corpus with the previous release byte for byte (legal keys read exactly the same; only keys that could never be sent differ), and pins that the server's refusal is read by its code alone — nothing in the package matches its sentence. |
@@ -411,7 +411,7 @@ guard still cross-checks the table by name).
411
411
  | `scripts/run-store-center-wiring-capability-test.mjs` | The two cloud-mode capability positions (engine ≥7.106.0), read the same four-state way as the other capability readers. The store posture (`capabilities.store`: declared backend, assembled backend, whether it is durable, an optional degradation reason, and the SQL engine seat) is always present on a current engine, so an absent key means an older engine and is never read as “not durable”; words are carried as reported rather than folded into known ones, the SQL seat is judged by the very same rule as the SQL-posture reader, and one malformed seat discards the whole reading. The center wiring (`capabilities.centerWiring`) is present only when the engine is connected to a configuration center: with no quota lease configured its three lease-dependent keys are legitimately absent, and an absent key is split by the same response — a current engine (it carries the store posture) is reported as not connected, an older one as not reported. Two predicates live in the package (does this replica persist; is the caller's budget lease-enforced — known words matched exactly, everything else unknown), and each reader has one table of doctor sentences in which no two states read alike, the older-engine sentence never claims “not durable” or “not connected”, the two failure postures cannot be said the wrong way round, and unrecognised words are shown escaped and bounded. Both are checked against the engine's own projections: every form they mint reads back exactly, every word in their vocabularies lands on a known arm, and the member lists the engine declares for these two positions equal the reading's own keys. |
412
412
  | `scripts/run-park-failed-reason-projection-test.mjs` | The fail-soft true cause when a parked approval cannot be decided and the engine's own `done{suspended}` terminal is the one handed back: that terminal is still returned as-is (no second terminal), but it carries `_sema_park_failed_reason` — the same sentence, byte for byte, that the durable leg puts on its synthesized terminal — and the result frame carries it through with credentials washed the same way `errors[]` is (value only; same sentence as the durable leg after washing). The key is left off when this host has no decision surface at all (no approval card port, a port installed under another session key, or no question overlay): that sentence's way out is to decide on the card, which such a host never shows. Pinned with a policy-fold refusal on decide, both stall exits, a host-retracted card, credential samples through the full bridge on both legs, the no-decision-surface forms, and the absent forms (user interrupt, malformed values, ordinary terminals). |
413
413
  | `scripts/run-models-dropped-entries-projection-test.mjs` | The live model catalog's dropped-entries reader (`droppedModelEntriesOf`): which catalog entries the engine did not accept and why, read from `GET /v1/models` `droppedEntries`. An absent key (an older server) reads as `not_reported` — never as an empty list — while a present empty array is a real "nothing was dropped". Rows keep the server's order; a malformed row (non-integer index, empty reason code, bad identity, accessor fields, sparse holes) is counted as unreadable without dropping its siblings; unknown reason codes and additive keys pass through unchanged. Only own data properties are read, accessors never run, a throwing read never escapes, and the result is frozen. The exported row type is derived from the SDK's `models.list()` return type, so it compiles on the peer floor SDK as well. |
414
- | `scripts/run-model-catalog-read-projection-test.mjs` | The single reader of the `GET /v1/models` body (`readLiveModelCatalog`) and the readers built on the catalog it returns. A row is keyed by its `name` (the value a request submits and the `@` handle); a malformed row is counted and dropped without losing its siblings, a later row repeating a name is counted, and the result is a frozen deep copy. The default model is the server's answer read from `defaultModel` only (`liveDefaultModelOf`): two rows sharing an upstream id resolve by `defaultModel.name`, a default missing from the catalog reads as `not_listed`, and the first row is never offered as the default. Per-row readers answer whether a model may be `@`-mentioned (`atMentionOf`: an absent key means the deployment has no mention list, not "cannot be mentioned") and what line-compatibility declaration it carries (`modelCompatOf`); the accepted thinking levels stay with `effortLevelsOf`, never re-derived from the compatibility block. `liveModelRowsOf` gives one frozen view per row with every field taken from those same readers; a context window is carried only when it is a positive finite number, with no lower floor. The desktop seat's `listModels` returns this same catalog and its outbound gate shares the row rule. A host can also ask whether the engine's own catalog serves a given model name: `modelAcceptanceOf` answers accepted, refused or unknown, and unknown is never folded into either of the others — the name missing, no catalog, a catalog that cannot be read or that lost rows while being read, and the words an engine always admits itself (tier words, tier aliases, `default`, which the catalog does not list) all read unknown; only a name that a fully read catalog does not list (or lists as disabled) is refused. The one-line notice `modelNotServedNotice` is produced for a refused reading only, in both languages, and quotes the model names through the untrusted-text path. |
414
+ | `scripts/run-model-catalog-read-projection-test.mjs` | The single reader of the `GET /v1/models` body (`readLiveModelCatalog`) and the readers built on the catalog it returns. A row is keyed by its `name` (the value a request submits and the `@` handle); a malformed row is counted and dropped without losing its siblings, a later row repeating a name is counted, and the result is a frozen deep copy. The default model is the server's answer read from `defaultModel` only (`liveDefaultModelOf`): two rows sharing an upstream id resolve by `defaultModel.name`, a default missing from the catalog reads as `not_listed`, and the first row is never offered as the default. Per-row readers answer whether a model may be `@`-mentioned (`atMentionOf`: an absent key means the deployment has no mention list, not "cannot be mentioned") and what line-compatibility declaration it carries (`modelCompatOf`); the accepted thinking levels stay with `effortLevelsOf`, never re-derived from the compatibility block. `liveModelRowsOf` gives one frozen view per row with every field taken from those same readers; a context window is carried only when it is a positive finite number, with no lower floor. The desktop seat's `listModels` returns this same catalog and its outbound gate shares the row rule. A host can also ask whether the engine's own catalog serves a given model name: `modelAcceptanceOf` answers accepted, refused or unknown, and unknown is never folded into either of the others — the name missing, no catalog, a catalog that cannot be read or that lost rows while being read, and — when the model list carries no `aliases` (an older server) — the words an engine always admits itself (tier words, tier aliases, `default`, which the rows do not list) all read unknown; a name that a fully read catalog does not list (or lists as disabled) is refused. When the model list carries `aliases` (the extra words the engine accepts besides row names), a word is accepted when it is a row name or an alias, matched by exact name only and never inferred from a row's upstream id; tier words and `default` are then judged like any other word, and an absent `aliases` key keeps them unknown (an older server). A row or alias that the deployment's mention list marks `atMentionable: false` is refused with its own reason, and an `aliases` value that cannot be read leaves an unmatched word unknown. The one-line notice `modelNotServedNotice` is produced for a refused reading only, in both languages, with a separate sentence for a word the deployment's list does not allow, and quotes the model names through the untrusted-text path. |
415
415
  | `scripts/run-seat-carriers-test.mjs` | The desktop seat's request carriers and the park bit. One inbound judge for `sendMessage` (`checkSendMessageRequest` / `isSendMessageRequest`): a closed key set, each key's vocabulary taken from the package's own tuple (permission modes, reasoning efforts, rewind modes, turn settings) and the attachment cap; the two subagent keys are judged (boolean, or `true` / `{ ttlMs?, max? }` with non-negative integers); an unregistered key, an accessor or a throwing read is refused with a key path; so is a non-enumerable own key, whatever its value (a structured clone would drop it, so the gate does not let it pass unread). The request and its three nested closed shapes (`rewind`, `settings`, `retainSubagentSessions`) must be plain records, so `Date`, `Map`, `Set`, class-instance and custom-prototype carriers are refused. The rewind carrier `{ mode, targetMessageUuid }` uses the same tuple as `rewindSpecForMode`, and the request keys it is projected to equal what that projector emits. `SEAT_TURN_SETTINGS_ORIGINS` records, for every settings sub-key of the request builder, whether the renderer carries it or the host supplies it; host-supplied keys are refused on the seat. Optional seat positions — verbs and carrier keys alike — are answered only by the seat capability table (`rewind` added). `tool_permission_resolved` may carry `parked: true` only when its outcome is `expired`, minted from the closing frame through `approvalCompleteParked`, the same reader the frame router uses. |
416
416
  | `scripts/run-compaction-started-projection-test.mjs` | The `compaction_started` frame (a compaction pass announced at its commit point, before the summary call) projected to an internal arm and then to the additive `compaction_started` chrome event — `pass` and `trigger` required (open-set trigger), `startedAt` only when it is a finite number, malformed frames dropped, zero transcript rows, the lane told honestly; checked against the server package's own frame builders (the start frame and the outcome frame carry the same `pass`). |
417
417
  | `scripts/run-compaction-status-tracker-test.mjs` | The single reading of "is a compaction running": `createCompactionStatusTracker()` pairs `compaction_started` with the frame that ends the same `pass` (the landed boundary, a failed or mooted outcome, or the end of the stream), never lets a sub-agent's pass light the main lane, ignores outcomes that carry no `pass`, and gives the interactive lane `{ compacting, since? }`; `compactionStatusFrameOf` turns each change into a `system/status` frame in the reference client's shape for the print lane (no heartbeat, no output-size field). The suite covers interleaved passes, the sub-agent counter-example, end-of-stream closing, older servers, hostile input, both lanes fed the same sequence, and the server package's own frame builders. An opening frame whose `trigger` is not a non-empty string is not recognised on any of the three ways the tracker can be fed (the same rule the projection applies): it lights nothing, is not recorded, produces no status frame, and a good frame for the same `pass` still opens. |
@@ -452,7 +452,7 @@ guard still cross-checks the table by name).
452
452
  | `scripts/run-registrar-tables-test.mjs` | The four registrar table bodies — this Guards table and the three census tables in the repository's negative-control record — are **generated** from `scripts/gates-manifest.json`, the one file that describes a suite. Every row's text must equal what the generator emits, the manifest's suite set must equal the suites on disk, each entry must declare how it is negative-controlled (rehearsed, blind, or behavioural, with the census taker itself declared as such since it does not appear in its own tables), and each row's outward prose is scanned against the published-surface word list — the same list the packaging-hygiene guard uses, shared rather than copied — before the generator may write it into this file. Adding a guard is therefore one manifest entry plus one generator run instead of six hand edits across three files, and a description that drifts in one place and not the others stops being expressible. The row count is no longer what is compared: the earlier arrangement checked the census tables by **length**, so rows naming the wrong suites reconciled green. Positive controls run entirely on in-memory copies — a changed description, a dropped entry, an added entry, a changed class and a hand-edited row on disk each have to make the same judgement speak — and the quieter halves are pinned too: nothing outside a table body may move, a line inside one that is not a recognisable row makes the generator refuse rather than drop it, byte equality is backed by a column-count check (a cell holding a bare pipe splits a row into extra columns, and a code span does not protect it), and the malformed rows kept byte-for-byte as they are found are registered individually, so the registration turns red the day it stops being needed rather than outliving its reason — audited in both directions, since a registration pointing at a row that is no longer malformed and one pointing at a guard that was reclassified or deleted are both exemptions nobody reads |
453
453
  | `scripts/run-cc-message-key-census-test.mjs` | The one rule behind CC-shaped messages this package emits: every top-level key on a `user` / `assistant` / `result` / `system` message must be a member the CC SDK mirror (`@sema-agent/agent-types`) declares for that same arm, or one of this package's `_sema_`-prefixed additive keys, or a row in a dated transition table that goes red the day its retire version arrives. Mint sites are found syntactically (identifier, quoted and computed `type` names alike) and their key sets are resolved syntactically too — inline literals, both branches of a conditional, the nullish-coalescing and logical or/and operators, `const` initializers and every `return` of a helper — so a type assertion, a `Partial<Pick<…>>` narrowing or a computed name cannot launder a key past the check, while a spread the tool cannot follow (a parameter, a `let`, a member access) fails the tool, never the product. Arms that carry a `subtype` discriminator are checked against that subtype's own member set, so a key declared only for another subtype does not pass on the strength of the arm-wide union. The runtime section drives the real adapter pipeline and pins the tool-result record's SDK-spelled `tool_use_result` (the transitional camelCase twin rides along by reference until 0.83.0). A second runtime section feeds the same tool-result frame to both the interactive adapter and the non-interactive frame builder and requires the structured-result key and the no-output marker to be present together, absent together and equal on the two outputs, while the transitional camelCase name stays on the transcript record only; a static section checks a per-key routing table for the internal tool-result frame against the key sets resolved at the three mint sites, in both directions, and requires the non-interactive frame's structured-result value to trace back to the very same syntax node the transcript record uses — so recomputing or copying that logic on the non-interactive path fails even when the values happen to agree. |
454
454
  | `scripts/run-rule-removal-consequence-test.mjs` | The one sentence a rule-removal confirmation surface shows for what removing the rule will actually do: one line per behaviour the rule could have been enforcing, plus a neutral line for when that behaviour cannot be read back, so a caller that hits an out-of-set or missing value never falls back to a specific claim it cannot support. The guard compares the actual output against frozen text rather than merely checking that some string came back, so a dropped word or a swapped clause is caught the day it lands, and the four sentences are pinned pairwise distinct. The line for a rule that was denying something is pinned to say the removal widens what can run rather than echoing the wording used for a rule that asks again — the two are opposite directions, and sharing a sentence between them would tell the person confirming the removal the opposite of what is about to happen. The neutral line is checked from the other side for the same reason: it must not contain a word that belongs to only one of the three behaviours, because that would answer on behalf of a state the caller was unable to determine. The lookup that turns a raw stored or transmitted value into one of the three behaviours reads by strict equality only, proven with a fully trapped proxy and a counting getter to show it never touches a property on whatever it is handed, so a value with a legitimate-looking word sitting on its prototype chain is rejected exactly like any other out-of-set value rather than being unwrapped. A closing self-check mutates one character out of each frozen sentence and asserts the exact-match comparison actually fails on it, so the guard cannot pass by checking only that a string of some kind came back |
455
- | `scripts/run-cloud-effective-projection-test.mjs` | The cloud control plane's effective-configuration response beyond its four configuration domains, and what a locally started engine does with the models document derived from it. Three top-level keys are read with the meaning their producer gives them: `warnings` (degradation warnings from the build that produced the served view — an empty list is a clean build, an absent key is an older server that cannot tell), and `budget` / `runtimeCaps`, where `null` means two different things: nothing resolves for this principal when you view yourself, and values withheld when the response previews another principal. A missing key or a wrong type is a third state, unknown, and none of the three is ever folded into a zero, a `false` or "no budget". A malformed warning row, budget field or cap costs only itself, and a known budget field of the wrong type is named as not shown rather than silently read as "no limit on this axis"; warning kinds are an open set, so a kind this client does not recognise still produces a warning line. The budget and cap readers are reconciled against the installed settings schema. The single wording source puts degradation warnings first and keeps every "not set / withheld / unknown" sentence free of digits, while a zero the server really sent is shown as a zero. On the models side, the document is written in the shape of the installed settings schema: the default model rides `roles.default` and the @-mention policy rides `atMention` (`only` carries its list, `none` is written, and `all` is left to the schema default). Every role goes through one gate: a role that names something outside the catalog is dropped and recorded, and a role that names a catalog model is carried only when the host injects an entry check that accepts that entry — one rejected entry costs only the roles that name it — while without the check no such role is written and each one is recorded with the reason `unvouched` (the missing check is the cause, not the role), because the package cannot tell which entries the local engine will discard and a role naming one makes it discard the whole models domain (a role chosen by capability or price names no entry and is always carried). When the host injects an entry check the document also carries the tier groups and the active tier group (without the check neither is written), and every catalog reference the local engine's schema would reject — a role, @-mention entry, tier binding or active group that names something outside the catalog served to this principal — is dropped and recorded, because one dangling reference makes the local engine discard the whole models domain and fall back to its environment catalog; this is proven by reading the produced document with the installed file store. The @-mention policy is narrowed with the settings contract's own narrowing function: an allowlist pruned to empty becomes "nothing may be mentioned" (never "everything") and gets one warning saying so, and a policy this client cannot read is not written (the schema default applies) and gets its own warning. The keys the settings schema retired (`default`, `atModelAllowlist`) are never written and, from 0.98.0, never read: a reply carrying them and the same reply without them give byte-identical documents, issue lists, notices and pool-delete answers. Both generations of the same response produce the same document, byte for byte, and the gate compares the result with what the settings contract's own one-time migration makes of the older shape. Warning kinds with their own sentence are reconciled against the kinds the installed schema can produce. A per-model budget in which no field could be read is never described as having no limits. Registry annotation keys on model entries (`origin`, `overridesTeam`) are removed before the models document is written: the local engine's schema does not accept them, and a configuration refresh would otherwise be rejected as a whole. A model entry the host-injected entry check rejects is left out of the document and references to it are dropped: the local engine drops such an entry at startup, but a refresh rejects the whole configuration over it, so the gate requires a clean read of the produced document; when no entry passes the check, the catalog is kept as sent and gets its own warning, which the gate proves by reading the document back. The check receives a copy, so it cannot alter what is written. Tier words outside the local schema's closed set are dropped and recorded as unsupported, and the package's tier word list is reconciled against the installed schema in both directions; an active tier group is judged against group names, never model names. Since 0.96.0 the MCP leg's account of HTTP transport keys is reconciled against the installed settings contract: the connector assertion header is not forwarded while the request shape has no such key (the guard turns red the day it does), and an entry that names both the principal header and the assertion header is left out with the reason `principal-and-assertion-header` instead of having one of the two picked, answering exactly as the contract's own parse does whenever both are well-formed names; the dropped entry's header references are not reported as missing environment names. The request shape is read with the type checker, so a quoted or inherited declaration counts as declared. From 0.98.0 an MCP http entry's `assertionHeader` is forwarded as written on both the cloud path and the local `.mcp.json` path, through one public reader that also leaves out an entry setting both identity headers, and a notice names every server that declares it, because no supported server sends that header yet. The written shadow models document is a data copy: changing the input afterwards, or a `model` getter that answers differently on a second read, does not change it. |
455
+ | `scripts/run-cloud-effective-projection-test.mjs` | The cloud control plane's effective-configuration response beyond its four configuration domains, and what a locally started engine does with the models document derived from it. Three top-level keys are read with the meaning their producer gives them: `warnings` (degradation warnings from the build that produced the served view — an empty list is a clean build, an absent key is an older server that cannot tell), and `budget` / `runtimeCaps`, where `null` means two different things: nothing resolves for this principal when you view yourself, and values withheld when the response previews another principal. A missing key or a wrong type is a third state, unknown, and none of the three is ever folded into a zero, a `false` or "no budget". A malformed warning row, budget field or cap costs only itself, and a known budget field of the wrong type is named as not shown rather than silently read as "no limit on this axis"; warning kinds are an open set, so a kind this client does not recognise still produces a warning line. The budget and cap readers are reconciled against the installed settings schema. The single wording source puts degradation warnings first and keeps every "not set / withheld / unknown" sentence free of digits, while a zero the server really sent is shown as a zero. On the models side, the document is written in the shape of the installed settings schema: the default model rides `roles.default` and the @-mention policy rides `atMention` (`only` carries its list, `none` is written, and `all` is left to the schema default). Every role goes through one gate: a role that names something outside the catalog is dropped and recorded, and a role that names a catalog model is carried only when the host injects an entry check that accepts that entry — one rejected entry costs only the roles that name it — while without the check no such role is written and each one is recorded with the reason `unvouched` (the missing check is the cause, not the role), because the package cannot tell which entries the local engine will discard and a role naming one makes it discard the whole models domain (a role chosen by capability or price names no entry and is always carried). When the host injects an entry check the document also carries the tier groups and the active tier group (without the check neither is written), and every catalog reference the local engine's schema would reject — a role, @-mention entry, tier binding or active group that names something outside the catalog served to this principal — is dropped and recorded, because one dangling reference makes the local engine discard the whole models domain and fall back to its environment catalog; this is proven by reading the produced document with the installed file store. The @-mention policy is narrowed with the settings contract's own narrowing function: an allowlist pruned to empty becomes "nothing may be mentioned" (never "everything") and gets one warning saying so, and a policy this client cannot read is not written (the schema default applies) and gets its own warning. The keys the settings schema retired (`default`, `atModelAllowlist`) are never written and, from 0.98.0, never read: a reply carrying them and the same reply without them give byte-identical documents, issue lists, notices and pool-delete answers. Both generations of the same response produce the same document, byte for byte, and the gate compares the result with what the settings contract's own one-time migration makes of the older shape. Warning kinds with their own sentence are reconciled against the kinds the installed schema can produce. A per-model budget in which no field could be read is never described as having no limits. Registry annotation keys on model entries (`origin`, `overridesTeam`) are removed before the models document is written: the local engine's schema does not accept them, and a configuration refresh would otherwise be rejected as a whole. A model entry the host-injected entry check rejects is left out of the document and references to it are dropped: the local engine drops such an entry at startup, but a refresh rejects the whole configuration over it, so the gate requires a clean read of the produced document; when no entry passes the check, the catalog is kept as sent and gets its own warning, which the gate proves by reading the document back. The check receives a copy, so it cannot alter what is written. Tier words outside the local schema's closed set are dropped and recorded as unsupported, and the package's tier word list is reconciled against the installed schema in both directions; an active tier group is judged against group names, never model names. Since 0.96.0 the MCP leg's account of HTTP transport keys is reconciled against the installed settings contract: the connector assertion header is not forwarded while the request shape has no such key (the guard turns red the day it does), and an entry that names both the principal header and the assertion header is left out with the reason `principal-and-assertion-header` instead of having one of the two picked, answering exactly as the contract's own parse does whenever both are well-formed names; the dropped entry's header references are not reported as missing environment names. The request shape is read with the type checker, so a quoted or inherited declaration counts as declared. From 0.98.0 an MCP http entry's `assertionHeader` is forwarded as written on both the cloud path and the local `.mcp.json` path, through one public reader that also leaves out an entry setting both identity headers, and a notice names every server that declares it, because no supported server sends that header yet. The written shadow models document is a data copy: changing the input afterwards, or a `model` getter that answers differently on a second read, does not change it. From 0.108.0 the projection carries the names of the A2A peers the response declares (`a2a`), judged on the raw response before any parsing: a response without that domain reads as absent (an older server), one that is there but cannot be read reads as unreadable, and neither is ever an empty peer list, which only a present domain gives; the cell that parses first with the settings contract shows the absence turning into an empty list, names the wire-name rule rejects are counted as unreadable rows rather than shown, a real response built by the settings contract reads back with its peer names, and the warning row for keys the settings contract stripped from a peer goes through the existing warning reading and sentence. |
456
456
  | `scripts/run-plugin-hooks-projection-test.mjs` | Plugin hooks: each command hook an enabled plugin declares is decided one by one as running in the engine, running in this client, or not running at all, and the page of hooks sent with a request is built from the same per-turn plan the client uses to skip its own copies, so one hook never runs in two places. Governance is judged first and always wins — a managed hooks switch-off, an untrusted workspace, safe or bare mode, or a governance read that fails sends no plugin hook and does not list it as a gap; managed-hooks-only (set directly, or through a merged non-managed hooks switch-off) keeps only managed plugins; the plugin-only customization lock does not touch plugin hooks. A hook reaches the engine only when this client started the engine on this machine, the engine reports plugin-hook support, the entry is a command, the plugin declares no sensitive option, and the event still fits the engine's per-event limits; the gate walks that matrix cell by cell, including the limit boundaries and a session goal hook counting toward them. A fact that was never read is reported as not known rather than as a fact: a host that does not say where the engine runs gets a "not known whether this client started the engine" reason, an engine whose capabilities have not been read yet gets a "not known yet whether it supports plugin hooks" reason, and the plan's two engine facts are null in those cases, not false. Events the engine never fires run only if the client says it fires them itself, and hooks the upstream behaviour itself refuses (option references in a shell-form command, an unset option in exec form, malformed entries) run nowhere. Exec-form arguments are passed element by element with only saved non-sensitive option references filled in; path placeholders are left for the executor. Sensitive option values never reach the request: with a host that wrongly supplies one, every string in the plan, the request body, the notice, the labels and the log is searched for it across eight cells. A host without the plugin reader keeps the previous request body and gets exactly one warning per settings port; plugin data that throws while it is being read (a throwing getter, a revoked proxy) is treated like a failing reader — no plugin hooks this turn, settings hooks still sent, nothing thrown; the not-running notice names the plugin and events, never a command or an option value, and escapes control characters in names. Command hooks from settings that carry arguments (a non-empty `args` array, which is the exec form, or any other non-null value) are removed from the request until the engine reports support for arguments, because the engine would otherwise drop the arguments and run the bare command through a shell; an empty `args` array is not treated as carrying arguments when the command is made only of letters, digits and `_ . / : + -` (the shell runs the same executable), so such a guard still reaches the engine, while an empty array on a command with spaces or shell characters is removed; `args` on a prompt or http entry, a null `args`, or an entry with no type is left alone, and those go out unchanged. MCP tool hooks, which the engine cannot parse, are removed from every request: a plan records each one and its not-running notice lists them — the gate checks request bodies against the engine's own hooks schema, and the raw settings against it as a positive control. Malformed entries still pass through for the engine to reject loudly. Since 0.98.0 the page of hooks is built only from a plan: `hooksForWire` takes `{ plan }` and throws a TypeError for anything else (nothing passed, null, a value that is not a plan, a near-plan, a plan passed directly, an options object whose `plan` cannot be read, or a host value that throws while it is checked), naming the right call and never echoing the value or logging what the host value threw; the gate walks those shapes one by one and checks the built output for leftovers of the retired path. A `plugin` key written by hand on a settings hook is stripped before sending (even when its value is undefined), because only hooks that come from the plugin reader may carry plugin context; the settings document itself is left untouched and a debug line records the count. The two hand-copied tables, the engine-fired event list and the engine limits, are checked against their owners. Since 0.89.0 the same per-turn decision also gives every settings hook on an engine-fired event exactly one executor: a command, HTTP, prompt or agent hook from settings goes to the engine when the engine reports that it runs hooks sent with requests and either this client started the engine on this machine or this client cannot run that hook itself; otherwise this client runs it and the hook is left out of the request. A hook the engine is known to skip (a PowerShell command, or hooks the engine ignores entirely) that this client cannot run either is listed as not running; one the engine may or may not reach (capabilities not read yet, or a prompt or agent hook after the fourth one on an event) is still sent and not listed. The client learns which of its own settings hooks to skip from `settingsHooksKeptLocally`, which matches hooks by content against the request body only, returns the same array when nothing is skipped, and for the two compaction events follows whether the engine actually compacted. When the prompt was not written by the user, prompt-submit hooks from settings and plugins are left out on both sides. A host that reports events must also report which hook types its own executor runs on each event: with that table, a plugin hook it cannot run is reported as not running rather than as running in this client; without it, prompt and agent hooks from settings on engine events are treated as hooks the client cannot run and stay in the request for the engine. A settings hook whose required field is unusable (a command, URL or prompt that is not a non-empty string) is not assigned to either side and is sent unchanged, so the service still rejects it; malformed rows in a plan handed back by the host never make the not-running notice throw and never authorise skipping a hook; and a missing, empty or `*` matcher counts as the same match-all matcher when the client decides what to skip. A matrix over source, event, hook type, capability, engine ownership, host shape, prompt origin and compaction outcome checks that every hook runs at most once and exactly once whenever either side can run it, while the print lane and hosts without a local executor keep the previous request byte for byte. An optional leg runs the request body through an installed server's hook runner, current and previous release, together with the client-side skip, and counts one run per prompt. A plan in the previous release's six-key shape is read the same way by the request side and the skip side, so every hook it sends runs once; the client side of the skip reads groups and entries the way the client's executor does (class instances and inherited getters included); and on the two compaction events a prompt or agent hook past the engine's per-event limit runs in the client when the client can run it. The managed http-hook lists (allowed URLs and allowed environment variables) are read when the hooks plan is assembled and applied to every http hook that would be sent to the engine: absent means no restriction, an empty list allows nothing, a malformed list is read as empty and named, a credential in a URL never appears in the plan, and the lists are read from every settings layer the contract admits (the managed, user and flag layers; the project and local layers are never read), combined by intersection when several layers write one, with each dropped hook naming the layer whose list dropped it; a layer whose settings cannot be read counts as an unknown list and no http hook is sent that turn. |
457
457
  | `scripts/run-memory-saved-projection-test.mjs` | Engine memory writes (a successful `Remember` tool call) moved off the transcript onto the additive `memory_saved` chrome event, driven through the real pipeline: zero transcript rows for the write (the transcript is byte-identical to the same frames with the write reported as not successful), exactly one event whose `notes` carry the note text verbatim (notes, not file paths) and whose key set is exactly kind / laneProof / id / notes; no event for a missing, empty or non-string note, a non-`true` `ok`, a tool error, a missing result or another tool name; two writes give two events in order with distinct ids; a sub-agent write rides the sub-agent lane and an empty parent id emits nothing rather than falling back to the main lane; the `id` is derived from the write's wire key (the tool-end event id, else the tool-start event id, else the call id; empty ids count as absent), so projecting the same wire events twice gives the same id, and it never collides with the tool result row of the same or another call; the event sits right after the tool result row; the arm is registered as required. |
458
458
  | `scripts/run-result-frame-projection-test.mjs` | Result frames and the synthesized terminal rows. The CC key `terminal_reason` is minted on result frames only where it follows from what the engine reported: `completed` on success, `max_turns`, `budget_exhausted` and `structured_output_retry_exhausted` for the three matching engine codes, on both the done-frame path and the failed-event path. Every other outcome leaves the key absent as an own property rather than present with an undefined value: wall-clock and token-budget limits, the classifier denial limit, cancellation, unknown codes, blocked, paused, unreadable or missing terminal records, and the busy-session refusal. The public reader `terminalReasonForResult` shares the minting predicate and is checked to agree with the minted key on every frame the gate produces. Both the minted key and the reader derive the word from the frame's CC subtype (success with `is_error` strictly false, and the three limit subtypes), not from the error code, so a replayed row whose status is paused, blocked or unrecognised never carries a word that contradicts its subtype. The four words are checked against the mirrored CC union, and the minting file is checked to hold no hand-copied code literals. The renamed superset keys (`_sema_error_code`, `_sema_salvaged_result`, `_sema_model_degraded`, `_sema_selected_model`, and the row flag `_sema_api_error_message`) are driven through the real stream pipeline. Each must be present under its new name, the old name must be absent, and every frame the gate saw is swept for old names. The selected model appears on error envelopes whenever the terminal record carries it, and never on a failed event, which has no record. It stays separate from the provider-reported model name. The two in-package readers still work: the interactive result arm reads the salvaged text under its new name, and the print init gate treats the renamed flag as the run having ended. Since 0.86.0 the result arm no longer reads `result` on an error envelope, meaning a frame whose subtype is a string starting with `error_`, including error subtypes it does not recognise yet; a compile-time check pins that every CC error subtype carries that prefix. Such an envelope that carries the salvaged text only under the old `result` key yields no answer row, no appended suffix and no divergence event; when both names are present, or the new one is present but not a string, the old one is never read. Everything else reads `result` exactly as before, in all three producing cases (whole answer, missing suffix, divergence): success frames, a success frame flagged `is_error: true` (checked equal to the same frame with `is_error: false`), and frames whose subtype is a string without that prefix (empty, unrecognised, `Success`, a bare `error`), absent, or not a string (each checked equal to the same frame with subtype `success`). From 0.94.0 four root readers return the result frame's string superset keys (`resultErrorCodeOf`, `resultSelectedModelOf`, `resultSalvagedResultOf`, `resultParkFailedReasonOf`): they read only the frame's own data properties, never fall back to the retired names, run no getter and never throw. A synthesized terminal row that carries `_sema_api_error_message: true` has its text credential-washed on both projection paths (minted by the stream projection, or rebuilt by `adapt()` from an older transcript row), so the flag itself tells a client the text is already washed; model rows are left byte-for-byte alone. |
@@ -534,7 +534,7 @@ guard still cross-checks the table by name).
534
534
  | `scripts/run-session-id-path-segment-test.mjs` | The session-id path-segment judgment (`isSessionIdPathSegment`). A session id is spliced into `/v1/sessions/<id>/…`; URI encoding leaves `.` and `..` untouched and URL resolution swallows them as dot segments, which re-aims a request at a different route, so the id is usable only when it is a non-empty string, not a dot segment, and encodable. The suite cross-checks the judgment against a generated pool resolved by the platform URL parser, drives every call site that hands a session id to an engine route (memory erase, memory status, MCP panel and reconnect, background stop and agent registry read, delegated prompt) and checks that dot segments send nothing while a valid id still goes out, and keeps a syntax-tree roster so a new call site that skips the judgment turns the gate red. |
535
535
  | `scripts/run-rewind-engine-action-test.mjs` | The two pure judgements the terminal client used to keep for its rewind command. The dispatch gate says whether, right now, the engine has explicitly said that a given rewind mode cannot be done: each of the three modes (rewind with code restore, rewind the conversation only, restore code only) reads the same availability judges the package already ships for the menu, only an explicit off refuses, and a missing, unreadable or unobserved reading never refuses. The action judgement takes the chosen option, the target position the host has already resolved (session root, after the root, or undecidable, with the engine entry handle if there is one) and the capability reading, and answers whether to arm the one-shot rewind keys, to reopen the engine session (the two modes that carry a resume point, chosen at the session root, which the server will not rewind past) or to do nothing, with a reason; an undecidable position never acts in either direction, and the code-only mode looks only at the handle. The preview used by the confirmation screen is the same answer's kind. The guard checks both judgements against hand-written truth tables over every bit combination and every target shape, that the dispatch gate and the menu can never disagree, that the default reading follows the current engine target, that hostile inputs never throw and never act, and that the module keeps no mode list or resume-point list of its own. A live comparison against the terminal client's own copies runs in the layering guard. |
536
536
  | `scripts/run-tool-result-content-projection-test.mjs` | How a tool's result body becomes the content of the tool result in the transcript. A result that carries images keeps them: each image whose data is a string and whose type is supported (JPEG, PNG, GIF or WebP) appears as an image block with exactly the bytes the engine sent (the data is not re-validated, matching the reference client), in order, in both the interactive and the print output, while the structured result next to it stays the same text as before. A block that cannot be shown (an image without string data or with an unsupported type, or a block of an unknown kind) is left out and the content ends with one notice saying that unsupported content was omitted, so nothing disappears silently. Plain text and text-only results are unchanged strings, a body the engine already shortened stays a string with the truncation note, and an error result's content is always text. The text readings of a result body (the flattening helpers and the subagent viewer's result slot) come from the same projection and agree with each other. That projection is also the one text reading behind the public `toolEndOutputText` (an empty block list reads as an empty string, an image as `[image]`, an object that is not a string or a block list as absent) and behind the classifier-denial and aborted-call checks, so a frame that carries an image or an unreadable block is never mistaken for one of those engine-minted bodies, and a classifier denial is recognised only when it arrives as a bare string or a single block (a signature split across blocks is not a denial). Whether a result is an error is decided once for both the interactive and the print output: a structured result reporting a failing exit code is an error in both, whatever the tool's name and even when the result carries no body. The task-output tool's content is the model-facing text in both outputs, with the structured task record kept beside it. A result block that reports a media item as not delivered (a descriptor with the media type, an optional MIME type and the decoded byte count, or a summary block with the number of folded blocks and their total bytes) is shown as one sentence at its own position instead of being swallowed by the single unsupported-content notice, and a block whose fields are malformed stays an unreadable block. The same judgement is public: `toolOutputMediaAbsenceOf` reads such a block and `toolOutputMediaAbsenceSentence` gives its sentence in English or Chinese. |
537
- | `scripts/run-refused-park-family-test.mjs` | The refused-resume family (`refusedParkFromError`, `refusedParkContent`, `refusedParkBlockedContent`): three 409 codes whose refusal comes back every time the same decision is sent again (the stored park is not supported by this engine version, its resource ledger carries a retired wall-clock allowance, or the engine's gate refuses its stored task configuration or permission-mode seed). The suite checks the reader on SDK error objects and on flattened failure facts (status policy, field token, still-pending reading, getters that throw), compares every sentence byte for byte against a frozen copy of the terminal's previous implementation over each combination of code, field, pending state, lane and run id, pins the deliberate differences one by one, and checks that the sentences are correct per lane, never print an unknown code, pass the package hygiene list and do not trip the already-resolved word scan. |
537
+ | `scripts/run-refused-park-family-test.mjs` | The refused-resume family (`refusedParkFromError`, `refusedParkContent`, `refusedParkBlockedContent`): three 409 codes whose refusal comes back every time the same decision is sent again (the stored park is not supported by this engine version, its resource ledger carries a retired wall-clock allowance, or the engine's gate refuses its stored task configuration or permission-mode seed). The suite checks the reader on SDK error objects and on flattened failure facts (status policy, field token, still-pending reading, getters that throw), compares every sentence byte for byte against a frozen copy of the terminal's previous implementation over each combination of code, field, pending state, lane and run id, pins the deliberate differences one by one, and checks that the sentences are correct per lane, never print an unknown code, pass the package hygiene list and do not trip the already-resolved word scan. Since 0.108.0 the interactive way-out sentence holds for both engine generations: send a message and the client offers to cancel the run, answering the card first if it comes back instead (en / zh). |
538
538
  | `scripts/run-prompt-blocked-sentence-projection-test.mjs` | The sentence for a run whose first prompt was held back by a deployment's `userPromptSubmit` hook. The engine frames its own explanation for the model in an envelope that carries the session's reminder mark, and the terminal row used to print that envelope verbatim (`Run blocked: <system-reminder mark="…">…`) while the machine-readable cause beside it went unread. The package now has one sentence-maker for the `blocked` terminal, shared by the terminal row and the result frame's first error: a hook that refused the prompt reads `UserPromptSubmit operation blocked by hook:` followed by the hook's own words (treated as untrusted text — control and bidirectional characters shown as visible escapes, credentials washed, folded to one line, envelope tags removed even when spelled with zero-width characters — and since 0.104.0 also when the engine hands them over in its defused entity spelling `&lt;system-reminder>`, when they are `task-notification` tags, or when the engine's length cut left half a tag glued to its truncation mark `…` (half a tag name without that mark is ordinary text and stays, so a trailing comparison such as `elapsed < t` is kept); only the display copy is decoded, and only the two entities the installed engine actually emits, which the suite pins by running the engine's own defusal functions); a hook that crashed, timed out, or was cancelled mid-check gets one fixed sentence each, saying the prompt was not submitted; an unrecognised cause word prints only the lead and never echoes the word. When the machine-readable cause is missing or malformed, the old `Run blocked: <reason>` line stays, with a whole engine envelope unwrapped so no tag or mark reaches the screen, and an agent's own free-text reason is byte-for-byte what it was. The new line counts as a governance stop (it is not an `API Error:`) and is recognised by the same row predicate as `Run stopped` / `Run blocked`. The suite also pins the single mint site in source, covers the interactive lane, and checks the tag remover is linear on near-miss input. |
539
539
  | `scripts/run-active-run-busy-read-projection-test.mjs` | How the package reads a rejected submission (the session already has a run in flight) in its terminal row, in its result frame and in the public discriminator `activeRunBusySignal`: one discriminator body and one carrier rule, shared by all three. The server's rejected and conflict terminal frames carry `status: 'failed'` since 7.125.0; the older idempotent-replay form — a cached copy of the original 409 conflict body wrapped as `done{ result, replay: true }`, with neither `status` nor a terminal word — is no longer recognised, since the package's support floor moved past the servers that sent it. The suite pins what such a body reads as today (the discriminator answers null, the row is the single `Outcome unknown` line, the result frame carries the unknown-outcome flag, the public projector agrees, with or without the replay flag, code only, id only), checks over a pool of sixteen frames that the discriminator and the row agree in both directions (including a cause-form frame that carries the busy code), pins which objects count as the carrier, pins that the retired folding function and its flag are gone from the source and the build, and pins in source that the discriminator and the carrier rule are each defined once. |
540
540
  | `scripts/run-fleet-terminal-retain-test.mjs` | The fleet ledger's rule for terminal rows has one home and one function. A terminal row (completed, failed, killed) lives only in the retention pool and never in the active set, whichever way it arrives — a task frame or a reconnect snapshot — and both arms go through the same admission function. A row brought back by a reconnect snapshot therefore expires with the retention window exactly like one that arrived as a frame; before, the snapshot arm left it in the active set for good, and a lost removal frame did the same to a frame-delivered row, and clearing the pool could not clear it. The suite refutes the change on three axes: timing (snapshot before or after the terminal frame, interleaved with removal, repeated delivery, resurrection, a later snapshot that does not carry the row, a row being viewed, pool clearing, and every terminal word through both paths with identical answers), concurrency (twin rows of one task in either order, a notification-retired entry meeting a snapshot row, notifications against a snapshot-carried row, two sessions' ledgers), and failure class (a status outside the terminal vocabulary, absent, or null is never turned into a terminal row; rows without a usable id, with a non-string id, or null entries are dropped and counted, never thrown on). It also pins that the removal hook still reports a known row for a settled terminal row, because downstream bookkeeping credits a removal only when neither the stale mark nor the unknown-row mark is present. Finally it pins the terminal evidence the ledger keeps past the window (status word plus cycle, per task tail, bounded): after a row has aged out or the pool was cleared, a same-cycle completed notification cannot turn a known failure or kill into a success, while a newer cycle, a revival, and the upgrade direction still pass; the evidence itself is cycle-monotonic, so an older generation's terminal row, or one arriving late after a revival, neither overwrites nor re-plants it. Since 0.97.0 it also pins the ledger's single per-task rule, applied in the one admission function to task frames and snapshot rows alike: a reading from an older cycle than the task has already shown is ignored (it neither revives the row, nor withdraws the terminal evidence, nor lets a snapshot drop the task's newer in-flight row); a newer cycle retires older in-flight aliases of the same task; within one cycle, or when the order cannot be told, an in-flight alias wins over a terminal row; otherwise the terminal row settles the task and twin rows collapse into one. Every arrival order of a set of readings, through either arm, gives the same projection and the same in-flight reading. A notification from an older cycle than the task has shown leaves the rows alone even when the running row's latest frame carried no cycle, and the bounded per-task cycle record never evicts a task that still has rows. |
@@ -546,12 +546,12 @@ guard still cross-checks the table by name).
546
546
  | `scripts/run-seat-apply-config-reason-test.mjs` | The seat's model-configuration receipt now carries a closed set of reasons instead of a free-form string. When a host refuses to write the configuration it answers with one of five words — the model catalog on this device shadows the write, the cheap model asks for a different provider, the request was not valid, the file could not be written, or the app cannot change the configuration right now — and the one-line human sentence for each word is minted in exactly one place, so a client renders it instead of keeping its own table. Free-text diagnostics moved to an optional `detail` field that is display-only. The outbound gate refuses any other word, including prototype-chain names and case variants, and refuses a failed receipt with no reason. The guard also reads the built declaration for the exact member list, compiles probes that must fail to type-check, and pushes each word through the gate, a structured clone and the sentence reader to show the renderer reads the same sentence the host chose. |
547
547
  | `scripts/run-seat-stream-history-test.mjs` | The seat's streaming-preview chunk and the engine session history projection each have one definition now. Live preview text used to be a shape the desktop host built by hand and the web client unpicked by hand; the package now mints it (`seatStreamDeltaMessage`) and reads it (`readSeatStreamDelta`, which tells a chunk, an unknown inner event, a malformed chunk and an ordinary message apart, so a chunk is never mistaken for a committed message). Rebuilding a transcript from the engine's stored session rows used to be written once per client with diverging rules; `projectEngineSessionMessages` is the single projection: it cuts out the engine's own prepended content using the provenance marks the engine stamps on each user row (never by guessing from text shape, so a user's own text that merely looks like a reminder tag is kept), keeps tool results as tool-result blocks, maps the engine's block vocabulary to the chat-message vocabulary, never invents a timestamp the row does not carry, copies tool arguments and unknown blocks as plain JSON so what it returns can always be cloned and sent, and reports every row or block it could not read instead of dropping it silently. The guard checks the minted bytes against the previous hand-built shape, runs a real adapter stream through mint, gate, structured clone and read, pins ten stored rows to an exact expected transcript, checks the provenance rules case by case (segments across blocks, malformed ranges, absent marks), and feeds hostile rows (getters, throwing proxies, sparse arrays, functions and cycles inside tool arguments) to show one bad row never takes the others down. A replayed tool-result row that the engine archived as parked at an approval gate now carries a machine-readable parked flag on its tool-result block, so a replay does not render it as a failure. |
548
548
  | `scripts/run-seat-recents-test.mjs` | The seat's recent folders. The recent-folders list is read in exactly one way: the folders of the session records (derived, never registered by the host) joined with the folders the host registered (the ones the user picked, whether or not a session ever opened there), de-duplicated by exact path, most recent first, and a path present on both sides counts once at its later time. `recentFoldersOf` is the pure reader; `readRecentFolders` is the orchestrating reader that calls `listRecentFolders` only when the seat capability table says `recents` is available, and that keeps the session-record half when the host half is not offered or cannot be read. The seat gets two optional verbs, `listRecentFolders` and `noteRecentFolder`, and one capability bit `recents` that governs both (an absent bit means the host did not say, never "none"). The guard checks the registration, the outbound gates row by row (plain objects only, so what passes the gate is what survives structured cloning), the capability reading and its three distinct sentences, the reader's absence and ordering rules (a reply whose rows are all unreadable is reported as unreadable, never as an empty list), and an end-to-end chain from the host's capability publication through the renderer's reading to the host's verb double. |
549
- | `scripts/run-approval-card-v1-projection-test.mjs` | Projection of the v1 approval card (the `card` of an `approval_request` frame) into approval-card request facts through `approvalCardFactsOfV1`. For every key the v1 card shares with the live approval frame, the same value, well-formed or malformed, fed to both paths yields the same card request key and value; rule offers land in the read-only slot because the ask-decision route carries no rule; the real-approval bit is read through the single real-approval reader (top level or `risk`); every key in the SDK card key list has a recorded disposition, so a new SDK card key fails the gate; keys outside the card contract are not read, host positions are not minted and the session-answer route fact is not stamped; the grant-binding state matches the live path; refusal evidence that is present on the card but unreadable (malformed, inherited or throwing) is recorded as `unreadableEvidence` identically on both paths; non-objects yield undefined, one throwing key drops only that key and records it, and the result is frozen; the v1 card, live-frame and generic-card projections answer every shared key identically. |
549
+ | `scripts/run-approval-card-v1-projection-test.mjs` | Projection of the v1 approval card (the `card` of an `approval_request` frame) into approval-card request facts through `approvalCardFactsOfV1`. For every key the v1 card shares with the live approval frame, the same value, well-formed or malformed, fed to both paths yields the same card request key and value; rule offers land in the read-only slot because the ask-decision route carries no rule; the real-approval bit is read through the single real-approval reader (top level or `risk`); every key in the SDK card key list has a recorded disposition, so a new SDK card key fails the gate; keys outside the card contract are not read, host positions are not minted and the session-answer route fact is not stamped; the grant-binding state matches the live path; refusal evidence that is present on the card but unreadable (malformed, inherited or throwing) is recorded as `unreadableEvidence` identically on both paths; non-objects yield undefined, one throwing key drops only that key and records it, and the result is frozen; the v1 card, live-frame and generic-card projections answer every shared key identically; the two who-asked positions (`agentId`, `askReasons`) are read by one reader on the live frame and on the card, covering well-formed, absent and malformed shapes (bad items are dropped one by one, an unreadable array drops the whole position), open-ended words kept as they are, getters never run and the output frozen, and the generic card does not read them. |
550
550
  | `scripts/run-approval-card-reading-projection-test.mjs` | Single reading of approval-card facts. The live-frame projection `approvalCardFactsOfFrame` yields, key for key, the facts the live approval path hands to the card port (well-formed and malformed frames alike), including the route, grant-binding and unreadable-evidence facts `standingRowsOf` needs, and a live card whose must-ask evidence could not be read is never answered automatically; the generic-card projection `approvalCardFactsOfGeneric` reads the version-independent keys with the same readers as the v1 projection, reads nothing else, marks the payload unreadable and leaves every automatic-answer lane at unknown; the approval-window reader `approvalWindowKeysOf` accepts only non-negative finite values, needs a current frame, stamps the arrival anchor only when a window is present, requires a coherent envelope for unknown frame shapes, and matches the window on the live card; `probeCauseRowsOf` renders the probe-cause roots and further rows or nothing when the shape does not match, with untrusted text neutralised; `approvalCardReasons` orders the single-sentence reasons and the engine's own message, each line equal to its source sentence, deduplicated, within a line budget that reports what it hides; `readRequiresRealApproval` reads the top level or `risk` and every package reading point agrees with it; `countOwnedPendingRows` counts owned rows this client can present and throws instead of answering zero when the list cannot be read. On the durable parked-approval path the card's tool input is the pending row's `input` — the bytes that will run — and the event stream's `tool_start.args` is used only when the row carries no real input; when both are present and differ as JSON values (compared node by node, stopping at the first difference, within a node and depth budget) the card carries `argsRewritten: true` with an explanatory note, a comparison that cannot be decided adds nothing, and when they match the card is byte-identical to the one built without the stream copy. The live approval card reads its tool input through the same single reader: the frame's own `args` (the bytes that will run) are used whenever they are a plain object, the stream copy is only a comparison partner then, a difference adds the same `argsRewritten` flag and note, and the stream copy is shown only when the frame carries no real input, with a note saying it is the model's original call; the suite also checks from the source syntax tree that both paths call that one reader and that no other function repeats its order or comparison. |
551
551
  | `scripts/run-fail-sentence-test.mjs` | The wording of failure sentences that reach the user, in two parts. (1) A scenario refusal now has one sentence owned by the package: `scenarioNotAllowedContent` renders the subject, the allowed list (names escaped but never truncated, empty list omitted) and, for a fresh request only, the way out of omitting the scenario; the resumed-work form leaves that way out off, since a decision has no scenario to omit. The four decision exits (approve, deny, question card, plan review) append the sentence after the resume-policy sentence when the engine folded the refusal into `resume_blocked_by_policy`, and the failed outcomes of the two approval legs carry the folded original code as `blockedBy` next to the outer `errorCode` (present only when the outer code is the fold code and the body names a non-empty original). (2) The remaining user-facing failure sentences read a thrown value the same single way as the earlier seven: the four write and read exits (persisted rules, session policy, memory governance, MCP probe) no longer fall back to the class name or `[object Object]`; the subagent steer exit and the plan-review outcome sentence use the same reading and send the original thrown value's log form to the host log; the parked-row wait and the local rule parse take an optional `onFailure(log)` so their pure modules can hand the original to the host log; and the retry row's stalled `detail` goes through the same display exit as the four error arms. |
552
552
  | `scripts/run-engine-account-keys-projection-test.mjs` | The projection of five engine keys that older packages dropped on the way to the screen: a tool call's `recovered` start frame and `notExecuted` end frame (literal-true marks, read from own data properties only — never from a getter — and never turned into a claim about whether the call ran: absence means nothing), the completed run's machine-readable detail (the provider ended the last turn with a stop reason the engine does not read), the provider's account-condition word on a refused run, and the two notices that report an unknown stop reason and a stream that kept talking after its first ending. The suite drives the built package through the projection, the transcript adapter and the print-mode frame, and checks that absent, falsy, malformed and hostile values produce no claim and never throw, that the terminal reader keeps the run's kind whatever its detail looks like and isolates each key, and that the single reader for the marks is the only way the sources read them. |
553
553
  | `scripts/run-seat-devices-test.mjs` | The seat's device face. Whether the device face is usable, and what it can do, is read in exactly one way: `devicesFaceOf` takes whether the host wired the device verbs (the seat capability `devices`, or on a direct host whether it holds the SDK slice), the engine's device-lane reading when there is one, and the outcome of the last device list, and answers one of unavailable (with a closed reason), probe pending, probe failed, verbs off, or manage. On the seat lane there is no capability reading, so the first device list is the lane probe: until it answers, only that one read-only request is allowed and every write stays closed (`devicesWritesAllowed`). An unarmed verb and a host that reports `false` are the same face with the same sentence. The five seat verbs return the package emitters' outcome shapes, and the outbound gates check every arm as a closed shape (a missing required key, a bad value, an extra key or an unknown arm is refused, naming the path); the one-time enrollment token passes only at the top of the successful enrollment-token outcome. The guard also compares the direct-lane answers with the browser client's previous judgement cell by cell, walks a 140-cell seat matrix against an independent specification, and replays the cold-start sequence. The seat's credential-shaped key-name judge is exported from the package root and pinned to be the very one the seat's second guard uses (one declaration, one internal call site, one word list); its fixed list of judged names and near-misses such as metering keys is checked, and so is the absence of a judge of the same kind at the settings-schema package root. |
554
- | `scripts/run-seat-engine-target-test.mjs` | The seat's engine target (local or Cloud). The engine target has one state, owned by the host and read on the seat through the store `engineTargetStateStore`; six optional verbs switch the mode, run the registry device sign-in, connect or configure a Cloud engine, and clear it. Credentials never cross the seat: the state and every verb outcome are closed shapes at every level (an extra key is refused, naming its path), credential-shaped key names are refused as a second line, and the one inbound credential is the pasted engine token on the configure request, which the inbound judge checks for shape only and never echoes. Failures carry words, not sentences: `engineTargetRefusalText` and `cloudLoginFailureText` render them, and `cloudLoginFailureOf` reads a sign-in failure from the error's own data fields only (the server's error description is never shown; a client-side deadline counts as an expired code). "The engine changed" has one signal, the state's `generation`: `watchEngineTarget` reports each push with a `switched` flag and an `isCurrent(generation)` check, so a refetch started before a switch is dropped when it returns. `seatMcpReadingOf` turns the package's MCP reading into the seat view (an unreadable reading is never shown as an empty list). The guard covers all of this, including the minimal counterexamples for leaked credential keys and the three restart sequences. |
554
+ | `scripts/run-seat-engine-target-test.mjs` | The seat's engine target (local or Cloud). The engine target has one state, owned by the host and read on the seat through the store `engineTargetStateStore`; six optional verbs switch the mode, run the registry device sign-in, connect or configure a Cloud engine, and clear it. Credentials never cross the seat: the state and every verb outcome are closed shapes at every level (an extra key is refused, naming its path), credential-shaped key names are refused as a second line, and the one inbound credential is the pasted engine token on the configure request, which the inbound judge checks for shape only and never echoes. Failures carry words, not sentences: `engineTargetRefusalText` and `cloudLoginFailureText` render them, and `cloudLoginFailureOf` reads a sign-in failure from the error's own data fields only (the server's error description is never shown; a client-side deadline counts as an expired code). "The engine changed" has one signal, the state's `generation`: `watchEngineTarget` reports each push with a `switched` flag and an `isCurrent(generation)` check, so a refetch started before a switch is dropped when it returns. `seatMcpReadingOf` turns the package's MCP reading into the seat view (an unreadable reading is never shown as an empty list). The guard covers all of this, including the minimal counterexamples for leaked credential keys and the three restart sequences. The refusal list also carries `cloud_unreachable` (a Cloud engine is configured but cannot be reached right now); its sentence never mentions saving, since a new session has no save step. |
555
555
  | `scripts/run-onboard-judgement-test.mjs` | Adding a model, judged in one place. The decisions for onboarding a model live only in this package: the probe verdict (`probeModelEndpoint` — the verdict request is a streamed one-token request and `ok` is earned only by the model's first token within the deadline; a provider turning requests away is `busy`, headers without a token is `no_token`, a 2xx reply that is not a model answer is `shape`; the model list only feeds a similar-id hint; the key goes only to its own base URL and redirects are never followed), whether to write (`onboardWriteVerdict` — refused when the endpoint proved the setup wrong, and written as unverified only on explicit request when no verdict could be had), the write plan (`onboardWritePlan` — entry name, the entry's own key slot, update in place or create, the default model, and the other catalog entries' problems, with the engine's route-pairing rule through the core value ports), and the two naming algorithms. Writing files, keys, locks and restarts stay with the host. The seat gets three optional verbs under one capability `onboard`, with closed outcome shapes; the API key travels only inbound, at the top of the request, and is never echoed. The guard drives the probe with a fake fetch over both lanes, checks key redaction in five echo forms, the write rule, the write plan cell by cell, the naming algorithms against a frozen reference, and the seat shapes. |
556
556
  | `scripts/run-session-sync-file-history-test.mjs` | The user-facing facts for a session sync that moved the conversation but not its file history. One sentence says it (only the conversation was synced, why, what it means for restoring files with /rewind, and that syncing again will not add it), and a reader decides whether it is said at all: only when the sync result's own file-history outcome is "skipped", and not when the source manifest states outright that the file-history envelope is empty (no tracked files and no rewind boundaries); anything unreadable is said rather than swallowed. A second reader gives the occurrence identity (the session and the source's last entry) so a client can avoid repeating the sentence when nothing new was synced, and a judge tells the 404 for a missing file-history blob (status 404 and exactly the code not_found.blob) apart from every other 404. The guard pins the sentence byte for byte, walks each outcome of the real SDK push, pull and import calls against fake peers, and checks that hostile input (accessors, revoked proxies, inherited properties) never throws. |
557
557
  | `scripts/run-file-history-tighten-test.mjs` | What a client does when the user has turned file checkpointing off but the engine's file-history capture capability cannot be judged. A four-way verdict reads the capability the same way the request fragment does: no restriction requested, declare the per-run opt-out (the engine says it can be switched off), already off (the engine does not capture), or unjudged (not observed, not reported, a word this client does not know, or a malformed reading), and only the last refuses the send, with a sentence chosen by cause (not read yet, so try again in a moment; read but not readable, or not reported, so trying again will not change it) that says nothing was sent, why, and the way out. A single gate answers before a submission: anything judged costs nothing, while an unjudged reading triggers exactly one single-flight re-probe of the engine's capabilities (a body that already landed but is unreadable for this one field is fixed in place; a failed or off-generation re-probe leaves the previous reading untouched for every other reader) and the verdict is taken again. The guard pins the verdict table, the three refusal sentences, the one-probe and single-flight counts against a real capability cache, and the failure and ordering cases. |
@@ -614,7 +614,7 @@ guard still cross-checks the table by name).
614
614
  | `scripts/run-standing-rows-test.mjs` | Single judge `standingRowsOf(card, host?)` for the three standing rows on an approval card: allow for this session, don't ask again, and the deployment-list session row. Every row widens what runs without asking, so each one fails closed. An unreadable card, `rememberRefused` from the engine (own or inherited) or a present-but-unreadable value closes all three rows, even when rule offers are present. The session row needs the route fact `allowSessionCarried`, which the live-frame and pending-row paths stamp and the v1 path does not. The deployment-list row needs every ask class to be the deployment-list class, checked against the installed engine's `classifyAsk`. Engine rule offers in either slot give `engine_offer`; an unreadable offer key gives none. A local whole-tool rule is offered only when no engine lane closed the door, the arguments are available, the grant binding reads `none`, a standing allow rule could clear the ask, and the host reports a generic card face and that the rule would be sent; each precondition is flipped on its own. The live-frame, pending-row and v1 paths are exercised end to end, the grant-binding reading (since 0.98.0 the engine's own `boundGrantAbsence` word, not the tool name) is compared form by form on all three paths with the server's three grant-binding functions when a 7.129.0-or-later server fixture is installed, and may only be stricter where the card cannot tell which of them the server applies, malformed, inherited or throwing refusal evidence on the live-frame, pending-row and v1 carriers is recorded on the card as `unreadableEvidence` and closes the rows instead of reading as absent (checked end to end through the real paths); results are frozen, and hostile host facts never throw. Since 0.97.0 the seat request's allow-for-this-session bit `sessionRow` is minted by `toolPermissionRequestAskBits(card, requestId)` from the same premise as the judge's session row and the request id's domain (`live:` and `durable:` carry a session answer; `question:`, `elicit:` and `plan:` do not); an unreadable or refused remember flag never lets the seat show that row, and the crossing never copies the bit from its input — a seat request, or any input carrying a request id, passed back through it gets no bit. The deployment-list question class names come only from the generated ask-class table. The session row and the deployment-list session row are withheld when the card carries unreadable evidence or an unreadable card payload. |
615
615
  | `scripts/run-steer-tier-test.mjs` | Steer priority goes through one gate. The capability reader for `fleet.steerPriority` keeps three states, takes the word as an open set and treats `fleet: false` as not reported; its judge answers `engine` only for the exact word. The steer body has one constructor, `steerBodyOf`, used by the router and by clients that build their own request: `next` and no priority never enter the body (byte-identical to the previous releases on six call shapes), `now` and `later` enter it only when the reading is `engine`, and otherwise the constructor throws a refusal with a stable code and no request is sent. Against the 7.116.0 server package the guard runs the capabilities route, the steer route with the engine's own steer verb and an in-memory checkpoint store: the words the client admits are accepted, the words it refuses are rejected with the same field error, same-key replays stay idempotent, a real tier change is refused, and a `now` steer's cut notice carries the receipt's message id. It also pins the one sentence for a same-key replay (it does not tell the user to resend under a new key unless the text changed), the rewritten refusal sentences, the single receipt reader, the cut-notice verdict and sentence through both projection layers, the first-beat order (bounded capability wait, then reading, then send) and the engine-switch chain where an invalidated reading sends nothing |
616
616
  | `scripts/run-session-policy-grammar-test.mjs` | Reading and rewriting a session’s own rule record never changes which rule grammar its entries are read in. The record carries a grammar stamp: the read exposes it (absent means a record stored before the stamp, which the engine reads in its earlier grammar — never folded into a number), and both rewriting paths — tightening and removing refused entries — send the same stamp back. When a record has no stamp and a rewrite would carry back one of its allow entries written in a tool’s own rule form (such as Bash(git:*)), which the earlier grammar reads as allowing nothing and the current grammar reads as a working allow rule, the rewrite sends nothing and returns a machine-readable refusal naming those entries — for every caller identity alike, since an identity allowed to widen rules would otherwise have the write accepted. An engine refusal of the same kind maps to the same outcome, and a write receipt that comes back without the stamp it was sent with, or with an older one, is never reported as written. The guard runs each case for both caller identities against a modelled engine and, when the matching server build is installed, against the real engine write judge and the server’s error mapping, recording the pre-fix widening as its failing baseline; over a generated corpus it checks that every entry the engine reads differently in the earlier grammar is one the package refuses to carry back, and that every extra entry it refuses is one the engine would refuse to write anyway. Malformed stamps, accessors, per-key throwing proxies and a polluted prototype are refused rather than read as absent. |
617
- | `scripts/run-hooks-merged-disable-projection-test.mjs` | The fourth governance leg of the hooks projection: `disableAllHooks` set in a non-managed settings source. The value that counts is the **merged** one, computed by the package from the per-layer raw documents handed over through `HostSettingsSource.readLayer` (the former optional `SettingsPort.mergedDisableAllHooks()` host answer retired in 0.101.0), because a per-source approximation ("any source says true") reads user `true` with local `false` backwards — the merged value there is `false` and every source's hooks ship. When the merged value is `true`, only managed-settings hooks are sent to the engine: non-managed settings can switch off their own hooks, never the managed ones, and a managed `disableAllHooks` is still judged first and sends nothing at all. A full matrix over the four sources, each true, false or absent, is merged with the reference rule (later sources override earlier ones, managed settings last) and every cell's projection is asserted. The reader is the only authority: per-source values never second-guess it, and only a strict `true` counts. A host that does not implement it keeps the previous behaviour and gets exactly one warning per installed settings port, never one per request, and none on paths where the reader would not have been consulted; a reader that throws is treated as `true`, so managed hooks still ship. The session goal's Stop hook and the final-verification yield rule, which reads the projected hooks, follow the same verdict, and the trust gate and the three managed gates are evaluated before the reader is ever called. The last leg pins the member's declared shape in the built declarations: optional, no parameters, returning a boolean or `undefined`. The flag settings source (a settings file or inline settings given at startup) is projected after the local source, and settings hooks stay concatenated managed first (managed → user → project → local → flag): the engine runs hooks in request order, and three of its budgets go to whoever comes first — the per-event wall-clock budget, the per-event cap on model-backed entries, and the total cap on added context — so a managed hook placed after the others could be crowded out and silently not run. The flag source moves with user, project and local under every managed or merged gate; its exec-form entries are dropped and warned about like any other settings source, and the not-run notice names it `Flag settings`. An optional leg feeds the request body to an installed server's hook runner and requires the managed deny, block and context to take effect while non-managed hooks exhaust each of those budgets, and pins the known cost of that order: a non-managed `PreToolUse` hook placed after the managed one can still rewrite the input after the managed check allowed it. Without an installed server the leg reports that it did not run. Safe and bare mode: the host reports its startup mode through the optional `SettingsPort.hooksStartupMode()`, which both the plain and the plan path read and which answers `'safe'` or `'bare'`; the flag already carried by the plugin hooks reading, which only the plan path reads and which cannot tell the two modes apart, counts as safe mode unless the reader named a mode. In either mode only managed-settings hooks are sent from the settings sources: bare mode is handled like safe mode, so managed hooks are still sent, also under the managed-only gates. In both modes plugin hooks are excluded as before and the session goal's Stop hook is still sent. Only the two exact words count; a host that does not implement the reader, returns `undefined`, returns any other value or throws keeps the previous request byte for byte, checked for every such reply, both flag states, both paths and with or without a session goal; any other value and a throw each leave one debug line, and so does a reader written as a property instead of a method, which narrows nothing. The reader is called at most once per request and not at all when hooks are already switched off entirely. The optional server leg requires the left-out sources' hooks not to run while the managed ones still run and still deny, in either mode. A hook written in more than one settings place is sent once: entries on the same event, in groups that are identical apart from their hooks (same matcher), with the same identity — command, shell, arguments and condition for command hooks; type, prompt and condition for prompt and agent hooks; URL for HTTP hooks; server, tool and input for MCP tool hooks — are sent once, at the position of their first occurrence in the concatenation order and with the fields of the copy from the highest-precedence source (managed, then flag, local, project, user; within one source the later copy), so a managed copy keeps both its place and its values and a user copy duplicated in flag settings keeps its place but uses the flag copy's timeout; the other entries keep their order, groups without duplicates are sent as they were, and a group left empty is not sent. Different matchers, events, a one-byte command difference, a different condition, an absent versus explicit shell, different types or extra group keys are not merged; malformed entries and entries whose identity cannot be computed are left alone; plugin groups and the session goal hook do not take part, and hooks removed by safe or bare mode never come back. The optional server leg requires such a duplicate to run once with its added context appearing once, and a user copy with a one-second timeout duplicated by a flag copy with a ten-second timeout to run to completion on a two-second command. |
617
+ | `scripts/run-hooks-merged-disable-projection-test.mjs` | The fourth governance leg of the hooks projection: `disableAllHooks` set in a non-managed settings source. The value that counts is the **merged** one, computed by the package from the per-layer raw documents handed over through `HostSettingsSource.readLayer` (the former optional `SettingsPort.mergedDisableAllHooks()` host answer retired in 0.101.0), because a per-source approximation ("any source says true") reads user `true` with local `false` backwards — the merged value there is `false` and every source's hooks ship. When the merged value is `true`, only managed-settings hooks are sent to the engine: non-managed settings can switch off their own hooks, never the managed ones, and a managed `disableAllHooks` is still judged first and sends nothing at all. A full matrix over the four sources, each true, false or absent, is merged with the reference rule (later sources override earlier ones, managed settings last) and every cell's projection is asserted. The reader is the only authority: per-source values never second-guess it, and only a strict `true` counts. A host that does not implement it keeps the previous behaviour and gets exactly one warning per installed settings port, never one per request, and none on paths where the reader would not have been consulted; a reader that throws is treated as `true`, so managed hooks still ship. The session goal's Stop hook and the final-verification yield rule, which reads the projected hooks, follow the same verdict, and the trust gate and the three managed gates are evaluated before the reader is ever called. The last leg pins the member's declared shape in the built declarations: optional, no parameters, returning a boolean or `undefined`. The flag settings source (a settings file or inline settings given at startup) is projected after the local source, and settings hooks stay concatenated managed first (managed → user → project → local → flag): the engine runs hooks in request order, and three of its budgets go to whoever comes first — the per-event wall-clock budget, the per-event cap on model-backed entries, and the total cap on added context — so a managed hook placed after the others could be crowded out and silently not run. The flag source moves with user, project and local under every managed or merged gate; its exec-form entries are dropped and warned about like any other settings source, and the not-run notice names it `Flag settings`. An optional leg feeds the request body to an installed server's hook runner and requires the managed deny, block and context to take effect while non-managed hooks exhaust each of those budgets, and pins the known cost of that order: a non-managed `PreToolUse` hook placed after the managed one can still rewrite the input after the managed check allowed it. Without an installed server the leg reports that it did not run. Safe and bare mode: the host reports its startup mode through the optional `SettingsPort.hooksStartupMode()`, which both the plain and the plan path read and which answers `'safe'` or `'bare'`; the flag already carried by the plugin hooks reading, which only the plan path reads and which cannot tell the two modes apart, counts as safe mode unless the reader named a mode. In either mode only managed-settings hooks are sent from the settings sources: bare mode is handled like safe mode, so managed hooks are still sent, also under the managed-only gates. In both modes plugin hooks are excluded as before and the session goal's Stop hook is still sent. Only the two exact words count; a host that does not implement the reader, returns `undefined`, returns any other value or throws keeps the previous request byte for byte, checked for every such reply, both flag states, both paths and with or without a session goal; any other value and a throw each leave one debug line, and so does a reader written as a property instead of a method, which narrows nothing. The reader is called at most once per request and not at all when hooks are already switched off entirely. The optional server leg requires the left-out sources' hooks not to run while the managed ones still run and still deny, in either mode. A hook written in more than one settings place is sent once: entries on the same event, in groups that are identical apart from their hooks (same matcher), with the same identity — command, shell, arguments and condition for command hooks; type, prompt and condition for prompt and agent hooks; URL for HTTP hooks; server, tool and input for MCP tool hooks — are sent once, at the position of their first occurrence in the concatenation order and with the fields of the copy from the highest-precedence source (managed, then flag, local, project, user; within one source the later copy), so a managed copy keeps both its place and its values and a user copy duplicated in flag settings keeps its place but uses the flag copy's timeout; the other entries keep their order, groups without duplicates are sent as they were, and a group left empty is not sent. Different matchers, events, a one-byte command difference, a different condition, an absent versus explicit shell, different types or extra group keys are not merged; malformed entries and entries whose identity cannot be computed are left alone; plugin groups and the session goal hook do not take part, and hooks removed by safe or bare mode never come back. The optional server leg requires such a duplicate to run once with its added context appearing once, and a user copy with a one-second timeout duplicated by a flag copy with a ten-second timeout to run to completion on a two-second command. When the managed settings turn all hooks off, the not-run notice list gains exactly one line saying so; in every other case that line is absent. |
618
618
  | `scripts/run-inherit-env-intent-projection-test.mjs` | Whether the interactive lane may ask for the user's whole environment in tool shells. The engine says, through one capability word on its capabilities answer, that it keeps its own credentials out of shells it starts; this guard pins the four readings of that word (confirmed only for a literal top-level true; absent, declined with the value's shape, unreadable, or not yet observed otherwise, never read from a getter), the one decision built on it (interactive lane, project context confirmed, word confirmed and identity verified, all four, or the request omits the field with one named reason), and the single sentence clients show when the word is not confirmed. Looser spellings, nested groups and stringly true values must never open it, and the server fixture is checked by generation: one before 7.129.0 does not publish the word anywhere in its release, while 7.129.0 and later mint it at the top level of the capabilities answer, and the real minting function's answer, landed through the capabilities cache, is read by this reader as confirmed or declined by its boolean value. Since 0.100.0 it also pins the sibling word for credential values (a fact that can be false; absent on older servers) read the same way by its own reader. Since 0.101.0 the judgement is two-armed: the credential-values word is an input of the decision (the request asks for the whole environment only when both words are confirmed), a missing or unreadable value-word reading omits the field instead of silently opening it, an identity that is not verified is reported before the value word is, only properties the input and the readings own count (an inherited or polluted property is read as missing), and a second sentence says that the engine keeps its own credential variables out of shells but has not confirmed that it also keeps out copies under other names; the truth table, the loosening-direction negative controls for the second word, both sentences in English and Chinese, and the end-to-end path through the capabilities cache are pinned. |
619
619
  | `scripts/run-submit-refusal-projection-test.mjs` | Four kinds of refusal that mean the request itself needs fixing, read by code with one sentence each. Permission rule lists that are too long or hold a bad entry are named precisely (which list, how many entries against the limit, or which entry by position) for a new request and, with different wording, when parked work cannot continue with the request stored for it; the same sentence is appended to a decision that fails for that reason. A tool declaration the engine refuses at the start of a run is read by its code rather than its HTTP status, so a status that looks like a temporary outage is not presented as one: the sentence says the same request will be refused the same way. A deployment refusing a single-user-only setting is attributed from the request the caller sent (the whole-environment shell setting, bypassPermissions, both, or unknown) and always says nothing ran and the local settings are not at fault. A body-shape refusal exposes its two lists of unrecognized and unsupported keys exactly as sent (only well-formed lists are read; a missing list is not treated as empty), with a three-way check of whether any listed key is a settings path. |
620
620
  | `scripts/run-server-7114-fixture-test.mjs` | Adoption checks against the published server package's real task admission, run on every configured server package (the newest one and each older one in the list), each held to an exact expectation for its generation. From 7.114.0, a tool-event hook matcher that selects a tool's alias but not its current name is refused with a field-invalid error naming both names, in every spelling the server reads (literal, pattern, padded, comma list, pipe list, empty segment), and a matcher that also selects the current name is accepted; 7.113 refuses only the literal and the pipe-list spellings; 7.111 and older accept all nine. A server generation outside the table fails the check instead of being guessed, and with fixtures required each generation band must be covered. This package passes the matcher through byte for byte, the request it assembles gets the same answer from the server as the hand-written one, and its local skip check keys on the matcher as written. From 7.115.0 the refusal additionally carries a machine-readable `field` and a `detail` naming the event, the alias and the current name, with the sentence and the error code unchanged; older generations carry neither. |
@@ -684,7 +684,7 @@ guard still cross-checks the table by name).
684
684
  | `scripts/run-server-7141-pick-projection-test.mjs` | Readings that follow the server's 7.140 and 7.141 changes. The repin is checked against the installed type package (rule entries may be a string or a `{ rule, layer }` object, the memory backend word is `mysql`, the new keys are optional). The memory sync reading appears only when the server sent a well-formed `memorySync` and reports a parked capture as parked, and the sentence for a paused sync names the layers only when every name is printable and short, and names the operator's way out only when every layer reports it; an erasure refused because the personal store could not be judged reads as not erased; the engine lease on the health reading needs both numbers; a workflow completion carries its completion id through the stream, the adapter and the notification facts, while the engine's diagnostics text is offered for display only and never enters the model-facing notification body or the queued item; a subagent tool call the engine closed without running is shown as not executed rather than as a failure; the permission-mode write and MCP transport capability bits are read with absence kept apart from false, and an MCP server whose transport the engine says it does not support is held back with a sentence naming the transport. When the engine reports layered rule entries, deny and ask rules from settings files go out as `{ rule, layer }` (deduplicated within a layer, not across layers), request-level rules and allow rules stay plain strings, and with the bit absent the body is byte-identical to the previous release. Names the server or type package removed or that the package does not consume have no code-position reads in the package sources, and tasks are submitted only through the streaming call, so the reply body of a non-streaming submit is never parsed. An MCP entry's `sessionMetaKey` goes onto the request as written on both the local and the center config paths, and is left out when not set. |
685
685
  | `scripts/run-notification-completion-dedup-test.mjs` | One completion reaching the client over several channels is shown once. A background completion can arrive as the live notification frame, as a parked-inbox entry replayed later, as a frame replayed after a reconnect, and as a fleet notification; the service now stamps the same completion id on all of them. The render de-duplication of task notifications reads that id: a notification is identified by its locally computed key, which is always present, plus the completion id when the notification is terminal, not external and carries one, and every key a notification carries is recorded as soon as it is seen. A notification whose local key was already seen is a replay of the same frame and is skipped entirely, as before; one whose only match is the completion id is another copy of the same completion and is not rendered again, while its other duties (withdrawing a still-queued card of the same run, closing the panel row, the terminal hint) still run exactly as they did before. So two copies of the same completion whose local keys disagree (for example one carries a sequence number and the other does not) render once, three mixed copies render once, and two completions that differ in both the id and the local key still render twice. The local key stays because three things never carry the id: external notifications, frames from servers that predate the id on inbox entries and replays, and records restored from a saved transcript or seeded by a host; a frame that carries the id is still matched against those records, and the exported ledger keeps both keys so a host reading it by the local key sees no change. The cross-channel checks that stop a card already shown from a status probe, a fleet notification or a poll result from being fed or shown a second time stay as they were, because some of those sources never carry the id; the gate pins each of them, and the queue withdrawal that removes a still-queued card when the engine's echo arrives. Ordering, concurrency and failure are each exercised: all 24 arrival orders of four channels show the completion exactly once, two interleaved streams on one adapter render it once, and an unreadable, empty or non-string id falls back to the local key without throwing |
686
686
  | `scripts/run-background-task-table-projection-test.mjs` | Background-task table rows after a compaction, read the way the engine mints them: a row is the `- [<id>] Task "<description>" ` prefix, a status phrase, and at most one optional tail that starts with ` — ` and runs to the end of the line. The tail is projected verbatim onto the optional `read` field of the task-status attachment (absent means the engine sent none — never an empty string), rows without a tail stay byte-identical to the previous release (a frozen copy of the old reader and a seeded random pool of 4000 batches prove it), a `parked` row is recognised with or without a tail, a second ` — ` inside the tail stays part of it, and unknown phrases and the `(+N more …)` footer line are still not recognised. The status phrase is always read after the last `" ` (quote plus space) on the line, so a description that itself contains a phrase never turns an unknown status into a known one; a table whose row description breaks across lines (an opening quote never closed on a line that is not the last non-empty one) is not read as rows at all and falls back to the generic row. The engine's own table renderer is driven for real, status by status and read form by read form. |
687
- | `scripts/run-permission-mode-verb-projection-test.mjs` | Switching the permission mode of a running turn. `setRunPermissionMode()` sends the switch only when the permission-mode write capability reading is present and true; otherwise it answers `unavailable` without sending anything. The outcome is always a value, never a throw, and only `switched` carries a mode: the mode now in force is the engine receipt's `permissionMode.effective`, not the word that was sent. A fold by this deployment's floor or the task's settings is reported in `admission`, kept apart from the engine's own fold. `persisted: false` comes with a reason and a notice that a later resume returns to the earlier mode. Rejections are classified by status and machine code (not found / not running / refused / not available); a 500 `corrupt`, which the server sends before the switch reaches the engine, is a refusal, while other server errors and lost connections stay unconfirmed; an answer that does not match the documented seven-key shape, or contradicts itself, is unconfirmed rather than read as a switch. `runPermissionModeSwitchLine()` gives one sentence per outcome in English or Chinese. |
687
+ | `scripts/run-permission-mode-verb-projection-test.mjs` | Switching the permission mode of a running turn. `setRunPermissionMode()` sends the switch only when the permission-mode write capability reading is present and true; otherwise it answers `unavailable` without sending anything. The outcome is always a value, never a throw, and only `switched` carries a mode: the mode now in force is the engine receipt's `permissionMode.effective`, not the word that was sent. A fold by this deployment's floor or the task's settings is reported in `admission`, kept apart from the engine's own fold. `persisted: false` comes with a reason and a notice that a later resume returns to the earlier mode. Rejections are classified by status and machine code (not found / not running / refused / not available); a 500 `corrupt`, which the server sends before the switch reaches the engine, is a refusal, while other server errors and lost connections stay unconfirmed; an answer that does not match the documented seven-key shape, or contradicts itself, is unconfirmed rather than read as a switch. `runPermissionModeSwitchLine()` gives one sentence per outcome in English or Chinese. A 501 answer's machine code rides on the `unavailable` outcome (open set, carried verbatim) and the two known causes each get their own sentence; `permissionModeTitle()` maps an engine mode word to the name shown in the interface, and the switched sentence uses it. |
688
688
  | `scripts/run-run-ledger-snapshot-test.mjs` | Run-ledger snapshot reader. `readRunLedgerSnapshot` reads, for a run parked at a gate, the highest ledger sequence number, the parked call's own `tool_start` frame and, optionally, a fold over the numbered frames. It answers only when three pieces of evidence agree — the reader proved the end of the stream, the tail sits next to a park marker, and two concurrent passes end on the same number — and the run is parked both before and after the read; otherwise it answers `null`. A pass cut short by the server's draining frame is not a completed read and is re-read on the package's draining ladder. A fold that depends on frame order rejects a pass whose numbers go backwards or repeat, or that carries an unnumbered frame the fold cares about, and both passes must fold to the same answer. All readers are injected, nothing throws, and the budget is a hard wall-clock limit: a parked-state read or a ledger iterator that hangs and ignores the abort signal still yields `null` when the budget runs out. |
689
689
  | `scripts/run-approval-frame-live-facts-projection-test.mjs` | Approvals replayed after a disconnect or adopted after a server restart arrive without the three out-of-card live facts (mandated flag, mandate word, origin word). When all three are provably absent on a v1 opening frame, the card records them as unreadable, so the automatic-answer verdict says unknown on every lane and the hook scope is deny-only; a session-allow rule can no longer answer such an approval. Frames that carry any of the three, legacy-family frames, and frames the type guard rejects are unchanged. Evidence that already proves the question must be asked is read before the unreadable record, so such cards still answer must-ask and the hook still skips. Cells run real server captures through the whole-frame export, the live-card entry and the suspended-row entry and expect the same facts and verdicts from each. |
690
690
  | `scripts/run-project-seed-projection-test.mjs` | Seeding a handed-off session with a project package (a gzip tar of a project folder that has no git repository). `projectSeedGateOf()` decides whether a submission carries the package: only the hand-off arm, only when the target deployment reports an isolated execution lane, and only once an upload id is in hand; the mirror and side-car arms never carry it, and a lane the deployment does not describe is not treated as isolated. `projectSeedRefusalOf()` reads the four refusal codes (an invalid package at upload; a lane that is not isolated, a session that already has its seed, or an upload that belongs to another session at submission) with their machine-readable fields as sent, and `projectSeedRefusalContent()` / `projectSeedGateContent()` give one sentence per outcome in English or Chinese, with no codes or ids in the text and the problem path shown as untrusted text. Checked against the server's own lane table, bundle reader and refusal bodies through the real client. |
@@ -695,15 +695,20 @@ guard still cross-checks the table by name).
695
695
  | `scripts/run-sdk-33-pick-projection-test.mjs` | What the client relies on from the generation of the client library it was built with, and how it answers the engine's binding refusal. The approval and resume interfaces the client asks a host to hand in are written so that a real client object from the current library generation, the previous one and the oldest supported one fits them without casts; the check compiles a host's wiring against each of those generations. Resuming a paused run sends no request body and still honours a cancel signal. A decision on a pending approval echoes both binding fields exactly as the listed row shows them and is stopped locally, with nothing sent, when either field is missing, empty or not a string; the newest server's own judge is shown to accept every decision the client sends and to refuse every shape the client stops, and older servers are shown to never reach their gap-filling path. When the engine refuses a decision for missing binding, `decideRequiresBindingFromError()` reads which fields it named and `decideRequiresBindingContent()` gives one sentence telling the user to fetch the approvals again and decide from the card as listed; the same sentence reaches the decision outcome a durable approval reports. Queue rows are sorted into families from the word list the library publishes. The sorter the client's own pending-checkpoint readers use (`checkpointQueueRowSortOf`: checkpoint, excluded, or unreadable) is exported from the package root as the same function, and reads the family only through that one family reader. |
696
696
  | `scripts/run-approval-seat-judge-projection-test.mjs` | The approval-seat judge `approvalSeatOf`: whether the lane a run is submitted on has anyone or anything that can answer approval questions, answered in three states (seated, unseated, unknown) with a machine-readable basis word for every cell. It reads the submission identity (the lane comes only from the one lane derivation), the `approvalReach` value on the request (bad values are judged by the same predicate the request assembler uses), and the host's one statement about whether an answering face exists on that lane (the same fact the declaration fragment takes). Every cell of 12 identities by 4 word forms by 5 statement forms is pinned against a hand-written table; hostile inputs (accessors, revoked proxies, throwing traps) never throw and never turn into an absent value; the judge agrees with the request assembler on which values are bad and with the lane table on which lanes carry the declaration; and a second engine word planted into a copy of the built output is answered as unknown, never guessed. |
697
697
  | `scripts/run-sdk-34-pick-projection-test.mjs` | What the client relies on from the newest client library and settings contract generation, and how it reads settings written for another tool. A deterministic stored-session failure from older servers is still never offered as a retry, whether the library lists the code or not. The approval card words, the pending-row verdict words and the replay-loss notice read the same as the library declares them. The key-name echo used to match a server refusal uses the settings contract's own echo rule, and the display output never passes any of the characters that rule strips. A project settings file written for another tool is read for restrictions only: its deny and ask rules reach the request (tagged with their layer when the engine accepts layered rules), nothing else in it is read, and one line names the parts that were not read; an unreadable copy of that file asks or tells about its restrictions only. |
698
- | `scripts/run-server-7145-pick-projection-test.mjs` | What the client reads from the server's newest rule-import receipt and approval-queue facts. `readCcImportRedeemCounts()` takes the whole redeem response: the current receipt is counted row by row (persisted, already present, and rows whose outcome is unknown, listed with their rule identity and the engine's sentence ready for display, never counted as success), a receipt row it cannot account for makes the whole reading unknown instead of a partial count, and the older envelope is still read for older servers the way it was before. A parked approval left by a background sub-agent is claimed by the session that started it, or by the root of its delegation tree, and never by another session; the subscription feed re-emits when that link arrives late. Damaged rows in the assistant inbox stay listed with the engine's verdict. Checked against the server's own rule-import lane, approval stream and checkpoint store. |
699
- | `scripts/run-core-7630-pick-projection-test.mjs` | Readings that follow the engine's 7.63 changes. The notice catalog matches the installed engine code for code (117 codes): three new codes are listed with the engine's audience and no reader of their own, and `session.records_skipped`, which the engine dropped from its catalog while servers inside the supported range still send it, is still treated as listed with audience `user`, through a single table of such codes that the engine catalog never overlaps. The two memory-harvest notices count as one occurrence per harvest report. A parked row the engine cannot read is listed without a gate kind and with a damaged verdict: the verdict is read as the engine gave it, the row never becomes an approval card, and no default gate kind is invented. A tool approval parked before the engine bound the tool name into its hash is read as damaged, a row that does not match its reading rather than an engine fault. The six workspace refusal codes keep the families they already belong to: the two `config.` codes are configuration refusals, none of the six offers a retry, and a failed agent card carries its code and sentence unchanged. A background fork child's delegation link is read like any other background child's. |
698
+ | `scripts/run-server-7145-pick-projection-test.mjs` | What the client reads from the server's newest rule-import receipt and approval-queue facts. `readCcImportRedeemCounts()` takes the whole redeem response: the current receipt is counted row by row (persisted, already present, rows whose outcome is unknown, listed with their rule identity and the engine's sentence ready for display, and rows reported as definitely refused, listed with their rule identity in their own optional list, neither ever counted as success), a receipt row it cannot account for makes the whole reading unknown instead of a partial count, and the older envelope is still read for older servers the way it was before. A parked approval left by a background sub-agent is claimed by the session that started it, or by the root of its delegation tree, and never by another session; the subscription feed re-emits when that link arrives late. Damaged rows in the assistant inbox stay listed with the engine's verdict. Checked against the server's own rule-import lane, approval stream and checkpoint store. |
699
+ | `scripts/run-core-7630-pick-projection-test.mjs` | Readings that follow the engine's 7.63 changes. The notice catalog matches the installed engine code for code (at least the 117 codes of 7.63): three new codes are listed with the engine's audience and no reader of their own, and `session.records_skipped`, which the engine dropped from its catalog while servers inside the supported range still send it, is still treated as listed with audience `user`, through a single table of such codes that the engine catalog never overlaps. The two memory-harvest notices count as one occurrence per harvest report. A parked row the engine cannot read is listed without a gate kind and with a damaged verdict: the verdict is read as the engine gave it, the row never becomes an approval card, and no default gate kind is invented. A tool approval parked before the engine bound the tool name into its hash is read as damaged, a row that does not match its reading rather than an engine fault. The six workspace refusal codes keep the families they already belong to: the two `config.` codes are configuration refusals, none of the six offers a retry, and a failed agent card carries its code and sentence unchanged. A background fork child's delegation link is read like any other background child's. |
700
+ | `scripts/run-core-7640-pick-projection-test.mjs` | Readings that follow the engine's 7.64 changes. The notice catalog matches the installed engine code for code (118 codes); `delegation.delivery_discarded` is listed with the engine's audience (operator) and no reader of its own. The four refusal notices that can come from a delegated sub-agent (`delegation.ask_unresolvable`, `permission.ask_released_by_mode`, `permission.read_deny_unapplied`, `classifier.unavailable`) say where the call ran: in this session, in a named sub-agent session, or unreadable when the sub-agent fields are present but malformed, in which case the call's session is unknown rather than taken to be the tree root; `noticeCallSessionOf()` answers that session. A gate record's denial carries the engine's echoed request segments verbatim, in order, with a segment dropped only when a required field is unreadable. `catalogDefaultModelVerdict()` asks the engine whether the default model the catalog resolves to can run a task that names no model, handing over the caller's model face by reference without reading it, and refuses with `config.default_model_missing` carrying the engine's own code; with the engine port missing it answers unknown. Checked against the installed engine's own catalog, gate-record screen and model-face question. |
700
701
  | `scripts/run-seat-computer-use-switch-test.mjs` | Host switch for computer use. Whether the host currently allows computer use, where that value came from, whether an environment variable locks it, and when a change takes effect are answered once, by the host that carries the feature. The seat capability table gains `computerUseSwitch`, reported `true` only by a host that really carries computer use; a browser lane (no seat store) and the terminal never report it, and an absent capability means the switch is not rendered at all, not that it is off. `getComputerUse()` returns `{ enabled, source, lockedByEnv, appliesOn }`; `setComputerUse()` takes `{ enabled: false }` or `{ enabled: true, confirmed: true }` and returns `{ ok: true, changed, state }` or `{ ok: false, reason, state }`, a refusal being a receipt rather than an exception. The words are three frozen tuples: sources `env` / `settings` / `default`, refusal reasons `locked-by-env` / `confirmation-required` / `invalid-request` / `write-failed`, and the effect point `next-engine-start`. The request is untrusted input: `checkSetComputerUseRequest` accepts exactly the two shapes, and `planSetComputerUse(request, lockedByEnv)` decides, in the order malformed shape, then lock, then missing confirmation, whether the host may write; a lock flag that is not strictly `false` counts as locked, and the function never throws. Both outbound gates are closed shapes that also refuse two contradictions: a state locked by the environment must name `env` as its source, and a `locked-by-env` refusal must carry a locked state; the converse, and the locked flag on a successful receipt, are left to the host. Each key of the request is read once and the value judged is the value used. The suite checks registration, both outbound gates key by key, the inbound judge, the planner against an independent reference over a generated pool, the capability reading (including the browser lane and the bare name `computerUse`, which is not the capability), and an end-to-end host double that passes every receipt through the same gate. |
701
702
  | `scripts/run-server-7144-pick-projection-test.mjs` | What the client reads from the server's newest session, approval-queue and live-text facts. Each retired, renamed or added server fact has exactly one reading: an older notice or error code that servers in the supported range still send keeps its own reader until the support floor moves past it, and the new code beside it is never folded into the old view or the other way round. Two new error codes are recognised and passed through unchanged. A cross-session message held for review shows an expiry time only when the server sends one; it is never computed from the hold time. A resume refusal tells the engine's row judge tripping over a row (an engine fault) apart from a row that does not read the way it says it should. Text that arrives in several blocks lands as one transcript row per block, and a credential split across two blocks stays out of the transcript on servers that mask it. The user-facing sentence for a refused fork or log replacement on a session archive that still carries an earlier version's owner stamp is read by code only (the generic conflict of earlier servers does not match), is worded in English and Chinese word for word from the server's fixed sentence (compared against the server fixture when present), and carries no machine code. |
702
703
  | `scripts/run-frame-arm-completion-projection-test.mjs` | Two frames the server sends in normal operation no longer count as dropped frames. The single-card close frame of the in-stream approval family (`approval_complete`) goes through the same envelope reading as the open and revoke frames and reaches the host as an `approval_complete` chrome event carrying the frame as-is, whose stream context the existing approval frame context reader answers. The session title notice (`session_titled`) becomes a `session_titled` chrome event with the session id and title, read as own non-empty string fields; the session row stays the only source of the title. Approval-card frames (open, revoke and close) never count as proof that a submission created a run: the delivery ledger and the event leg's refusal detection read one shared check, so a busy refusal that follows a replayed card still reaches `onRefused`. Malformed copies are still reported as `malformed`, frames the projector does not know are still reported, and both events stay on the host side of a seat. |
703
704
  | `scripts/run-plan-review-decide-card-projection-test.mjs` | Plan-review decisions answer the card that was shown. The gate a plan-review decision answers has one source: the card facts that come with it. The package's own card and a reopened card hand over their own facts; the public decision call reads the `card` option. That gate identity goes into the decision body and is the gate the result is compared with afterwards. Approving needs a card that offers approval (a plan shown as written, with a gate identity); without one nothing is sent and the result says why, in the card's own words when the card withheld approval. If the gate moved on before the decision arrived, the engine refuses it and the result says the plan on the card is no longer the one waiting, without reopening anything. If the engine cannot recognise a gate identity that is still current, the result says so and the next decision on that gate is sent without it. The gate drives the public decision call, the package card and a reopened card against an injected client: missing and malformed cards, a card for another task, withheld cards, a moved gate, the unrecognised-gate path and its alternatives, the bounded set of unrecognised gates, the public types, and that the reason is set at most once per decision and never reaches the public result object. |
704
- | `scripts/run-session-engine-lane-projection-test.mjs` | Which engine a session lives on, decided once when the session starts. `sessionEngineBindingFor` turns the engine a host is wired to into a fixed identity key (a versioned JSON tuple for the host's own engine, for a declared engine by center, space and name, or for a direct address) and fails with a reason instead of guessing when a piece is missing; `commitSessionEngine` is the only writer of the session's engine snapshot (first write, identical replay, a different value refused, an older record restored only on proof) and raises a notice when the host's own engine is recorded as a Cloud session; `readSessionEngine` only reads the snapshot; `sessionDispatchTarget` picks the target for a session from that snapshot and the host's wiring snapshot and refuses with one of four reasons instead of falling back to the current default; `foldSessionPlacement` keeps a session's execution lane ordered by the device revision. The seat session row carries `engine` and `executionLane`, and the seat capability `engineModeDefaultOnly` says that switching the engine only changes the default for new sessions. |
705
- | `scripts/run-park-row-card-leg-projection-test.mjs` | Durable pending rows that the engine says it cannot continue: a row whose `rowVerdict` is not `ok` (written by a newer engine, bytes this engine does not resume, an unknown verdict word, or an unreadable verdict) is not shown as an approvable or answerable card and no decision is sent, on both card legs (durable fetch and question-card reopen triage) and in the pending-row count; the outcome carries the verdict and the engine's own sentence, and the fail-soft closing sentence no longer says to decide on the card. Rows with an `ok` verdict and rows from servers that do not send a verdict behave as before. Also pinned: the one way-out sentence shared by the unbound-row refusal and the local `binding_missing` stop, and the synthesized card frame carrying `sourceSessionId`. The same boundary applies when a host hands the decide or answer entry a row it fetched itself, when the verdict position is an accessor (it is not run), and to rows whose gate the engine cannot read (listed by newer engines with a non-`ok` verdict), while rows on a gate this leg does not own are still left to their own leg. Pinned on rows read by the real checkpoint store of two server generations and on rows summarized by the installed engine. When a reopen is refused because this engine cannot continue the parked question row, the self-heal row carries the engine sentence and the cannot-be-decided-here way out, never the resume or decide-on-the-engine way outs. |
705
+ | `scripts/run-session-engine-lane-projection-test.mjs` | Which engine a session lives on, decided once when the session starts. `sessionEngineBindingFor` turns the engine a host is wired to into a fixed identity key (a versioned JSON tuple for the host's own engine, for a declared engine by center, space and name, or for a direct address) and fails with a reason instead of guessing when a piece is missing; `commitSessionEngine` is the only writer of the session's engine snapshot (first write, identical replay, a different value refused, an older record restored only on proof) and raises a notice when the host's own engine is recorded as a Cloud session; `readSessionEngine` only reads the snapshot; `sessionDispatchTarget` picks the target for a session from that snapshot and the host's wiring snapshot and refuses with one of four reasons instead of falling back to the current default; `foldSessionPlacement` keeps a session's execution lane ordered by the device revision. The seat session row carries `engine` and `executionLane`, and the seat capability `engineModeDefaultOnly` says that switching the engine only changes the default for new sessions. The dispatch refusals also include `session-ambiguous` (the same session id sits on more than one engine's ledger); the host judges it and takes the refusal from `sessionDispatchRefusedFor`, and the dispatch judgement itself never answers it. A new session's lane intent (`start({ engine })`) uses exactly the two words a session's engine `kind` carries and the binding commit accepts. |
706
+ | `scripts/run-park-row-card-leg-projection-test.mjs` | Durable pending rows that the engine says it cannot continue: a row whose `rowVerdict` is not `ok` (written by a newer engine, bytes this engine does not resume, an unknown verdict word, or an unreadable verdict) is not shown as an approvable or answerable card and no decision is sent, on both card legs (durable fetch and question-card reopen triage) and in the pending-row count; the outcome carries the verdict and the engine's own sentence, and the fail-soft closing sentence no longer says to decide on the card. Rows with an `ok` verdict and rows from servers that do not send a verdict behave as before. Also pinned: the one way-out sentence shared by the unbound-row refusal and the local `binding_missing` stop, and the synthesized card frame carrying `sourceSessionId`. The same boundary applies when a host hands the decide or answer entry a row it fetched itself, when the verdict position is an accessor (it is not run), and to rows whose gate the engine cannot read (listed by newer engines with a non-`ok` verdict), while rows on a gate this leg does not own are still left to their own leg. Pinned on rows read by the real checkpoint store of two server generations and on rows summarized by the installed engine. When a reopen is refused because this engine cannot continue the parked question row, the self-heal row carries the engine sentence and the cannot-be-decided-here way out, never the resume or decide-on-the-engine way outs. Since 0.108.0 the `settled` verdict of the question-card triage and of the bounded re-check loop also carries `rowTaskId` (the judged row's own run id, present only together with `rowVerdict`; a sole child-run row resolved by fallback yields the child's id, never the requester's; `unborn` / `row` verdicts never carry it), and the root entry exports `checkpointRowBarOf` / `checkpointRowBarSentence` / `CheckpointRowBar` (engine sentence first, package sentence when unreadable). Since 0.108.0 the way-out sentence follows the pending row's own expiry position: with a finite `deadline` it still says to cancel the run or let the approval expire, with `deadline: null` (no expiry timer) it says to cancel the run and that this approval has no expiry timer, and when the position is absent or unreadable it only says to cancel the run; the decide-refusal receipt and the local `binding_missing` stop have no row in hand and always use the cancel-only sentence, the bridge stop reads the row's `deadline`, and the self-heal row reads the optional `_sema_rowDeadline` the host hands next to `_sema_rowVerdict`. Pinned on three states in both languages, a reader table that never runs accessors, and the host verdict in eight shapes. |
706
707
  | `scripts/run-steer-input-ids-projection-test.mjs` | Steered input ids reach the host, and inputs the engine reports as unused are matched by id. The only id a steered input has is the `messageId` the server puts on the steer receipt; the client never mints one. Every steer path in the package that holds a receipt hands that id to the host: the control router's `steer()` returns the receipt as received for live (200) and parked (202) deliveries, and the self-heal path puts it on the `running-steered` outcome (absent when the receipt carries none). `readUserInputUndrained()` reads `detail.inputIds` only when the list names exactly the counted inputs, and `undrainedInputsVerdict()` matches it against the host's ids: an id the host sent and has not yet resent under the same id is resent, an id already resent once is handed to the user, and everything else (an unnamed notice, the server's crash report, a sub-agent copy, or ids the host never sent) is reported by count only. If the host's record of resent ids cannot be read, nothing is resent. When the child-session field on an undrained-input notice is present but unreadable, the id list is withheld and nothing is resent; a notice for this session is still resent by id. |
708
+ | `scripts/run-hooks-unread-keys-projection-test.mjs` | Hook settings carry only the keys the server's hook reader reads. A matcher group or hook entry that also holds a key the reader does not read (a failure policy, rewake text, a cloud placement, arguments on a non-command hook, a misspelled group key) is not sent: the server refuses a whole new request for such a key and would otherwise drop it on replay, so the key never silently stops meaning what it says. Which keys the reader reads comes from the same settings-contract classifier the server uses (its projection of each piece) plus the server's own wire keys, with no field list of the package's own. Settings hooks are recorded with the reason `key_not_read` and the coordinates of every unread key, plugin hooks get the same reason; a hook that will then run nowhere is listed in the hooks-not-run notice with coordinates in the same form as the server's `unknownKeys` and the way out (remove the key, or use a server that reads it). The turn is never refused for this, managed settings included. When the build-time copy of the classifier is newer than the connected server's own, a per-generation table of the server's reader also removes the entry keys that generation does not read (a server version that is unreported, unreadable or newer than the table is treated as not reading them), and a dropped failure policy adds a sentence that a hook set to block on failure will not block. Checked against a real server admission path: the projected body is accepted, the body as written is refused, and the package's coordinates equal the server's list. |
709
+ | `scripts/run-task-notification-admission-test.mjs` | Wake-up submissions for task notifications are confirmed only on positive evidence. `taskNotificationAdmissionOf` classifies one read-back of the run bound to a submission key as `admitted`, `pending`, `not_found` or `unreadable`, with a reason from a closed set of eight; the verdict follows from the reason. The read-back is passed in one of three explicit forms: the 200 record, a not-found form the caller passes only after recognising the endpoint's own 404, or an error form for everything else. `admitted` needs either an ISO date-time in the record's `admittedAt` (a field the service has not shipped yet; read when present, and `null` is not taken as a no) or an integer `result.stats.turns` above zero, the same reading the service uses to latch acceptance. A running row however long it has run, zero-turn completed, failed, parked or blocked rows, rows with token usage but no turns, and missing or unknown status words or codes are all `pending`; the status word, error code and token count are never read. Errors are never inspected: any error form, a raw error object, a malformed body or an unrecognised input is `unreadable`, never `not_found`. Only own data properties are read, accessors never run, and the function never throws. `batchCompatible` now refuses to batch a task notification whose fact carries a completion id, whether it is the head or the follower, so each such notification goes in its own submission; the host passes the queue item's notification fact through on the batching entry. The suite covers the two pause points before placement and before engine output, the point between a zero-work refusal being written and its key being released, reads counted through instrumented accessors and proxies, the reason and verdict sets in both directions at compile time and at run time, deliberately broken judges that must fail the matching cells, and the record shape against the type package's declarations. |
710
+ | `scripts/run-hooks-authored-source-projection-test.mjs` | Hook group sources on the wire are set only by this package. The server runs each hook matcher group in a phase chosen by the group's `source` (a managed-policy source runs in the final managed phase), so a `source` written in a settings file or in a plugin's hook manifest would let a project, user, local or unmanaged-plugin hook claim a managed phase. Any `source` written there, whatever its value (even one that matches where it came from), keeps that matcher group off the request: settings hooks are recorded with the reason `source_not_authored`, plugin hooks get the same reason, and a hook that will then run nowhere is listed in the hooks-not-run notice with the coordinate `settings.hooks.<event>.<group>.source` and the way out (remove the key). The key is never stripped or replaced with another value. A group whose `source` cannot be read plainly (an accessor, a proxy trap) counts as written. Pieces the settings classifier does not accept are left to it and to the pre-send check, so a malformed guard still refuses the turn. Every request body the suite builds is swept: no matcher group carries a `source`. Checked against a real server's hook reader: the body as written puts the hook in a managed phase, the projected body leaves every group unmanaged. |
711
+ | `scripts/run-seat-actions-test.mjs` | The seat action channel. The host can ask the window it hosts to do one of a closed set of things over one one-way event, `onSeatAction`: open the new-project dialog (`new-project`), or open a fresh new-session page with text placed in the prompt box and, optionally, a folder for the next session (`prefill-composer`), which is never sent on the user's behalf and never replaces a draft the user has already typed. Focusing a session is not an action word: it already travels as the `focusedSessionState` store push, and there is no route-opening word. The window first claims the words it really handles with `claimSeatActions`, and the host offers an entry point (a menu item, a link route) only for claimed words, so a host never shows an entry that does nothing. A claim may only name words from the closed set: one word outside it refuses the whole claim and names that entry, repeated words are harmless and are collapsed in closed-set order, and a malformed claim reads as an empty claim. The outbound gate is a closed shape per action (an unknown action word, an extra key, an over-long or control-character text, a bidirectional-override character or a lone surrogate is refused by name; accessors are never run and the gate never throws), and the window-side reader `readSeatAction` reads an unknown word as `unknown` (dropped, leaving a trace) rather than as malformed, and never throws. One optional capability bit `seatActions` governs the claim verb and the event; a host that does not report it reads as `not_reported` and the window neither claims nor subscribes. The suite checks the registration, both gates and the reader key by key, the capability reading, and an end-to-end host double that sends only claimed words. |
707
712
 
708
713
  Each suite carries a floor that only moves up — a refactor that stops executing a group of
709
714
  assertions is a failure, not a quieter pass. Guards anchor on the **installed artefact's content**
@@ -48,6 +48,7 @@ export type ReopenCardVerdict = {
48
48
  _sema_rowVerdict?: Exclude<CheckpointRowVerdictReading, {
49
49
  readonly kind: 'ok';
50
50
  }>;
51
+ _sema_rowDeadline?: number | null;
51
52
  } | {
52
53
  reopened: true;
53
54
  firstSight: boolean;
@@ -154,6 +155,7 @@ export type SelfHealOutcome = {
154
155
  rowVerdict?: Exclude<CheckpointRowVerdictReading, {
155
156
  readonly kind: 'ok';
156
157
  }>;
158
+ rowDeadline?: 'armed' | 'none';
157
159
  } | {
158
160
  kind: 'ask-decided-without-card';
159
161
  taskId: string;
@@ -5,8 +5,8 @@ import { registeredPlanReviewCardFacts } from '../planReviewCardFacts.js';
5
5
  import { DEFAULT_SESSION_KEY } from '../sessionSlot.js';
6
6
  import { ASK_PARK_AUTO_REOPEN_BLOCKS, askParkAutoReopenBlockedContent } from '../hitl/askParkAutoReopenBlock.js';
7
7
  import { corruptStoredRowContent, corruptStoredRowWhere, effectiveWireErrorCode } from '../wireErrorTriage.js';
8
- import { readThrownStatusSlot } from '../wireFailureShape.js';
9
- import { checkpointRowBarOf, checkpointRowBarSentence } from '../checkpointRowVerdict.js';
8
+ import { readThrownStatusSlot, slotReadOf } from '../wireFailureShape.js';
9
+ import { checkpointDeadlineStateOf, checkpointRowBarOf, checkpointRowBarSentence, rowBarWayOutContent } from '../checkpointRowVerdict.js';
10
10
  import { guardedRead, readOwnDataNonEmptyString, readOwnDataValue } from '../ownKey.js';
11
11
  import { steerBodyOf, UNOBSERVED_STEER_PRIORITY } from '../controlRouter.js';
12
12
  import { atMostOnceFailureClass } from '../atMostOnce.js';
@@ -215,6 +215,7 @@ const REOPEN_VERDICT_KEYS = [
215
215
  '_sema_reopenRefusal',
216
216
  '_sema_policyBlocked',
217
217
  '_sema_rowVerdict',
218
+ '_sema_rowDeadline',
218
219
  ];
219
220
  function snapshotReopenVerdict(raw) {
220
221
  if (raw === null || raw === undefined)
@@ -338,6 +339,20 @@ function reopenRowVerdictOf(verdict) {
338
339
  return undefined;
339
340
  }
340
341
  }
342
+ function reopenRowDeadlineStateOf(verdict) {
343
+ try {
344
+ if (verdict.reopened !== false)
345
+ return undefined;
346
+ const slot = slotReadOf(verdict, '_sema_rowDeadline');
347
+ if (slot.presence !== 'present')
348
+ return undefined;
349
+ const state = checkpointDeadlineStateOf({ deadline: slot.value });
350
+ return state === 'unknown' ? undefined : state;
351
+ }
352
+ catch {
353
+ return undefined;
354
+ }
355
+ }
341
356
  function reopenRefusalOf(verdict) {
342
357
  try {
343
358
  if (verdict.reopened !== false)
@@ -722,6 +737,7 @@ async function askParkArm(taskId, signal, runs, carrierVerdict, deps) {
722
737
  const noView = reopenRefusedForNoPresentationSurface(verdict);
723
738
  const policyBlocked = inFlight || noView ? undefined : reopenPolicyBlockedOf(verdict);
724
739
  const rowVerdict = inFlight ? undefined : reopenRowVerdictOf(verdict);
740
+ const rowDeadline = rowVerdict !== undefined ? reopenRowDeadlineStateOf(verdict) : undefined;
725
741
  const errorCode = inFlight || noView || reopenRefusedForUserDismissal(verdict) || policyBlocked !== undefined || rowVerdict !== undefined ? undefined : reopenErrorCodeOf(verdict);
726
742
  return {
727
743
  kind: 'ask-reopen-failed',
@@ -733,6 +749,7 @@ async function askParkArm(taskId, signal, runs, carrierVerdict, deps) {
733
749
  ...(errorCode !== undefined ? { errorCode } : {}),
734
750
  ...(policyBlocked !== undefined ? { policyBlocked } : {}),
735
751
  ...(rowVerdict !== undefined ? { rowVerdict } : {}),
752
+ ...(rowDeadline !== undefined ? { rowDeadline } : {}),
736
753
  };
737
754
  }
738
755
  async function probeRunStatusOnce(taskId, durable, durableGet, deps) {
@@ -1215,7 +1232,7 @@ function activeRunSelfHealBaseRow(outcome, wayOut, following = false, keepSessio
1215
1232
  return say(lang, 'session.selfHeal.row.askReopenFailed.rowUnresumable', {
1216
1233
  taskId: outcome.taskId,
1217
1234
  sentence: checkpointRowBarSentence(outcome.rowVerdict),
1218
- unbound: msg('approval.decide.unboundWayOut'),
1235
+ unbound: rowBarWayOutContent(outcome.rowVerdict, outcome.rowDeadline ?? 'unknown', lang),
1219
1236
  });
1220
1237
  }
1221
1238
  if (outcome.policyBlocked !== undefined)