@sema-agent/core 7.6.3 → 7.8.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 (164) hide show
  1. package/CHANGELOG.md +70 -10
  2. package/dist/agents/agent-transcript-tool.js +2 -2
  3. package/dist/agents/list-agents-tool.js +2 -3
  4. package/dist/agents/observer.js +2 -2
  5. package/dist/agents/peer-admission.d.ts +1 -1
  6. package/dist/agents/send-message-tool.js +2 -1
  7. package/dist/agents/subagent-steps.js +5 -4
  8. package/dist/agents/subagent.js +2 -3
  9. package/dist/brain/anthropic.js +8 -2
  10. package/dist/brain/open-responses.js +5 -3
  11. package/dist/brain/openai.js +31 -8
  12. package/dist/brain/reasoning.d.ts +32 -0
  13. package/dist/brain/reasoning.js +18 -0
  14. package/dist/core/ask-question.js +2 -2
  15. package/dist/core/auto-mode-defaults.d.ts +16 -0
  16. package/dist/core/auto-mode-defaults.js +1 -0
  17. package/dist/core/auto-mode.d.ts +19 -0
  18. package/dist/core/auto-mode.js +74 -56
  19. package/dist/core/checkpoint-execution-record.d.ts +110 -0
  20. package/dist/core/checkpoint-execution-record.js +49 -0
  21. package/dist/core/checkpoint-store.d.ts +114 -2
  22. package/dist/core/checkpoint-store.js +35 -2
  23. package/dist/core/context-edit.d.ts +0 -22
  24. package/dist/core/context-edit.js +2 -11
  25. package/dist/core/engine-notice.d.ts +17 -0
  26. package/dist/core/fs-write-gate-policy.d.ts +7 -1
  27. package/dist/core/fs-write-gate-policy.js +7 -7
  28. package/dist/core/gate-fold.js +2 -2
  29. package/dist/core/gate-lanes.js +4 -3
  30. package/dist/core/governance-codes.d.ts +1 -1
  31. package/dist/core/governance-codes.js +8 -0
  32. package/dist/core/hooks.d.ts +8 -2
  33. package/dist/core/hooks.js +1 -1
  34. package/dist/core/lsp.js +2 -2
  35. package/dist/core/mcp-server-spec.d.ts +17 -0
  36. package/dist/core/mcp.js +3 -1
  37. package/dist/core/memory-engine/tools.js +4 -12
  38. package/dist/core/permission-rules.d.ts +32 -2
  39. package/dist/core/permission-rules.js +43 -37
  40. package/dist/core/present-plan-tool.js +3 -4
  41. package/dist/core/runner/active-skill-scope.js +7 -4
  42. package/dist/core/runner/clock-and-limits.d.ts +117 -0
  43. package/dist/core/runner/clock-and-limits.js +118 -0
  44. package/dist/core/runner/compaction-call-options.d.ts +1 -1
  45. package/dist/core/runner/contracts.d.ts +37 -3
  46. package/dist/core/runner/decide-continuation.d.ts +98 -0
  47. package/dist/core/runner/decide-continuation.js +133 -0
  48. package/dist/core/runner/execution-record.d.ts +26 -0
  49. package/dist/core/runner/execution-record.js +19 -0
  50. package/dist/core/runner/git-leg-delivery.d.ts +28 -0
  51. package/dist/core/runner/git-leg-delivery.js +94 -0
  52. package/dist/core/runner/initial-run-state.d.ts +14 -0
  53. package/dist/core/runner/initial-run-state.js +11 -0
  54. package/dist/core/runner/prepare-ask-lane.d.ts +1 -1
  55. package/dist/core/runner/prepare-caps-and-workflow.d.ts +5 -4
  56. package/dist/core/runner/prepare-caps-and-workflow.js +33 -11
  57. package/dist/core/runner/prepare-defer-classify.d.ts +5 -26
  58. package/dist/core/runner/prepare-defer-classify.js +10 -10
  59. package/dist/core/runner/prepare-delegation-surface.d.ts +5 -4
  60. package/dist/core/runner/prepare-delegation-surface.js +15 -17
  61. package/dist/core/runner/prepare-gate-stations.d.ts +4 -1
  62. package/dist/core/runner/prepare-gate-stations.js +3 -1
  63. package/dist/core/runner/prepare-hands-readface.d.ts +3 -2
  64. package/dist/core/runner/prepare-hands-readface.js +3 -2
  65. package/dist/core/runner/prepare-inherited-gate.d.ts +1 -1
  66. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  67. package/dist/core/runner/prepare-lsp.d.ts +4 -3
  68. package/dist/core/runner/prepare-lsp.js +3 -2
  69. package/dist/core/runner/prepare-memory-engine-session.d.ts +4 -4
  70. package/dist/core/runner/prepare-memory-engine-session.js +4 -2
  71. package/dist/core/runner/prepare-offload-wrappers.js +7 -5
  72. package/dist/core/runner/prepare-park-ask.d.ts +4 -0
  73. package/dist/core/runner/prepare-park-ask.js +4 -1
  74. package/dist/core/runner/prepare-project-context.d.ts +5 -23
  75. package/dist/core/runner/prepare-project-context.js +7 -5
  76. package/dist/core/runner/prepare-prompt-assembly.d.ts +1 -1
  77. package/dist/core/runner/prepare-protocol-tools.d.ts +5 -1
  78. package/dist/core/runner/prepare-protocol-tools.js +38 -21
  79. package/dist/core/runner/prepare-question-face.d.ts +4 -3
  80. package/dist/core/runner/prepare-question-face.js +5 -3
  81. package/dist/core/runner/prepare-safety-scan.js +42 -2
  82. package/dist/core/runner/prepare-task.js +37 -19
  83. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -2
  84. package/dist/core/runner/prepare-tool-disclosure-mount.js +3 -1
  85. package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -0
  86. package/dist/core/runner/prepare-wiring-manifest.js +3 -8
  87. package/dist/core/runner/run-harness-handlers.d.ts +46 -0
  88. package/dist/core/runner/run-harness-handlers.js +345 -0
  89. package/dist/core/runner/run-turn-boundary.d.ts +69 -0
  90. package/dist/core/runner/run-turn-boundary.js +694 -0
  91. package/dist/core/runner/runtask.d.ts +0 -68
  92. package/dist/core/runner/runtask.js +66 -1486
  93. package/dist/core/runner/session-rule-policy.js +9 -4
  94. package/dist/core/runner/steer-admission.d.ts +17 -0
  95. package/dist/core/runner/steer-admission.js +17 -0
  96. package/dist/core/runner/synthetic-tools.js +5 -8
  97. package/dist/core/runner/tool-disclosure.d.ts +4 -0
  98. package/dist/core/runner/tool-disclosure.js +9 -5
  99. package/dist/core/runner/tool-end-body.d.ts +71 -0
  100. package/dist/core/runner/tool-end-body.js +75 -0
  101. package/dist/core/runner/tool-face-overlay.d.ts +40 -0
  102. package/dist/core/runner/tool-face-overlay.js +125 -0
  103. package/dist/core/runner/tool-output-projection.d.ts +10 -0
  104. package/dist/core/runner/tool-output-projection.js +33 -10
  105. package/dist/core/sensitive-path-policy.d.ts +3 -0
  106. package/dist/core/sensitive-path-policy.js +6 -5
  107. package/dist/core/shared-memory/tools.js +3 -6
  108. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -0
  109. package/dist/core/store-contracts/checkpoint-store-contract.js +108 -0
  110. package/dist/core/task-event.d.ts +10 -0
  111. package/dist/core/task-registry.js +4 -5
  112. package/dist/core/task-spec.d.ts +8 -0
  113. package/dist/core/tool-catalog-entries.d.ts +48 -0
  114. package/dist/core/tool-catalog-entries.js +174 -0
  115. package/dist/core/tool-conformance.d.ts +22 -0
  116. package/dist/core/tool-conformance.js +37 -0
  117. package/dist/core/tool-face.d.ts +171 -0
  118. package/dist/core/tool-face.js +25 -0
  119. package/dist/core/tool-policy.d.ts +24 -0
  120. package/dist/core/tool-policy.js +4 -2
  121. package/dist/core/tool-registry.d.ts +114 -0
  122. package/dist/core/tool-registry.js +114 -0
  123. package/dist/core/tool-result-budget.d.ts +0 -3
  124. package/dist/core/tool-result-budget.js +4 -3
  125. package/dist/core/tool-result-store.d.ts +0 -18
  126. package/dist/core/tool-result-store.js +2 -18
  127. package/dist/core/tool-roster.d.ts +450 -0
  128. package/dist/core/tool-roster.js +497 -0
  129. package/dist/core/tool-spec.d.ts +54 -3
  130. package/dist/core/tools.js +7 -0
  131. package/dist/core/trace.d.ts +24 -0
  132. package/dist/core/wiring-manifest.d.ts +13 -0
  133. package/dist/core/wiring-manifest.js +2 -1
  134. package/dist/core/write-protect.d.ts +2 -1
  135. package/dist/core/write-protect.js +5 -4
  136. package/dist/engine/compaction/utils.d.ts +6 -0
  137. package/dist/engine/compaction/utils.js +19 -19
  138. package/dist/engine/loop/types.d.ts +6 -0
  139. package/dist/index.d.ts +20 -8
  140. package/dist/index.js +19 -7
  141. package/dist/orchestration/goal.js +2 -1
  142. package/dist/orchestration/run-spec.js +3 -5
  143. package/dist/orchestration/run-workflow-tool.js +2 -2
  144. package/dist/prompt-assembly/event-registry.js +3 -3
  145. package/dist/prompt-assembly/tool-catalog.d.ts +2 -2
  146. package/dist/stores/file/checkpoint-store.d.ts +7 -0
  147. package/dist/stores/file/checkpoint-store.js +20 -1
  148. package/dist/tools/fs/fs-bash.d.ts +1 -1
  149. package/dist/tools/fs/fs-bash.js +7 -11
  150. package/dist/tools/fs/fs-read.js +2 -2
  151. package/dist/tools/fs/fs-search-tools.d.ts +0 -7
  152. package/dist/tools/fs/fs-search-tools.js +3 -16
  153. package/dist/tools/fs/fs-write.js +4 -6
  154. package/dist/tools/fs/repo-map.js +2 -2
  155. package/dist/tools/fs/safety.d.ts +2 -1
  156. package/dist/tools/fs/safety.js +6 -5
  157. package/dist/tools/monitor.js +2 -2
  158. package/dist/tools/scheduler-tools.js +5 -8
  159. package/dist/tools/task-list.js +5 -8
  160. package/dist/tools/todo.js +2 -2
  161. package/dist/tools/web.js +3 -6
  162. package/dist/tools/worktree.js +3 -4
  163. package/package.json +4 -1
  164. package/test/export-surface.snapshot.json +183 -7
@@ -1,4 +1,5 @@
1
- import { canonicalizeTarget, fileArgPath } from "../../tools/fs/safety.js";
1
+ import { canonicalizeTarget } from "../../tools/fs/safety.js";
2
+ import { pathTargetValue } from "../tool-registry.js";
2
3
  import { PATH_WRITE_TOOLS, isWithin } from "./session-rule-policy.js";
3
4
  import { parseSkillToolEntry, skillSpecifierRejection } from "../skill-tool-specifier.js";
4
5
  export class ActiveSkillScope {
@@ -74,7 +75,9 @@ export function createActiveSkillScopePolicy(opts) {
74
75
  }
75
76
  }
76
77
  const pathConstrainingActive = manifests.some((m) => m.allowPaths && m.allowPaths.length > 0);
77
- if (pathConstrainingActive && !PATH_WRITE_TOOLS.has(toolName)) {
78
+ const declaredTarget = req.face?.pathTarget;
79
+ const skillScopeEligible = declaredTarget !== undefined && declaredTarget.skillScopeEligible === true;
80
+ if (pathConstrainingActive && !skillScopeEligible) {
78
81
  const eff = toolEffects?.get(toolName) ?? "write";
79
82
  if (eff !== "read") {
80
83
  const ids = manifests.filter((m) => m.allowPaths?.length).map((m) => m.lineageId).join(", ");
@@ -85,10 +88,10 @@ export function createActiveSkillScopePolicy(opts) {
85
88
  };
86
89
  }
87
90
  }
88
- if (PATH_WRITE_TOOLS.has(toolName)) {
91
+ if (skillScopeEligible) {
89
92
  const constraining = manifests.filter((m) => m.allowPaths && m.allowPaths.length > 0);
90
93
  if (constraining.length > 0) {
91
- const path = fileArgPath(req.args);
94
+ const path = pathTargetValue(req.args, declaredTarget);
92
95
  if (typeof path !== "string" || path.length === 0) {
93
96
  return {
94
97
  action: "deny",
@@ -0,0 +1,117 @@
1
+ /**
2
+ * The run loop's CLOCK and LIMIT machinery — the pieces that turn a budget, a deadline or a ceiling into
3
+ * something a lane can act on, shared by the turn boundary, the run body and the end-of-task lane:
4
+ * · the limit-approach frames (budget-measured, advisory) and the turn-cap read;
5
+ * · the governance-read budget against the environment's suspend deadline, the chunked absolute-deadline
6
+ * race that enforces it, and the never-abandoned ledger charge with its slow-settle disclosure;
7
+ * · the typed terminal for an external stop cause that could not be suspended durably;
8
+ * · the once-per-task disclosure of an auto-compaction trigger that cannot be evaluated (a WeakSet keyed
9
+ * on the task's `Prepared`, so the latch dies with the task and never touches the contract surface);
10
+ * · the hard walltime backstop timer, chunked past the largest delay a timer honours.
11
+ *
12
+ * Every clock read is `Date.now()` or a timer; every host read is through the arguments a caller hands in.
13
+ * Layer-1 machinery rather than a driven lane — initial-run-state.ts says why no run-loop machinery module
14
+ * wears a family prefix.
15
+ */
16
+ import { type AgentHarness } from "../../internal/harness.js";
17
+ import type { PlatformLimitReason } from "../checkpoint-store.js";
18
+ import type { TaskSpec } from "../types.js";
19
+ import type { Prepared } from "./contracts.js";
20
+ /**
21
+ * design/164 — the LIMIT-APPROACH frames, the successor to the retired deadline-nudge family. Two facts
22
+ * make them a different mechanism, not a rename:
23
+ * - they are measured in BUDGET (the tightest armed axis's fill ratio), not in wall-clock, so they need
24
+ * no throughput estimate and cannot mis-fire on a starved event loop;
25
+ * - they are ADVISORY. Neither frame stops anything; the run ends when a ceiling is actually crossed.
26
+ *
27
+ * Rendered by {@link limitApproachFrames}; registered in the prompt-assembly event registry under
28
+ * `limit_approach_converge` / `limit_approach_deliver`.
29
+ */
30
+ export declare const LIMIT_APPROACH_DEFAULT_THRESHOLDS: readonly [number, number];
31
+ /** The output-token figure the pre-call worst-case assumes when the model declares no cap of its own.
32
+ * Shared by the $ and token arms so the two gates cannot disagree about the same hypothetical call. */
33
+ export declare const DEFAULT_PRECALL_OUTPUT_TOKENS = 4096;
34
+ /** The two frame bodies, indexed by how many have already been sent (0 → converge, 1 → deliver now). */
35
+ export declare function limitApproachFrames(index: 0 | 1, axis: string, percent: number, mark: string | undefined): string;
36
+ /**
37
+ * Resolve the effective turn cap. `undefined` = no turn cap; `0` (explicit) = unbounded.
38
+ *
39
+ * design/164: this is now a pure read of what the caller wrote. The two clauses it used to carry —
40
+ * "a wall-clock bound was set, so drop the turn cap" and "neither was set, so apply the engine's
41
+ * safety net" — are both gone: the axes no longer interact (each binds on its own), and there is no
42
+ * engine-side net to apply. A task with no `maxTurns` has no turn ceiling.
43
+ */
44
+ export declare function resolveMaxTurns(limits?: {
45
+ maxTurns?: number;
46
+ }): number | undefined;
47
+ /**
48
+ * How long past the environment's suspend deadline the turn-boundary lane will keep waiting for the
49
+ * usage ledger's READ.
50
+ *
51
+ * The precedence ruling (usage_window > env_lifetime) means the ledger must be read BEFORE the env
52
+ * cause can be adopted — you cannot rank a cause you have not read. But that read is store I/O with no
53
+ * bound of its own, and past this instant every millisecond it spends is spent out of the margin that
54
+ * exists to mint the env checkpoint. A stalled ledger must not cost a deployment the checkpoint it
55
+ * could still have had, so the read runs against an ABSOLUTE deadline (`envLifetimeSuspendAt + this`)
56
+ * and a run that gets no answer by then stops for the cause it can still prove.
57
+ *
58
+ * 5s against the 60s suspend margin: two orders of magnitude above any healthy ledger read (in-memory
59
+ * and file stores answer in ms; a remote one in tens of ms), and it still leaves 55s — more than the
60
+ * margin was sized for — to pause the workspace, mint the checkpoint and commit it. Not a knob: it is a
61
+ * fraction of an engine constant, and a deployment that wants a wider band declares a shorter lifetime.
62
+ *
63
+ * ABSOLUTE, never a fresh per-boundary allowance: a boundary that finds the deadline one millisecond
64
+ * away must not hand a stalled ledger the whole margin, and a boundary that is already overdue must get
65
+ * LESS than the budget, not the same. {@link raceUntilDeadline} is what keeps that true for a deadline
66
+ * of any distance, without an unrepresentable timer.
67
+ */
68
+ export declare const ENV_DUE_GOVERNANCE_READ_BUDGET_MS = 5000;
69
+ export declare const GOVERNANCE_READ_STALLED: unique symbol;
70
+ /** Await a ledger charge, firing `onSlow` ONCE if it has not settled after
71
+ * {@link CHARGE_SETTLE_DISCLOSE_MS}. The charge itself is always awaited to completion. */
72
+ export declare function awaitChargeWithSlowDisclosure<T>(charge: Promise<T>, onSlow: () => void, discloseAfterMs?: number): Promise<T>;
73
+ /**
74
+ * Await `p` until the absolute epoch `deadline`, then give up with {@link GOVERNANCE_READ_STALLED}.
75
+ *
76
+ * Chunked, and deliberately so: the deadline is derived from a caller-declared environment lifetime, so
77
+ * the distance to it can exceed {@link MAX_TIMER_DELAY_MS} — a single timer armed for that distance
78
+ * would fire at once and abandon a perfectly healthy read. Each chunk re-reads the wall clock, so the
79
+ * decision is always made against the deadline itself rather than against an allowance computed once.
80
+ *
81
+ * Exported for its own unit pin (the same posture as the brain-call guardrail primitive): the
82
+ * behaviors below are clock behaviors, and only a virtual clock can assert them without spending the
83
+ * wall-clock time they describe. Not re-exported from the package index.
84
+ *
85
+ * An already-overdue deadline still gets ONE zero-delay pass: a promise that is settled (or settles in a
86
+ * microtask, which is every in-process store) must be allowed to win, because giving up on an answer we
87
+ * already hold would be a fabricated stall. The second pass is what makes the loop terminate.
88
+ */
89
+ export declare function raceUntilDeadline<T>(p: Promise<T>, deadline: number): Promise<T | typeof GOVERNANCE_READ_STALLED>;
90
+ /**
91
+ * design/164 件四/件五 — the TYPED terminal for an external stop cause that could NOT be suspended durably.
92
+ * Carried on `rs.limits.platformTerminal` and adopted as the run's `threw`, so the caller reads the cause
93
+ * on `TaskResult.errorCode` (`errorCodeOf` lifts `.code`) instead of the anonymous error an abort produces.
94
+ * `retryAfterMs` rides the error object because a wait hint has no `TaskResult` seat — a host reads it from
95
+ * the `RunnerDeps.onError` sink this is also delivered to.
96
+ */
97
+ export declare function platformLimitTerminal(reason: PlatformLimitReason, retryAfterMs: number | undefined, moment?: "entry" | "turn_boundary" | "resume"): Error & {
98
+ code: string;
99
+ retryAfterMs?: number;
100
+ };
101
+ export declare function discloseUnevaluableWindow(u: {
102
+ estTokens: number;
103
+ windowField: string;
104
+ windowValue?: number;
105
+ modelId?: string;
106
+ }, prepared: Prepared, tracer: TaskSpec["tracer"], taskId: string, onError: ((err: unknown, context: {
107
+ phase: "config";
108
+ sessionId: string;
109
+ }) => void) | undefined): void;
110
+ /** 1.296 件2a — report the hard-abort timer's lateness on `task.end` only above this bar: sub-5s
111
+ * jitter is ordinary scheduling noise (GC pauses, sync work) and would only add trace lint. */
112
+ export declare const TIMER_LATENESS_REPORT_MS = 5000;
113
+ export declare function startTimeout(harness: AgentHarness, abortController: AbortController, remainingMs?: number, softSuspendable?: boolean): {
114
+ fired: boolean;
115
+ latenessMs?: number;
116
+ clear: () => void;
117
+ };
@@ -0,0 +1,118 @@
1
+ import {} from "../../internal/harness.js";
2
+ import { openSystemReminder } from "../reminder-mint.js";
3
+ import { emitTrace } from "../trace.js";
4
+ export const LIMIT_APPROACH_DEFAULT_THRESHOLDS = [0.8, 0.95];
5
+ export const DEFAULT_PRECALL_OUTPUT_TOKENS = 4096;
6
+ export function limitApproachFrames(index, axis, percent, mark) {
7
+ return index === 0
8
+ ? `${openSystemReminder(mark)}[limit approach] This task has used about ${percent}% of its ${axis}. ` +
9
+ `Start converging: finish the step you are on, drop work that is not required for the deliverable, and ` +
10
+ `avoid starting anything you cannot complete within what is left.</system-reminder>`
11
+ : `${openSystemReminder(mark)}[limit approach] About ${percent}% of the ${axis} is gone. Deliver now: ` +
12
+ `stop exploring, write up what you have — including what is incomplete and what you would do next — ` +
13
+ `and end the task. Work that is not reported before the allowance runs out is lost.</system-reminder>`;
14
+ }
15
+ export function resolveMaxTurns(limits) {
16
+ return limits?.maxTurns;
17
+ }
18
+ export const ENV_DUE_GOVERNANCE_READ_BUDGET_MS = 5_000;
19
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
20
+ const DEADLINE_TICK = Symbol("deadline-tick");
21
+ export const GOVERNANCE_READ_STALLED = Symbol("governance-read-stalled");
22
+ const CHARGE_SETTLE_DISCLOSE_MS = 10_000;
23
+ export async function awaitChargeWithSlowDisclosure(charge, onSlow, discloseAfterMs = CHARGE_SETTLE_DISCLOSE_MS) {
24
+ let timer = setTimeout(() => {
25
+ timer = undefined;
26
+ try {
27
+ onSlow();
28
+ }
29
+ catch {
30
+ }
31
+ }, discloseAfterMs);
32
+ try {
33
+ return await charge;
34
+ }
35
+ finally {
36
+ if (timer !== undefined)
37
+ clearTimeout(timer);
38
+ }
39
+ }
40
+ export async function raceUntilDeadline(p, deadline) {
41
+ for (let firstPass = true;; firstPass = false) {
42
+ const remaining = deadline - Date.now();
43
+ if (remaining <= 0 && !firstPass)
44
+ return GOVERNANCE_READ_STALLED;
45
+ let timer;
46
+ const tick = new Promise((res) => {
47
+ timer = setTimeout(() => res(DEADLINE_TICK), Math.max(0, Math.min(remaining, MAX_TIMER_DELAY_MS)));
48
+ });
49
+ const out = await Promise.race([p, tick]).finally(() => clearTimeout(timer));
50
+ if (out !== DEADLINE_TICK)
51
+ return out;
52
+ }
53
+ }
54
+ export function platformLimitTerminal(reason, retryAfterMs, moment = "turn_boundary") {
55
+ const message = moment === "entry"
56
+ ? `a deployment usage window is exhausted (RunnerDeps.usageWindows), so the task was refused before its first model call — nothing ran and nothing was spent. There is no checkpoint to suspend into at this point, so the caller re-submits after the window frees, in ${String(retryAfterMs ?? 0)}ms.`
57
+ : moment === "resume"
58
+ ? `a deployment usage window is exhausted (RunnerDeps.usageWindows), so the resumed leg was stopped BEFORE its first model call — nothing was delivered or executed on this leg. The window frees in ${String(retryAfterMs ?? 0)}ms.`
59
+ : reason === "env_lifetime"
60
+ ? "the execution environment's declared lifetime is expiring and the run could not be suspended durably (not eligible, or the suspend was refused — see remoteEnvFailures / onError). It was stopped at a clean turn boundary instead of being killed mid-turn when the platform reclaims the environment."
61
+ : `a deployment usage window is exhausted (RunnerDeps.usageWindows) and the run could not be suspended durably, so it was stopped at a clean turn boundary. The window frees in ${String(retryAfterMs ?? 0)}ms.`;
62
+ const e = new Error(message);
63
+ e.code = reason === "env_lifetime" ? "env.lifetime_expired" : "usage.window_exhausted";
64
+ if (retryAfterMs !== undefined)
65
+ e.retryAfterMs = retryAfterMs;
66
+ return e;
67
+ }
68
+ const disclosedUnevaluableWindow = new WeakSet();
69
+ export function discloseUnevaluableWindow(u, prepared, tracer, taskId, onError) {
70
+ if (disclosedUnevaluableWindow.has(prepared))
71
+ return;
72
+ disclosedUnevaluableWindow.add(prepared);
73
+ emitTrace(tracer, () => ({ kind: "compaction.unevaluable", version: 1, taskId, estTokens: u.estTokens, ts: Date.now() }));
74
+ const modelClause = u.modelId !== undefined ? ` "${u.modelId}"` : "";
75
+ const cause = u.windowValue !== undefined
76
+ ? `its \`${u.windowField}\` is ${u.windowValue}, which is not a positive number of tokens — that field takes precedence over the ` +
77
+ `others, so fixing or removing it is what restores the threshold`
78
+ : `it declares no context window at all (\`autoCompactTokens\` / \`contextTokens\` / \`contextWindow\` are all absent) — declare one to restore the threshold`;
79
+ try {
80
+ onError?.(new Error(`auto-compaction cannot run for model${modelClause}: ${cause}. Until then there is no threshold to compare against, so the ` +
81
+ `context will grow until the provider refuses the request. The conversation is already about ${u.estTokens} estimated tokens.`), { phase: "config", sessionId: prepared.sessionId });
82
+ }
83
+ catch {
84
+ }
85
+ }
86
+ const WALLTIME_SUSPEND_GRACE_SEC = 120;
87
+ export const TIMER_LATENESS_REPORT_MS = 5_000;
88
+ export function startTimeout(harness, abortController, remainingMs, softSuspendable = false) {
89
+ const state = { fired: false, clear: () => { } };
90
+ if (remainingMs !== undefined) {
91
+ const hardMs = Math.max(0, remainingMs + (softSuspendable ? WALLTIME_SUSPEND_GRACE_SEC * 1000 : 0));
92
+ const scheduledAtMs = Date.now() + hardMs;
93
+ let leftMs = hardMs;
94
+ let timer;
95
+ const armChunk = () => {
96
+ const chunk = Math.max(0, Math.min(leftMs, MAX_TIMER_DELAY_MS));
97
+ timer = setTimeout(() => {
98
+ leftMs -= chunk;
99
+ if (leftMs > 0) {
100
+ armChunk();
101
+ return;
102
+ }
103
+ fireHardAbort();
104
+ }, chunk);
105
+ };
106
+ const fireHardAbort = () => {
107
+ if (state.fired)
108
+ return;
109
+ state.fired = true;
110
+ state.latenessMs = Math.max(0, Date.now() - scheduledAtMs);
111
+ abortController.abort();
112
+ void harness.abort();
113
+ };
114
+ armChunk();
115
+ state.clear = () => clearTimeout(timer);
116
+ }
117
+ return state;
118
+ }
@@ -15,7 +15,7 @@ import type { Prepared } from "./contracts.js";
15
15
  * end-of-task lane's deliberate ABSENCE of `signal`) is a legitimate per-lane difference and stays at
16
16
  * the call site, where its reason is written down.
17
17
  *
18
- * Every export is a free function: the turn-boundary lane assembles its call inside `makeTurnBoundary`
18
+ * Every export is a free function: the turn-boundary lane assembles its call inside `createTurnBoundary`
19
19
  * (a module-level function with no `this`), so a method would not be reachable from all three sites.
20
20
  * Nothing here reads or writes runner state — pure assembly over `spec` + `Prepared`.
21
21
  */
@@ -27,6 +27,7 @@ import type { AutoModeArmingRecipe } from "../auto-mode-arming.js";
27
27
  import type { AutoModeDecider, AutoModeDenialTracker } from "../auto-mode.js";
28
28
  import type { CacheBreakDetector, ToolFingerprintInput } from "../cache-break-detector.js";
29
29
  import type { ActorAssertion, Checkpoint, CheckpointState, CheckpointStore, CheckpointToken, PendingSteerEntry, PlatformLimitReason, ReopenReason, ResourceLedger, ResourceLimitReason, ResumeOutcome, SerializedCheckpointState } from "../checkpoint-store.js";
30
+ import type { ExecutionOutcomeRecordWord } from "../checkpoint-execution-record.js";
30
31
  import type { ClearedProjectionLedger, ContextEditMachine, OccurrenceIndex } from "../context-edit.js";
31
32
  import type { HookInvocationIdentity, Hooks, OrgGateVerdict, PermissionDeniedPayload, PostToolBatchCall, PostToolBatchResult, ToolGateInput } from "../hooks.js";
32
33
  import type { MaterializedMcp } from "../mcp.js";
@@ -717,7 +718,10 @@ export interface Prepared {
717
718
  * the human already adjudicated it). Same wrapping (offload + ctx) the harness runs with.
718
719
  *
719
720
  * ONE array, mutated in place; identity is the contract (every closure that resolves a name at run time
720
- * reads this exact array). WRITER TABLE (design/238 R-3) — the only stations that write it, in prepare order:
721
+ * reads this exact array). Since design/388 the array is PRIVATE to the `RosterBuilder` (tool-roster.ts)
722
+ * every station below writes through its verbs (`mount` / `mountAll` / `unmountWhere` / `replaceAt` /
723
+ * `replaceDomain`) and this field is the builder's read face. WRITER TABLE (design/238 R-3) — the only
724
+ * stations that write it, in prepare order:
721
725
  * · caps-and-workflow — MINTS it (`spec.tools` through the ctx wrap + the large-result wrapper), then pushes
722
726
  * ReportBlocked (enableBlockedReport ≠ false), ReportFindings (no caller tool of that name/alias),
723
727
  * ExitPlanMode + EnterPlanMode (enablePlanMode ∧ the plan-review face), Workflow (self-orchestration active
@@ -739,7 +743,28 @@ export interface Prepared {
739
743
  * memory session or a recorder channel); the refresh seam re-runs the same pass through `contentOriginWrapRef`
740
744
  * after a re-splice (identity-tracked, so a wrapper is never wrapped again).
741
745
  * tool-disclosure and listings only READ it (`harnessTools` is a derived list; placeholders never enter here). */
742
- tools: AgentTool[];
746
+ tools: readonly AgentTool[];
747
+ /**
748
+ * design/388 §2.5 — this leg's minted TOOL ROSTER (the same object `wiringManifest.tools` carries):
749
+ * one row per mounted instance in wire order. Minted ONCE at the prompt-assembly station (the same
750
+ * pass that projects the prompt manifest's tool rows, so the two agree by construction); the park
751
+ * station reads the gated call's identity off it, the exemption probe hands it to the host.
752
+ */
753
+ toolRoster: import("../tool-roster.js").ToolRoster;
754
+ /**
755
+ * design/388 §2.5 — the run-time delta seat: every roster change after prepare (the RefreshMcpTools
756
+ * seam) re-mints the roster here and publishes a `ToolRosterDelta`. The run loop subscribes at the
757
+ * point it emits `wiring_manifest` and forwards each delta as a `tool_roster_delta` event; until it
758
+ * does, deltas are buffered in order.
759
+ */
760
+ toolRosterDeltas: import("../tool-roster.js").ToolRosterDeltaSeat;
761
+ /**
762
+ * design/388 L4 — the leg's `tool_end.structured` projector: the engine card set ∪ the caller rows' declared
763
+ * `renderHints.resultCards`; an undeclared `details.type` is dropped and announced ONCE per leg
764
+ * (`config.tool_card_undeclared`). The run loop's tool_end body projection reads it in place of the
765
+ * module-level `structuredFrom` (which has no announce seat).
766
+ */
767
+ structuredProjector: (result: unknown) => unknown;
743
768
  /** Name→effect map for every tool this task can call (design/44 §3). Used by the abort-path orphan
744
769
  * reconcile (design/64 §9) to make interrupted tool_results effect-aware (read/idempotent = safe to
745
770
  * repeat; write/unknown = outcome unknown). Unknown names default to `write` (conservative). */
@@ -1984,7 +2009,7 @@ export interface ParkAsk {
1984
2009
  * Grouped by lifecycle semantics per the B15 pre-recon partition. Function members start as inert
1985
2010
  * placeholders and are assigned at their ORIGINAL declaration sites (the original code could not
1986
2011
  * call them earlier either — TDZ proved it). Not on the npm face; this extraction is what enabled
1987
- * makeTurnBoundary / makeHarnessHandlers (三期, both in runtask.ts).
2012
+ * createTurnBoundary / createHarnessHandlers (三期, both in runtask.ts).
1988
2013
  */
1989
2014
  export interface RunState {
1990
2015
  telemetry: {
@@ -2196,6 +2221,15 @@ export interface ResumeRun {
2196
2221
  * replay the persisted winner (a system retry of the approved action), while a `tool_unavailable` reopen
2197
2222
  * lets a human re-decide with the tool present (a fresh decision is allowed — preserves P-7). */
2198
2223
  onEnvRestoreFailed?: (reason: ReopenReason) => Promise<void>;
2224
+ /** The row's execution-record verb, bound to THIS resume's token and scope by `resumeStream` (the
2225
+ * same closure form as {@link onEnvRestoreFailed}, but REQUIRED: the store contract requires the
2226
+ * verb, so there is no store this seat could be absent for). The pending-call resolver calls it
2227
+ * exactly once per resolution, right after it emits the resolved call's `tool_end` frame, with the
2228
+ * {@link import("../gate-outcome.js").GateOutcome} that frame carried — every disposition, the
2229
+ * executed allow included (see `CheckpointRow.executionOutcome`: absence means unknown, never
2230
+ * allowed). The word it answers is read through `EXECUTION_RECORD_LEAVES_ROW_UNRECORDED`; a
2231
+ * conflict throw propagates (an engine defect, not a store condition). */
2232
+ recordExecutionOutcome: (gate: import("../gate-outcome.js").GateOutcome) => Promise<ExecutionOutcomeRecordWord>;
2199
2233
  /** RB-471/FR-C1 — set in the run body right after `applyResumeDecision` completes: the negative-
2200
2234
  * decision twin of `pendingActionStarted`. A reject/deny consumes its gate BY BEING DELIVERED, and
2201
2235
  * this bit is the delivery fact — every throw-arm cause (prepare failure, `session_not_found`, a
@@ -0,0 +1,98 @@
1
+ /**
2
+ * The resumed leg's DECIDE vocabulary — everything the resume engine needs to read a decide outcome and
3
+ * carry it into the continuation, none of which is a step the driver calls with an Input:
4
+ * · was the decision NEGATIVE (a delivered refusal consumes its gate; no reopen compensation may fire);
5
+ * · is a replayed winner the SAME decision as the persisted one (design/80 D-1 reopen-by-reason guard,
6
+ * with the structural JSON equality it needs);
7
+ * · which call a CONTENT-ASK checkpoint parks on, and the one-shot answering face a decide's answer is
8
+ * bound to for the redeemed call;
9
+ * · the capture-time domain checks on a decide's TEXT payloads, and the label a rejected value gets;
10
+ * · the model-facing text for a deferred batch sibling, and the continuation prompt itself.
11
+ *
12
+ * Pure over its arguments (the answering face is the one closure, and it closes over its own arguments
13
+ * only). Run-loop machinery (layer 1), named without the `resume-` family prefix: that prefix marks a
14
+ * driven rung of the resume decision ladder, which this is not.
15
+ */
16
+ import { type OnQuestion, type QuestionAnswer } from "../ask-question.js";
17
+ import { type Checkpoint, type ResolvedOutcome } from "../checkpoint-store.js";
18
+ import type { ResumeRun } from "./contracts.js";
19
+ /**
20
+ * RB-471 ([2315]) — was this resume driven by a NEGATIVE human decision (plan_review/dry_run_review
21
+ * `reject`, policy_ask `deny`)? Such a decision consumes its gate by being DELIVERED: there is no owed
22
+ * action whose non-start could waste it, so the reopen compensation (both the throw-path and the
23
+ * settle-path arm) must never fire for it — reopening re-asks a gate the human already refused, and on
24
+ * the plan_review reject leg it minted a zombie gate beside the re-plan's NEW park (the session then
25
+ * reported the same decidePath locked no matter how many times the human decided).
26
+ */
27
+ export declare function resumeDecisionWasNegative(resume: ResumeRun): boolean;
28
+ /** Model-facing result for a batch sibling of a suspended call that v1 does not blind-run (§4.ter). */
29
+ export declare const DEFERRED_REISSUE: string;
30
+ /**
31
+ * Structural deep equality of two JSON-serializable values (order-independent over object keys). Used ONLY
32
+ * by the Runner's `resumeStream` design/80 D-1 reopen-by-reason guard to confirm an `env_failed` re-resume
33
+ * replays the EXACT persisted winner (its `updatedInput` payload included). This is a same-runtime
34
+ * value-vs-value comparison of two payloads that already round-trip JSON in the durable store — it is NOT
35
+ * the cross-runtime `boundInputHash` canonicalization (design/80 D-1 §2, out of scope for slice 1b), which
36
+ * the spec deliberately keeps server-minted-opaque to avoid false mismatches.
37
+ */
38
+ export declare function deepJsonEqual(a: unknown, b: unknown): boolean;
39
+ /** design/80 D-1 (reopen-by-reason): two resume winners are the SAME decision iff they bind the same call,
40
+ * the same allow/deny verdict, the same (deep-equal) `updatedInput` rewrite AND the same (deep-equal)
41
+ * content-ask `answer`. An `env_failed` re-resume must replay an identical winner; any divergence is a
42
+ * re-vote (rejected).
43
+ *
44
+ * ORDER MATTERS — `incoming` is the decide being replayed, `persisted` is the row's recorded winner. The
45
+ * `reason` dimension is compared ASYMMETRICALLY (see below), so the two arguments are not interchangeable. */
46
+ export declare function sameWinner(incoming: ResolvedOutcome, persisted: ResolvedOutcome): boolean;
47
+ /** The tool call a CONTENT-ASK checkpoint parks on — the pending `tool_approval` call of the reserved
48
+ * question tool — or `undefined` when this checkpoint parks on anything else (a side-effecting tool's
49
+ * approval, a resource slice, a review pause). The single predicate both the answer-binding guard and the
50
+ * answering-face injection key on, so "is this decide answering a question?" is asked exactly one way. */
51
+ export declare function pendingContentAskCallId(cp: Checkpoint): string | undefined;
52
+ /**
53
+ * Bind a decide's content-ask answer as the resumed leg's answering face (ruled 2026-08-04) — the same
54
+ * shape the documented live arm builds by hand (`{ ...taskConfig, onQuestion: async () => answer }`), so
55
+ * the redeemed call executes against the SAME tool and the SAME untrusted-answer fence rather than a
56
+ * second, parallel answer path.
57
+ *
58
+ * ONE-SHOT, bound to the redeemed call id: a decide answers the question the operator was shown, and
59
+ * nothing else. Should the resumed leg ask a NEW question, this face must not replay the previous answer
60
+ * to it (the questions differ; the fence's `selected ⊆ options` would strip the selections and the
61
+ * operator's note would be re-delivered out of context). It falls through to whatever answering face the
62
+ * resume already had, and — with none — refuses, which the tool reports to the model as "no answer was
63
+ * obtained", its honest headless outcome. In the durable topology this fallthrough is normally
64
+ * unreachable: a second question is adjudicated `ask` and parks for its own decide before executing.
65
+ */
66
+ export declare function answerFaceForRedeemedCall(answer: QuestionAnswer, redeemedCallId: string, base: OnQuestion | undefined,
67
+ /** Digest of the question batch the operator was shown, so redemption cannot be claimed by a
68
+ * DIFFERENT question that happens to arrive under the same call id. */
69
+ redeemedQuestionsHash: string): OnQuestion;
70
+ /**
71
+ * A label for a REJECTED deployment-supplied value, safe to put in an error message. Never coerces:
72
+ * `String(x)` on an object with no `toPrimitive` path (a null-prototype record, a throwing
73
+ * `Symbol.toPrimitive`) throws, which would replace this module's typed refusal with a raw TypeError —
74
+ * the malformed input escaping the very classification the refusal exists to give.
75
+ */
76
+ export declare function describeSuppliedValue(value: unknown): string;
77
+ /**
78
+ * Capture-time domain check for a decide outcome's TEXT payloads — the review lanes' `editedPlan` /
79
+ * `reason` and the approval lane's deny `reason`. All are persisted (the recorded winner's payload,
80
+ * compared when an `env_failed` reopen replays it) and all are fenced into the model-facing
81
+ * continuation reminder. The declared wire type is a string; a value outside it reads one way in this
82
+ * process and another after a JSON round-trip, and reaches the reminder as whatever its stringification
83
+ * happens to be. Worse, the pre-CAS tag-hygiene check and the fence BOTH reach the value through
84
+ * `String.prototype.replace`: an object carrying its own `replace` is therefore read twice and may
85
+ * answer differently each time — the check passing on text it never inspected. Refused pre-CAS so the
86
+ * row stays `pending` for a well-formed decide, exactly like the other capture-time domain refusals,
87
+ * and BEFORE the hygiene check so no guard ever consults a value outside the domain. The rejected field
88
+ * is named in the message only — `CheckpointError`'s structured `detail.field` is a closed set covering
89
+ * the approval lane's payload names, and widening it is a wire-surface change of its own.
90
+ */
91
+ export declare function assertOutcomeText(value: string | undefined, field: "editedPlan" | "reason"): void;
92
+ /**
93
+ * The continuation prompt for a resumed run (design/45 §B6). The vendored harness only exposes
94
+ * `prompt(text)` (it always appends a user message — there is no "continue from tool results" entry), so
95
+ * resume re-enters via a `<system-reminder>`-wrapped user message that tells the model the gated decision
96
+ * was applied. The "user message wart" is accepted (API-valid, and clearer than a silent continuation).
97
+ */
98
+ export declare function resumeContinuation(resume: ResumeRun, mark: string | undefined): string;
@@ -0,0 +1,133 @@
1
+ import { ASK_USER_QUESTION_TOOL_NAME, isLiveQuestionFace, markBoundOnlyQuestionFace } from "../ask-question.js";
2
+ import { boundInputHashOf } from "../canonical-json.js";
3
+ import { CheckpointError } from "../checkpoint-store.js";
4
+ import { formatHookFeedback } from "../hooks.js";
5
+ import { delimitUntrusted, REVIEWER_NOTE_MAX_BODY } from "../untrusted-text.js";
6
+ export function resumeDecisionWasNegative(resume) {
7
+ const o = resume.outcome;
8
+ return o.decision === "reject" || o.decision === "deny";
9
+ }
10
+ export const DEFERRED_REISSUE = "[DEFERRED] This tool call shared a batch with a call that suspended for durable approval, so it was " +
11
+ "NOT executed on resume. If you still need it, issue it again now.";
12
+ export function deepJsonEqual(a, b) {
13
+ if (a === b)
14
+ return true;
15
+ if (typeof a !== typeof b || a === null || b === null || typeof a !== "object") {
16
+ return false;
17
+ }
18
+ const aArr = Array.isArray(a);
19
+ const bArr = Array.isArray(b);
20
+ if (aArr !== bArr)
21
+ return false;
22
+ if (aArr && bArr) {
23
+ if (a.length !== b.length)
24
+ return false;
25
+ for (let i = 0; i < a.length; i++) {
26
+ if (!deepJsonEqual(a[i], b[i]))
27
+ return false;
28
+ }
29
+ const aExtra = Object.keys(a).filter((k) => !isCanonicalIndexKey(k, a.length));
30
+ const bExtra = Object.keys(b).filter((k) => !isCanonicalIndexKey(k, b.length));
31
+ if (aExtra.length !== bExtra.length)
32
+ return false;
33
+ return aExtra.every((k) => Object.prototype.hasOwnProperty.call(b, k) && deepJsonEqual(Reflect.get(a, k), Reflect.get(b, k)));
34
+ }
35
+ const ao = a;
36
+ const bo = b;
37
+ const aKeys = Object.keys(ao);
38
+ const bKeys = Object.keys(bo);
39
+ if (aKeys.length !== bKeys.length)
40
+ return false;
41
+ return aKeys.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && deepJsonEqual(ao[k], bo[k]));
42
+ }
43
+ function isCanonicalIndexKey(key, length) {
44
+ return /^(0|[1-9]\d*)$/.test(key) && Number(key) < length;
45
+ }
46
+ export function sameWinner(incoming, persisted) {
47
+ return (incoming.boundCallId === persisted.boundCallId &&
48
+ incoming.decision === persisted.decision &&
49
+ deepJsonEqual(incoming.updatedInput, persisted.updatedInput) &&
50
+ deepJsonEqual(incoming.answer, persisted.answer) &&
51
+ (persisted.reason === undefined || incoming.reason === persisted.reason));
52
+ }
53
+ export function pendingContentAskCallId(cp) {
54
+ return cp.pendingAction.kind === "tool_approval" && cp.pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME
55
+ ? cp.pendingAction.toolCallId
56
+ : undefined;
57
+ }
58
+ export function answerFaceForRedeemedCall(answer, redeemedCallId, base, redeemedQuestionsHash) {
59
+ let consumed = false;
60
+ const face = async (req, signal) => {
61
+ if (!consumed && req.toolCallId === redeemedCallId && boundInputHashOf(req.questions) === redeemedQuestionsHash) {
62
+ consumed = true;
63
+ return answer;
64
+ }
65
+ if (base !== undefined)
66
+ return base(req, signal);
67
+ throw new Error("this resumed leg's answer was bound to the decided question only — a new question has no answer on this leg");
68
+ };
69
+ return isLiveQuestionFace(base) ? face : markBoundOnlyQuestionFace(face);
70
+ }
71
+ export function describeSuppliedValue(value) {
72
+ return typeof value === "string" ? value : value === null ? "null" : typeof value;
73
+ }
74
+ export function assertOutcomeText(value, field) {
75
+ if (value !== undefined && typeof value !== "string") {
76
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume \`${field}\` is not a plain string (got ${typeof value}) — a decide's text payload is an operator's plain data, not a live object; refusing pre-CAS, the checkpoint stays pending`);
77
+ }
78
+ }
79
+ export function resumeContinuation(resume, mark) {
80
+ if (resume.outcome.gate === "wake") {
81
+ return formatHookFeedback("You were WOKEN from a parked pause by an operator. Before continuing, re-orient from the workspace: " +
82
+ "run `git status` and review your recent changes / last commits to confirm what is already done, then " +
83
+ "continue the remaining work. Do NOT restart the task or re-run work that is already committed.", mark);
84
+ }
85
+ if (resume.outcome.gate === "resource_limit") {
86
+ return formatHookFeedback("You were resumed after a pause. Before continuing, re-orient from the workspace: run `git status` and " +
87
+ "review your recent changes / last commits to confirm what is already done, then continue the " +
88
+ "remaining work. Do NOT restart the task or re-run work that is already committed.", mark);
89
+ }
90
+ if (resume.outcome.gate === "dry_run_review") {
91
+ const verdict = resume.outcome.decision === "approve"
92
+ ? "Your predicted change was REVIEWED and APPROVED; it has been applied"
93
+ : `Your predicted change was REVIEWED and REJECTED${resume.outcome.reason ? `: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : ""}; it was NOT applied`;
94
+ return formatHookFeedback(`You were resumed after a dry-run review. ${verdict}. Before continuing, re-orient from the workspace ` +
95
+ "(run `git status` and review your recent changes) to confirm the current state, then continue the " +
96
+ "remaining work. Do NOT restart the task or re-run work that is already done.", mark);
97
+ }
98
+ if (resume.outcome.gate === "plan_review") {
99
+ if (resume.outcome.decision === "approve") {
100
+ return formatHookFeedback("Your proposed PLAN was REVIEWED and APPROVED. Proceed with that plan now — begin executing it. " +
101
+ "This is a RESUMED task; do NOT re-plan or restart from scratch, just carry out the approved plan.", mark);
102
+ }
103
+ if (resume.outcome.decision === "edit") {
104
+ if (!resume.outcome.editedPlan) {
105
+ return formatHookFeedback("Your proposed PLAN was REVIEWED and EDITED, but no revised plan text was supplied — proceed with your " +
106
+ "ORIGINAL plan as-is, begin executing it now. This is a RESUMED task; do NOT re-plan or restart from scratch.", mark);
107
+ }
108
+ return formatHookFeedback("Your proposed PLAN was REVIEWED and EDITED by a human reviewer. Proceed with the REVISED plan below " +
109
+ "(it supersedes your earlier plan); begin executing it now. This is a RESUMED task; do NOT re-plan " +
110
+ "or restart from scratch, just carry out the revised plan.\n\nThe REVISED plan to follow is:\n" +
111
+ delimitUntrusted("revised plan", resume.outcome.editedPlan), mark);
112
+ }
113
+ const why = resume.outcome.reason ? ` Reviewer note: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : "";
114
+ return formatHookFeedback(`Your proposed PLAN was REVIEWED and REJECTED; it was NOT executed.${why} Produce a NEW plan that ` +
115
+ "addresses the concern, then continue. This is a RESUMED task — re-plan from the current state; do " +
116
+ "NOT execute the rejected plan.", mark);
117
+ }
118
+ const { pendingAction } = resume.cp;
119
+ if (pendingAction.kind !== "tool_approval") {
120
+ return formatHookFeedback("This is a RESUMED task — continue from where you left off without restarting or re-running prior work.", mark);
121
+ }
122
+ const decided = resume.outcome.decision === "allow"
123
+ ? "was APPROVED and has now been executed — its result is in the tool results above"
124
+ : `was DENIED${resume.outcome.reason ? `: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : ""}`;
125
+ const remaining = pendingAction.batchToolCallIds.filter((id) => id !== pendingAction.toolCallId && !pendingAction.completedCallIds.includes(id)).length;
126
+ const batchNote = remaining > 0
127
+ ? ` ${remaining} other tool call(s) from that batch were returned as deferred and not run — re-issue any you still need.`
128
+ : "";
129
+ return formatHookFeedback(`The pending tool call "${pendingAction.toolName}" ${decided}.${batchNote} ` +
130
+ `This is a RESUMED task — every step before this point is already done and its results are in the ` +
131
+ `conversation above. Do NOT restart the task or re-run any tool you already ran; continue from this ` +
132
+ `exact point, building on the existing results, and finish the remaining work.`, mark);
133
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The execution-record settle half of the pending-call resolver's belt (run-loop machinery, layer 1):
3
+ * what the resolver does with the word the row's `recordExecutionOutcome` verb answered. The verb itself
4
+ * is bound by `resumeStream` (ResumeRun.recordExecutionOutcome); the resolver calls it once per
5
+ * resolution, right after the resolved call's `tool_end` frame, and hands the answer here. The rule is a
6
+ * table lookup, not a condition list: the words that leave the row unrecorded
7
+ * (`EXECUTION_RECORD_LEAVES_ROW_UNRECORDED`) announce `checkpoint.execution_outcome_unrecorded`; the
8
+ * others say nothing. The execution result is never changed by the answer — the record is an account of
9
+ * what ran, not a gate on it — so this module has no return value and no throw of its own (a conflict is
10
+ * the verb's own throw and propagates past the resolver untouched).
11
+ */
12
+ import { type ExecutionOutcomeRecordWord } from "../checkpoint-execution-record.js";
13
+ import type { GateOutcome } from "../gate-outcome.js";
14
+ import { type RunnerDeps } from "../types.js";
15
+ /** The facts the unrecorded notice carries — all from the resumed leg; the secret token never rides. */
16
+ export interface ExecutionRecordFacts {
17
+ toolName: string;
18
+ gate: GateOutcome;
19
+ sessionId: string;
20
+ runId: string;
21
+ scope: string;
22
+ checkpointId?: string;
23
+ }
24
+ /** Settle the record verb's answer: announce the unrecorded words through the deployment's notice sink;
25
+ * the recorded words are silent. */
26
+ export declare function settleExecutionRecord(onNotice: RunnerDeps["onNotice"], word: ExecutionOutcomeRecordWord, facts: ExecutionRecordFacts): void;
@@ -0,0 +1,19 @@
1
+ import { EXECUTION_RECORD_LEAVES_ROW_UNRECORDED } from "../checkpoint-execution-record.js";
2
+ import { deliverEngineNotice } from "../types.js";
3
+ export function settleExecutionRecord(onNotice, word, facts) {
4
+ if (!EXECUTION_RECORD_LEAVES_ROW_UNRECORDED[word])
5
+ return;
6
+ deliverEngineNotice(onNotice, {
7
+ code: "checkpoint.execution_outcome_unrecorded",
8
+ message: `The resolved tool call "${facts.toolName}" was disposed (${facts.gate.disposition.kind}) but its execution record could not be filed on the checkpoint: ` +
9
+ `the store answered "${word}" — the row is no longer a resolved row of this scope (reopened, expired or removed under this leg). ` +
10
+ `The execution result stands as delivered; the row reads unknown on its execution axis.`,
11
+ detail: {
12
+ sessionId: facts.sessionId,
13
+ runId: facts.runId,
14
+ scope: facts.scope,
15
+ ...(facts.checkpointId !== undefined ? { checkpointId: facts.checkpointId } : {}),
16
+ word,
17
+ },
18
+ });
19
+ }