@sema-agent/core 7.11.1 → 7.12.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 (78) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/core/auto-mode-arming.d.ts +10 -14
  3. package/dist/core/auto-mode-arming.js +3 -9
  4. package/dist/core/auto-mode-defaults.d.ts +0 -2
  5. package/dist/core/auto-mode-defaults.js +0 -1
  6. package/dist/core/auto-mode-rebuild.d.ts +6 -13
  7. package/dist/core/auto-mode-rebuild.js +0 -2
  8. package/dist/core/auto-mode.d.ts +30 -89
  9. package/dist/core/auto-mode.js +12 -59
  10. package/dist/core/checkpoint-store.d.ts +1 -3
  11. package/dist/core/gate-fold.js +1 -9
  12. package/dist/core/gate-lanes.js +15 -9
  13. package/dist/core/hooks.d.ts +6 -0
  14. package/dist/core/runner/compaction-knobs.d.ts +45 -0
  15. package/dist/core/runner/compaction-knobs.js +3 -0
  16. package/dist/core/runner/contracts.d.ts +49 -10
  17. package/dist/core/runner/denial-limit-arms.d.ts +10 -13
  18. package/dist/core/runner/denial-limit-arms.js +9 -7
  19. package/dist/core/runner/gate-exit.js +9 -1
  20. package/dist/core/runner/prepare-caps-and-workflow.d.ts +1 -1
  21. package/dist/core/runner/prepare-caps-and-workflow.js +0 -5
  22. package/dist/core/runner/prepare-suspend-saga.d.ts +0 -2
  23. package/dist/core/runner/prepare-suspend-saga.js +2 -10
  24. package/dist/core/runner/prepare-task.js +1 -1
  25. package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
  26. package/dist/core/runner/prepare-wiring-manifest.js +1 -8
  27. package/dist/core/runner/run-attachment-seats.d.ts +22 -0
  28. package/dist/core/runner/run-attachment-seats.js +187 -0
  29. package/dist/core/runner/run-brain-sinks.d.ts +29 -0
  30. package/dist/core/runner/run-brain-sinks.js +61 -0
  31. package/dist/core/runner/run-clock-and-content.d.ts +52 -0
  32. package/dist/core/runner/run-clock-and-content.js +26 -0
  33. package/dist/core/runner/run-compaction-machinery.d.ts +35 -0
  34. package/dist/core/runner/run-compaction-machinery.js +98 -0
  35. package/dist/core/runner/run-git-lane.d.ts +64 -0
  36. package/dist/core/runner/run-git-lane.js +102 -0
  37. package/dist/core/runner/run-identity-wiring.d.ts +77 -0
  38. package/dist/core/runner/run-identity-wiring.js +85 -0
  39. package/dist/core/runner/run-leg.d.ts +106 -0
  40. package/dist/core/runner/run-leg.js +462 -0
  41. package/dist/core/runner/run-notification-lane.d.ts +55 -0
  42. package/dist/core/runner/run-notification-lane.js +128 -0
  43. package/dist/core/runner/run-reasoning-seat.d.ts +27 -0
  44. package/dist/core/runner/run-reasoning-seat.js +48 -0
  45. package/dist/core/runner/run-recovery-lanes.d.ts +54 -0
  46. package/dist/core/runner/run-recovery-lanes.js +180 -0
  47. package/dist/core/runner/run-settle-and-teardown.d.ts +109 -0
  48. package/dist/core/runner/run-settle-and-teardown.js +324 -0
  49. package/dist/core/runner/run-stop-and-final-verify.d.ts +32 -0
  50. package/dist/core/runner/run-stop-and-final-verify.js +159 -0
  51. package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +38 -0
  52. package/dist/core/runner/run-telemetry-and-budget-seats.js +159 -0
  53. package/dist/core/runner/run-terminal-adoption.d.ts +99 -0
  54. package/dist/core/runner/run-terminal-adoption.js +120 -0
  55. package/dist/core/runner/run-tool-mount-facts.d.ts +26 -0
  56. package/dist/core/runner/run-tool-mount-facts.js +58 -0
  57. package/dist/core/runner/run-turn-boundary.d.ts +0 -35
  58. package/dist/core/runner/run-turn-boundary.js +1 -3
  59. package/dist/core/runner/runtask.d.ts +14 -3
  60. package/dist/core/runner/runtask.js +125 -2102
  61. package/dist/core/runner-deps.d.ts +4 -14
  62. package/dist/core/store-contracts/workflow-journal-store-contract.d.ts +7 -0
  63. package/dist/core/store-contracts/workflow-journal-store-contract.js +85 -0
  64. package/dist/core/tool-policy.d.ts +37 -93
  65. package/dist/core/tool-policy.js +1 -11
  66. package/dist/core/trace.d.ts +6 -7
  67. package/dist/core/wiring-manifest.d.ts +5 -22
  68. package/dist/core/wiring-manifest.js +3 -11
  69. package/dist/core/workflow-journal-store.d.ts +35 -4
  70. package/dist/core/workflow-journal-store.js +19 -2
  71. package/dist/index.d.ts +3 -2
  72. package/dist/index.js +3 -2
  73. package/dist/orchestration/workflow.js +2 -0
  74. package/dist/stores/file/workflow-journal-store.d.ts +7 -10
  75. package/dist/stores/file/workflow-journal-store.js +2 -4
  76. package/dist/tools/fs/fs-bash.js +3 -2
  77. package/package.json +1 -1
  78. package/test/export-surface.snapshot.json +10 -10
@@ -0,0 +1,324 @@
1
+ import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
2
+ import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
3
+ import { resolveCheckpointStore } from "../checkpoint-store.js";
4
+ import { hookSeatExpiredError, runHookSeat } from "../hooks.js";
5
+ import { hasDestroy, isIsolated } from "../remote-env.js";
6
+ import { defaultTaskRegistry } from "../task-registry.js";
7
+ import { emitTrace } from "../trace.js";
8
+ import { assembleResult } from "./assemble-result.js";
9
+ import { TIMER_LATENESS_REPORT_MS } from "./clock-and-limits.js";
10
+ import { resumeDecisionWasNegative } from "./decide-continuation.js";
11
+ import { drainForwardedFramesBeforeDone, forwardsSubagentEvents } from "./prepare-run-refs.js";
12
+ import { settleTeardownLeg } from "./teardown-bounded.js";
13
+ import { amendTerminal, terminalProjection } from "./terminal-projection.js";
14
+ export async function runSettleAndTeardown(input) {
15
+ const { spec, queue, prepared, resume, internals, rs, stats, ident, parentToolCallId, subagentName, loopLatch, timeout, ownCommittedTailRef, emitDelegationLifecycle, taskIdRef, manualCompactRef, drainManualCompact, final, threw, abortedLive, strandedHumanAnswers, reasoningResolution, comp, setResult, onSuggestions, onSuspend, runner, sessions, suggestNextPrompts, teardownOwnedEnv } = input;
16
+ try {
17
+ if (comp?.compacted) {
18
+ queue.push({
19
+ type: "compacted",
20
+ trigger: "auto",
21
+ tokensBefore: comp.tokensBefore ?? 0,
22
+ ...(comp.postTriggerTokens !== undefined ? { tokensAfter: comp.postTriggerTokens } : {}),
23
+ ...(comp.triggerTokens !== undefined ? { triggerTokensBefore: comp.triggerTokens } : {}),
24
+ ...(comp.durationMs !== undefined ? { durationMs: comp.durationMs } : {}),
25
+ ...(comp.phaseDurations !== undefined ? { phaseDurations: comp.phaseDurations } : {}),
26
+ ...(comp.firstKeptEntryId !== undefined ? { preserved_segment: { firstKeptEntryId: comp.firstKeptEntryId } } : {}),
27
+ ...(comp.attachedFiles !== undefined ? { attachedFiles: comp.attachedFiles } : {}),
28
+ ...(comp.modelFallback ? { modelFallback: true } : {}),
29
+ ...(comp.fallbackReason !== undefined ? { fallbackReason: comp.fallbackReason } : {}),
30
+ ...(comp.clampedRatio !== undefined ? { clampedRatio: comp.clampedRatio } : {}),
31
+ ...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
32
+ ...ident(),
33
+ });
34
+ if (comp.phaseDurations !== undefined && comp.durationMs !== undefined) {
35
+ const pd = comp.phaseDurations;
36
+ const pdDur = comp.durationMs;
37
+ emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.phase_timings", version: 1, taskId: rs.telemetry.taskId, ...pd, durationMs: pdDur, ts: Date.now() }));
38
+ }
39
+ prepared.cacheBreakDetector?.notifyCompaction();
40
+ }
41
+ if (prepared.nestedStats.tasks > 0) {
42
+ stats.nested = {
43
+ tokens: prepared.nestedStats.tokens,
44
+ turns: prepared.nestedStats.turns,
45
+ tasks: prepared.nestedStats.tasks,
46
+ ...(prepared.nestedStats.anyUnpriced ? {} : { costMicroUsd: prepared.nestedStats.costMicroUsd }),
47
+ };
48
+ }
49
+ if (stats.totalInputTokens > 0) {
50
+ const rawHit = stats.cachedTokens / stats.totalInputTokens;
51
+ if (stats.cachedTokens > stats.totalInputTokens) {
52
+ runner.deps.onError?.(new Error(`prompt-cache: usage/api mismatch — cachedTokens (${stats.cachedTokens}) > totalInputTokens (${stats.totalInputTokens}); the cache family is likely wrong. Does model.api match the brain serving it? Override with model.params.promptCacheFamily = "input-includes-cached" | "input-excludes-cached".`), { phase: "prompt-cache", sessionId: prepared.sessionId });
53
+ }
54
+ stats.cacheHitRate = Math.min(1, rawHit);
55
+ if (!rs.telemetry.cacheBreakReported && stats.turns >= 2 && stats.totalInputTokens >= 8000 && stats.cacheHitRate < 0.15) {
56
+ const cacheWritten = stats.cacheWriteTokens + stats.cacheWriteTokensLong;
57
+ const cause = rs.degrade.degraded
58
+ ? `a mid-task model switch (${rs.degrade.degraded.from} → ${rs.degrade.degraded.to}, degradation) reset the prefix cache — this is the likely cause`
59
+ : cacheWritten > 0
60
+ ? `this task reported ${cacheWritten} cache-write tokens across its calls (compaction included) but served almost none back as reads — consistent with a prompt prefix that CHANGES between turns (client-side: volatile content up front, or per-turn tool churn/reorder) and, less often, with server-side eviction. Keep volatile content (memory/timestamps/ids) out of the prefix and the tool list stable in membership AND order`
61
+ : `no call in this task reported any cache-write tokens — and this API family may not report them at all (an openai-shaped usage row carries cached READS only), so the write side is no evidence here; check that the prompt prefix (system prompt + tool list, membership AND order) is byte-stable across turns and that this route caches this model`;
62
+ runner.deps.onError?.(new Error(`prompt-cache: low prefix-cache hit rate ${(stats.cacheHitRate * 100).toFixed(0)}% over ${stats.turns} turns (${stats.totalInputTokens} prompt tokens) — ${cause}. See design/09.`), { phase: "prompt-cache", sessionId: prepared.sessionId });
63
+ }
64
+ }
65
+ if (prepared.resourceLedger &&
66
+ (resume?.cp.gate.kind === "resource_limit" || prepared.pausedRef.current?.cause.gate.kind === "resource_limit")) {
67
+ stats.costMicroUsd += prepared.resourceLedger.spentMicroUsd;
68
+ stats.tokens += prepared.resourceLedger.spentTokens;
69
+ stats.turns += prepared.resourceLedger.spentTurns;
70
+ }
71
+ if (prepared.humanReviewRef.count > 0) {
72
+ stats.humanReview = {
73
+ count: prepared.humanReviewRef.count,
74
+ totalWaitMs: prepared.humanReviewRef.totalWaitMs,
75
+ gates: [...prepared.humanReviewRef.gates],
76
+ };
77
+ }
78
+ const reminderDisclosuresActive = Object.keys(prepared.reminderDisclosureCounts).length > 0;
79
+ if (rs.counters.finalVerifyInjections > 0 || rs.attach.attachmentsInjected > 0 || rs.counters.repetitionCuts > 0 || rs.counters.repetitionSpared > 0 || rs.counters.approachNoticesSent > 0 || reminderDisclosuresActive) {
80
+ stats.mechanisms = {
81
+ ...(rs.counters.finalVerifyInjections > 0 ? { finalVerifyInjected: true } : {}),
82
+ ...(rs.counters.finalVerifyInjections > 0 ? { finalVerifyInjections: rs.counters.finalVerifyInjections } : {}),
83
+ ...(rs.attach.attachmentsInjected > 0 ? { attachmentsInjected: rs.attach.attachmentsInjected } : {}),
84
+ ...(rs.counters.approachNoticesSent > 0 ? { approachNoticesSent: rs.counters.approachNoticesSent } : {}),
85
+ ...(rs.counters.repetitionCuts > 0 ? { repetitionCuts: rs.counters.repetitionCuts } : {}),
86
+ ...(rs.counters.repetitionSpared > 0 ? { repetitionSpared: rs.counters.repetitionSpared } : {}),
87
+ ...(rs.counters.repetitionEvents.length > 0 ? { repetitionEvents: rs.counters.repetitionEvents } : {}),
88
+ ...(reminderDisclosuresActive ? { reminderDisclosures: { ...prepared.reminderDisclosureCounts } } : {}),
89
+ };
90
+ }
91
+ const result = assembleResult(spec, prepared.sessionId, final, stats, {
92
+ threw,
93
+ runId: prepared.runId,
94
+ model: prepared.model.id,
95
+ unpricedSpend: rs.telemetry.unpricedSpend,
96
+ rewindNotes: prepared.rewindNotes,
97
+ editedFiles: prepared.editedFilesSnapshot(),
98
+ haltedOnUserRejection: prepared.batchHaltRef.current !== undefined,
99
+ userHalted: loopLatch.userHalted,
100
+ strandedHumanAnswers,
101
+ remoteEnvFailures: prepared.remoteEnvFailures,
102
+ effectiveReadFace: prepared.effectiveReadFace,
103
+ effectiveReadDenyPatterns: prepared.effectiveReadDenyPatterns,
104
+ effectiveMemoryScopes: prepared.effectiveMemoryScopes,
105
+ effectiveReasoning: reasoningResolution.current,
106
+ retryAfterMs: rs.limits.platformTerminal?.retryAfterMs,
107
+ abortedForTimeout: timeout.fired,
108
+ abortedForTurns: rs.limits.turnsExceeded,
109
+ abortedLive,
110
+ budgetHit: rs.limits.budgetHit,
111
+ budgetAxis: rs.limits.budgetAxis,
112
+ blockedReason: prepared.blockedRef.reason,
113
+ conflict: prepared.conflictRef.hit,
114
+ gitCoreOverBudget: prepared.gitStatusRef.terminalCode === "irreducible_core_over_budget",
115
+ outputInvalid: rs.degrade.outputInvalid,
116
+ suspendLoop: prepared.suspendLoopRef.hit,
117
+ ...(prepared.pausedRef.current !== undefined ? { paused: prepared.pausedRef.current.cause } : {}),
118
+ });
119
+ if (resume !== undefined &&
120
+ resume.pendingActionStarted !== true &&
121
+ result.terminal.kind !== "completed" &&
122
+ result.terminal.kind !== "paused" &&
123
+ !(resumeDecisionWasNegative(resume) && resume.decisionDelivered === true) &&
124
+ resume.onEnvRestoreFailed !== undefined) {
125
+ try {
126
+ await resume.onEnvRestoreFailed("env_failed");
127
+ amendTerminal(result, "resume.reopened_unstarted", (m) => `${m ?? "the resume leg did not complete"} — the approved action never started, so the checkpoint was REOPENED (pending): the same approval can be resumed again, do not ask for a fresh one`);
128
+ }
129
+ catch {
130
+ amendTerminal(result, "resume.reopen_failed_unstarted", (m) => `${m ?? "the resume leg did not complete"} — the approved action never started, and reopening the checkpoint FAILED: its state is unprovable from here. Re-read it before deciding; do NOT issue a fresh approval on the assumption the old one is dead`);
131
+ }
132
+ }
133
+ prepared.sealReadStateSeat?.(ownCommittedTailRef.current);
134
+ {
135
+ const wss = prepared.workspaceStateSettle;
136
+ if (wss !== undefined && result.terminal.kind !== "paused") {
137
+ const curCwd = prepared.cwdRef?.current !== undefined && prepared.cwdRef.current !== wss.rootCanonical ? prepared.cwdRef.current : undefined;
138
+ const curWt = prepared.worktreeSessionRef?.current;
139
+ const changed = curCwd !== wss.restoredHandsCwd || curWt?.worktreeDir !== wss.restoredWorktreeDir;
140
+ if (curCwd !== undefined || curWt !== undefined || changed || wss.baselineUnknown === true) {
141
+ await settleTeardownLeg(() => prepared.session.appendWorkspaceState({
142
+ taskRoot: wss.rootCanonical,
143
+ ...(curCwd !== undefined ? { handsCwd: curCwd } : {}),
144
+ ...(curWt !== undefined ? { activeWorktree: { ...curWt } } : {}),
145
+ }), "session.appendWorkspaceState (settle leg)", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
146
+ }
147
+ }
148
+ }
149
+ const stopFailureHook = (spec.hooks ?? runner.deps.hooks)?.stopFailure;
150
+ if (stopFailureHook !== undefined &&
151
+ result.terminal.kind === "failed" &&
152
+ final?.stopReason === "error" &&
153
+ !isDegenerateCutMessage(final) &&
154
+ threw === undefined &&
155
+ rs.limits.budgetHit === undefined &&
156
+ !rs.degrade.outputInvalid &&
157
+ !prepared.suspendLoopRef.hit &&
158
+ !abortedLive &&
159
+ result.terminal.code !== "conflict") {
160
+ const failedCause = result.terminal;
161
+ try {
162
+ const failureSeat = await runHookSeat("stopFailure", { timeoutMs: prepared.hookTimeoutMs }, (sig) => stopFailureHook({
163
+ identity: prepared.hookIdentity,
164
+ error: failedCause.message ?? "model error",
165
+ ...(failedCause.code !== undefined ? { errorKind: failedCause.code } : {}),
166
+ turns: stats.turns,
167
+ signal: sig,
168
+ }));
169
+ if (failureSeat.expired) {
170
+ try {
171
+ runner.deps.onError?.(hookSeatExpiredError("stopFailure", prepared.hookTimeoutMs, failureSeat.cause, "the terminal observation was abandoned; the assembled TaskResult is unchanged"), { phase: "hook", sessionId: prepared.sessionId });
172
+ }
173
+ catch {
174
+ }
175
+ }
176
+ }
177
+ catch (err) {
178
+ try {
179
+ runner.deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "hook", sessionId: prepared.sessionId });
180
+ }
181
+ catch {
182
+ }
183
+ }
184
+ }
185
+ const terminalFace = terminalProjection(result.terminal);
186
+ emitDelegationLifecycle({
187
+ phase: "terminal",
188
+ identity: prepared.hookIdentity,
189
+ status: terminalFace.status,
190
+ turns: stats.turns,
191
+ ...(terminalFace.errorCode !== undefined ? { errorCode: terminalFace.errorCode } : {}),
192
+ });
193
+ if (taskIdRef !== undefined)
194
+ taskIdRef.delegationTerminalOwed = undefined;
195
+ if (rs.degrade.degraded)
196
+ result.degraded = rs.degrade.degraded;
197
+ if (prepared.outputRef.set)
198
+ result.structuredOutput = prepared.outputRef.value;
199
+ const committedToken = prepared.pausedRef.current?.cause.token;
200
+ const committedScope = prepared.pausedRef.current?.scope;
201
+ if (committedToken !== undefined) {
202
+ const store = resolveCheckpointStore(spec, runner.deps);
203
+ if (store && committedScope !== undefined) {
204
+ onSuspend({
205
+ env: prepared.ownedEnv,
206
+ token: committedToken,
207
+ scope: committedScope,
208
+ store,
209
+ sessionId: prepared.sessionId,
210
+ });
211
+ }
212
+ }
213
+ if (resume && committedToken === undefined) {
214
+ await settleTeardownLeg(() => sessions.unpin?.(resume.cp.sessionId), "sessions.unpin (resumed-terminal leg)", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
215
+ }
216
+ if (prepared.memoryEngineSession) {
217
+ const mes = prepared.memoryEngineSession;
218
+ await settleTeardownLeg(() => mes.harvest(), "memoryEngineSession.harvest (settle leg)", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
219
+ }
220
+ emitTrace(rs.telemetry.tracer, () => ({
221
+ kind: "task.end",
222
+ version: 1,
223
+ taskId: rs.telemetry.taskId,
224
+ runId: rs.telemetry.runId,
225
+ status: terminalFace.status,
226
+ errorCode: terminalFace.errorCode,
227
+ turns: stats.turns,
228
+ tokens: stats.tokens,
229
+ ...(rs.telemetry.unpricedSpend ? {} : { costMicroUsd: stats.costMicroUsd }),
230
+ durationMs: Date.now() - rs.telemetry.taskStart,
231
+ ...(prepared.outputRef.set ? { hasStructuredOutput: true } : {}),
232
+ ...(stats.mechanisms !== undefined
233
+ ? { mechanisms: (({ repetitionEvents: _events, ...scalars }) => scalars)(stats.mechanisms) }
234
+ : {}),
235
+ ...(timeout.latenessMs !== undefined && timeout.latenessMs > TIMER_LATENESS_REPORT_MS
236
+ ? { timerLatenessMs: timeout.latenessMs }
237
+ : {}),
238
+ ...(rs.counters.walltimeSyncBackstopFired ? { walltimeSyncBackstop: true } : {}),
239
+ ts: Date.now(),
240
+ }));
241
+ setResult(result);
242
+ if (parentToolCallId !== undefined && result.terminal.kind !== "paused") {
243
+ const terminalTick = {
244
+ type: "task_progress",
245
+ taskId: rs.telemetry.taskId,
246
+ ...(internals?.delegationTaskType !== undefined ? { taskType: internals.delegationTaskType } : {}),
247
+ ...(internals?.cycleSeq !== undefined ? { seq: internals.cycleSeq } : {}),
248
+ ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
249
+ ...(subagentName ? { name: subagentName } : {}),
250
+ model: prepared.model.id,
251
+ usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
252
+ status: result.terminal.kind === "completed" ? "completed" : "failed",
253
+ ...ident(),
254
+ };
255
+ queue.push(terminalTick);
256
+ try {
257
+ internals?.onForwardEvent?.(terminalTick);
258
+ }
259
+ catch {
260
+ }
261
+ }
262
+ if (manualCompactRef.waiters.length > 0) {
263
+ queue.push({ type: "compaction_outcome", outcome: "mooted", trigger: "manual", reason: "task_ending", ...ident() });
264
+ drainManualCompact("mooted");
265
+ }
266
+ manualCompactRef.emitMooted = undefined;
267
+ if (forwardsSubagentEvents(spec))
268
+ await drainForwardedFramesBeforeDone();
269
+ queue.push({ type: "done", result });
270
+ queue.close();
271
+ await prepared.fileHistoryBoundary?.settle();
272
+ if (spec.suggestNextPrompts && result.terminal.kind === "completed") {
273
+ onSuggestions(suggestNextPrompts(spec, prepared, result));
274
+ }
275
+ }
276
+ finally {
277
+ const ownedEnvDying = prepared.ownedEnv !== undefined &&
278
+ hasDestroy(prepared.ownedEnv) &&
279
+ isIsolated(prepared.ownedEnv) &&
280
+ prepared.pausedRef.current === undefined;
281
+ await settleTeardownLeg(() => defaultTaskRegistry.settleKilledForOwner({ owner: spec.taskId ?? prepared.sessionId, scope: spec.principal ?? "default", sessionId: prepared.sessionId }, {
282
+ source: abortedLive &&
283
+ !timeout.fired &&
284
+ !rs.limits.turnsExceeded &&
285
+ rs.limits.budgetHit === undefined &&
286
+ prepared.pausedRef.current === undefined
287
+ ? "user"
288
+ : "parent",
289
+ skipSessionScoped: true,
290
+ envDying: ownedEnvDying,
291
+ retainProcesses: spec.retainBackgroundProcesses === true,
292
+ }), "taskRegistry.settleKilledForOwner", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
293
+ const bgEnv = prepared.ownedEnv ?? runner.deps.executionEnv;
294
+ const retainBgBySpec = spec.retainBackgroundProcesses === true && !ownedEnvDying;
295
+ if (bgEnv && hasBackgroundShell(bgEnv) && !retainBgBySpec) {
296
+ try {
297
+ const keepAlive = ownedEnvDying && bgEnv === prepared.ownedEnv
298
+ ? []
299
+ : [
300
+ ...defaultTaskRegistry.sessionResidentShellIds(prepared.sessionId, bgEnv),
301
+ ...defaultTaskRegistry.timeoutResidentShellIds(bgEnv),
302
+ ...defaultTaskRegistry.retainedShellIds(bgEnv),
303
+ ];
304
+ await settleTeardownLeg(() => sweepBackgroundShells(bgEnv, defaultTaskRegistry, keepAlive.length > 0 ? { except: keepAlive } : undefined), "sweepBackgroundShells", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
305
+ }
306
+ catch (e) {
307
+ runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId });
308
+ }
309
+ }
310
+ defaultTaskRegistry.clearBackgroundForOwner({
311
+ owner: spec.taskId ?? prepared.sessionId,
312
+ scope: spec.principal ?? "default",
313
+ });
314
+ defaultTaskRegistry.abortBackgroundAgentsForOwner({
315
+ owner: spec.taskId ?? prepared.sessionId,
316
+ scope: spec.principal ?? "default",
317
+ }, { skipSessionScoped: true });
318
+ if (prepared.subagentRetain) {
319
+ await settleTeardownLeg(() => prepared.subagentRetain.disposeAll(), "subagentRetain.disposeAll", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
320
+ }
321
+ await settleTeardownLeg(() => teardownOwnedEnv(prepared), "teardownOwnedEnv", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
322
+ }
323
+ return {};
324
+ }
@@ -0,0 +1,32 @@
1
+ import type { PushQueue } from "../push-queue.js";
2
+ import type { TaskEvent, TaskSpec } from "../types.js";
3
+ import type { Stats } from "./assemble-result.js";
4
+ import type { Prepared, RunnerDepsSeat, RunState } from "./contracts.js";
5
+ export interface RunStopAndFinalVerifyInput {
6
+ /** borrowed-readonly — the task spec: the hooks slot and the `finalVerification` opt-in. */
7
+ spec: TaskSpec;
8
+ /** borrowed-mutable — the leg's prepared seat: the stop gate is installed on its harness; the abort signal, the
9
+ * pause and halt holders, the output seat, the session, the hook bound and identity are read inside the gate. */
10
+ prepared: Prepared;
11
+ /** borrowed-mutable — the run's event queue: the injection echo frames. */
12
+ queue: PushQueue<TaskEvent>;
13
+ /** borrowed-mutable — the run's mutable state: the injection counter (`counters.finalVerifyInjections`) is bumped;
14
+ * the grounding latches, the write latch and the turn cap are read. */
15
+ rs: RunState;
16
+ /** borrowed-readonly — the run's usage counters, read by the headroom guards. */
17
+ stats: Stats;
18
+ /** borrowed-readonly — the run's event-identity mint (the echo frames). */
19
+ ident: () => {
20
+ eventId: string;
21
+ parentToolCallId?: string;
22
+ sourceTaskId?: string;
23
+ };
24
+ /** borrowed-readonly — the monotonic walltime deadline (the clock lane's), read by the budget fill. */
25
+ walltimeMonotonicDeadline: number | undefined;
26
+ /** borrowed-readonly — the Runner's deployment deps, read LIVE (`hooks` at resolution, `onError` in the gate). */
27
+ runner: RunnerDepsSeat;
28
+ }
29
+ /** Nothing comes back: the gate is INSTALLED on the borrowed harness, and the seat's closures are read by it alone. */
30
+ export interface RunStopAndFinalVerifyResult {
31
+ }
32
+ export declare function runStopAndFinalVerify(input: RunStopAndFinalVerifyInput): RunStopAndFinalVerifyResult;
@@ -0,0 +1,159 @@
1
+ import { openSystemReminder } from "../reminder-mint.js";
2
+ import { formatHookFeedback, hookSeatExpiredError, runHookSeat } from "../hooks.js";
3
+ const STOP_HOOK_BLOCK_CAP = 8;
4
+ export function runStopAndFinalVerify(input) {
5
+ const { spec, prepared, queue, rs, stats, ident, walltimeMonotonicDeadline, runner } = input;
6
+ const stopHook = (spec.hooks ?? runner.deps.hooks)?.stop;
7
+ const finalVerificationOn = spec.finalVerification === true;
8
+ const finalVerifyBudgetFill = () => {
9
+ let worst = 0;
10
+ if (rs.budget.maxTokensWindow !== undefined && rs.budget.maxTokensWindow > 0)
11
+ worst = Math.max(worst, stats.tokens / rs.budget.maxTokensWindow);
12
+ if (rs.budget.maxCostMicroUsd !== undefined && rs.budget.maxCostMicroUsd > 0)
13
+ worst = Math.max(worst, stats.costMicroUsd / rs.budget.maxCostMicroUsd);
14
+ if (walltimeMonotonicDeadline !== undefined && prepared.suspendForResource === undefined) {
15
+ const windowMs = walltimeMonotonicDeadline - rs.telemetry.taskStartMonotonic;
16
+ if (windowMs > 0)
17
+ worst = Math.max(worst, (performance.now() - rs.telemetry.taskStartMonotonic) / windowMs);
18
+ }
19
+ return worst;
20
+ };
21
+ const emitFinalVerifyEcho = (body) => {
22
+ queue.push({ type: "steering_injected", source: "final_verification", preview: body.slice(0, 220), ...ident() });
23
+ };
24
+ if (stopHook || finalVerificationOn) {
25
+ let consecutiveBlocks = 0;
26
+ prepared.harness.setStopGate(async () => {
27
+ if (prepared.abortController.signal.aborted || prepared.pausedRef.current !== undefined || prepared.batchHaltRef.current !== undefined)
28
+ return [];
29
+ if (finalVerificationOn &&
30
+ (rs.counters.finalVerifyInjections === 0 || (rs.counters.finalVerifyInjections === 1 && rs.counters.groundingSignalPreR9 && !rs.counters.groundingSignalPostR9)) &&
31
+ rs.counters.wroteThisRun &&
32
+ prepared.outputRef.set !== true &&
33
+ !(rs.limits.effectiveMaxTurns !== undefined && rs.limits.effectiveMaxTurns > 0 && stats.turns >= rs.limits.effectiveMaxTurns - 1) &&
34
+ finalVerifyBudgetFill() < 0.9) {
35
+ rs.counters.finalVerifyInjections += 1;
36
+ if (rs.counters.finalVerifyInjections === 2) {
37
+ const reentryBody = openSystemReminder(prepared.reminderMark) +
38
+ "[final verification] Your tool calls in this run worked with raw bytes, structural parsing, " +
39
+ "or checksum/digest computation — the deliverable very likely embeds verifiable structure (structural fields, an " +
40
+ "embedded checksum-family value, reference data it must match, or a replayable deterministic path). You MUST " +
41
+ "execute the grounding check that structure supports — recompute the embedded value and compare it against the " +
42
+ "declared one, re-parse the structure from the raw bytes and reconcile it with your output, compare against the " +
43
+ "reference data, or replay the deterministic path — and REPORT the check's concrete result before finishing. " +
44
+ "A closing statement without a reported check result is not verification. If you already ran such a check, state " +
45
+ "its concrete result now; if the check mismatches, fix the deliverable first. This is the final reminder from " +
46
+ "this verification gate — it will not intervene again.</system-reminder>";
47
+ emitFinalVerifyEcho(reentryBody);
48
+ return [
49
+ {
50
+ role: "user",
51
+ engineMinted: true,
52
+ content: reentryBody,
53
+ timestamp: Date.now(),
54
+ },
55
+ ];
56
+ }
57
+ const nudgeBody = openSystemReminder(prepared.reminderMark) +
58
+ "[final verification] Before finishing: re-verify the FINAL deliverable through its REAL entry point, " +
59
+ "exactly as the acceptance criteria would exercise it — execute the binary/function/endpoint directly and read the ACTUAL " +
60
+ "output and exit code. Do NOT rely on earlier self-tests, shell redirections, or assumptions (a program that prints to " +
61
+ "stdout is not a program that writes the required file). If anything mismatches the task's requirements, fix it before " +
62
+ "finishing. " +
63
+ "Treat verification writes as state-harmless: when the deliverable itself is a persisted final " +
64
+ "state (for example a committed or pushed file, a deployed artifact, or a required output file), " +
65
+ "do not change that state merely to test it. This constrains HOW you verify — it is never a " +
66
+ "license to skip the real acceptance path or to check a substitute of your own making: expected " +
67
+ "values must come from the task's requirements, never from content you generated. If the real " +
68
+ "acceptance path requires a write, use disposable inputs or an isolated target, end in the exact " +
69
+ "required final state, and verify that final state before finishing. " +
70
+ "If the work relied on a third-party API, library, or model, check the usage contract the object itself declares " +
71
+ "(docstrings, metadata, configuration — e.g. prompt conventions shipped with a model) and confirm your calls follow " +
72
+ "it rather than a default symmetric usage. Verify not only that the deliverable EXISTS but that the METHOD that " +
73
+ "produced it matches the task's requirements. " +
74
+ "Choose the verification SURFACE deliberately: check against the reference data, oracle, or evaluation tooling the " +
75
+ "task itself provides — re-running your own implementation and getting the same answer is self-consistency, not " +
76
+ "correctness — and cross-check through an independent second path where the task or environment offers one " +
77
+ "(checksums, runtime artifacts). Verify the PERSISTED artifact — re-read what is actually on disk or committed, " +
78
+ "not in-memory state — against every hard constraint from the original task text (numeric bounds, allowed-value " +
79
+ "lists, naming semantics, required files), reconciling whole-set completeness: nothing missing, nothing duplicated. " +
80
+ "If the deliverable embeds verifiable structure — structural fields, an embedded checksum-family value, " +
81
+ "reference data it must match, or a replayable deterministic path — you MUST execute the grounding check " +
82
+ "that structure supports and REPORT its concrete result in your closing summary: for such a deliverable, " +
83
+ "no reported check result means the work is not finished. " +
84
+ "If the task produced neither an executable deliverable nor any verifiable structure or acceptance " +
85
+ "oracle to check against, briefly confirm completion and stop. " +
86
+ "Residue YOUR OWN testing created (scratch files, running processes, generated outputs the task does not ask for) " +
87
+ "is not protected state — if the task's required final state is a clean target, removing your own residue is part " +
88
+ "of delivering it. " +
89
+ "Verification must never LAUNDER uncertainty: if part of your conclusion was uncertain before this check, keep " +
90
+ "reporting it as uncertain unless the check you actually ran resolved it — a re-stated conclusion is not new " +
91
+ "evidence.</system-reminder>";
92
+ emitFinalVerifyEcho(nudgeBody);
93
+ return [
94
+ {
95
+ role: "user",
96
+ engineMinted: true,
97
+ content: nudgeBody,
98
+ timestamp: Date.now(),
99
+ },
100
+ ];
101
+ }
102
+ if (!stopHook)
103
+ return [];
104
+ let result;
105
+ try {
106
+ const stopSeat = await runHookSeat("stop", { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal, abortEnds: true }, (sig) => stopHook({
107
+ stopHookActive: consecutiveBlocks > 0,
108
+ consecutiveBlocks,
109
+ getBranch: () => prepared.session.getBranch(),
110
+ identity: prepared.hookIdentity,
111
+ signal: sig,
112
+ }));
113
+ if (stopSeat.expired) {
114
+ if (stopSeat.cause === "timeout") {
115
+ runner.deps.onError?.(hookSeatExpiredError("stop", prepared.hookTimeoutMs, stopSeat.cause, "the run was allowed to END (the seat's own no-opinion answer); no pushback and no additional context were injected"), { phase: "hook", sessionId: prepared.sessionId });
116
+ }
117
+ consecutiveBlocks = 0;
118
+ return [];
119
+ }
120
+ result = stopSeat.value;
121
+ }
122
+ catch (err) {
123
+ runner.deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "hook", sessionId: prepared.sessionId });
124
+ return [];
125
+ }
126
+ const messages = [];
127
+ if (result?.additionalContext) {
128
+ messages.push({
129
+ role: "user",
130
+ engineMinted: true,
131
+ content: formatHookFeedback(`Stop hook additional context: ${result.additionalContext}`, prepared.reminderMark),
132
+ timestamp: Date.now(),
133
+ });
134
+ }
135
+ if (messages.length === 0 && !result?.block) {
136
+ consecutiveBlocks = 0;
137
+ return messages;
138
+ }
139
+ consecutiveBlocks++;
140
+ const cap = STOP_HOOK_BLOCK_CAP;
141
+ if (consecutiveBlocks > cap) {
142
+ runner.deps.onError?.(new Error(`a Stop hook kept the turn from ending ${consecutiveBlocks} consecutive times — overriding and ending the run. ` +
143
+ `Both a block and an additionalContext-only push-back count (CC 2.1.220). ` +
144
+ `Check ctx.stopHookActive in the hook and return success while it's true.`), { phase: "hook", sessionId: prepared.sessionId });
145
+ return [];
146
+ }
147
+ if (result?.block) {
148
+ messages.push({
149
+ role: "user",
150
+ engineMinted: true,
151
+ content: formatHookFeedback(`Stop hook stopped continuation: ${result.block}`, prepared.reminderMark),
152
+ timestamp: Date.now(),
153
+ });
154
+ }
155
+ return messages;
156
+ });
157
+ }
158
+ return {};
159
+ }
@@ -0,0 +1,38 @@
1
+ import { type ModelPricing } from "../pricing.js";
2
+ import type { SessionStore } from "../session.js";
3
+ import type { RunnerDeps, TaskSpec } from "../types.js";
4
+ import type { Stats } from "./assemble-result.js";
5
+ import type { Prepared, ResumeRun, RunnerDepsSeat, RunState, TaskIdRef } from "./contracts.js";
6
+ export interface RunTelemetryAndBudgetSeatsInput {
7
+ /** borrowed-readonly — the task spec: limits, degrade, tracer, output retries, the resource-suspend totals. */
8
+ spec: TaskSpec;
9
+ /** borrowed-mutable — the leg's prepared seat: `liveSpendRef.get` is installed, `humanReviewRef` and `planModeRef`
10
+ * are written on a resume; the model, the ledger, the session id and the usage governance are read. */
11
+ prepared: Prepared;
12
+ /** borrowed-readonly — the resume plan when this leg resumes a checkpoint (the human-review latency, the plan re-arm). */
13
+ resume: ResumeRun | undefined;
14
+ /** borrowed-readonly — the run's usage counters (this leg's own spend), read by the live-spend and budget closures. */
15
+ stats: Stats;
16
+ /** borrowed-mutable — the stream layer's backstop carrier: the effective task id and session id are published here. */
17
+ taskIdRef: TaskIdRef | undefined;
18
+ /** borrowed-readonly — the invocation's frozen tracer (#499), absence included; undefined when the seat was omitted. */
19
+ entryTracer: {
20
+ tracer: TaskSpec["tracer"];
21
+ } | undefined;
22
+ /** borrowed-readonly — the model catalog generation this task resolved against (pinned before prepare for swapDeps). */
23
+ modelCatalog: RunnerDeps["models"];
24
+ /** borrowed-readonly — the run's canonical task id (`spec.taskId ?? sessionId`), shared with the identity mint. */
25
+ runSourceTaskId: string;
26
+ /** borrowed-mutable — the run's mutable state: this lane is the WRITER of the `telemetry`, `degrade`, `limits` and
27
+ * `budget` groups' initial values and function members. */
28
+ rs: RunState;
29
+ /** borrowed-readonly — the Runner's deployment deps, read LIVE (`pricing`, `onError`, `tracer`). */
30
+ runner: RunnerDepsSeat;
31
+ /** borrowed-readonly — the Runner's session store: the run is recorded on it (`noteTaskRun`, best-effort, loud). */
32
+ sessions: SessionStore;
33
+ }
34
+ export interface RunTelemetryAndBudgetSeatsResult {
35
+ /** #462: the price-table door — a table adopted MID-RUN that cannot price marks the spend unpriced. */
36
+ noteUnevaluablePriceTable: (p: ModelPricing) => void;
37
+ }
38
+ export declare function runTelemetryAndBudgetSeats(input: RunTelemetryAndBudgetSeatsInput): RunTelemetryAndBudgetSeatsResult;