@sema-agent/core 7.13.0 → 7.15.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 (102) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/agents/subagent.d.ts +4 -9
  3. package/dist/agents/subagent.js +16 -12
  4. package/dist/agents/teacher.js +1 -1
  5. package/dist/brain/reasoning.d.ts +23 -0
  6. package/dist/brain/reasoning.js +35 -4
  7. package/dist/brain/stream-engine.js +35 -17
  8. package/dist/brain/timeout.d.ts +25 -4
  9. package/dist/brain/timeout.js +1 -1
  10. package/dist/core/ask-origin.d.ts +30 -11
  11. package/dist/core/ask-origin.js +10 -5
  12. package/dist/core/auto-mode.d.ts +3 -3
  13. package/dist/core/checkpoint-seat.d.ts +18 -0
  14. package/dist/core/checkpoint-seat.js +6 -0
  15. package/dist/core/checkpoint-store.d.ts +23 -31
  16. package/dist/core/checkpoint-store.js +7 -4
  17. package/dist/core/engine-notice.d.ts +3 -1
  18. package/dist/core/env-knob-announce.d.ts +16 -0
  19. package/dist/core/env-knob-announce.js +14 -0
  20. package/dist/core/gate-fold.js +2 -0
  21. package/dist/core/gate-lanes.js +21 -12
  22. package/dist/core/gate-outcome.d.ts +42 -3
  23. package/dist/core/gate-outcome.js +22 -0
  24. package/dist/core/governance-codes.js +1 -1
  25. package/dist/core/hooks.d.ts +9 -19
  26. package/dist/core/hooks.js +6 -4
  27. package/dist/core/mcp.d.ts +3 -2
  28. package/dist/core/mcp.js +5 -11
  29. package/dist/core/memory-engine/engine.d.ts +2 -2
  30. package/dist/core/runner/advertised-writable-dirs.d.ts +74 -0
  31. package/dist/core/runner/advertised-writable-dirs.js +48 -0
  32. package/dist/core/runner/assemble-result.d.ts +5 -0
  33. package/dist/core/runner/contracts.d.ts +2 -2
  34. package/dist/core/runner/denial-limit-arms.d.ts +7 -9
  35. package/dist/core/runner/denial-limit-arms.js +8 -10
  36. package/dist/core/runner/gate-exit.d.ts +15 -3
  37. package/dist/core/runner/gate-exit.js +6 -4
  38. package/dist/core/runner/permission-rule-lanes.d.ts +3 -1
  39. package/dist/core/runner/permission-rule-lanes.js +1 -1
  40. package/dist/core/runner/prepare-caps-and-workflow.d.ts +34 -4
  41. package/dist/core/runner/prepare-caps-and-workflow.js +47 -12
  42. package/dist/core/runner/prepare-defer-classify.d.ts +1 -1
  43. package/dist/core/runner/prepare-defer-classify.js +12 -10
  44. package/dist/core/runner/prepare-gate-stations.d.ts +4 -5
  45. package/dist/core/runner/prepare-gate-stations.js +2 -2
  46. package/dist/core/runner/prepare-hands-readface.d.ts +18 -12
  47. package/dist/core/runner/prepare-hands-readface.js +32 -43
  48. package/dist/core/runner/prepare-inherited-gate.d.ts +7 -5
  49. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  50. package/dist/core/runner/prepare-memory.d.ts +31 -34
  51. package/dist/core/runner/prepare-memory.js +73 -51
  52. package/dist/core/runner/prepare-park-ask.d.ts +5 -6
  53. package/dist/core/runner/prepare-park-ask.js +3 -3
  54. package/dist/core/runner/prepare-policy-chain.d.ts +2 -2
  55. package/dist/core/runner/prepare-policy-chain.js +7 -4
  56. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -0
  57. package/dist/core/runner/prepare-prompt-inputs.js +2 -2
  58. package/dist/core/runner/prepare-question-face.d.ts +10 -8
  59. package/dist/core/runner/prepare-question-face.js +1 -3
  60. package/dist/core/runner/prepare-safety-scan.js +1 -1
  61. package/dist/core/runner/prepare-task.js +60 -53
  62. package/dist/core/runner/prepare-wiring-manifest.d.ts +2 -2
  63. package/dist/core/runner/prepare-wiring-manifest.js +1 -1
  64. package/dist/core/runner/run-compaction-machinery.js +2 -0
  65. package/dist/core/runner/run-harness-handlers.js +3 -1
  66. package/dist/core/runner/stream-settle-backstop.js +1 -1
  67. package/dist/core/runner/tool-defer-gate.d.ts +86 -0
  68. package/dist/core/runner/tool-defer-gate.js +57 -0
  69. package/dist/core/runner/tool-disclosure.d.ts +0 -36
  70. package/dist/core/runner/tool-disclosure.js +0 -43
  71. package/dist/core/runner-deps.d.ts +16 -9
  72. package/dist/core/runtime-caps.d.ts +21 -0
  73. package/dist/core/runtime-caps.js +5 -1
  74. package/dist/core/task-event.d.ts +11 -2
  75. package/dist/core/task-registry-shared.js +8 -6
  76. package/dist/core/task-result.d.ts +15 -0
  77. package/dist/core/task-spec.d.ts +8 -2
  78. package/dist/core/terminal-cause.d.ts +6 -2
  79. package/dist/core/tool-policy.d.ts +34 -28
  80. package/dist/core/tool-policy.js +29 -5
  81. package/dist/core/tool-roster.js +2 -0
  82. package/dist/core/tool-spec.d.ts +20 -16
  83. package/dist/core/types.d.ts +3 -2
  84. package/dist/core/types.js +1 -0
  85. package/dist/core/wiring-manifest.d.ts +16 -9
  86. package/dist/core/wiring-manifest.js +8 -3
  87. package/dist/index.d.ts +7 -6
  88. package/dist/index.js +5 -4
  89. package/dist/orchestration/run-workflow-tool.d.ts +8 -8
  90. package/dist/orchestration/run-workflow-tool.js +1 -1
  91. package/dist/orchestration/workflow-primitives.d.ts +4 -3
  92. package/dist/orchestration/workflow-primitives.js +3 -3
  93. package/dist/orchestration/workflow-types.d.ts +14 -0
  94. package/dist/orchestration/workflow.d.ts +41 -4
  95. package/dist/orchestration/workflow.js +23 -5
  96. package/dist/tools/fs/fs-bash.d.ts +41 -0
  97. package/dist/tools/fs/fs-bash.js +86 -29
  98. package/dist/tools/fs/fs-shared.js +9 -3
  99. package/dist/tools/fs/read-deny.d.ts +24 -8
  100. package/dist/tools/fs/read-deny.js +20 -1
  101. package/package.json +2 -1
  102. package/test/export-surface.snapshot.json +29 -1
@@ -246,6 +246,11 @@ async function sleepAnnouncingRetry(totalMs, signal, frame) {
246
246
  return;
247
247
  }
248
248
  }
249
+ function midstreamRetryAnnouncement(failure) {
250
+ return failure.kind === "stalled"
251
+ ? { errClass: "stall", detail: `${failure.watchdog} watchdog fired after ${failure.timeoutMs}ms (${failure.knob}), retrying` }
252
+ : { errClass: "transport", detail: "connection lost, reconnecting" };
253
+ }
249
254
  export function runStreamingBrain(args) {
250
255
  const { model, doFetch, signal, config, httpLabel, buildRequest, makeParser, stallTimeouts } = args;
251
256
  const out = createAssistantMessageEventStream();
@@ -471,7 +476,7 @@ export function runStreamingBrain(args) {
471
476
  const decoder = new TextDecoder();
472
477
  let buffer = "";
473
478
  let firstTokenSeen = false;
474
- let firstTokenTimedOut = false;
479
+ let firedFirstTokenDeadlineMs;
475
480
  let ftTimer;
476
481
  let waitTimer;
477
482
  const clearWaitTimer = () => {
@@ -482,7 +487,7 @@ export function runStreamingBrain(args) {
482
487
  };
483
488
  if (firstTokenTimeoutMs && firstTokenTimeoutMs > 0) {
484
489
  ftTimer = setTimeout(() => {
485
- firstTokenTimedOut = true;
490
+ firedFirstTokenDeadlineMs = firstTokenTimeoutMs;
486
491
  clearWaitTimer();
487
492
  void reader.cancel().catch(() => undefined);
488
493
  }, firstTokenTimeoutMs);
@@ -492,7 +497,7 @@ export function runStreamingBrain(args) {
492
497
  const scheduleWaitFrame = (delayMs) => {
493
498
  waitTimer = setTimeout(() => {
494
499
  waitTimer = undefined;
495
- if (firstTokenSeen || firstTokenTimedOut || Date.now() - waitStartedAt >= firstTokenTimeoutMs)
500
+ if (firstTokenSeen || firedFirstTokenDeadlineMs !== undefined || Date.now() - waitStartedAt >= firstTokenTimeoutMs)
496
501
  return;
497
502
  announcedWait = true;
498
503
  emitWaitFrame({
@@ -507,16 +512,16 @@ export function runStreamingBrain(args) {
507
512
  scheduleWaitFrame(WAITING_FIRST_TOKEN_AFTER_MS);
508
513
  }
509
514
  }
510
- let idleTimedOut = false;
515
+ let firedIdleDeadlineMs;
511
516
  let idleTimer;
512
517
  const bumpIdle = () => {
513
518
  if (!idleTimeoutMs || idleTimeoutMs <= 0)
514
519
  return;
515
- idleTimedOut = false;
520
+ firedIdleDeadlineMs = undefined;
516
521
  if (idleTimer)
517
522
  clearTimeout(idleTimer);
518
523
  idleTimer = setTimeout(() => {
519
- idleTimedOut = true;
524
+ firedIdleDeadlineMs = idleTimeoutMs;
520
525
  void reader.cancel().catch(() => undefined);
521
526
  }, idleTimeoutMs);
522
527
  };
@@ -525,7 +530,7 @@ export function runStreamingBrain(args) {
525
530
  if (firstTokenSeen)
526
531
  return;
527
532
  firstTokenSeen = true;
528
- firstTokenTimedOut = false;
533
+ firedFirstTokenDeadlineMs = undefined;
529
534
  if (ftTimer) {
530
535
  clearTimeout(ftTimer);
531
536
  ftTimer = undefined;
@@ -615,11 +620,23 @@ export function runStreamingBrain(args) {
615
620
  clearTimeout(idleTimer);
616
621
  idleTimer = undefined;
617
622
  }
618
- if (!failure && firstTokenTimedOut) {
619
- failure = { kind: "stalled", err: new BrainError("network", `first-token timeout after ${firstTokenTimeoutMs}ms`, undefined, { apiError: true }) };
623
+ if (!failure && firedFirstTokenDeadlineMs !== undefined) {
624
+ failure = {
625
+ kind: "stalled",
626
+ watchdog: "first-token",
627
+ knob: "firstTokenTimeoutMs",
628
+ timeoutMs: firedFirstTokenDeadlineMs,
629
+ err: new BrainError("network", `first-token timeout after ${firedFirstTokenDeadlineMs}ms`, undefined, { apiError: true }),
630
+ };
620
631
  }
621
- if (!failure && idleTimedOut) {
622
- failure = { kind: "stalled", err: new BrainError("network", `stream idle timeout after ${idleTimeoutMs}ms`, undefined, { apiError: true }) };
632
+ if (!failure && firedIdleDeadlineMs !== undefined) {
633
+ failure = {
634
+ kind: "stalled",
635
+ watchdog: "stream idle",
636
+ knob: "idleTimeoutMs",
637
+ timeoutMs: firedIdleDeadlineMs,
638
+ err: new BrainError("network", `stream idle timeout after ${firedIdleDeadlineMs}ms`, undefined, { apiError: true }),
639
+ };
623
640
  }
624
641
  if (!failure) {
625
642
  buffer += decoder.decode();
@@ -641,22 +658,23 @@ export function runStreamingBrain(args) {
641
658
  throw failure.err;
642
659
  cleanup?.();
643
660
  cleanup = undefined;
661
+ const announcement = midstreamRetryAnnouncement(failure);
644
662
  if (snap.hasOnlyThinking) {
645
663
  if (thinkingRetries >= THINKING_RETRY_BUDGET)
646
664
  throw failure.err;
647
665
  thinkingRetries++;
648
666
  parser.sealForRetry();
649
667
  const delayMs = 100 * thinkingRetries;
650
- emitBrainTelemetry({ kind: "retry", attempt: thinkingRetries, phase: "midstream", errClass: "transport", nextDelayMs: delayMs });
668
+ emitBrainTelemetry({ kind: "retry", attempt: thinkingRetries, phase: "midstream", errClass: announcement.errClass, nextDelayMs: delayMs });
651
669
  announcedRetry = true;
652
670
  await sleepAnnouncingRetry(delayMs, signal, (remainingMs) => ({
653
671
  phase: "reconnecting",
654
- detail: "connection lost, reconnecting",
672
+ detail: announcement.detail,
655
673
  retryInSec: Math.ceil(remainingMs / 1000),
656
674
  retryInMs: remainingMs,
657
675
  attempt: thinkingRetries,
658
676
  maxRetries: THINKING_RETRY_BUDGET,
659
- errClass: "transport",
677
+ errClass: announcement.errClass,
660
678
  }));
661
679
  continue;
662
680
  }
@@ -664,16 +682,16 @@ export function runStreamingBrain(args) {
664
682
  throw failure.err;
665
683
  const delayMs = retryBackoffMs(baseDelay, attempt);
666
684
  attempt++;
667
- emitBrainTelemetry({ kind: "retry", attempt, phase: "midstream", errClass: "transport", nextDelayMs: delayMs });
685
+ emitBrainTelemetry({ kind: "retry", attempt, phase: "midstream", errClass: announcement.errClass, nextDelayMs: delayMs });
668
686
  announcedRetry = true;
669
687
  await sleepAnnouncingRetry(delayMs, signal, (remainingMs) => ({
670
688
  phase: "reconnecting",
671
- detail: "connection lost, reconnecting",
689
+ detail: announcement.detail,
672
690
  retryInSec: Math.ceil(remainingMs / 1000),
673
691
  retryInMs: remainingMs,
674
692
  attempt,
675
693
  maxRetries,
676
- errClass: "transport",
694
+ errClass: announcement.errClass,
677
695
  }));
678
696
  }
679
697
  }
@@ -38,17 +38,38 @@ export declare function resolveStallTimeoutMs(value: number | undefined, knob: s
38
38
  * - connect 30s: bounds the fetch→response-HEADERS wait — the one segment every post-headers
39
39
  * watchdog is blind to. On a streaming API the model's time is spent AFTER headers, so 30s with
40
40
  * none at all is a hung gateway, not a slow model.
41
- * - first-token 120s: bounds the headers→first-delta wait, NOT the model's thought (the first
41
+ * - first-token 600s: bounds the headers→first-delta wait, NOT the model's thought (the first
42
42
  * THINKING delta already counts as the first token, design/17 O5 — see
43
- * {@link BrainTimeoutConfig.firstTokenTimeoutMs}), so a slow reasoner is safe; 2 minutes with
44
- * literally NOTHING is a hung gateway.
43
+ * {@link BrainTimeoutConfig.firstTokenTimeoutMs}), so a slow reasoner is safe. The old 120s rested
44
+ * on "2 minutes with literally NOTHING is a hung gateway", which is FALSE for a real class of
45
+ * backend: a self-hosted server working through a long prefill emits nothing at all for 2–3
46
+ * minutes, and the watchdog cancelled it — burning the prefill and re-sending a request that then
47
+ * burned it again. A guardrail whose reason is false for healthy traffic is not a guardrail. 600s
48
+ * is the same order of magnitude as the reference implementation's own request timeout, and the
49
+ * shapes this bound exists for (a gateway that accepted the request and then died) are still
50
+ * caught, just later; the task's own hard abort remains the outer bound for everything else.
51
+ * HONEST CEILING (measured, adversarial round): this watchdog is not the only clock over a
52
+ * byte-silent stream, so 600s is the bound this engine ASKS FOR, not the silence a deployment is
53
+ * necessarily granted. On Node's own `fetch` — the default transport for both adapters, and the
54
+ * one a deployment gets by supplying no `fetchImpl` — undici destroys a body that has produced no
55
+ * bytes for 300s (`bodyTimeout`, default `300e3`), so byte-silence past 300s ends the attempt
56
+ * there and arrives as a read rejection reported `transport`. That report is CORRECT (the
57
+ * transport really did cut it) and the seat to change it exists: `fetchImpl` on both brain configs
58
+ * takes any `fetch`, so a deployment needing the full 600s passes one built on an undici
59
+ * `Agent`/`Dispatcher` with its own `bodyTimeout`. Named here so the next reader who measures ~300s
60
+ * of tolerance does not conclude this number is a lie.
61
+ * The OTHER clock that used to sit below this one is fixed rather than disclosed:
62
+ * {@link import("../orchestration/workflow.js").WORKFLOW_AGENT_STALL_MS} is now DERIVED from
63
+ * {@link STALL_CONNECT_MS} + this value, so the coarse agent-restart no longer sits UNDER a single
64
+ * silent provider call by construction. Move this number and that one follows. It is a floor, not
65
+ * a guarantee of ordering — see that constant's own comment for the cases that still invert.
45
66
  * - idle 90s: mid-stream silence after output started — a healthy stream emits steadily
46
67
  * (comfortably above any provider keep-alive cadence).
47
68
  * ADVISORY: a brain whose construction-time {@link BrainTimeoutConfig} sets the corresponding field
48
69
  * keeps it (deployment sovereignty; see the `StreamOptions.stallTimeouts` priority chain).
49
70
  */
50
71
  export declare const STALL_CONNECT_MS = 30000;
51
- export declare const STALL_FIRST_TOKEN_MS = 120000;
72
+ export declare const STALL_FIRST_TOKEN_MS = 600000;
52
73
  export declare const STALL_IDLE_MS = 90000;
53
74
  export interface BrainTimeoutConfig {
54
75
  /** Abort the request if `fetch` doesn't return response headers within this many ms. Retryable. */
@@ -15,7 +15,7 @@ export function resolveStallTimeoutMs(value, knob) {
15
15
  return value;
16
16
  }
17
17
  export const STALL_CONNECT_MS = 30_000;
18
- export const STALL_FIRST_TOKEN_MS = 120_000;
18
+ export const STALL_FIRST_TOKEN_MS = 600_000;
19
19
  export const STALL_IDLE_MS = 90_000;
20
20
  export function createConnectController(connectTimeoutMs, outerSignal) {
21
21
  const ac = new AbortController();
@@ -33,9 +33,10 @@ import type { PermissionResult } from "./tool-policy.js";
33
33
  *
34
34
  * - `content_question` — the reserved question tool: a content-ROUTING signal, not a permission
35
35
  * question; it leaves the gate through the content channel, never through a permission decider.
36
- * - `unresolvable` — the call is MARKED by an ancestor (an inherited approver-unavailable float, or
36
+ * - `ancestor_marked` — the call is MARKED by an ancestor (an inherited approver-unavailable float, or
37
37
  * a durable mandate floated down because this task can park it): it must reach the park with no
38
- * synchronous decision-maker in between.
38
+ * synchronous decision-maker in between. The word names WHO raised it (an ancestor's mark), like every
39
+ * other member; whether the ask can be resolved is the park's business, not the origin's.
39
40
  * - `org_unavailable` — the deployment is org-governed and could not read its snapshot: the fail-closed
40
41
  * tighten, cleared by a person only ("does the org forbid this?" is unanswered, and no automatic
41
42
  * lane may answer it).
@@ -69,7 +70,7 @@ import type { PermissionResult } from "./tool-policy.js";
69
70
  * observer or an approval card can say WHICH engine layer raised the question, which one word
70
71
  * could not.
71
72
  */
72
- export declare const ASK_ORIGINS: readonly ["content_question", "unresolvable", "org_unavailable", "org_rule", "rule_store_unavailable", "hook", "ask_rule", "denial_limit_fallback", "shell_gate_tighten", "safety_tighten", "policy"];
73
+ export declare const ASK_ORIGINS: readonly ["content_question", "ancestor_marked", "org_unavailable", "org_rule", "rule_store_unavailable", "hook", "ask_rule", "denial_limit_fallback", "shell_gate_tighten", "safety_tighten", "policy"];
73
74
  export type AskOrigin = (typeof ASK_ORIGINS)[number];
74
75
  /** Whether a value is a member of the closed origin set. Exported because the word crosses process
75
76
  * boundaries: a host validating one it received must not hand-roll the set. */
@@ -83,6 +84,23 @@ export declare function isAskOrigin(v: unknown): v is AskOrigin;
83
84
  * other direction (a row for a word that is not in the set).
84
85
  */
85
86
  export type AssertAllKeysHandled<T extends never> = T;
87
+ /**
88
+ * WHICH mechanism left the persisted-rule lane unable to enforce the person's deny/ask rows for a call —
89
+ * the two-word set behind the one origin word `rule_store_unavailable` (the AUTHORITY semantics are
90
+ * identical; the mechanism differs, and a consumer that renders "the store is down" for a call the lexer
91
+ * could not read is telling the person to fix the wrong thing):
92
+ * - `store` — the wired store could not be READ for this call (a read failure, a timeout, a thrown adapter);
93
+ * - `call` — the store was read, but the CALL could not be read against the person's rows (the
94
+ * tightening lexer's `unreadable` word: an expansion where a rule reads a word, an unterminated quote,
95
+ * a syntax error — the ask's message carries the lexer's own sentence).
96
+ * The gate reads the lane's answer ONCE into {@link AskOriginFacts.ruleStore}, derives the origin from it and
97
+ * stamps the word onto the surviving ask (`ruleStoreUnreadable`), which the approval request and the durable
98
+ * row carry beside `origin`.
99
+ */
100
+ export declare const RULE_STORE_UNREADABLE_KINDS: readonly ["store", "call"];
101
+ export type RuleStoreUnreadable = (typeof RULE_STORE_UNREADABLE_KINDS)[number];
102
+ /** Membership test for {@link RuleStoreUnreadable} — the screen a row reader applies to a stored word. */
103
+ export declare function isRuleStoreUnreadable(v: unknown): v is RuleStoreUnreadable;
86
104
  /** The facts about one call that only the gate holds — the derivation's inputs beyond the decision
87
105
  * itself. Every member is a plain boolean/word read ONCE by the gate, so the derivation is a pure
88
106
  * function that a test can drive arm by arm. */
@@ -95,9 +113,10 @@ export interface AskOriginFacts {
95
113
  * read-back off the decision (a policy can put `decisionReason: "org_rule"` on its own ask; the
96
114
  * gate's cell cannot be written by a policy). */
97
115
  readonly org: "rule" | "unavailable" | undefined;
98
- /** The gate's own record of its persisted-rule lane's READ: `"unavailable"` when the wired store could
99
- * not be read for this call (the lane's deny/ask rows are unenforceable). Written only by the gate. */
100
- readonly ruleStore?: "unavailable" | undefined;
116
+ /** The gate's own record of its persisted-rule lane's READ: which {@link RuleStoreUnreadable} mechanism
117
+ * left the lane's deny/ask rows unenforceable for this call, or undefined (the lane read the store and
118
+ * the call). Written only by the gate. */
119
+ readonly ruleStore?: RuleStoreUnreadable | undefined;
101
120
  /** Which of the gate's OWN post-fold tightens minted the standing ask, if one did — again the gate's
102
121
  * record of its own layer (a tighten replaces an `allow` wholesale, so at most one fires per pass),
103
122
  * never a read-back of `decisionReason: "safety"` off a decision a policy could have composed. */
@@ -135,7 +154,7 @@ export declare function askOriginOf(decision: Extract<PermissionResult, {
135
154
  */
136
155
  export declare const ORIGIN_IMPLIES_REAL_APPROVAL: {
137
156
  readonly content_question: false;
138
- readonly unresolvable: false;
157
+ readonly ancestor_marked: false;
139
158
  readonly org_unavailable: "at_derivation";
140
159
  readonly org_rule: "at_derivation";
141
160
  readonly rule_store_unavailable: "at_derivation";
@@ -156,7 +175,7 @@ export type RealApprovalTableCoversEveryOrigin = AssertAllKeysHandled<Exclude<As
156
175
  * answer, each for its own reason (kept beside its row so the reason travels with the fact):
157
176
  * - `content_question`: a permission decider's verdict is neither an answer nor a park — letting it
158
177
  * speak would skip the durable mandate, the park and the content route in one step.
159
- * - `unresolvable`: a marked call must reach the park with no synchronous decision-maker between.
178
+ * - `ancestor_marked`: a marked call must reach the park with no synchronous decision-maker between.
160
179
  * - `org_unavailable` / `org_rule`: judgment may clear a `requiresRealApproval` bit a POLICY minted,
161
180
  * but not the org's — while adjudication is unavailable the org's answer is unknown, and an org
162
181
  * ask carries the org's own "a person nods" semantic.
@@ -185,7 +204,7 @@ export type RealApprovalTableCoversEveryOrigin = AssertAllKeysHandled<Exclude<As
185
204
  */
186
205
  declare const CLASSIFIER_MAY_ANSWER: {
187
206
  readonly content_question: false;
188
- readonly unresolvable: false;
207
+ readonly ancestor_marked: false;
189
208
  readonly org_unavailable: false;
190
209
  readonly org_rule: false;
191
210
  readonly rule_store_unavailable: false;
@@ -215,8 +234,8 @@ export declare function classifierMayAnswer(origin: AskOrigin): boolean;
215
234
  * · `egress` — the tool's own external-write mark, judged next: it is the tool's declaration even
216
235
  * when the coarse doctrine also installed a shell tier on the same seat;
217
236
  * · `shellGated` + tier `"always"` — the operator's per-call confirmation doctrine
218
- * (`"operator_always"`); the classify doctrine installs `"maybe"`, and THOSE asks stay the rule
219
- * lane's home turf (`undefined`) — that is the don't-ask-again main case, and the per-call member
237
+ * (`"operator_always"`); the shell seat installs `"maybe"` under the off and classify doctrines,
238
+ * and THOSE asks stay the rule lane's home turf (`undefined`) — that is the don't-ask-again main case, and the per-call member
220
239
  * above is deliberately the ONLY thing that carves a mandate out of it;
221
240
  * · a tool's OWN `"always"`/`"maybe"` irreversibility tier without the doctrine (`"tool_marks"`).
222
241
  *
@@ -1,8 +1,13 @@
1
- export const ASK_ORIGINS = ["content_question", "unresolvable", "org_unavailable", "org_rule", "rule_store_unavailable", "hook", "ask_rule", "denial_limit_fallback", "shell_gate_tighten", "safety_tighten", "policy"];
1
+ export const ASK_ORIGINS = ["content_question", "ancestor_marked", "org_unavailable", "org_rule", "rule_store_unavailable", "hook", "ask_rule", "denial_limit_fallback", "shell_gate_tighten", "safety_tighten", "policy"];
2
2
  const ASK_ORIGIN_SET = new Set(ASK_ORIGINS);
3
3
  export function isAskOrigin(v) {
4
4
  return ASK_ORIGIN_SET.has(v);
5
5
  }
6
+ export const RULE_STORE_UNREADABLE_KINDS = ["store", "call"];
7
+ const RULE_STORE_UNREADABLE_SET = new Set(RULE_STORE_UNREADABLE_KINDS);
8
+ export function isRuleStoreUnreadable(v) {
9
+ return RULE_STORE_UNREADABLE_SET.has(v);
10
+ }
6
11
  export function askOriginOf(decision, facts) {
7
12
  const origin = originLadder(decision, facts);
8
13
  if (ORIGIN_IMPLIES_REAL_APPROVAL[origin] === "at_derivation" && decision.requiresRealApproval !== true) {
@@ -14,12 +19,12 @@ function originLadder(decision, facts) {
14
19
  if (facts.contentQuestion)
15
20
  return "content_question";
16
21
  if (facts.markedUnresolvable)
17
- return "unresolvable";
22
+ return "ancestor_marked";
18
23
  if (facts.org === "unavailable")
19
24
  return "org_unavailable";
20
25
  if (facts.org === "rule")
21
26
  return "org_rule";
22
- if (facts.ruleStore === "unavailable")
27
+ if (facts.ruleStore !== undefined)
23
28
  return "rule_store_unavailable";
24
29
  if (decision.decisionReason === "hook")
25
30
  return "hook";
@@ -35,7 +40,7 @@ function originLadder(decision, facts) {
35
40
  }
36
41
  export const ORIGIN_IMPLIES_REAL_APPROVAL = {
37
42
  content_question: false,
38
- unresolvable: false,
43
+ ancestor_marked: false,
39
44
  org_unavailable: "at_derivation",
40
45
  org_rule: "at_derivation",
41
46
  rule_store_unavailable: "at_derivation",
@@ -48,7 +53,7 @@ export const ORIGIN_IMPLIES_REAL_APPROVAL = {
48
53
  };
49
54
  const CLASSIFIER_MAY_ANSWER = {
50
55
  content_question: false,
51
- unresolvable: false,
56
+ ancestor_marked: false,
52
57
  org_unavailable: false,
53
58
  org_rule: false,
54
59
  rule_store_unavailable: false,
@@ -1,8 +1,8 @@
1
1
  import type { ToolCallRequest } from "./tool-policy.js";
2
2
  /**
3
- * WHY an `unavailable` verdict could not run — the closed set behind the verdict arm, the deny's
4
- * `classifierUnavailable` fact (the deny observer's word and the model-facing sentence's parenthetical) and
5
- * the `auto_mode.classified` trace frame's `cause`, so the three spell one word. `error` = the model leg
3
+ * WHY an `unavailable` verdict could not run — the closed set behind the verdict arm, the model-facing
4
+ * sentence's parenthetical and the `auto_mode.classified` trace frame's `cause`, so the three spell one
5
+ * word (the deny's structural face carries only the round's FORM, `GateDisposition.cause`). `error` = the model leg
6
6
  * threw or rejected (a route failure at classify time reads here too — the derived-route pre-flight fell
7
7
  * back BEFORE any decide, so there is no separate word for it); `timeout` = the round-trip cap.
8
8
  */
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The checkpoint SEAT a run hands its children — the projection of `resolveCheckpointStore(spec, deps)` (the store the
3
+ * run parks in) plus the caller's own off switch onto the one value both delegation lanes copy verbatim onto a child
4
+ * spec (`ToolExecuteContext.checkpointStoreForChildren` on the Task lane, `RunWorkflowToolDeps.parentCheckpointStore`
5
+ * on the workflow lane). Minted once per prepare by the driver; read by the lanes and by the plan-pair face.
6
+ */
7
+ import type { CheckpointStore } from "./checkpoint-store.js";
8
+ /** The seat a run hands its children: the store it resolved, or the word `"disabled"` when the caller disarmed the run. */
9
+ export type CheckpointSeat = CheckpointStore | "disabled";
10
+ /** Project a run's spec + resolved store onto the seat its children inherit: `"disabled"` when the caller disarmed the
11
+ * run (a removal that presses on the whole subtree), else the resolved store, else NOTHING — an unset spec on a
12
+ * deployment without a store hands down no seat, and the child's own door resolves its runner's deployment (a split
13
+ * child-execution runner may carry a store the host has none of; that topology is the deployment's own author). */
14
+ export declare function checkpointSeatOf(spec: {
15
+ checkpointStore?: CheckpointStore | "disabled";
16
+ }, resolved: CheckpointStore | undefined): CheckpointSeat | undefined;
17
+ /** The store a seat names — none for `"disabled"` and for no seat. */
18
+ export declare function checkpointStoreOfSeat(seat: CheckpointSeat | undefined): CheckpointStore | undefined;
@@ -0,0 +1,6 @@
1
+ export function checkpointSeatOf(spec, resolved) {
2
+ return spec.checkpointStore === "disabled" ? "disabled" : resolved;
3
+ }
4
+ export function checkpointStoreOfSeat(seat) {
5
+ return seat === "disabled" ? undefined : seat;
6
+ }
@@ -8,7 +8,7 @@ import type { RepairBundle } from "../agents/repair-loop.js";
8
8
  import type { WorkspaceHandle } from "./remote-env.js";
9
9
  import type { ConsolidationNote } from "./runner/memory-consolidation.js";
10
10
  import type { SessionPermissionRules } from "./session-policy-store.js";
11
- import type { NestedUsageAccum, TaskResult } from "./types.js";
11
+ import type { NestedUsageAccum, ShellGateDoctrine, TaskResult } from "./types.js";
12
12
  import type { ActiveWorktreeSession } from "../internal/harness-types.js";
13
13
  /**
14
14
  * design/45 — the **durable-checkpoint** primitive: cross-process / resumable `suspend`/`resume`.
@@ -120,10 +120,9 @@ export interface RiskDescriptor {
120
120
  /** ToolEmu-style severity tier 1..5 (5 = most severe). The inbox sorts DESC by this. Deterministic — the
121
121
  * pure {@link riskSeverity} of {@link axes}. The ORDERING is what matters (the inbox's triage key). */
122
122
  severity: 1 | 2 | 3 | 4 | 5;
123
- /** Which safety axes tripped — for `irreversible_ask`, derived from the D-2 {@link SafetyAxis} (+ the
124
- * shell-gate). `shell` marks a coarse shellGate tighten (a bash command gated only because the deployment
125
- * set `shellGate`, with NO explicit per-tool egress/irreversible mark). Empty `{}` for a plain budgetable
126
- * `human` ask. Self-contained so the inbox needn't cross-ref `safetyAxis`. */
123
+ /** Which safety axes tripped — for `irreversible_ask`, derived from the D-2 {@link SafetyAxis} (+ the shell
124
+ * seat). `shell` marks a tighten by the engine's shell seat (read-boundary judge and/or `shellGate` doctrine,
125
+ * NO explicit per-tool egress/irreversible mark). Empty `{}` for a plain budgetable `human` ask. */
127
126
  axes: {
128
127
  egress?: boolean;
129
128
  irreversible?: boolean;
@@ -131,12 +130,12 @@ export interface RiskDescriptor {
131
130
  };
132
131
  /** The tool whose call is gated (mirrors the gate's `toolName`). */
133
132
  toolName: string;
134
- /** Ruled 2026-08-05 (forensics): the run's resolved shellGate doctrine, present ONLY when this ask
135
- * was minted through the shell gate (coarse `always`, or a `classify` probe that declined to
136
- * auto-allow). Absent for per-tool safety-axis / plain policy asks. Answers "which doctrine was
137
- * live when this gate fired" from the persisted row alone three artifact layers could not
138
- * answer that in the field. */
139
- shellGateDoctrine?: "classify" | "always";
133
+ /** The run's resolved shellGate doctrine, present ONLY when this ask was minted through the engine's shell
134
+ * seat (coarse `always`; a `classify` probe that declined to auto-allow; the read-boundary judge asking under
135
+ * `off` the boundary is judged under every doctrine, so the vocabulary is `TaskSpec.shellGate`'s whole).
136
+ * Absent for per-tool safety-axis / plain policy asks. Answers "which doctrine was live when this gate
137
+ * fired" from the persisted row alone — three artifact layers could not answer that in the field. */
138
+ shellGateDoctrine?: ShellGateDoctrine;
140
139
  /** A REDACTED, length-capped ONE-LINE summary of the call (the command for a shell gate; a brief key-arg
141
140
  * digest otherwise) for the inbox preview. NEUTRALIZED via
142
141
  * {@link import("./permission-rule-model.js").renderUntrustedCommandText} + length-capped (it is
@@ -406,11 +405,11 @@ export declare function buildRiskDescriptor(input: {
406
405
  args: unknown;
407
406
  /** The D-2 {@link SafetyAxis} threaded to the mint (egress/irreversible), or `undefined` for a plain ask. */
408
407
  safety?: SafetyAxis;
409
- /** True ONLY when this is a `bash` call gated coarsely by `shellGate` (no explicit per-tool egress/irreversible
410
- * mark) — drives the `shell` axis + severity-3 coarse grade. */
408
+ /** True ONLY when this is a shell call whose tier is the engine's shell seat's (read-boundary judge and/or
409
+ * `shellGate` doctrine, no explicit per-tool egress/irreversible mark) — drives the `shell` axis + severity 3. */
411
410
  shellGated?: boolean;
412
411
  /** The resolved doctrine to persist when `shellGated` (see {@link RiskDescriptor.shellGateDoctrine}). */
413
- shellGateDoctrine?: "classify" | "always";
412
+ shellGateDoctrine?: ShellGateDoctrine;
414
413
  /** #144: the matched-but-outranked persisted rule to persist (see {@link RiskDescriptor.shadowedRule}). */
415
414
  shadowedRule?: string;
416
415
  /** backlog #239: the reversibility probe's cause for not clearing this call (see
@@ -444,14 +443,14 @@ export declare function buildRiskDescriptor(input: {
444
443
  export interface RealApprovalGateBit {
445
444
  /** The parked ask's ORIGIN word — a projection of the same closed set the card, the observer and the
446
445
  * `tool_end` frame carry ({@link import("./ask-origin.js").AskOrigin}), never a second vocabulary. It
447
- * is the ask's ROUTING word: a marked call reads `unresolvable` even when the org governs it, because
446
+ * is the ask's ROUTING word: a marked call reads `ancestor_marked` even when the org governs it, because
448
447
  * the ladder routes a marked call to the park before anything else. Rows minted before the projection
449
448
  * carried a three-word subset of this set (`org_rule` / `org_unavailable` / `policy`), so every stored
450
449
  * value is still a member. */
451
450
  origin: import("./ask-origin.js").AskOrigin;
452
451
  /** The ORG FACT the ask was minted under (`"rule"` = an org rule asked, `"unavailable"` = org adjudication
453
452
  * unreachable), kept beside the routing word because routing can outrank the word (a marked call reads
454
- * `unresolvable`) while the governance stays. The #120 governed mark and the resume belts read THIS
453
+ * `ancestor_marked`) while the governance stays. The #120 governed mark and the resume belts read THIS
455
454
  * member through {@link realApprovalOrgFact} (absent on rows minted before it: the org words imply it). */
456
455
  org?: "rule" | "unavailable";
457
456
  }
@@ -813,14 +812,10 @@ export type PendingAction = {
813
812
  * Written only when it is a member of the set; a pre-existing row reads absent (unreported).
814
813
  * Display metadata — the resume belts keep reading the gate's own bit. */
815
814
  origin?: import("./ask-origin.js").AskOrigin;
816
- /** #616 (additive, same precedent) — the PARK twin of `AskRequest.classifierUnavailable`: present ⇔ the
817
- * auto-mode classifier was consulted on the parked ask and could not run, with the verdict's own cause
818
- * word ({@link import("./auto-mode.js").AutoModeUnavailableCause}). Written only when the cause is a
819
- * member of the set; a pre-existing row reads absent. Display metadata (a durable card's "asked because
820
- * the classifier was unavailable" sentence) — the resume belts never read it. */
821
- classifierUnavailable?: {
822
- readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
823
- };
815
+ /** #688 C3 (additive, same precedent as `origin`) — the mechanism behind an `origin: "rule_store_unavailable"`
816
+ * (`store` / `call`, {@link import("./ask-origin.js").RULE_STORE_UNREADABLE_KINDS}). Written only when it is a
817
+ * member of the set; a pre-existing row reads absent. Display metadata for the durable card. */
818
+ ruleStoreUnreadable?: import("./ask-origin.js").RuleStoreUnreadable;
824
819
  /**
825
820
  * design/80 D-1 §2 (slice 1a.2): the server-minted **opaque** boundInputHash of {@link args} — a
826
821
  * SHA-256 (hex) via {@link import("./canonical-json.js").boundInputHashOf}, computed ONCE here at
@@ -1114,7 +1109,7 @@ export interface CheckpointState {
1114
1109
  rev: number;
1115
1110
  rules: SessionPermissionRules;
1116
1111
  }>;
1117
- shellGate?: "off" | "always" | "classify";
1112
+ shellGate?: ShellGateDoctrine;
1118
1113
  /** The chain's AUTO-MODE INTENT at suspend (data half, same law as `shellGate`): `true` when the
1119
1114
  * suspended leg was an auto-mode task — its own seat, the bit its live chain carried, or the bit
1120
1115
  * an earlier suspend of the same chain recorded (carried forward across a re-suspend). A resume
@@ -1926,12 +1921,9 @@ export interface CheckpointSummary {
1926
1921
  * value is a member of the closed set ({@link import("./ask-origin.js").isAskOrigin}) — a bad value
1927
1922
  * reads as absent, never as a word. OMITTED when the row carries none (a pre-existing row). */
1928
1923
  origin?: import("./ask-origin.js").AskOrigin;
1929
- /** The row's `PendingAction.tool_approval.classifierUnavailable` (#616), projected for the same one-call
1930
- * reason. Echoed ONLY when the stored cause is a member of the closed set
1931
- * ({@link import("./auto-mode.js").isAutoModeUnavailableCause}); OMITTED when the row carries none. */
1932
- classifierUnavailable?: {
1933
- readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
1934
- };
1924
+ /** The row's `PendingAction.tool_approval.ruleStoreUnreadable` (#688 C3), echoed ONLY when the stored word is a
1925
+ * member of the closed set ({@link import("./ask-origin.js").isRuleStoreUnreadable}); OMITTED otherwise. */
1926
+ ruleStoreUnreadable?: import("./ask-origin.js").RuleStoreUnreadable;
1935
1927
  /**
1936
1928
  * #438: PRESENCE ONLY — this pending `tool_approval` row's payload carries at least one DIRECTIONAL
1937
1929
  * format control (the {@link import("./tool-policy.js").AskRequest.hasBidiControls} character class;
@@ -2,8 +2,8 @@ import { randomBytes, randomUUID } from "node:crypto";
2
2
  import { uuidv7 } from "../internal/harness.js";
3
3
  import { PROBE_CAUSE_PATH_MAX, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
4
4
  import { carriesBidiControls } from "./tool-policy.js";
5
- import { isAutoModeUnavailableCause, readDenialLimitFallback } from "./auto-mode.js";
6
- import { isAskOrigin } from "./ask-origin.js";
5
+ import { readDenialLimitFallback } from "./auto-mode.js";
6
+ import { isAskOrigin, isRuleStoreUnreadable } from "./ask-origin.js";
7
7
  import { checkpointExecutionRecorded, executionRecordDisposition, ExecutionOutcomeConflictError, } from "./checkpoint-execution-record.js";
8
8
  import { renderUntrustedCommandText, stripFormatCharacters } from "./permission-rule-model.js";
9
9
  import { redactSecrets } from "./untrusted-egress.js";
@@ -277,8 +277,11 @@ export function summarizeCheckpoint(cp) {
277
277
  const fb = readDenialLimitFallback(tool?.denialLimitFallback);
278
278
  return fb !== undefined ? { denialLimitFallback: fb } : {};
279
279
  })(),
280
- ...(tool !== undefined && isAskOrigin(tool.origin) ? { origin: tool.origin } : {}),
281
- ...(tool !== undefined && isAutoModeUnavailableCause(tool.classifierUnavailable?.cause) ? { classifierUnavailable: { cause: tool.classifierUnavailable.cause } } : {}),
280
+ ...(() => {
281
+ const origin = tool?.origin;
282
+ const ruleStoreUnreadable = tool?.ruleStoreUnreadable;
283
+ return { ...(isAskOrigin(origin) ? { origin } : {}), ...(isRuleStoreUnreadable(ruleStoreUnreadable) ? { ruleStoreUnreadable } : {}) };
284
+ })(),
282
285
  ...(hasBidi ? { hasBidiControls: true } : {}),
283
286
  };
284
287
  }
@@ -430,7 +430,9 @@ export interface EngineNotice {
430
430
  * consolidation run, so the CLEAN arm's authoring prompts were composed WITHOUT the marked
431
431
  * rows: anything this run's clean products say was written without them, and their silence on a
432
432
  * topic therefore does not mean the library is silent on it (the withheld rows still stand and
433
- * are still readable). TWO counts, deliberately separate sentences and separate keys
433
+ * are still readable). OPERATOR audience (#692 C-a): the run happens outside any session, so
434
+ * there is no `sessionId` to route a user row to — the receipt's `withheldFromCleanArm` /
435
+ * `withheldNeverSent` are the reader-facing half. TWO counts, deliberately separate sentences and separate keys —
434
436
  * `withheld` rows were kept out of the clean arm's prompts, and `neverSent` of those were not
435
437
  * sent to the consolidation model AT ALL (a lone marked row has no group to fold, so it is
436
438
  * withheld from every call). "Withheld from the clean arm" and "never left this machine" are
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Say a knob-resolution fact once per process.
3
+ *
4
+ * Swallow-guarded, the same posture as every other advisory sink here (`deliverEngineNotice`,
5
+ * `emitBrainStatus`, `emitBrainTelemetry`): an announcement must never become a failure. The write is
6
+ * not to a channel this process owns — a host is free to replace `console.warn` with its own logger,
7
+ * and a `Console` built with `ignoreErrors:false` throws on a full or closed stream. These resolvers
8
+ * sit on hot paths that have already done irreversible work (the shell's output clip runs AFTER the
9
+ * command ran), so an unguarded write could destroy a completed command's output and invite the caller
10
+ * to run it again. The line is ledgered BEFORE delivery so a throwing sink cannot turn one fact into a
11
+ * warning per call either.
12
+ */
13
+ export declare function announceEnvKnob(line: string): void;
14
+ /** Test seam: the announcement de-dupes per process, so a test asserting the line must be able to clear
15
+ * the ledger. Never called by production code. */
16
+ export declare function __resetEnvKnobAnnouncements(): void;
@@ -0,0 +1,14 @@
1
+ const announcedEnvKnobLines = new Set();
2
+ export function announceEnvKnob(line) {
3
+ if (announcedEnvKnobLines.has(line))
4
+ return;
5
+ announcedEnvKnobLines.add(line);
6
+ try {
7
+ console.warn(line);
8
+ }
9
+ catch {
10
+ }
11
+ }
12
+ export function __resetEnvKnobAnnouncements() {
13
+ announcedEnvKnobLines.clear();
14
+ }
@@ -36,6 +36,8 @@ export async function runGateFold(pass) {
36
36
  const refusedBy = ledger.fromDecision(pass.decision);
37
37
  if (refusedBy !== undefined)
38
38
  pass.deniedBy = refusedBy;
39
+ if (refusedBy !== undefined)
40
+ pass.denyCause = ledger.causeOf(pass.decision);
39
41
  }
40
42
  if ((pass.decision.action === "allow" || pass.decision.action === "ask") && pass.decision.updatedInput !== undefined) {
41
43
  pass.policyRewrite = pass.decision.updatedInput;