@sema-agent/core 7.12.0 → 7.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/core/ask-origin.d.ts +55 -0
  3. package/dist/core/ask-origin.js +21 -0
  4. package/dist/core/engine-notice.d.ts +18 -0
  5. package/dist/core/gate-lanes.d.ts +0 -40
  6. package/dist/core/gate-lanes.js +3 -17
  7. package/dist/core/governance-codes.d.ts +1 -1
  8. package/dist/core/governance-codes.js +2 -0
  9. package/dist/core/hooks.d.ts +1 -1
  10. package/dist/core/hooks.js +1 -1
  11. package/dist/core/runner/assemble-result.d.ts +45 -1
  12. package/dist/core/runner/assemble-result.js +4 -1
  13. package/dist/core/runner/compaction-seams.d.ts +42 -0
  14. package/dist/core/runner/compaction-seams.js +80 -0
  15. package/dist/core/runner/contracts.d.ts +44 -6
  16. package/dist/core/runner/denial-limit-arms.js +3 -2
  17. package/dist/core/runner/permission-rule-lanes.d.ts +2 -1
  18. package/dist/core/runner/permission-rule-lanes.js +2 -1
  19. package/dist/core/runner/prepare-policy-chain.js +3 -2
  20. package/dist/core/runner/prepare-protocol-tools.js +5 -0
  21. package/dist/core/runner/prepare-safety-scan.js +11 -16
  22. package/dist/core/runner/prepare-task.d.ts +9 -1
  23. package/dist/core/runner/prepare-task.js +6 -1
  24. package/dist/core/runner/resume-claim.d.ts +2 -2
  25. package/dist/core/runner/resume-preflight.d.ts +2 -2
  26. package/dist/core/runner/run-attachment-seats.d.ts +2 -2
  27. package/dist/core/runner/run-git-lane.d.ts +1 -1
  28. package/dist/core/runner/run-harness-handlers.js +2 -0
  29. package/dist/core/runner/run-identity-wiring.d.ts +7 -0
  30. package/dist/core/runner/run-identity-wiring.js +3 -1
  31. package/dist/core/runner/run-leg.d.ts +7 -5
  32. package/dist/core/runner/run-leg.js +256 -5
  33. package/dist/core/runner/run-notification-lane.d.ts +4 -3
  34. package/dist/core/runner/run-recovery-lanes.d.ts +4 -17
  35. package/dist/core/runner/run-recovery-lanes.js +5 -4
  36. package/dist/core/runner/run-settle-and-teardown.d.ts +14 -10
  37. package/dist/core/runner/run-settle-and-teardown.js +112 -4
  38. package/dist/core/runner/run-stop-and-final-verify.d.ts +2 -2
  39. package/dist/core/runner/run-terminal-adoption.d.ts +16 -16
  40. package/dist/core/runner/run-terminal-adoption.js +87 -7
  41. package/dist/core/runner/run-turn-boundary.js +5 -4
  42. package/dist/core/runner/runtask.d.ts +10 -90
  43. package/dist/core/runner/runtask.js +77 -596
  44. package/dist/core/runner/stream-lifecycle-verbs.js +10 -1
  45. package/dist/core/task-stream.d.ts +10 -1
  46. package/dist/core/tool-face.d.ts +8 -0
  47. package/dist/core/tool-face.js +1 -0
  48. package/dist/core/tool-policy.d.ts +5 -3
  49. package/dist/core/tool-registry.d.ts +11 -3
  50. package/dist/core/tool-registry.js +7 -1
  51. package/dist/core/tool-roster.d.ts +26 -0
  52. package/dist/core/tool-roster.js +38 -7
  53. package/dist/index.d.ts +3 -3
  54. package/dist/index.js +3 -3
  55. package/dist/server/http.js +2 -1
  56. package/package.json +1 -1
  57. package/test/export-surface.snapshot.json +11 -1
@@ -4,7 +4,9 @@
4
4
  * stop, the platform lane's external cause — which alone overrides a real throw), the usage ledger's FINAL charge with its
5
5
  * slow-charge disclosure, the live-abort orphan reconcile and its synthetic `tool_end` frames, the held session-write
6
6
  * drain, the user-interruption marker, the durable park's carried caller input and the held agent_end account's settle,
7
- * the dropped post-compact announce disclosure, and the end-of-task compaction pass (`finish`).
7
+ * the dropped post-compact announce disclosure, and the end-of-task compaction pass (`finish` — since design/393 S7 the lane's
8
+ * own module-private function, verbatim from the Runner's private method: the pass has one caller, and a sibling file would be a
9
+ * lane-to-lane reach, the S6 legs' reading).
8
10
  *
9
11
  * Async (six first-level awaits, the same awaits at the same positions). It ends by entering the settle lane (`next`) in
10
12
  * its LAST continuation — the settle's first read sits on the tick `finish` resolved on, as it did in the one-function
@@ -14,13 +16,14 @@
14
16
  */
15
17
  import type { AssistantMessage } from "../../internal/llm.js";
16
18
  import type { Model } from "../../internal/llm.js";
17
- import type { MaybeCompactOptions } from "../auto-compaction.js";
19
+ import { type MaybeCompactOptions } from "../auto-compaction.js";
20
+ import type { SessionStore } from "../session.js";
18
21
  import type { PushQueue } from "../push-queue.js";
19
22
  import { type Brain, type TaskEvent, type TaskSpec } from "../types.js";
20
23
  import { type Stats } from "./assemble-result.js";
21
24
  import type { EndOfTaskCompaction, Prepared, RunnerDepsSeat, RunState } from "./contracts.js";
22
25
  export interface RunTerminalAdoptionInput<R> {
23
- /** borrowed-readonly — the task spec: the task id the account's notices name; `finish`'s first argument. */
26
+ /** borrowed-readonly — the task spec: the task id the account's notices name; `finish`'s first argument (the end-of-task pass below). */
24
27
  spec: TaskSpec;
25
28
  /** borrowed-mutable — the run's event queue: the reconcile's synthetic `tool_end` frames and the end-of-task compaction's failure frame. */
26
29
  queue: PushQueue<TaskEvent>;
@@ -62,6 +65,11 @@ export interface RunTerminalAdoptionInput<R> {
62
65
  followUp: number;
63
66
  } | undefined;
64
67
  };
68
+ /** borrowed-readonly — #677: did the run end with a person's steer / follow-up still pending (the identity lane's read face)? Folded
69
+ * into the answer-settled verdict this lane evaluates once. */
70
+ userInputLostAtEnd: {
71
+ readonly current: boolean;
72
+ };
65
73
  /** borrowed-readonly — the leg lane's final assistant message (its `stopReason` is one of the interruption facts). */
66
74
  final: AssistantMessage | undefined;
67
75
  /** borrowed-readonly — what the leg threw (undefined on a clean end); this lane may LIFT a typed cause over it and hands the adopted value on. */
@@ -73,19 +81,8 @@ export interface RunTerminalAdoptionInput<R> {
73
81
  /** borrowed-readonly — the Runner's deployment deps, read LIVE (`onError`, `checkpointStore`, `onNotice`); the one home of why it is
74
82
  * a seat and not a captured object is {@link RunnerDepsSeat}. */
75
83
  runner: RunnerDepsSeat;
76
- /** borrowed-readonly — the Runner's end-of-task compaction pass (its private method, as a delegate). */
77
- finish: (spec: TaskSpec, prepared: Prepared, opts: {
78
- skipCompaction?: boolean;
79
- minTokens?: number;
80
- brain?: Brain;
81
- windowSafety?: Pick<MaybeCompactOptions, "fallbackBudget" | "onWindowSafety">;
82
- onCompactionFailed?: (reason: string) => void;
83
- telemetry?: {
84
- tracer: TaskSpec["tracer"];
85
- taskId: string;
86
- };
87
- cancelled?: boolean;
88
- }) => Promise<EndOfTaskCompaction>;
84
+ /** borrowed-readonly — the session store: the end-of-task pass touches the leg's session through it (`finish`'s teardown legs). */
85
+ sessions: SessionStore;
89
86
  /** borrowed-readonly — the settle lane, entered in this lane's last continuation with the adopted cause and the compaction outcome. */
90
87
  next: (adopted: RunTerminalAdoptionResult) => Promise<R>;
91
88
  }
@@ -95,5 +92,8 @@ export interface RunTerminalAdoptionResult {
95
92
  threw: unknown;
96
93
  /** The end-of-task compaction pass's outcome. */
97
94
  comp: EndOfTaskCompaction;
95
+ /** #677 — this lane's ONE evaluation of {@link answerSettled}: a clean `"stop"` final with no queued continuation lost. The
96
+ * settle lane hands it to the arbitration (`ResultFlags.answerSettled`), the teardown attribution and the notice. */
97
+ answerSettled: boolean;
98
98
  }
99
99
  export declare function runTerminalAdoption<R>(input: RunTerminalAdoptionInput<R>): Promise<R>;
@@ -1,14 +1,20 @@
1
+ import { DEFAULT_COMPACTION_INSTRUCTIONS, maybeCompact } from "../auto-compaction.js";
2
+ import { DEFAULT_COMPACTION_SETTINGS } from "../../internal/harness.js";
1
3
  import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
2
4
  import { appendInterruptionMarker, reconcileInterruptedSession } from "../session-reconcile.js";
3
5
  import { emitTrace } from "../trace.js";
4
6
  import { deliverEngineNotice, undrainedUserInputNotices } from "../types.js";
5
- import { errorCodeOf } from "./assemble-result.js";
6
- import { awaitChargeWithSlowDisclosure } from "./clock-and-limits.js";
7
+ import { answerSettled, errorCodeOf } from "./assemble-result.js";
8
+ import { awaitChargeWithSlowDisclosure, discloseUnevaluableWindow } from "./clock-and-limits.js";
9
+ import { buildWorkingFileAttachments, centerAdoptionOption, contextInstructionFilesOption, emitInputTruncated, forkContextOption } from "./compaction-call-options.js";
10
+ import { compactionHookOptions, recordCompactionReuse, seamCCompactionOptions } from "./compaction-seams.js";
11
+ import { gitRestateOption } from "./git-leg-delivery.js";
7
12
  import { MAX_CONSECUTIVE_COMPACTION_FAILURES } from "./compaction-knobs.js";
8
13
  import { reconciledToolEndBody } from "./tool-end-body.js";
9
14
  export async function runTerminalAdoption(input) {
10
- const { spec, queue, prepared, rs, stats, ident, emitCommitted, startedToolCallIds, toolLabels, timeout, compactionBrain, compactionBreaker, windowSafetyOptions, undrainedUserAtEnd, final, abortedLive, userInterruptedLive, runner, finish, next } = input;
15
+ const { spec, queue, prepared, rs, stats, ident, emitCommitted, startedToolCallIds, toolLabels, timeout, compactionBrain, compactionBreaker, windowSafetyOptions, undrainedUserAtEnd, userInputLostAtEnd, final, abortedLive, userInterruptedLive, runner, sessions, next } = input;
11
16
  let { threw } = input;
17
+ const settled = answerSettled(final, userInputLostAtEnd.current || prepared.workCutRef.continuation || prepared.workCutRef.abortedBeforeTurnEnd);
12
18
  if (threw === undefined && prepared.brainCallGuardrailRef.timedOut !== undefined) {
13
19
  threw = prepared.brainCallGuardrailRef.timedOut;
14
20
  }
@@ -58,7 +64,7 @@ export async function runTerminalAdoption(input) {
58
64
  });
59
65
  }
60
66
  }
61
- if (interrupted && userInterruptedLive && reconcileComplete) {
67
+ if (interrupted && userInterruptedLive && reconcileComplete && !settled) {
62
68
  try {
63
69
  const entryId = await appendInterruptionMarker(prepared.session, { toolUseInFlight: orphansClosed > 0 });
64
70
  emitCommitted(entryId, "user");
@@ -109,12 +115,86 @@ export async function runTerminalAdoption(input) {
109
115
  }
110
116
  const comp = await finish(spec, prepared, {
111
117
  telemetry: { tracer: rs.telemetry.tracer, taskId: rs.telemetry.taskId },
112
- skipCompaction: durablyPaused || compactionBreaker.failures >= MAX_CONSECUTIVE_COMPACTION_FAILURES,
118
+ skipCompaction: durablyPaused || (abortedLive && settled) || compactionBreaker.failures >= MAX_CONSECUTIVE_COMPACTION_FAILURES,
113
119
  minTokens: rs.counters.compactionFloor,
114
120
  brain: compactionBrain,
115
121
  windowSafety: windowSafetyOptions(prepared.model),
116
122
  onCompactionFailed: (reason) => queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "auto", reason, ...ident() }),
117
123
  cancelled: abortedLive,
118
- });
119
- return await next({ threw, comp });
124
+ }, runner, sessions);
125
+ return await next({ threw, comp, answerSettled: settled });
126
+ }
127
+ async function finish(spec, prepared, opts, runner, sessions) {
128
+ let comp;
129
+ if (!opts?.skipCompaction) {
130
+ try {
131
+ const finishComp = await maybeCompact({
132
+ session: prepared.session,
133
+ epochDeclaredSections: prepared.epochDeclaredSections,
134
+ ...centerAdoptionOption(prepared),
135
+ model: prepared.model,
136
+ compactionModel: prepared.compModel,
137
+ ...forkContextOption(prepared, false),
138
+ brain: opts?.brain ?? runner.deps.brain,
139
+ getApiKeyAndHeaders: spec.getApiKeyAndHeaders,
140
+ thinking: prepared.thinking,
141
+ settings: { ...DEFAULT_COMPACTION_SETTINGS, ...spec.compaction },
142
+ customInstructions: spec.compaction?.instructions ?? DEFAULT_COMPACTION_INSTRUCTIONS,
143
+ minTokens: opts?.minTokens,
144
+ overheadTokens: prepared.promptOverheadTokens,
145
+ ...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
146
+ onInputTruncated: emitInputTruncated(opts?.telemetry?.tracer ?? spec.tracer ?? runner.deps.tracer, opts?.telemetry?.taskId ?? spec.taskId ?? prepared.sessionId),
147
+ ...(opts?.windowSafety ?? {}),
148
+ workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
149
+ ...contextInstructionFilesOption(prepared),
150
+ ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
151
+ ...seamCCompactionOptions(runner, prepared),
152
+ ...gitRestateOption(prepared),
153
+ ...compactionHookOptions(runner, spec, prepared.sessionId, "auto", prepared.hookIdentity, {
154
+ timeoutMs: prepared.hookTimeoutMs,
155
+ ...(opts?.cancelled === true ? { signal: prepared.abortController.signal } : {}),
156
+ }),
157
+ });
158
+ recordCompactionReuse(runner, prepared, finishComp);
159
+ if (finishComp.unevaluableWindow) {
160
+ discloseUnevaluableWindow(finishComp.unevaluableWindow, prepared, opts?.telemetry?.tracer ?? spec.tracer ?? runner.deps.tracer, opts?.telemetry?.taskId ?? spec.taskId ?? prepared.sessionId, runner.deps.onError);
161
+ }
162
+ comp = finishComp;
163
+ }
164
+ catch (err) {
165
+ const failMsg = String(err instanceof Error ? err.message : err);
166
+ const failReason = failMsg.length > 512 ? `${failMsg.slice(0, 512)}…` : failMsg;
167
+ emitTrace(opts?.telemetry?.tracer ?? spec.tracer ?? runner.deps.tracer, () => ({
168
+ kind: "compaction.failed",
169
+ version: 1,
170
+ taskId: opts?.telemetry?.taskId ?? spec.taskId ?? prepared.sessionId,
171
+ trigger: "auto",
172
+ reason: failReason,
173
+ ts: Date.now(),
174
+ }));
175
+ opts?.onCompactionFailed?.(failReason);
176
+ runner.deps.onError?.(err, { phase: "compaction", sessionId: prepared.sessionId });
177
+ }
178
+ }
179
+ try {
180
+ await sessions.touch(prepared.sessionId);
181
+ }
182
+ catch (err) {
183
+ runner.deps.onError?.(err, { phase: "config", sessionId: prepared.sessionId });
184
+ }
185
+ try {
186
+ await prepared.mcp.dispose();
187
+ }
188
+ catch (err) {
189
+ runner.deps.onError?.(err, { phase: "mcp", sessionId: prepared.sessionId });
190
+ }
191
+ if (prepared.a2a !== undefined) {
192
+ try {
193
+ await prepared.a2a.dispose();
194
+ }
195
+ catch (err) {
196
+ runner.deps.onError?.(err, { phase: "a2a", sessionId: prepared.sessionId });
197
+ }
198
+ }
199
+ return comp;
120
200
  }
@@ -14,11 +14,12 @@ import { workflowSizeGuidelineChangeNotice } from "../../orchestration/workflow-
14
14
  import { awaitChargeWithSlowDisclosure, discloseUnevaluableWindow, ENV_DUE_GOVERNANCE_READ_BUDGET_MS, GOVERNANCE_READ_STALLED, LIMIT_APPROACH_DEFAULT_THRESHOLDS, limitApproachFrames, platformLimitTerminal, raceUntilDeadline } from "./clock-and-limits.js";
15
15
  import { gitRestateOption } from "./git-leg-delivery.js";
16
16
  import { COMPACTION_FREED_EPSILON, COMPACTION_REGROWTH_FACTOR, MAX_CONSECUTIVE_COMPACTION_FAILURES } from "./compaction-knobs.js";
17
+ import { compactionHookOptions, recordCompactionReuse, seamCCompactionOptions } from "./compaction-seams.js";
17
18
  const BATCH_CONTEXT_MIN_KEEP_BYTES = 160;
18
19
  const BATCH_TRUNCATION_MARKER = "\n…[truncated]";
19
20
  export function createTurnBoundary(input) {
20
21
  const { prepared, stats, rs, deps } = input;
21
- const { spec, queue, manualCompactRef, todoToolMounted, taskToolsMounted, walltimeMonotonicDeadline, timeout, ident, postToolBatchHook, batchArgs, compactionBrain, withinTaskCompaction, compactionBreaker, windowSafetyOptions, rapidRefill, drainManualCompact, runnerHooks } = deps;
22
+ const { spec, queue, manualCompactRef, todoToolMounted, taskToolsMounted, walltimeMonotonicDeadline, timeout, ident, postToolBatchHook, batchArgs, compactionBrain, withinTaskCompaction, compactionBreaker, windowSafetyOptions, rapidRefill, drainManualCompact, runnerHooks, runner } = deps;
22
23
  let finalVerifySeenAtLastBoundary = 0;
23
24
  const onTurnBoundary = async (event) => {
24
25
  let boundarySteered = false;
@@ -560,10 +561,10 @@ export function createTurnBoundary(input) {
560
561
  workingFileAttachments: buildWorkingFileAttachments(spec, prepared),
561
562
  ...contextInstructionFilesOption(prepared),
562
563
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
563
- ...runnerHooks.seamCCompactionOptions(prepared),
564
+ ...seamCCompactionOptions(runner, prepared),
564
565
  ...gitRestateOption(prepared),
565
566
  ...windowSafetyOptions(event.model),
566
- ...runnerHooks.compactionHookOptions(spec, prepared.sessionId, passTrigger),
567
+ ...compactionHookOptions(runner, spec, prepared.sessionId, passTrigger, prepared.hookIdentity, { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal }),
567
568
  });
568
569
  if (comp.blocked) {
569
570
  emitTrace(rs.telemetry.tracer, () => ({
@@ -618,7 +619,7 @@ export function createTurnBoundary(input) {
618
619
  queue.push({ type: "compaction_outcome", outcome, trigger: passTrigger, ...ident() });
619
620
  }
620
621
  }
621
- runnerHooks.recordCompactionReuse(prepared, comp);
622
+ recordCompactionReuse(runner, prepared, comp);
622
623
  if (comp.compacted) {
623
624
  queue.push({
624
625
  type: "compacted",
@@ -132,27 +132,18 @@ export declare class Runner implements RunnerSelfSeat {
132
132
  * on first iteration — you do NOT need to pull events for the run (and its `finish()`/env teardown) to
133
133
  * happen. Iterating just observes events (the buffer is backpressure-free); `.result()` awaits completion. */
134
134
  runTaskStream(spec: TaskSpec, resume?: ResumeRun, internals?: RunInternals): TaskStream;
135
- private runLocked;
136
- /**
137
- * design/393 S6 — the run body after `prepared` exists: the seat lanes R1–R3 (design/393 S5) — the identity wiring, the
138
- * telemetry and budget seats, and the attachment seats, whose continuation carries the rest. Entered by the notification
139
- * lane in its last continuation, with what it prepared and bound. Not `async`: it hands back the attachment lane's own
140
- * promise, so the driver adds no frame — and no microtask — of its own between the lanes and `runLocked`'s settlement.
141
- */
142
- private runSeatLanes;
143
- /**
144
- * design/393 S6 — the run body after the seat lanes' async rung: the seat lanes R4–R12 (design/393 S5), the two assemblies
145
- * the driver keeps (the harness handlers, the turn boundary) and the leg lanes. Entered by the attachment-seats lane in
146
- * its last continuation, with what the notification lane prepared and what the identity / budget lanes handed back.
147
- * Not `async` for the same reason as `runSeatLanes`: the leg lane's promise IS this method's.
148
- */
149
- private runAssembliesAndLegs;
150
135
  /**
151
- * design/100 §E12 — the post-completion prompt-suggestion pass. Never throws (failures route to
152
- * `onError(phase:"suggestions")` and resolve to `[]`), so it is fire-and-forget safe and `suggestions()`
153
- * never rejects. Also fills the budget-excluded `stats.suggestions` line.
136
+ * design/393 S7 (#674) — the run body as ONE driver method: `runLocked`'s signature, the lane order, and the two assemblies
137
+ * the driver keeps (the harness handlers, the turn boundary). The lanes' products are threaded under the names they hand
138
+ * back; the two continuations the S6 driver spelled as private methods (`runSeatLanes` / `runAssembliesAndLegs`, joined by
139
+ * a `RunSeatsHandoff` re-spelling of two lanes' Results) are the `next` closures of the two async lanes they follow — the
140
+ * notification lane's (R0) and the attachment seats' (R3) — because a closure sees what the lanes before it bound, so
141
+ * nothing is re-spelled and nothing is re-destructured. Neither this method nor either closure is `async`: each lane's
142
+ * own promise IS this method's, so a prepare that throws reaches the run IIFE's catch / finally (the failure backstop,
143
+ * the session lock's release) on the tick it did, and each successor is entered on the tick its lane's last await
144
+ * resolved on (the S6 TICKS pin holds unchanged: no first host read moved off the tick the one-function body read it on).
154
145
  */
155
- private suggestNextPrompts;
146
+ private runLocked;
156
147
  /** Run a task to completion and return a machine-readable result. */
157
148
  runTask(spec: TaskSpec, internals?: RunInternals): Promise<TaskResult>;
158
149
  /**
@@ -260,77 +251,6 @@ export declare class Runner implements RunnerSelfSeat {
260
251
  * twin. Entered in the claim rung's last continuation, so its first statement runs on the tick the win was read.
261
252
  */
262
253
  private resumeHandoff;
263
- /**
264
- * design/45 resume step: with the harness idle and the branch rewound to the suspension leaf, resolve
265
- * the gated tool call and close the rest of the suspended batch, all by appending `toolResult`s to the
266
- * session (the next `harness.prompt(continuation)` replays them as context).
267
- * - `allow` → execute the pending tool ONCE (re-validating `updatedInput`/captured post-hook args),
268
- * append its real result. This deliberately bypasses the tool gate (the human already adjudicated)
269
- * and the PostToolUse hook (documented v1 gap — rare, acceptable).
270
- * - `deny` → append a model-readable denial result instead.
271
- * - the remaining batch siblings (#k+1..N) → deferred-reissue results (v1 doesn't blind-run them).
272
- */
273
- private applyResumeDecision;
274
- /** Resolve the single gated call of a resumed batch (design/45): execute it once on `allow`, or inject
275
- * a model-readable denial on `deny`. Appends exactly one `toolResult` for `pendingAction.toolCallId`.
276
- * `emit` streams `tool_start`/`tool_end` for the resolved call so a `resumeStream` observer sees the
277
- * approved tool actually execute (API#2 observability parity — service [15]/[37]; the gated call never
278
- * emitted execution events in the original run, it suspended at the gate before running).
279
- *
280
- * RB-22 RULING (2026-07-18, deliberate — the $ counterpart of RB-21's walltime question): a HUMAN-
281
- * APPROVED call executes even when the $ budget is already exhausted (`maxCostMicroUsd` ≤ spent).
282
- * The tool execution itself consumes no model spend — the budget's coordinate is LLM cost, and the
283
- * very next turn boundary's budget gate stops the run — so the overrun is ≈0 while refusing would
284
- * WASTE the human approval (the review effort is already sunk). This deliberately differs from the
285
- * walltime ruling (RB-21: NOT exempt) on principle: wall-clock exhaustion endangers the write-out
286
- * window (time cannot be clawed back), $ exhaustion after an approved no-spend action endangers
287
- * nothing. Pinned in resume tests; do not "fix" this into a pre-execution budget check. */
288
- private resolvePendingCall;
289
- /**
290
- * design/84 Seam C — the cost-optimization compaction options threaded into BOTH `maybeCompact` call
291
- * sites (within-task turn boundary + `finish()`). All three fields come from `RunnerDeps` (a trusted
292
- * FUNCTION seam — never `TaskSpec`, which is serializable/durable-resumable/untrusted-caller). The
293
- * Runner OWNS the consecutive-reuse counter (`prepared.compactionReuseRef`) so the
294
- * `maxConsecutiveProviderReuse` drift guard spans the whole task across both sites: it FEEDS the current
295
- * count in as `consecutiveProviderReuse`, and {@link recordCompactionReuse} updates it from the result.
296
- * Returns `undefined` when no provider is wired (so the call site spreads nothing → byte-identical to
297
- * the pre-design/84 behavior).
298
- */
299
- private seamCCompactionOptions;
300
- /** design/134 §3.2 — resolve the pre/postCompact lifecycle callbacks (whole-slot `spec.hooks ??
301
- * deps.hooks`, same resolution as the stop hook) and wrap each in a SWALLOW+TRACE shell before
302
- * threading them into maybeCompact. The wrapper owns the observability half of the R3 MED contract
303
- * (maybeCompact swallows defensively too, but has no sink): a throwing callback is reported via
304
- * `onError(phase:"hook")` and treated as absent; a `block` returned under a "forced" trigger is
305
- * reported as ignored (maybeCompact enforces the ignore — blocking a compaction the provider/trim
306
- * layer already demanded would kill the run). */
307
- private compactionHookOptions;
308
- /** design/84 Seam C — fold a finished compaction's `reused` flag into the run-scoped consecutive-reuse
309
- * counter: a reused (provider) summary increments it, a real (LLM) summary resets it to 0. A no-op
310
- * compaction (`compacted:false`) leaves the counter untouched. No-op when no provider is wired. */
311
- private recordCompactionReuse;
312
- private finish;
313
- /**
314
- * design/48 remote seam +: tear down a per-task env minted by `executionEnvFactory` (e.g. a
315
- * remote container, or a `withWorktreeIsolation` worktree) — this task owned its lifetime. Best-effort, like
316
- * `mcp.dispose`: a `destroy()` failure must not break the run. Only `ownedEnv` (factory-produced) is destroyed;
317
- * a caller-owned static `deps.executionEnv` outlives the task and is left untouched. The service control plane's
318
- * reaper is the backstop for an env orphaned by a rare pre-teardown throw (same posture as `mcp`).
319
- *
320
- * Runs from the run-loop tail AFTER every env-reader (finish()'s compaction `attachWorkingFiles` +
321
- * the file-history boundary settle), so the working tree is still readable when they run. This ORDER is
322
- * the fix for a teardown-order defect: the old in-`finish` destroy ran BEFORE the capture seat, so a
323
- * factory-minted remote/worktree lane captured an already-destroyed env (plain host/static lanes were
324
- * false-green).
325
- *
326
- * design/49 v1.5 / design/76 §2.5: a committed durable pause (`pausedRef.current`, any gate kind)
327
- * `suspendVM`-paused the env and persisted its `workspaceHandle` into the checkpoint — destroying it would
328
- * discard the paused VM and make resume fail to restore. Skip teardown; `resume()` rebuilds + `resumeVM`s it. A
329
- * cancelled pause is reaped via `TaskStream.destroy()` (design/51); an abandoned one is the service container
330
- * reaper's backstop. The gate here MIRRORS the tail's reap-stash (same durable fact
331
- * `pausedRef.current === undefined`), so exactly one path owns the env's fate.
332
- */
333
- private teardownOwnedEnv;
334
254
  }
335
255
  /** Convenience: one-shot run with explicit deps (creates a throwaway Runner). */
336
256
  export declare function runTask(spec: TaskSpec, deps: RunnerDeps): Promise<TaskResult>;