@sema-agent/core 7.11.2 → 7.13.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 (91) hide show
  1. package/CHANGELOG.md +86 -13
  2. package/dist/core/ask-origin.d.ts +55 -0
  3. package/dist/core/ask-origin.js +21 -0
  4. package/dist/core/auto-mode-arming.d.ts +10 -14
  5. package/dist/core/auto-mode-arming.js +3 -9
  6. package/dist/core/auto-mode-defaults.d.ts +0 -2
  7. package/dist/core/auto-mode-defaults.js +0 -1
  8. package/dist/core/auto-mode-rebuild.d.ts +6 -13
  9. package/dist/core/auto-mode-rebuild.js +0 -2
  10. package/dist/core/auto-mode.d.ts +30 -89
  11. package/dist/core/auto-mode.js +12 -59
  12. package/dist/core/checkpoint-store.d.ts +1 -3
  13. package/dist/core/engine-notice.d.ts +18 -0
  14. package/dist/core/gate-fold.js +1 -9
  15. package/dist/core/gate-lanes.d.ts +0 -40
  16. package/dist/core/gate-lanes.js +18 -26
  17. package/dist/core/governance-codes.d.ts +1 -1
  18. package/dist/core/governance-codes.js +2 -0
  19. package/dist/core/hooks.d.ts +7 -1
  20. package/dist/core/hooks.js +1 -1
  21. package/dist/core/runner/assemble-result.d.ts +45 -1
  22. package/dist/core/runner/assemble-result.js +4 -1
  23. package/dist/core/runner/compaction-seams.d.ts +42 -0
  24. package/dist/core/runner/compaction-seams.js +80 -0
  25. package/dist/core/runner/contracts.d.ts +85 -15
  26. package/dist/core/runner/denial-limit-arms.d.ts +10 -13
  27. package/dist/core/runner/denial-limit-arms.js +12 -9
  28. package/dist/core/runner/gate-exit.js +9 -1
  29. package/dist/core/runner/permission-rule-lanes.d.ts +2 -1
  30. package/dist/core/runner/permission-rule-lanes.js +2 -1
  31. package/dist/core/runner/prepare-caps-and-workflow.d.ts +1 -1
  32. package/dist/core/runner/prepare-caps-and-workflow.js +0 -5
  33. package/dist/core/runner/prepare-policy-chain.js +3 -2
  34. package/dist/core/runner/prepare-protocol-tools.js +5 -0
  35. package/dist/core/runner/prepare-safety-scan.js +11 -16
  36. package/dist/core/runner/prepare-suspend-saga.d.ts +0 -2
  37. package/dist/core/runner/prepare-suspend-saga.js +2 -10
  38. package/dist/core/runner/prepare-task.d.ts +9 -1
  39. package/dist/core/runner/prepare-task.js +7 -2
  40. package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
  41. package/dist/core/runner/prepare-wiring-manifest.js +1 -8
  42. package/dist/core/runner/resume-claim.d.ts +2 -2
  43. package/dist/core/runner/resume-preflight.d.ts +2 -2
  44. package/dist/core/runner/run-attachment-seats.d.ts +6 -4
  45. package/dist/core/runner/run-attachment-seats.js +2 -2
  46. package/dist/core/runner/run-git-lane.d.ts +1 -1
  47. package/dist/core/runner/run-harness-handlers.js +2 -0
  48. package/dist/core/runner/run-identity-wiring.d.ts +21 -33
  49. package/dist/core/runner/run-identity-wiring.js +6 -3
  50. package/dist/core/runner/run-leg.d.ts +108 -0
  51. package/dist/core/runner/run-leg.js +713 -0
  52. package/dist/core/runner/run-notification-lane.d.ts +56 -0
  53. package/dist/core/runner/run-notification-lane.js +128 -0
  54. package/dist/core/runner/run-reasoning-seat.d.ts +5 -5
  55. package/dist/core/runner/run-reasoning-seat.js +7 -7
  56. package/dist/core/runner/run-recovery-lanes.d.ts +4 -17
  57. package/dist/core/runner/run-recovery-lanes.js +5 -4
  58. package/dist/core/runner/run-settle-and-teardown.d.ts +113 -0
  59. package/dist/core/runner/run-settle-and-teardown.js +432 -0
  60. package/dist/core/runner/run-stop-and-final-verify.d.ts +2 -2
  61. package/dist/core/runner/run-terminal-adoption.d.ts +99 -0
  62. package/dist/core/runner/run-terminal-adoption.js +200 -0
  63. package/dist/core/runner/run-turn-boundary.js +5 -4
  64. package/dist/core/runner/runtask.d.ts +10 -79
  65. package/dist/core/runner/runtask.js +83 -1557
  66. package/dist/core/runner/stream-lifecycle-verbs.js +10 -1
  67. package/dist/core/runner-deps.d.ts +4 -14
  68. package/dist/core/store-contracts/workflow-journal-store-contract.d.ts +7 -0
  69. package/dist/core/store-contracts/workflow-journal-store-contract.js +85 -0
  70. package/dist/core/task-stream.d.ts +10 -1
  71. package/dist/core/tool-face.d.ts +8 -0
  72. package/dist/core/tool-face.js +1 -0
  73. package/dist/core/tool-policy.d.ts +42 -96
  74. package/dist/core/tool-policy.js +1 -11
  75. package/dist/core/tool-registry.d.ts +11 -3
  76. package/dist/core/tool-registry.js +7 -1
  77. package/dist/core/tool-roster.d.ts +26 -0
  78. package/dist/core/tool-roster.js +38 -7
  79. package/dist/core/trace.d.ts +6 -7
  80. package/dist/core/wiring-manifest.d.ts +5 -22
  81. package/dist/core/wiring-manifest.js +3 -11
  82. package/dist/core/workflow-journal-store.d.ts +35 -4
  83. package/dist/core/workflow-journal-store.js +19 -2
  84. package/dist/index.d.ts +6 -5
  85. package/dist/index.js +6 -5
  86. package/dist/orchestration/workflow.js +2 -0
  87. package/dist/server/http.js +2 -1
  88. package/dist/stores/file/workflow-journal-store.d.ts +7 -10
  89. package/dist/stores/file/workflow-journal-store.js +2 -4
  90. package/package.json +1 -1
  91. package/test/export-surface.snapshot.json +21 -11
@@ -114,9 +114,18 @@ export function streamLifecycleVerbs(input) {
114
114
  detachHub.request(toolCallId);
115
115
  };
116
116
  const interrupt = async () => {
117
+ const interruptRefused = (msg) => {
118
+ const e = new Error(`cannot interrupt: ${msg}`);
119
+ e.code = "steering.not_running";
120
+ return e;
121
+ };
122
+ if (live.resultValue)
123
+ throw interruptRefused("the task has already finished");
117
124
  const h = live.handle ?? (await orTimeout(ready));
118
125
  if (!h)
119
- return;
126
+ throw interruptRefused("the task is not running");
127
+ if (live.resultValue !== undefined || h.loop.ended)
128
+ throw interruptRefused("the task is no longer running");
120
129
  if (!h.abortController.signal.aborted)
121
130
  h.loop.userInterrupted = true;
122
131
  h.abortController.abort();
@@ -687,17 +687,10 @@ export interface RunnerDeps {
687
687
  sessionContext?: readonly string[];
688
688
  /** Transcript-window bounds for the classify prompt (defaults: 40 entries / 2000 chars each). */
689
689
  window?: import("./auto-mode-prompt.js").AutoModeWindowOptions;
690
- /** Classify round-trip cap, ms (default 15_000). */
690
+ /** Classify round-trip cap, ms (default 15_000). A round past the cap is `unavailable(timeout)`: the call is
691
+ * DENIED with a sentence that says so (CC 2.1.250 form), and the next call is classified afresh — there is no
692
+ * failure count and no session latch. */
691
693
  timeoutMs?: number;
692
- /** Consecutive-failure threshold opening the one-way session breaker (default 3). */
693
- failureThreshold?: number;
694
- /** Fired ONCE when the session breaker opens (the "退回非 auto" alarm — surface it to the operator). The engine
695
- * records the trip on the session's breaker read face (`WiringManifest.autoMode.breaker`, #616) BEFORE calling
696
- * this hook, so a deployment that wires nothing here still gets the read face. */
697
- onBreakerOpen?: (info: {
698
- consecutiveFailures: number;
699
- lastCause: import("./auto-mode.js").AutoModeBreakerCause;
700
- }) => void;
701
694
  /**
702
695
  * The classifier DENIAL LIMIT (CC 2.1.250 `FO`/`AKe`): a run whose classifier keeps blocking falls
703
696
  * back to a PERSON instead of being denied without end. Per run: a `block` first increments the
@@ -736,10 +729,7 @@ export interface RunnerDeps {
736
729
  * line); a deployment that treats those as sensitive at rest should keep them out of the arming
737
730
  * face rather than out of this flag.
738
731
  *
739
- * Recording is CONDITIONAL on the session breaker still being closed at park time: a session that
740
- * already fell back to non-auto hands nothing forward (the redemption must never run wider than the
741
- * ancestor it inherits). A `true` here with a non-serializable face announces once and records
742
- * nothing (`phase:"config"`).
732
+ * A `true` here with a non-serializable face announces once and records nothing (`phase:"config"`).
743
733
  */
744
734
  persistArming?: boolean;
745
735
  /**
@@ -0,0 +1,7 @@
1
+ import { type WorkflowJournalStore } from "../workflow-journal-store.js";
2
+ import { type ContractAssertionRunner } from "./contract-harness.js";
3
+ /**
4
+ * The full cross-backend contract for {@link WorkflowJournalStore}. Every case builds its own store via
5
+ * `make()`; with no runner the cases run sequentially and the first failure throws.
6
+ */
7
+ export declare function workflowJournalStoreContract(make: () => WorkflowJournalStore, runAssertion?: ContractAssertionRunner): Promise<void>;
@@ -0,0 +1,85 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { JOURNAL_OVERSIZE_ERROR_CODE, MAX_JOURNAL_RESULT_BYTES, } from "../workflow-journal-store.js";
3
+ import { beginContract } from "./contract-harness.js";
4
+ const key = (ordinal) => `${ordinal}:h`;
5
+ const completed = (ordinal, over = {}) => ({
6
+ taskId: `t-${ordinal}`,
7
+ sessionId: `s-${ordinal}`,
8
+ terminal: { kind: "completed" },
9
+ result: `r${ordinal}`,
10
+ structuredOutput: { ordinal, nested: { ok: true } },
11
+ stats: { turns: 1, tokens: 1, costMicroUsd: 0, nested: { tokens: 0, turns: 0, tasks: 0, costMicroUsd: 0 } },
12
+ ...over,
13
+ });
14
+ const parked = (ordinal, over = {}) => ({
15
+ taskId: `t-${ordinal}`,
16
+ sessionId: `s-${ordinal}`,
17
+ terminal: { kind: "paused", token: `tok-${ordinal}`, checkpointId: `cp-${ordinal}`, gate: { kind: "human", reason: "approve", toolName: "Write" } },
18
+ result: "",
19
+ structuredOutput: { partial: ordinal },
20
+ stats: { turns: 1, tokens: 1, costMicroUsd: 0, nested: { tokens: 3, turns: 2, tasks: 1, costMicroUsd: 1 } },
21
+ ...over,
22
+ });
23
+ const OVER_CAP = "x".repeat(MAX_JOURNAL_RESULT_BYTES + 1024);
24
+ const isOversizeRefusal = (e) => e instanceof Error && e.code === JOURNAL_OVERSIZE_ERROR_CODE;
25
+ export async function workflowJournalStoreContract(make, runAssertion) {
26
+ const { run, settle } = beginContract(runAssertion);
27
+ run("append → load returns entries ASCENDING by ordinal, both arms verbatim; a re-append per ordinal OVERWRITES", async () => {
28
+ const store = make();
29
+ await store.append("run-1", "tenant-a", { callKey: key(10), result: completed(10) });
30
+ await store.append("run-1", "tenant-a", { callKey: key(1), parked: parked(1) });
31
+ await store.append("run-1", "tenant-a", { callKey: key(0), result: completed(0) });
32
+ await store.append("run-1", "tenant-a", { callKey: key(2), result: completed(2) });
33
+ await store.append("run-1", "tenant-a", { callKey: key(0), result: completed(0, { result: "overwritten" }) });
34
+ const entries = await store.load("run-1", "tenant-a");
35
+ assert.deepEqual(entries.map((e) => e.callKey), [key(0), key(1), key(2), key(10)]);
36
+ assert.deepEqual(entries[0], { callKey: key(0), result: completed(0, { result: "overwritten" }) });
37
+ assert.deepEqual(entries[1], { callKey: key(1), parked: parked(1) });
38
+ });
39
+ run("scope wall: a cross-scope load is EMPTY — before and after the owner's own load, and after a refused cross-scope append — on both arms", async () => {
40
+ const store = make();
41
+ await store.append("run-1", "alice", { callKey: key(0), result: completed(0) });
42
+ await store.append("run-1", "alice", { callKey: key(1), parked: parked(1) });
43
+ assert.deepEqual(await store.load("run-1", "mallory"), []);
44
+ assert.equal((await store.load("run-1", "alice")).length, 2);
45
+ assert.deepEqual(await store.load("run-1", "mallory"), []);
46
+ await assert.rejects(store.append("run-1", "mallory", { callKey: key(2), result: completed(2) }));
47
+ await assert.rejects(store.append("run-1", "mallory", { callKey: key(3), parked: parked(3) }));
48
+ assert.deepEqual(await store.load("run-1", "mallory"), []);
49
+ assert.deepEqual((await store.load("run-1", "alice")).map((e) => e.callKey), [key(0), key(1)]);
50
+ });
51
+ run(`oversize RESULT arm ⇒ append rejects with code "${JOURNAL_OVERSIZE_ERROR_CODE}"; nothing journaled; the store stays usable`, async () => {
52
+ const store = make();
53
+ await store.append("run-1", "tenant-a", { callKey: key(0), result: completed(0) });
54
+ await assert.rejects(store.append("run-1", "tenant-a", { callKey: key(1), result: completed(1, { result: OVER_CAP }) }), isOversizeRefusal);
55
+ assert.deepEqual((await store.load("run-1", "tenant-a")).map((e) => e.callKey), [key(0)]);
56
+ await store.append("run-1", "tenant-a", { callKey: key(2), result: completed(2) });
57
+ assert.deepEqual((await store.load("run-1", "tenant-a")).map((e) => e.callKey), [key(0), key(2)]);
58
+ });
59
+ run(`oversize PARKED arm ⇒ append rejects with code "${JOURNAL_OVERSIZE_ERROR_CODE}"; nothing journaled (never a silent skip — a lost park is a resume running the ordinal live)`, async () => {
60
+ const store = make();
61
+ await store.append("run-1", "tenant-a", { callKey: key(0), result: completed(0) });
62
+ await assert.rejects(store.append("run-1", "tenant-a", { callKey: key(1), parked: parked(1, { result: OVER_CAP }) }), isOversizeRefusal);
63
+ assert.deepEqual((await store.load("run-1", "tenant-a")).map((e) => e.callKey), [key(0)]);
64
+ await store.append("run-1", "tenant-a", { callKey: key(1), parked: parked(1) });
65
+ assert.equal((await store.load("run-1", "tenant-a"))[1].parked?.terminal.kind, "paused");
66
+ });
67
+ run("the cap is inclusive: a payload of EXACTLY the cap's byte count is accepted on both arms", async () => {
68
+ const store = make();
69
+ const fit = (payload) => {
70
+ const base = Buffer.byteLength(JSON.stringify({ ...payload, result: "" }), "utf8");
71
+ return { ...payload, result: "y".repeat(MAX_JOURNAL_RESULT_BYTES - base) };
72
+ };
73
+ const r = fit(completed(0));
74
+ const p = fit(parked(1));
75
+ assert.equal(Buffer.byteLength(JSON.stringify(r), "utf8"), MAX_JOURNAL_RESULT_BYTES);
76
+ assert.equal(Buffer.byteLength(JSON.stringify(p), "utf8"), MAX_JOURNAL_RESULT_BYTES);
77
+ await store.append("run-1", "tenant-a", { callKey: key(0), result: r });
78
+ await store.append("run-1", "tenant-a", { callKey: key(1), parked: p });
79
+ const entries = await store.load("run-1", "tenant-a");
80
+ assert.deepEqual(entries.map((e) => e.callKey), [key(0), key(1)]);
81
+ assert.deepEqual(entries[0].result, r);
82
+ assert.deepEqual(entries[1].parked, p);
83
+ });
84
+ await settle();
85
+ }
@@ -277,7 +277,16 @@ export interface TaskStream extends AsyncIterable<TaskEvent> {
277
277
  }>;
278
278
  /**
279
279
  * Hard-**interrupt** the running task — a real abort (not a best-effort hint), equivalent to firing the
280
- * task's `signal` (design/47). No-op if the task already finished. For a SOFT redirect that lets the
280
+ * task's `signal` (design/47). Refused typed (`steering.not_running`, the steer-family liveness code
281
+ * `cannot interrupt: …`) once the task has finished (the loop's end included: the teardown window
282
+ * refuses like `halt`/`steer` do), never silently dropped. **A run that has already given its answer
283
+ * is not cancellable**: once its last turn ended `stopReason:"stop"` with no queued steer / follow-up
284
+ * left to serve (a continuation the person had already asked for makes that "stop" work in progress,
285
+ * not an answer), a stop landing on the run's
286
+ * tail (this verb, the task's `signal`, the walltime or turns limit — one law) still aborts what is
287
+ * left of that tail (a final-boundary compaction is mooted) but the run ends `completed` with the
288
+ * answer as `result`, writes no interruption marker, mints no `limits.*` code, and says so once with
289
+ * the `task.interrupt_unconsumed` notice. For a SOFT redirect that lets the
281
290
  * task keep running and adjust, use {@link steer} instead. For the CC-Esc "stop and wait for my
282
291
  * input" form — cut the turn, keep the ending clean and resumable — use {@link halt}.
283
292
  */
@@ -150,7 +150,15 @@ export interface ToolRuleFace {
150
150
  * budgets a MINTED name to — a minted name is always inside this wire bound by construction; a CALLER
151
151
  * declaration is judged against it at prepare. */
152
152
  export declare const TOOL_WIRE_NAME_MAX_CHARS = 128;
153
+ /** The roster's wire bound on a SHORT identity member: a contract's `contractId` / `implementationRevision`, and the
154
+ * `modelGate` class a definition carries. Bounded at the mint (`rosterMemberBoundProblem`), never truncated. */
153
155
  export declare const TOOL_CONTRACT_MAX_CHARS = 64;
156
+ /** The roster's wire bound on a row's `cardId` — a COMPOSITE minted from bounded members, so its bound is DERIVED
157
+ * from theirs rather than declared beside them: the widest form is `<mcp|a2a>:<peer>/<tool>` with the peer and the
158
+ * protocol tool segment each inside the wire NAME bound (`builtin:<name>` and `<caller|external>:<contractId>` are
159
+ * narrower). A row minted from in-bound members therefore fits by construction; the mint's own check on it is a
160
+ * tripwire for a new source form, never a working rule. */
161
+ export declare const TOOL_CARD_ID_MAX_CHARS: number;
154
162
  /** The roster's wire bound on a KEY — a schema property name, a path-target `param`/alias, a rule-face param. A key is a
155
163
  * SEMANTIC name every fence reads an argument by, so it is never truncated: a declaration over the bound is refused at
156
164
  * the door (caller) or dropped with a disclosed reason (MCP intake), and a row carries every key exactly. */
@@ -14,6 +14,7 @@ export function pathTargetSlotOnly(pt) {
14
14
  }
15
15
  export const TOOL_WIRE_NAME_MAX_CHARS = 128;
16
16
  export const TOOL_CONTRACT_MAX_CHARS = 64;
17
+ export const TOOL_CARD_ID_MAX_CHARS = "mcp:".length + TOOL_WIRE_NAME_MAX_CHARS + "/".length + TOOL_WIRE_NAME_MAX_CHARS;
17
18
  export const TOOL_KEY_MAX_CHARS = 256;
18
19
  export const TOOL_KEYS_MAX = 1024;
19
20
  export const RENDER_HINT_MAX_CHARS = 64;
@@ -19,9 +19,11 @@ export interface ToolCallRequest {
19
19
  * (how the target may be confirmed by path), the display family, and the engine-minted identity
20
20
  * (`capabilityId` / `contractId` / `shapeDigest`) an exemption store keys on. Stamped at the gate entry
21
21
  * from the live roster, so identity follows the OBJECT the call dispatches to — a caller tool that merely
22
- * took a built-in's name carries its own face, never the built-in's. ABSENT when a policy is invoked
23
- * outside a Runner (a bare producer): a path-confining face then reads the catalog's declaration for the
24
- * NAME (the pre-388 posture, never wider), and a skill-scope face treats the write as unconfirmable.
22
+ * took a built-in's name carries its own face, never the built-in's. ABSENT only on a BARE request (a
23
+ * policy invoked outside a Runner by a producer that stamped nothing): a PROTECTIVE fence (a deny-list)
24
+ * then reads the catalog's declaration for the NAME, tighten-only; a CONTAINMENT fence (a grant admitting a
25
+ * write by its path) REFUSES the request, typed (`policy.bare_request`, #626 ②) — stamp the face yourself
26
+ * (`toolCallFaceOf(roster, name)`) when driving a policy outside a Runner.
25
27
  */
26
28
  face?: ToolCallFace;
27
29
  /**
@@ -207,20 +209,12 @@ export type PermissionResult = {
207
209
  * same safe direction as `matchedAskRule`); it cannot state a window of its own, because the
208
210
  * route it would be a window for has not been chosen yet. */
209
211
  denialLimitFallback?: import("./auto-mode.js").UnarmedDenialLimitFallback;
210
- /** #616 (additive): the auto-mode classifier was CONSULTED on this ask and could not run the ask
211
- * flows the original chain exactly as it would have (routing, origin, bit and members unchanged),
212
- * carrying the station FACT beside them so a card can say "asked because the classifier was
213
- * unavailable (timeout)" instead of reading as ordinary hesitation. `cause` is the verdict's own
214
- * word ({@link import("./auto-mode.js").AutoModeUnavailableCause}: `error` / `timeout` /
215
- * `breaker_open`). A FACT, not an origin: CC 2.1.250 marks the same condition as a denial KIND on
216
- * the outcome (`automode-unavailable`) beside the decision's provenance, never in place of it, and
217
- * this engine's divergence (the ask reaches a person instead of being denied) does not move the
218
- * fact onto the origin axis either. ENGINE-STAMPED at the classifier stations (the gate's own and
219
- * the inherited-lane arms) only when the verdict was `unavailable`; `parse_error` stamps nothing
220
- * (the classifier ran and answered outside its contract — a different sentence). Display metadata:
221
- * nothing reads it to decide anything, so a policy that self-declares it can only put its own
222
- * sentence on its own card. Carried onto the approval request and the durable row by the carry
223
- * stations. */
212
+ /** #616 (additive): the classifier-unavailable FACT on an ASK `cause` is the verdict's own word
213
+ * ({@link import("./auto-mode.js").AutoModeUnavailableCause}). Since #661 the ENGINE no longer writes
214
+ * it here: an `unavailable` verdict is a DENY (the deny arm's member above), never an ask handed to a
215
+ * person. The member stays on the ask arm as display metadata a policy may self-declare on its own
216
+ * ask (nothing reads it to decide anything); the carry stations still copy it onto the approval
217
+ * request and the durable row when present. Retirement candidate for the next wire window. */
224
218
  classifierUnavailable?: {
225
219
  readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
226
220
  };
@@ -298,6 +292,16 @@ export type PermissionResult = {
298
292
  updatedInput?: unknown;
299
293
  message?: string;
300
294
  decisionReason?: DecisionReason;
295
+ /** #661 (additive): this deny is the classifier's UNAVAILABILITY — the auto-mode classifier was consulted
296
+ * and could not run, and the call is refused with a sentence that says so (CC 2.1.250 `Ze.unavailable` ⇒
297
+ * `deny`, `x1t`). `cause` is the verdict's own word ({@link import("./auto-mode.js").AutoModeUnavailableCause}).
298
+ * ENGINE-STAMPED beside `decisionReason: "classifier"` at the classifier stations (the gate's own and the
299
+ * inherited-lane arms) on an `unavailable` verdict only; a `block` and a `parse_error` deny carry nothing
300
+ * here. Read by the deny observer's payload ({@link import("./hooks.js").PermissionDeniedPayload}) and by
301
+ * nothing that decides. */
302
+ classifierUnavailable?: {
303
+ readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
304
+ };
301
305
  };
302
306
  /**
303
307
  * design/252 G-2 — WHY a piece of rule-provenance evidence is not on an ask.
@@ -540,100 +544,42 @@ export declare function constraintChainEntryOf(policy: ToolPolicy, meta?: Constr
540
544
  /**
541
545
  * One inherited-chain LAYER as both chain-entry mint sites see it — structurally an
542
546
  * `InheritedGate.parentConstraints[]` element, typed here structurally so this module (which owns the
543
- * digest) does not depend on the runner's shapes.
547
+ * digest) does not depend on the runner's shapes. The classifier itself is not read: a decider carries no
548
+ * state between rounds, so nothing about its health enters a chain entry.
544
549
  */
545
550
  export interface ConstraintChainLayerView {
546
551
  policy: ToolPolicy;
547
552
  autoMode?: {
548
- decider: {
549
- breakerOpen: () => boolean;
550
- consecutiveFailures?: () => number;
551
- };
552
553
  arming?: unknown;
553
554
  };
554
555
  durableMandate?: boolean;
555
556
  contentMandate?: boolean;
556
557
  }
557
558
  /**
558
- * The WRITE-side mint: the chain entry a durable PARK records for a live inherited layer.
559
- *
560
- * #503 safety bound lives HERE, at the persistence point rather than at arming time: the recipe is
561
- * carried only while the layer's breaker is UNTRIPPED AND UNTOUCHED. An open breaker is the ancestor
562
- * session's one-way "fell back to non-auto" latch — its arming has been withdrawn, and handing that
563
- * arming to a redemption would let the redeemed leg run auto-classified where the ancestor no longer does
564
- * (a redemption WIDER than the ancestor it inherits). The latch is read at the moment the row is written,
565
- * so an ancestor whose classifier died mid-run parks WITHOUT a recipe even though it was armed when the
566
- * chain was assembled.
567
- *
568
- * `breakerOpen()` alone is NOT the whole latch, and a closed-only test left a real widening: a rebuilt
569
- * decider starts a FRESH failure streak, so an ancestor sitting at 2 failures out of 3 — one bad
570
- * classification from dropping the whole session to non-auto — would hand a redemption the full budget
571
- * again, and the redeemed leg stays auto-classified through failures the ancestor would not have
572
- * survived. So the streak must read ZERO as well. A decider that does not expose it at all is UNKNOWN
573
- * state, and unknown takes the same arm as tripped: nothing is recorded (which is why a host that hand
574
- * rolls a decider cannot use the persisted-arming path — it must mint through `createAutoModeDecider`,
575
- * on both the arming and the redeeming side).
576
- *
577
- * Every read here is guarded, and every failure to read is treated as "withdrawn" — the opposite default
578
- * from the gate's demote-to-ask breaker reader, and for the same reason both are right: there, an
579
- * unreadable latch must not suppress a tighten; here, it must not license a carry-forward.
580
- *
581
- * ⚠️ WRITE SIDE ONLY. This gate reads MUTABLE state (a one-way latch, plus a streak that both grows and
582
- * resets to zero on any healthy classification). The resume re-supply must NOT re-run it — it samples a
583
- * different instant, and the two instants disagree for perfectly ordinary reasons: an ancestor keeps
584
- * running while its delegated child sits parked awaiting a human. See
585
- * {@link constraintChainEntryOfSuppliedLayer} for the verify side and the full account.
586
- *
587
- * ⚖️ WHAT THIS BOUND DOES NOT DO (recorded, precise shape). A withdrawn arming leaves the entry at
588
- * `autoModeArmed: true` with no recipe — digest-identical to an armed layer whose deployment never opted
589
- * in. So the digest does not, and cannot, stop a re-supplier from handing back a FRESH healthy decider of
590
- * its own: that is the pre-existing re-supply trust posture (see `resumeStream`'s contract — a caller
591
- * able to invoke it already holds full Runner authority, and the chain checks catch shape MISTAKES, not
592
- * a hostile trusted caller), and it is unchanged here. What this bound governs is what CORE hands
593
- * forward: a withdrawn ancestor's CRITERIA never travel, so a redemption cannot reconstruct the
594
- * ancestor's classifier from the row. For a fleet running `persistArming` everywhere, the pair
595
- * (`autoModeArmed` present, `autoModeArming` absent) is itself the readable signal that the ancestor's
596
- * arming was withdrawn or unrecordable, and such a host should re-supply a refusing decider rather than a
597
- * fresh one. Making the WITHDRAWAL itself digest-bound is a coherent stronger design — it needs a new
598
- * marker plus a new re-supply obligation to reproduce it (a withdrawn row carries no recipe, so the
599
- * marker cannot be derived from one), which is a scope decision for this seam's owner, not a silent
600
- * mechanism change.
559
+ * The WRITE-side mint: the chain entry a durable PARK records for a live inherited layer. The recipe is
560
+ * carried whenever the layer has one (the deployment opted into `persistArming`). The former gate — "only
561
+ * while the ancestor's breaker is untripped and untouched" went with the breaker: a decider carries no
562
+ * state between rounds, so there is no ancestor health to sample here and nothing that could make a
563
+ * redemption run wider than the ancestor it inherits.
564
+ *
565
+ * ⚖️ WHAT THE ROW DOES NOT SAY. An armed layer whose deployment never opted in leaves the entry at
566
+ * `autoModeArmed: true` with no recipe. The digest does not, and cannot, stop a re-supplier from handing
567
+ * back a FRESH decider of its own: that is the pre-existing re-supply trust posture (see `resumeStream`'s
568
+ * contract — a caller able to invoke it already holds full Runner authority, and the chain checks catch
569
+ * shape MISTAKES, not a hostile trusted caller). What CORE hands forward is the recipe when there is one,
570
+ * and nothing otherwise.
601
571
  */
602
572
  export declare function constraintChainEntryOfLayer(layer: ConstraintChainLayerView): ConstraintChainEntry;
603
573
  /**
604
574
  * The VERIFY-side mint: the entry a resume's RE-SUPPLIED layer must reproduce for the row it is
605
575
  * redeeming. Same body as the park mint, with the arming carry taken from the ROW rather than from the
606
- * layer's live classifier health so the digest equality is over bytes the row itself recorded, and
607
- * over nothing that can move while the row sits parked.
608
- *
609
- * WHY THE CARRY CANNOT BE RE-DERIVED HERE. Safety bound (see {@link constraintChainEntryOfLayer}) is a
610
- * statement about the ancestor at the instant the row was WRITTEN. Re-running it against the same live
611
- * layer at redemption time samples a different instant, and both directions of the disagreement strand an
612
- * approval a human already granted:
613
- * - the ancestor was healthy at the park (the row carries the recipe) and its breaker opened while the
614
- * child sat parked ⇒ the re-supply mints WITHOUT the recipe, the digests differ, and because the
615
- * latch is ONE-WAY the row is refused `resume.parent_constraint_mismatch` forever;
616
- * - the ancestor was one failure into its streak at the park (the row carries no recipe) and the streak
617
- * cleared on its next healthy classification ⇒ the re-supply mints WITH a recipe the row never
618
- * carried, same refusal.
619
- * Neither needs a host mistake: the ancestor and its siblings keep running while the child is parked, and
620
- * the same-process `resume()` verb re-supplies the very same live layer objects out of the Runner's own
621
- * parked-constraint registry. Nothing about the row changed; only a classifier's health did.
622
- *
623
- * WHAT THIS COSTS, NAMED. A row that recorded NO recipe no longer refuses a re-supply that hands one
624
- * back. That is not a tunable — the re-supply side literally cannot tell "the row withdrew a recipe this
625
- * same live layer still declares" (the second bullet above, a legitimate redemption) from "a re-supplier
626
- * is putting a recipe onto a row that withdrew one": both are the identical pair of inputs. It is also
627
- * thinner than it looks. The recipe is not an enforcement object — the DECIDER classifies, and the
628
- * re-supply trust posture already permits handing back a fresh healthy one, which is the capability that
629
- * would matter. What the row's absence still means is unchanged and is the property bound ① was written
630
- * for: a withdrawn ancestor's criteria never travel ON THE ROW, so no redemption can reconstruct that
631
- * ancestor's classifier from the checkpoint.
632
- *
633
- * WHAT THE DIGEST STILL BINDS. A row that DID record a recipe must have that recipe handed back: the
634
- * carry is on, so a re-supply that drops the arming, or hands back a different one, mints a different
635
- * entry and is refused pre-CAS exactly as before. That is the whole of the documented re-supply contract
636
- * — "hand back what the row carries".
576
+ * layer — the digest equality is over bytes the row itself recorded, never over what a live object says at
577
+ * a second instant. A row that DID record a recipe must have that recipe handed back: a re-supply that drops
578
+ * the arming, or hands back a different one, mints a different entry and is refused pre-CAS
579
+ * (`resume.parent_constraint_mismatch`). A row that recorded none does not refuse a re-supply that hands one
580
+ * back the recipe is not an enforcement object (the DECIDER classifies), and the re-supply trust posture
581
+ * already permits a fresh decider; what the row's absence still means is that no redemption can reconstruct
582
+ * the ancestor's classifier FROM THE CHECKPOINT.
637
583
  */
638
584
  export declare function constraintChainEntryOfSuppliedLayer(layer: ConstraintChainLayerView, recorded: ConstraintChainEntry | undefined): ConstraintChainEntry;
639
585
  /**
@@ -128,17 +128,7 @@ function chainEntryOfLayerCarrying(layer, carryArming) {
128
128
  });
129
129
  }
130
130
  export function constraintChainEntryOfLayer(layer) {
131
- let armingLive = false;
132
- if (layer.autoMode?.arming !== undefined) {
133
- try {
134
- const d = layer.autoMode.decider;
135
- armingLive = d.breakerOpen() !== true && typeof d.consecutiveFailures === "function" && d.consecutiveFailures() === 0;
136
- }
137
- catch {
138
- armingLive = false;
139
- }
140
- }
141
- return chainEntryOfLayerCarrying(layer, armingLive);
131
+ return chainEntryOfLayerCarrying(layer, layer.autoMode?.arming !== undefined);
142
132
  }
143
133
  export function constraintChainEntryOfSuppliedLayer(layer, recorded) {
144
134
  return chainEntryOfLayerCarrying(layer, recorded?.autoModeArming !== undefined);
@@ -62,13 +62,21 @@ type CallPathTarget = ToolCallPathTarget;
62
62
  * a different file, and the deny this floor exists to preserve simply misses.
63
63
  */
64
64
  export declare function protectivePathTargetOf(call: ToolCallLike): CallPathTarget | undefined;
65
+ /** The typed refusal a CONTAINMENT reader mints for a request that carries no face at all (#626 ② — design/388 B6's
66
+ * bare-producer migration seam, closed). */
67
+ export declare const BARE_REQUEST_CODE = "policy.bare_request";
65
68
  /**
66
69
  * The path target a CONTAINMENT fence reads (a grant admitting a write BY its path: session `allowDirs`, the fs-write
67
70
  * gate's accept/exempt dirs; the active-skill allowPaths face reads its own eligibility bit the same way): the object
68
71
  * face's OWN declaration only — a same-name tool that declares no path target is an UNCONFIRMABLE write (the catalog's
69
- * slot may not be the one it writes; a decoy in that slot would buy an admission), so the fence fails closed. A BARE
70
- * request (no face at all — a policy invoked outside a Runner) reads the catalog for the name: the pre-388 reading, the
71
- * documented migration seam for bare producers.
72
+ * slot may not be the one it writes; a decoy in that slot would buy an admission), so the fence fails closed.
73
+ *
74
+ * #626 ②: a BARE request (no face at all) is REFUSED, typed (`policy.bare_request`), never answered from the catalog
75
+ * for the name. A grant is a claim about the tool's OWN slot, and only a prepared leg's roster can say which object
76
+ * the call dispatches to — the catalog's declaration for a NAME is exactly the reading a same-name caller tool would
77
+ * exploit. Inside a Runner every gated request carries the leg's face (the gate entry, the resume belts and the
78
+ * pre-CAS edit recheck stamp it); a policy invoked outside a Runner stamps its own (`toolCallFaceOf(roster, name)`).
79
+ * The PROTECTIVE floor above keeps reading the catalog for a bare request: tighten-only, a deny can only gain a slot.
72
80
  */
73
81
  export declare function declaredPathTargetOf(call: ToolCallLike): CallPathTarget | undefined;
74
82
  /** Is this call a write a PROTECTIVE fence judges (a protected target whose access ≠ read)? The name sets are its static view. */
@@ -54,8 +54,14 @@ export function protectivePathTargetOf(call) {
54
54
  const floor = pathTargetOf(call.toolName);
55
55
  return floor === undefined || call.face === undefined ? floor : pathTargetSlotOnly(floor);
56
56
  }
57
+ export const BARE_REQUEST_CODE = "policy.bare_request";
57
58
  export function declaredPathTargetOf(call) {
58
- return call.face !== undefined ? call.face.pathTarget : pathTargetOf(call.toolName);
59
+ if (call.face === undefined) {
60
+ const e = new Error(`tool call ${JSON.stringify(call.toolName)} carries no face: a containment fence admits a write only by the tool's OWN declared path target, which a prepared leg's roster stamps on every gated request — a policy invoked outside a Runner must stamp \`face\` itself (toolCallFaceOf) rather than expect the catalog's declaration for the name`);
61
+ e.code = BARE_REQUEST_CODE;
62
+ throw e;
63
+ }
64
+ return call.face.pathTarget;
59
65
  }
60
66
  export function isProtectedWrite(call) {
61
67
  const pt = protectivePathTargetOf(call);
@@ -309,6 +309,32 @@ export declare function inputKeysOf(schema: TSchema | undefined): {
309
309
  /** The roster's KEY bounds over a schema (design/388 B20): a problem sentence, or undefined when every top-level key fits.
310
310
  * Shared by the caller declaration door (typed refusal) and the MCP intake (disclosed drop) — a key is never truncated. */
311
311
  export declare function schemaKeyBoundProblem(schema: TSchema | undefined): string | undefined;
312
+ /** The identity members of one roster row the wire bounds hold, as a DECLARATION (the door) or a MINTED row hands them. */
313
+ export interface RosterBoundMembers {
314
+ name: string;
315
+ aliases?: readonly string[];
316
+ /** An MCP/A2A mount's peer (the row's `origin.peer`) — a NAME on the roster, held to the wire name bound. */
317
+ peer?: string;
318
+ contractId?: string;
319
+ implementationRevision?: string;
320
+ modelGate?: string;
321
+ cardId?: string;
322
+ }
323
+ /** The typed refusal a member over its bound mints — the door's two codes, and the mint's own for the members no
324
+ * declaration door names (`modelGate`, the derived `cardId`). */
325
+ export type RosterBoundCode = "config.tool_name_too_long" | "config.tool_contract_too_long" | "config.tool_roster_bound";
326
+ /**
327
+ * design/388 B20 / #626 ① — the ONE bound judge over a row's identity members: a problem `{ code, message }`, or
328
+ * undefined when every member fits. Read by the declaration door (a `TaskSpec.tools` entry, refused before session
329
+ * acquire) and by the row mint (every source — an MCP/A2A peer, a definition that reached the mint by a route the
330
+ * door does not cover — refused before the row exists). A member over its bound is NEVER truncated onto the row: a
331
+ * truncated identity is a different identity (`judgeParkedToolIdentity` compares it across a park; the card
332
+ * namespace door reads it), and the schema's own bounds would then describe a row the mint never made.
333
+ */
334
+ export declare function rosterMemberBoundProblem(m: RosterBoundMembers): {
335
+ code: RosterBoundCode;
336
+ message: string;
337
+ } | undefined;
312
338
  /**
313
339
  * The rule vocabulary of ONE mounted instance (design/388 §2.2 ③ / D-3): DERIVED = the scalar top-level
314
340
  * keys ∪ the path target's `param` + aliases; a declared `params` list can only NARROW it (a declared
@@ -2,12 +2,12 @@ import { createHash } from "node:crypto";
2
2
  import { Type } from "typebox";
3
3
  import { getToolContract } from "../prompt-assembly/tool-catalog.js";
4
4
  import { protocolOf } from "./protocol-table.js";
5
- import { RENDER_HINT_ACTIVITY_MAX_CHARS, RENDER_HINT_MAX_CHARS, RENDER_HINT_MAX_LIST, TOOL_APPROVAL_CARDS, TOOL_FAMILIES, TOOL_KEYS_MAX, TOOL_KEY_MAX_CHARS, TOOL_MOUNT_TAGS, TOOL_PATH_ABSENCES, TOOL_PATH_BASES, } from "./tool-face.js";
5
+ import { RENDER_HINT_ACTIVITY_MAX_CHARS, RENDER_HINT_MAX_CHARS, RENDER_HINT_MAX_LIST, TOOL_APPROVAL_CARDS, TOOL_CARD_ID_MAX_CHARS, TOOL_CONTRACT_MAX_CHARS, TOOL_FAMILIES, TOOL_KEYS_MAX, TOOL_KEY_MAX_CHARS, TOOL_MOUNT_TAGS, TOOL_WIRE_NAME_MAX_CHARS, TOOL_PATH_ABSENCES, TOOL_PATH_BASES, } from "./tool-face.js";
6
6
  function lit(values) {
7
7
  return Type.Union(values.map((v) => Type.Literal(v)));
8
8
  }
9
- const Name = Type.String({ minLength: 1, maxLength: 128 });
10
- const Short = Type.String({ maxLength: 64 });
9
+ const Name = Type.String({ minLength: 1, maxLength: TOOL_WIRE_NAME_MAX_CHARS });
10
+ const Short = Type.String({ maxLength: TOOL_CONTRACT_MAX_CHARS });
11
11
  const Key = Type.String({ maxLength: TOOL_KEY_MAX_CHARS });
12
12
  const Digest16 = Type.String({ minLength: 16, maxLength: 16 });
13
13
  export const ROSTER_SOURCES = ["builtin", "caller", "host", "mcp", "a2a", "external", "synthetic"];
@@ -26,7 +26,7 @@ export const ToolRosterEntry = Type.Object({
26
26
  contract: Type.Object({ contractId: Short, implementationRevision: Short }, { additionalProperties: false }),
27
27
  shapeDigest: Digest16,
28
28
  wireSchemaDigest: Digest16,
29
- cardId: Type.String({ maxLength: 160 }),
29
+ cardId: Type.String({ maxLength: TOOL_CARD_ID_MAX_CHARS }),
30
30
  capabilityId: Digest16,
31
31
  inputKeys: Type.Array(Type.Object({ name: Key, type: lit(["string", "number", "integer", "boolean", "object", "array", "null", "mixed"]) }, { additionalProperties: false }), { maxItems: TOOL_KEYS_MAX }),
32
32
  effect: lit(["read", "write", "idempotent"]),
@@ -207,6 +207,26 @@ export function schemaKeyBoundProblem(schema) {
207
207
  return `the schema property ${JSON.stringify(long.slice(0, 40) + "…")} is ${long.length} characters; a roster key is at most ${TOOL_KEY_MAX_CHARS}`;
208
208
  return undefined;
209
209
  }
210
+ export function rosterMemberBoundProblem(m) {
211
+ const over = (label, value, bound) => `${label} ${JSON.stringify(value.slice(0, 40) + "…")} is ${value.length} characters; the roster's bound is ${bound}`;
212
+ const of = (text) => `tool ${JSON.stringify(m.name.slice(0, 40))}: ${text}`;
213
+ if (m.name.length > TOOL_WIRE_NAME_MAX_CHARS)
214
+ return { code: "config.tool_name_too_long", message: over("tool name", m.name, TOOL_WIRE_NAME_MAX_CHARS) };
215
+ const alias = m.aliases?.find((a) => a.length > TOOL_WIRE_NAME_MAX_CHARS);
216
+ if (alias !== undefined)
217
+ return { code: "config.tool_name_too_long", message: of(over("alias", alias, TOOL_WIRE_NAME_MAX_CHARS)) };
218
+ if (m.peer !== undefined && m.peer.length > TOOL_WIRE_NAME_MAX_CHARS)
219
+ return { code: "config.tool_name_too_long", message: of(over("peer", m.peer, TOOL_WIRE_NAME_MAX_CHARS)) };
220
+ for (const [label, v] of [["contractId", m.contractId], ["implementationRevision", m.implementationRevision]]) {
221
+ if (v !== undefined && v.length > TOOL_CONTRACT_MAX_CHARS)
222
+ return { code: "config.tool_contract_too_long", message: of(over(`contract ${label}`, v, TOOL_CONTRACT_MAX_CHARS)) };
223
+ }
224
+ if (m.modelGate !== undefined && m.modelGate.length > TOOL_CONTRACT_MAX_CHARS)
225
+ return { code: "config.tool_roster_bound", message: of(over("modelGate", m.modelGate, TOOL_CONTRACT_MAX_CHARS)) };
226
+ if (m.cardId !== undefined && m.cardId.length > TOOL_CARD_ID_MAX_CHARS)
227
+ return { code: "config.tool_roster_bound", message: of(over("cardId", m.cardId, TOOL_CARD_ID_MAX_CHARS)) };
228
+ return undefined;
229
+ }
210
230
  export function ruleFaceOfInstance(schema, declared, pathTarget) {
211
231
  const { scalar } = inputKeysOf(schema);
212
232
  const derived = new Set(scalar);
@@ -375,15 +395,26 @@ export function mintRosterEntry(tool, meta, facts) {
375
395
  const pathTarget = face.pathTarget;
376
396
  const rf = ruleFaceOfInstance(tool.parameters, face.ruleFace, pathTarget);
377
397
  const hints = face.renderHints !== undefined ? capRenderHints(face.renderHints) : undefined;
398
+ const bound = rosterMemberBoundProblem({
399
+ name: tool.name,
400
+ aliases: tool.aliases,
401
+ ...(peer !== null ? { peer } : {}),
402
+ contractId: d.contractId,
403
+ implementationRevision: d.implementationRevision,
404
+ ...(typeof face.modelGate === "string" ? { modelGate: face.modelGate } : {}),
405
+ cardId,
406
+ });
407
+ if (bound !== undefined)
408
+ throw configError(bound.code, bound.message);
378
409
  return {
379
410
  name: tool.name,
380
411
  aliases: [...new Set(tool.aliases ?? [])],
381
412
  source: meta.source,
382
413
  ...(meta.origin !== undefined ? { origin: { peer: meta.origin.peer, ...(meta.origin.declaredBy !== undefined ? { declaredBy: meta.origin.declaredBy } : {}) } } : {}),
383
- contract: { contractId: d.contractId.slice(0, 64), implementationRevision: d.implementationRevision.slice(0, 64) },
414
+ contract: { contractId: d.contractId, implementationRevision: d.implementationRevision },
384
415
  shapeDigest: d.shapeDigest,
385
416
  wireSchemaDigest: d.wireSchemaDigest,
386
- cardId: cardId.slice(0, 160),
417
+ cardId,
387
418
  capabilityId: mintCapabilityId({ source: meta.source, peer, name: tool.name, contractId: d.contractId }),
388
419
  inputKeys,
389
420
  effect,
@@ -417,7 +448,7 @@ export function mintRosterEntry(tool, meta, facts) {
417
448
  validateInput: typeof tool.validateInput === "function",
418
449
  deferred: facts.deferred.has(tool.name),
419
450
  alwaysLoad: face.alwaysLoad === true || face.mcpAlwaysLoad === true,
420
- ...(typeof face.modelGate === "string" ? { modelGate: face.modelGate.slice(0, 64) } : {}),
451
+ ...(typeof face.modelGate === "string" ? { modelGate: face.modelGate } : {}),
421
452
  ...(hints !== undefined ? { renderHints: hints } : {}),
422
453
  mountedBy: meta.mountedBy,
423
454
  };
@@ -529,11 +529,10 @@ export type TraceEvent = {
529
529
  } | {
530
530
  /**
531
531
  * The auto-mode classifier decided (or declined to decide) one pending ask — one frame per
532
- * `decide` call, including the breaker-open short-circuit, so a consumer can read the gate's
533
- * wall-clock cost per tool call and the failure cause behind an ask that reached a person. A
534
- * TRACE frame rather than a task event: it fires once per gated call, it is an operator's read
535
- * (why did that ask take four seconds / why did it fall back), and the session-level breaker
536
- * read face a shell renders is a separate, additive wire face. `ms` is the wall time from the
532
+ * `decide` call, so a consumer can read the gate's wall-clock cost per tool call and the failure
533
+ * cause behind a call the classifier could not evaluate. A TRACE frame rather than a task event: it
534
+ * fires once per gated call and it is an operator's read (why did that ask take four seconds / why
535
+ * was that call denied). `ms` is the wall time from the
537
536
  * decide call to its verdict (timeout included: a timed-out round reads the deadline).
538
537
  */
539
538
  kind: "auto_mode.classified";
@@ -547,8 +546,8 @@ export type TraceEvent = {
547
546
  ms: number;
548
547
  /** The verdict kind the gate acted on (`allow` / `block` / `unavailable` / `parse_error`). */
549
548
  verdict: "allow" | "block" | "unavailable" | "parse_error";
550
- /** Present iff `verdict === "unavailable"`: `error` / `timeout` / `breaker_open`. */
551
- cause?: "error" | "timeout" | "breaker_open";
549
+ /** Present iff `verdict === "unavailable"`: `error` / `timeout`. */
550
+ cause?: "error" | "timeout";
552
551
  ts: number;
553
552
  } | {
554
553
  /** C1 — the failover brain served this call from a FALLBACK entry (`createFailoverBrain`): the