@sema-agent/core 7.11.2 → 7.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +86 -13
  2. package/dist/core/ask-origin.d.ts +55 -0
  3. package/dist/core/ask-origin.js +21 -0
  4. package/dist/core/auto-mode-arming.d.ts +10 -14
  5. package/dist/core/auto-mode-arming.js +3 -9
  6. package/dist/core/auto-mode-defaults.d.ts +0 -2
  7. package/dist/core/auto-mode-defaults.js +0 -1
  8. package/dist/core/auto-mode-rebuild.d.ts +6 -13
  9. package/dist/core/auto-mode-rebuild.js +0 -2
  10. package/dist/core/auto-mode.d.ts +30 -89
  11. package/dist/core/auto-mode.js +12 -59
  12. package/dist/core/checkpoint-store.d.ts +1 -3
  13. package/dist/core/engine-notice.d.ts +18 -0
  14. package/dist/core/gate-fold.js +1 -9
  15. package/dist/core/gate-lanes.d.ts +0 -40
  16. package/dist/core/gate-lanes.js +18 -26
  17. package/dist/core/governance-codes.d.ts +1 -1
  18. package/dist/core/governance-codes.js +2 -0
  19. package/dist/core/hooks.d.ts +7 -1
  20. package/dist/core/hooks.js +1 -1
  21. package/dist/core/runner/assemble-result.d.ts +45 -1
  22. package/dist/core/runner/assemble-result.js +4 -1
  23. package/dist/core/runner/compaction-seams.d.ts +42 -0
  24. package/dist/core/runner/compaction-seams.js +80 -0
  25. package/dist/core/runner/contracts.d.ts +85 -15
  26. package/dist/core/runner/denial-limit-arms.d.ts +10 -13
  27. package/dist/core/runner/denial-limit-arms.js +12 -9
  28. package/dist/core/runner/gate-exit.js +9 -1
  29. package/dist/core/runner/permission-rule-lanes.d.ts +2 -1
  30. package/dist/core/runner/permission-rule-lanes.js +2 -1
  31. package/dist/core/runner/prepare-caps-and-workflow.d.ts +1 -1
  32. package/dist/core/runner/prepare-caps-and-workflow.js +0 -5
  33. package/dist/core/runner/prepare-policy-chain.js +3 -2
  34. package/dist/core/runner/prepare-protocol-tools.js +5 -0
  35. package/dist/core/runner/prepare-safety-scan.js +11 -16
  36. package/dist/core/runner/prepare-suspend-saga.d.ts +0 -2
  37. package/dist/core/runner/prepare-suspend-saga.js +2 -10
  38. package/dist/core/runner/prepare-task.d.ts +9 -1
  39. package/dist/core/runner/prepare-task.js +7 -2
  40. package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
  41. package/dist/core/runner/prepare-wiring-manifest.js +1 -8
  42. package/dist/core/runner/resume-claim.d.ts +2 -2
  43. package/dist/core/runner/resume-preflight.d.ts +2 -2
  44. package/dist/core/runner/run-attachment-seats.d.ts +6 -4
  45. package/dist/core/runner/run-attachment-seats.js +2 -2
  46. package/dist/core/runner/run-git-lane.d.ts +1 -1
  47. package/dist/core/runner/run-harness-handlers.js +2 -0
  48. package/dist/core/runner/run-identity-wiring.d.ts +21 -33
  49. package/dist/core/runner/run-identity-wiring.js +6 -3
  50. package/dist/core/runner/run-leg.d.ts +108 -0
  51. package/dist/core/runner/run-leg.js +713 -0
  52. package/dist/core/runner/run-notification-lane.d.ts +56 -0
  53. package/dist/core/runner/run-notification-lane.js +128 -0
  54. package/dist/core/runner/run-reasoning-seat.d.ts +5 -5
  55. package/dist/core/runner/run-reasoning-seat.js +7 -7
  56. package/dist/core/runner/run-recovery-lanes.d.ts +4 -17
  57. package/dist/core/runner/run-recovery-lanes.js +5 -4
  58. package/dist/core/runner/run-settle-and-teardown.d.ts +113 -0
  59. package/dist/core/runner/run-settle-and-teardown.js +432 -0
  60. package/dist/core/runner/run-stop-and-final-verify.d.ts +2 -2
  61. package/dist/core/runner/run-terminal-adoption.d.ts +99 -0
  62. package/dist/core/runner/run-terminal-adoption.js +200 -0
  63. package/dist/core/runner/run-turn-boundary.js +5 -4
  64. package/dist/core/runner/runtask.d.ts +10 -79
  65. package/dist/core/runner/runtask.js +83 -1557
  66. package/dist/core/runner/stream-lifecycle-verbs.js +10 -1
  67. package/dist/core/runner-deps.d.ts +4 -14
  68. package/dist/core/store-contracts/workflow-journal-store-contract.d.ts +7 -0
  69. package/dist/core/store-contracts/workflow-journal-store-contract.js +85 -0
  70. package/dist/core/task-stream.d.ts +10 -1
  71. package/dist/core/tool-face.d.ts +8 -0
  72. package/dist/core/tool-face.js +1 -0
  73. package/dist/core/tool-policy.d.ts +42 -96
  74. package/dist/core/tool-policy.js +1 -11
  75. package/dist/core/tool-registry.d.ts +11 -3
  76. package/dist/core/tool-registry.js +7 -1
  77. package/dist/core/tool-roster.d.ts +26 -0
  78. package/dist/core/tool-roster.js +38 -7
  79. package/dist/core/trace.d.ts +6 -7
  80. package/dist/core/wiring-manifest.d.ts +5 -22
  81. package/dist/core/wiring-manifest.js +3 -11
  82. package/dist/core/workflow-journal-store.d.ts +35 -4
  83. package/dist/core/workflow-journal-store.js +19 -2
  84. package/dist/index.d.ts +6 -5
  85. package/dist/index.js +6 -5
  86. package/dist/orchestration/workflow.js +2 -0
  87. package/dist/server/http.js +2 -1
  88. package/dist/stores/file/workflow-journal-store.d.ts +7 -10
  89. package/dist/stores/file/workflow-journal-store.js +2 -4
  90. package/package.json +1 -1
  91. package/test/export-surface.snapshot.json +21 -11
@@ -0,0 +1,432 @@
1
+ import { isSyntheticApiErrorMessage } from "../../internal/harness.js";
2
+ import { adjudicateDerivedRoute, fallbackToPrimaryNotice, sameRouteIdentity } from "../../brain/route-adjudicator.js";
3
+ import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
4
+ import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
5
+ import { resolveCheckpointStore } from "../checkpoint-store.js";
6
+ import { hookSeatExpiredError, runHookSeat } from "../hooks.js";
7
+ import { modelCostToPricing } from "../pricing.js";
8
+ import { hasDestroy, isIsolated } from "../remote-env.js";
9
+ import { resolveTaskModel } from "../roles.js";
10
+ import { defaultTaskRegistry } from "../task-registry.js";
11
+ import { emitTrace } from "../trace.js";
12
+ import { deliverEngineNotice } from "../types.js";
13
+ import { assembleResult } from "./assemble-result.js";
14
+ import { TIMER_LATENESS_REPORT_MS } from "./clock-and-limits.js";
15
+ import { resumeDecisionWasNegative } from "./decide-continuation.js";
16
+ import { drainForwardedFramesBeforeDone, forwardsSubagentEvents } from "./prepare-run-refs.js";
17
+ import { generatePromptSuggestions } from "./prompt-suggestions.js";
18
+ import { settleTeardownLeg } from "./teardown-bounded.js";
19
+ import { amendTerminal, terminalProjection } from "./terminal-projection.js";
20
+ export async function runSettleAndTeardown(input) {
21
+ const { spec, queue, prepared, resume, internals, rs, stats, ident, parentToolCallId, subagentName, loopLatch, timeout, ownCommittedTailRef, emitDelegationLifecycle, taskIdRef, manualCompactRef, drainManualCompact, final, threw, abortedLive, userInterruptedLive, answerSettled, strandedHumanAnswers, reasoningResolution, comp, setResult, onSuggestions, onSuspend, runner, sessions } = input;
22
+ try {
23
+ if (comp?.compacted) {
24
+ queue.push({
25
+ type: "compacted",
26
+ trigger: "auto",
27
+ tokensBefore: comp.tokensBefore ?? 0,
28
+ ...(comp.postTriggerTokens !== undefined ? { tokensAfter: comp.postTriggerTokens } : {}),
29
+ ...(comp.triggerTokens !== undefined ? { triggerTokensBefore: comp.triggerTokens } : {}),
30
+ ...(comp.durationMs !== undefined ? { durationMs: comp.durationMs } : {}),
31
+ ...(comp.phaseDurations !== undefined ? { phaseDurations: comp.phaseDurations } : {}),
32
+ ...(comp.firstKeptEntryId !== undefined ? { preserved_segment: { firstKeptEntryId: comp.firstKeptEntryId } } : {}),
33
+ ...(comp.attachedFiles !== undefined ? { attachedFiles: comp.attachedFiles } : {}),
34
+ ...(comp.modelFallback ? { modelFallback: true } : {}),
35
+ ...(comp.fallbackReason !== undefined ? { fallbackReason: comp.fallbackReason } : {}),
36
+ ...(comp.clampedRatio !== undefined ? { clampedRatio: comp.clampedRatio } : {}),
37
+ ...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
38
+ ...ident(),
39
+ });
40
+ if (comp.phaseDurations !== undefined && comp.durationMs !== undefined) {
41
+ const pd = comp.phaseDurations;
42
+ const pdDur = comp.durationMs;
43
+ emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.phase_timings", version: 1, taskId: rs.telemetry.taskId, ...pd, durationMs: pdDur, ts: Date.now() }));
44
+ }
45
+ prepared.cacheBreakDetector?.notifyCompaction();
46
+ }
47
+ if (prepared.nestedStats.tasks > 0) {
48
+ stats.nested = {
49
+ tokens: prepared.nestedStats.tokens,
50
+ turns: prepared.nestedStats.turns,
51
+ tasks: prepared.nestedStats.tasks,
52
+ ...(prepared.nestedStats.anyUnpriced ? {} : { costMicroUsd: prepared.nestedStats.costMicroUsd }),
53
+ };
54
+ }
55
+ if (stats.totalInputTokens > 0) {
56
+ const rawHit = stats.cachedTokens / stats.totalInputTokens;
57
+ if (stats.cachedTokens > stats.totalInputTokens) {
58
+ 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 });
59
+ }
60
+ stats.cacheHitRate = Math.min(1, rawHit);
61
+ if (!rs.telemetry.cacheBreakReported && stats.turns >= 2 && stats.totalInputTokens >= 8000 && stats.cacheHitRate < 0.15) {
62
+ const cacheWritten = stats.cacheWriteTokens + stats.cacheWriteTokensLong;
63
+ const cause = rs.degrade.degraded
64
+ ? `a mid-task model switch (${rs.degrade.degraded.from} → ${rs.degrade.degraded.to}, degradation) reset the prefix cache — this is the likely cause`
65
+ : cacheWritten > 0
66
+ ? `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`
67
+ : `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`;
68
+ 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 });
69
+ }
70
+ }
71
+ if (prepared.resourceLedger &&
72
+ (resume?.cp.gate.kind === "resource_limit" || prepared.pausedRef.current?.cause.gate.kind === "resource_limit")) {
73
+ stats.costMicroUsd += prepared.resourceLedger.spentMicroUsd;
74
+ stats.tokens += prepared.resourceLedger.spentTokens;
75
+ stats.turns += prepared.resourceLedger.spentTurns;
76
+ }
77
+ if (prepared.humanReviewRef.count > 0) {
78
+ stats.humanReview = {
79
+ count: prepared.humanReviewRef.count,
80
+ totalWaitMs: prepared.humanReviewRef.totalWaitMs,
81
+ gates: [...prepared.humanReviewRef.gates],
82
+ };
83
+ }
84
+ const reminderDisclosuresActive = Object.keys(prepared.reminderDisclosureCounts).length > 0;
85
+ if (rs.counters.finalVerifyInjections > 0 || rs.attach.attachmentsInjected > 0 || rs.counters.repetitionCuts > 0 || rs.counters.repetitionSpared > 0 || rs.counters.approachNoticesSent > 0 || reminderDisclosuresActive) {
86
+ stats.mechanisms = {
87
+ ...(rs.counters.finalVerifyInjections > 0 ? { finalVerifyInjected: true } : {}),
88
+ ...(rs.counters.finalVerifyInjections > 0 ? { finalVerifyInjections: rs.counters.finalVerifyInjections } : {}),
89
+ ...(rs.attach.attachmentsInjected > 0 ? { attachmentsInjected: rs.attach.attachmentsInjected } : {}),
90
+ ...(rs.counters.approachNoticesSent > 0 ? { approachNoticesSent: rs.counters.approachNoticesSent } : {}),
91
+ ...(rs.counters.repetitionCuts > 0 ? { repetitionCuts: rs.counters.repetitionCuts } : {}),
92
+ ...(rs.counters.repetitionSpared > 0 ? { repetitionSpared: rs.counters.repetitionSpared } : {}),
93
+ ...(rs.counters.repetitionEvents.length > 0 ? { repetitionEvents: rs.counters.repetitionEvents } : {}),
94
+ ...(reminderDisclosuresActive ? { reminderDisclosures: { ...prepared.reminderDisclosureCounts } } : {}),
95
+ };
96
+ }
97
+ const result = assembleResult(spec, prepared.sessionId, final, stats, {
98
+ threw,
99
+ runId: prepared.runId,
100
+ model: prepared.model.id,
101
+ unpricedSpend: rs.telemetry.unpricedSpend,
102
+ rewindNotes: prepared.rewindNotes,
103
+ editedFiles: prepared.editedFilesSnapshot(),
104
+ haltedOnUserRejection: prepared.batchHaltRef.current !== undefined,
105
+ userHalted: loopLatch.userHalted,
106
+ strandedHumanAnswers,
107
+ remoteEnvFailures: prepared.remoteEnvFailures,
108
+ effectiveReadFace: prepared.effectiveReadFace,
109
+ effectiveReadDenyPatterns: prepared.effectiveReadDenyPatterns,
110
+ effectiveMemoryScopes: prepared.effectiveMemoryScopes,
111
+ effectiveReasoning: reasoningResolution.current,
112
+ retryAfterMs: rs.limits.platformTerminal?.retryAfterMs,
113
+ abortedForTimeout: timeout.fired,
114
+ abortedForTurns: rs.limits.turnsExceeded,
115
+ abortedLive,
116
+ answerSettled,
117
+ budgetHit: rs.limits.budgetHit,
118
+ budgetAxis: rs.limits.budgetAxis,
119
+ blockedReason: prepared.blockedRef.reason,
120
+ conflict: prepared.conflictRef.hit,
121
+ gitCoreOverBudget: prepared.gitStatusRef.terminalCode === "irreducible_core_over_budget",
122
+ outputInvalid: rs.degrade.outputInvalid,
123
+ suspendLoop: prepared.suspendLoopRef.hit,
124
+ ...(prepared.pausedRef.current !== undefined ? { paused: prepared.pausedRef.current.cause } : {}),
125
+ });
126
+ if (abortedLive && answerSettled && result.terminal.kind === "completed") {
127
+ deliverEngineNotice(runner.deps.onNotice, {
128
+ code: "task.interrupt_unconsumed",
129
+ message: "a stop arrived after the run had already given its answer: nothing was cut or stopped by it — the answer stands " +
130
+ "as the result, the run ends completed, and no interruption marker or limit code records the stop.",
131
+ detail: {
132
+ sessionId: prepared.sessionId,
133
+ runId: rs.telemetry.runId,
134
+ ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}),
135
+ origin: userInterruptedLive ? "interrupt" : timeout.fired ? "walltime" : rs.limits.turnsExceeded ? "turns" : "signal",
136
+ },
137
+ });
138
+ }
139
+ if (resume !== undefined &&
140
+ resume.pendingActionStarted !== true &&
141
+ result.terminal.kind !== "completed" &&
142
+ result.terminal.kind !== "paused" &&
143
+ !(resumeDecisionWasNegative(resume) && resume.decisionDelivered === true) &&
144
+ resume.onEnvRestoreFailed !== undefined) {
145
+ try {
146
+ await resume.onEnvRestoreFailed("env_failed");
147
+ 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`);
148
+ }
149
+ catch {
150
+ 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`);
151
+ }
152
+ }
153
+ prepared.sealReadStateSeat?.(ownCommittedTailRef.current);
154
+ {
155
+ const wss = prepared.workspaceStateSettle;
156
+ if (wss !== undefined && result.terminal.kind !== "paused") {
157
+ const curCwd = prepared.cwdRef?.current !== undefined && prepared.cwdRef.current !== wss.rootCanonical ? prepared.cwdRef.current : undefined;
158
+ const curWt = prepared.worktreeSessionRef?.current;
159
+ const changed = curCwd !== wss.restoredHandsCwd || curWt?.worktreeDir !== wss.restoredWorktreeDir;
160
+ if (curCwd !== undefined || curWt !== undefined || changed || wss.baselineUnknown === true) {
161
+ await settleTeardownLeg(() => prepared.session.appendWorkspaceState({
162
+ taskRoot: wss.rootCanonical,
163
+ ...(curCwd !== undefined ? { handsCwd: curCwd } : {}),
164
+ ...(curWt !== undefined ? { activeWorktree: { ...curWt } } : {}),
165
+ }), "session.appendWorkspaceState (settle leg)", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
166
+ }
167
+ }
168
+ }
169
+ const stopFailureHook = (spec.hooks ?? runner.deps.hooks)?.stopFailure;
170
+ if (stopFailureHook !== undefined &&
171
+ result.terminal.kind === "failed" &&
172
+ final?.stopReason === "error" &&
173
+ !isDegenerateCutMessage(final) &&
174
+ threw === undefined &&
175
+ rs.limits.budgetHit === undefined &&
176
+ !rs.degrade.outputInvalid &&
177
+ !prepared.suspendLoopRef.hit &&
178
+ !abortedLive &&
179
+ result.terminal.code !== "conflict") {
180
+ const failedCause = result.terminal;
181
+ try {
182
+ const failureSeat = await runHookSeat("stopFailure", { timeoutMs: prepared.hookTimeoutMs }, (sig) => stopFailureHook({
183
+ identity: prepared.hookIdentity,
184
+ error: failedCause.message ?? "model error",
185
+ ...(failedCause.code !== undefined ? { errorKind: failedCause.code } : {}),
186
+ turns: stats.turns,
187
+ signal: sig,
188
+ }));
189
+ if (failureSeat.expired) {
190
+ try {
191
+ runner.deps.onError?.(hookSeatExpiredError("stopFailure", prepared.hookTimeoutMs, failureSeat.cause, "the terminal observation was abandoned; the assembled TaskResult is unchanged"), { phase: "hook", sessionId: prepared.sessionId });
192
+ }
193
+ catch {
194
+ }
195
+ }
196
+ }
197
+ catch (err) {
198
+ try {
199
+ runner.deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "hook", sessionId: prepared.sessionId });
200
+ }
201
+ catch {
202
+ }
203
+ }
204
+ }
205
+ const terminalFace = terminalProjection(result.terminal);
206
+ emitDelegationLifecycle({
207
+ phase: "terminal",
208
+ identity: prepared.hookIdentity,
209
+ status: terminalFace.status,
210
+ turns: stats.turns,
211
+ ...(terminalFace.errorCode !== undefined ? { errorCode: terminalFace.errorCode } : {}),
212
+ });
213
+ if (taskIdRef !== undefined)
214
+ taskIdRef.delegationTerminalOwed = undefined;
215
+ if (rs.degrade.degraded)
216
+ result.degraded = rs.degrade.degraded;
217
+ if (prepared.outputRef.set)
218
+ result.structuredOutput = prepared.outputRef.value;
219
+ const committedToken = prepared.pausedRef.current?.cause.token;
220
+ const committedScope = prepared.pausedRef.current?.scope;
221
+ if (committedToken !== undefined) {
222
+ const store = resolveCheckpointStore(spec, runner.deps);
223
+ if (store && committedScope !== undefined) {
224
+ onSuspend({
225
+ env: prepared.ownedEnv,
226
+ token: committedToken,
227
+ scope: committedScope,
228
+ store,
229
+ sessionId: prepared.sessionId,
230
+ });
231
+ }
232
+ }
233
+ if (resume && committedToken === undefined) {
234
+ await settleTeardownLeg(() => sessions.unpin?.(resume.cp.sessionId), "sessions.unpin (resumed-terminal leg)", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
235
+ }
236
+ if (prepared.memoryEngineSession) {
237
+ const mes = prepared.memoryEngineSession;
238
+ await settleTeardownLeg(() => mes.harvest(), "memoryEngineSession.harvest (settle leg)", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
239
+ }
240
+ emitTrace(rs.telemetry.tracer, () => ({
241
+ kind: "task.end",
242
+ version: 1,
243
+ taskId: rs.telemetry.taskId,
244
+ runId: rs.telemetry.runId,
245
+ status: terminalFace.status,
246
+ errorCode: terminalFace.errorCode,
247
+ turns: stats.turns,
248
+ tokens: stats.tokens,
249
+ ...(rs.telemetry.unpricedSpend ? {} : { costMicroUsd: stats.costMicroUsd }),
250
+ durationMs: Date.now() - rs.telemetry.taskStart,
251
+ ...(prepared.outputRef.set ? { hasStructuredOutput: true } : {}),
252
+ ...(stats.mechanisms !== undefined
253
+ ? { mechanisms: (({ repetitionEvents: _events, ...scalars }) => scalars)(stats.mechanisms) }
254
+ : {}),
255
+ ...(timeout.latenessMs !== undefined && timeout.latenessMs > TIMER_LATENESS_REPORT_MS
256
+ ? { timerLatenessMs: timeout.latenessMs }
257
+ : {}),
258
+ ...(rs.counters.walltimeSyncBackstopFired ? { walltimeSyncBackstop: true } : {}),
259
+ ts: Date.now(),
260
+ }));
261
+ setResult(result);
262
+ if (parentToolCallId !== undefined && result.terminal.kind !== "paused") {
263
+ const terminalTick = {
264
+ type: "task_progress",
265
+ taskId: rs.telemetry.taskId,
266
+ ...(internals?.delegationTaskType !== undefined ? { taskType: internals.delegationTaskType } : {}),
267
+ ...(internals?.cycleSeq !== undefined ? { seq: internals.cycleSeq } : {}),
268
+ ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
269
+ ...(subagentName ? { name: subagentName } : {}),
270
+ model: prepared.model.id,
271
+ usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
272
+ status: result.terminal.kind === "completed" ? "completed" : "failed",
273
+ ...ident(),
274
+ };
275
+ queue.push(terminalTick);
276
+ try {
277
+ internals?.onForwardEvent?.(terminalTick);
278
+ }
279
+ catch {
280
+ }
281
+ }
282
+ if (manualCompactRef.waiters.length > 0) {
283
+ queue.push({ type: "compaction_outcome", outcome: "mooted", trigger: "manual", reason: "task_ending", ...ident() });
284
+ drainManualCompact("mooted");
285
+ }
286
+ manualCompactRef.emitMooted = undefined;
287
+ if (forwardsSubagentEvents(spec))
288
+ await drainForwardedFramesBeforeDone();
289
+ queue.push({ type: "done", result });
290
+ queue.close();
291
+ await prepared.fileHistoryBoundary?.settle();
292
+ if (spec.suggestNextPrompts && result.terminal.kind === "completed" && !abortedLive) {
293
+ onSuggestions(suggestNextPrompts(spec, prepared, result, runner));
294
+ }
295
+ }
296
+ finally {
297
+ const ownedEnvDying = prepared.ownedEnv !== undefined &&
298
+ hasDestroy(prepared.ownedEnv) &&
299
+ isIsolated(prepared.ownedEnv) &&
300
+ prepared.pausedRef.current === undefined;
301
+ await settleTeardownLeg(() => defaultTaskRegistry.settleKilledForOwner({ owner: spec.taskId ?? prepared.sessionId, scope: spec.principal ?? "default", sessionId: prepared.sessionId }, {
302
+ source: abortedLive &&
303
+ !answerSettled &&
304
+ !timeout.fired &&
305
+ !rs.limits.turnsExceeded &&
306
+ rs.limits.budgetHit === undefined &&
307
+ prepared.pausedRef.current === undefined
308
+ ? "user"
309
+ : "parent",
310
+ skipSessionScoped: true,
311
+ envDying: ownedEnvDying,
312
+ retainProcesses: spec.retainBackgroundProcesses === true,
313
+ }), "taskRegistry.settleKilledForOwner", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
314
+ const bgEnv = prepared.ownedEnv ?? runner.deps.executionEnv;
315
+ const retainBgBySpec = spec.retainBackgroundProcesses === true && !ownedEnvDying;
316
+ if (bgEnv && hasBackgroundShell(bgEnv) && !retainBgBySpec) {
317
+ try {
318
+ const keepAlive = ownedEnvDying && bgEnv === prepared.ownedEnv
319
+ ? []
320
+ : [
321
+ ...defaultTaskRegistry.sessionResidentShellIds(prepared.sessionId, bgEnv),
322
+ ...defaultTaskRegistry.timeoutResidentShellIds(bgEnv),
323
+ ...defaultTaskRegistry.retainedShellIds(bgEnv),
324
+ ];
325
+ await settleTeardownLeg(() => sweepBackgroundShells(bgEnv, defaultTaskRegistry, keepAlive.length > 0 ? { except: keepAlive } : undefined), "sweepBackgroundShells", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
326
+ }
327
+ catch (e) {
328
+ runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId });
329
+ }
330
+ }
331
+ defaultTaskRegistry.clearBackgroundForOwner({
332
+ owner: spec.taskId ?? prepared.sessionId,
333
+ scope: spec.principal ?? "default",
334
+ });
335
+ defaultTaskRegistry.abortBackgroundAgentsForOwner({
336
+ owner: spec.taskId ?? prepared.sessionId,
337
+ scope: spec.principal ?? "default",
338
+ }, { skipSessionScoped: true });
339
+ if (prepared.subagentRetain) {
340
+ await settleTeardownLeg(() => prepared.subagentRetain.disposeAll(), "subagentRetain.disposeAll", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
341
+ }
342
+ await settleTeardownLeg(() => teardownOwnedEnv(prepared, runner), "teardownOwnedEnv", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
343
+ }
344
+ return {};
345
+ }
346
+ const SUGGESTIONS_DEFAULT_COUNT = 3;
347
+ const SUGGESTIONS_MAX_COUNT = 8;
348
+ const SUGGESTIONS_TIMEOUT_MS = 30_000;
349
+ const SUGGESTIONS_TRANSCRIPT_MESSAGES = 12;
350
+ async function suggestNextPrompts(spec, prepared, result, runner) {
351
+ const cfg = typeof spec.suggestNextPrompts === "object" ? spec.suggestNextPrompts : {};
352
+ const count = cfg.count && cfg.count > 0 ? Math.min(cfg.count, SUGGESTIONS_MAX_COUNT) : SUGGESTIONS_DEFAULT_COUNT;
353
+ const ac = new AbortController();
354
+ let timer;
355
+ const timeout = new Promise((resolve) => {
356
+ timer = setTimeout(() => {
357
+ ac.abort();
358
+ resolve([]);
359
+ }, SUGGESTIONS_TIMEOUT_MS);
360
+ });
361
+ const work = (async () => {
362
+ let model = prepared.model;
363
+ let thinking = prepared.thinking;
364
+ try {
365
+ const resolved = resolveTaskModel({ modelRole: cfg.role ?? "summarize", roles: spec.roles }, runner.deps);
366
+ model = resolved.model;
367
+ thinking = resolved.thinking;
368
+ }
369
+ catch (roleErr) {
370
+ if (cfg.role !== undefined) {
371
+ let asked;
372
+ try {
373
+ asked = String(cfg.role);
374
+ }
375
+ catch {
376
+ asked = `<unrenderable ${typeof cfg.role}>`;
377
+ }
378
+ try {
379
+ runner.deps.onError?.(new Error(`suggestNextPrompts.role ${JSON.stringify(asked.length > 80 ? `${asked.slice(0, 80)}…` : asked)} did not resolve to a model — the prompt-suggestion pass ran on the task's own model (${prepared.model.id}) instead. Configure that role in RunnerDeps.roles / TaskSpec.roles, or omit the field to use the "summarize" role.`, { cause: roleErr }), { phase: "suggestions", sessionId: prepared.sessionId });
380
+ }
381
+ catch {
382
+ }
383
+ }
384
+ }
385
+ if (!sameRouteIdentity(model, prepared.model)) {
386
+ const verdict = await adjudicateDerivedRoute({ brain: runner.deps.brain, model, getApiKeyAndHeaders: spec.getApiKeyAndHeaders });
387
+ if (verdict !== undefined && !verdict.ok) {
388
+ deliverEngineNotice(runner.deps.onNotice, fallbackToPrimaryNotice({ seat: "prompt-suggestions", from: model.id, to: prepared.model.id, verdict, sessionId: prepared.sessionId, runId: prepared.runId }));
389
+ model = prepared.model;
390
+ thinking = prepared.thinking;
391
+ }
392
+ }
393
+ const pricing = runner.deps.pricing?.[model.id] ?? modelCostToPricing(model.cost);
394
+ const ctx = await prepared.session.buildContext();
395
+ const transcript = ctx.messages.filter((m) => !isSyntheticApiErrorMessage(m)).slice(-SUGGESTIONS_TRANSCRIPT_MESSAGES);
396
+ const out = await generatePromptSuggestions({ brain: runner.deps.brain, model, pricing, thinking, getApiKeyAndHeaders: spec.getApiKeyAndHeaders, signal: ac.signal }, transcript, count);
397
+ if (out.tokens > 0 || out.costMicroUsd > 0) {
398
+ result.stats.suggestions = { tokens: out.tokens, costMicroUsd: out.costMicroUsd };
399
+ if (result.stats.costBreakdown) {
400
+ result.stats.costBreakdown.suggestionsMicroUsd = out.costMicroUsd;
401
+ }
402
+ }
403
+ return out.suggestions;
404
+ })();
405
+ work.catch(() => { });
406
+ try {
407
+ return await Promise.race([work, timeout]);
408
+ }
409
+ catch (err) {
410
+ try {
411
+ runner.deps.onError?.(err, { phase: "suggestions", sessionId: prepared.sessionId });
412
+ }
413
+ catch {
414
+ }
415
+ return [];
416
+ }
417
+ finally {
418
+ clearTimeout(timer);
419
+ }
420
+ }
421
+ async function teardownOwnedEnv(prepared, runner) {
422
+ if (prepared.ownedEnv &&
423
+ hasDestroy(prepared.ownedEnv) &&
424
+ prepared.pausedRef.current === undefined) {
425
+ try {
426
+ await prepared.ownedEnv.destroy();
427
+ }
428
+ catch (err) {
429
+ runner.deps.onError?.(err, { phase: "config", sessionId: prepared.sessionId });
430
+ }
431
+ }
432
+ }
@@ -1,7 +1,7 @@
1
1
  import type { PushQueue } from "../push-queue.js";
2
2
  import type { TaskEvent, TaskSpec } from "../types.js";
3
3
  import type { Stats } from "./assemble-result.js";
4
- import type { Prepared, RunnerDepsSeat, RunState } from "./contracts.js";
4
+ import type { InstallingPhaseResult, Prepared, RunnerDepsSeat, RunState } from "./contracts.js";
5
5
  export interface RunStopAndFinalVerifyInput {
6
6
  /** borrowed-readonly — the task spec: the hooks slot and the `finalVerification` opt-in. */
7
7
  spec: TaskSpec;
@@ -27,6 +27,6 @@ export interface RunStopAndFinalVerifyInput {
27
27
  runner: RunnerDepsSeat;
28
28
  }
29
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 {
30
+ export interface RunStopAndFinalVerifyResult extends InstallingPhaseResult {
31
31
  }
32
32
  export declare function runStopAndFinalVerify(input: RunStopAndFinalVerifyInput): RunStopAndFinalVerifyResult;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * design/393 S6 — the run body's TERMINAL ADOPTION (R14), verbatim from `Runner.runLocked`, between the leg's finally and
3
+ * the settle: the typed causes lifted over the leg's anonymous end (the brain-call guardrail's stall, the tool gate's own
4
+ * stop, the platform lane's external cause — which alone overrides a real throw), the usage ledger's FINAL charge with its
5
+ * slow-charge disclosure, the live-abort orphan reconcile and its synthetic `tool_end` frames, the held session-write
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` — 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).
10
+ *
11
+ * Async (six first-level awaits, the same awaits at the same positions). It ends by entering the settle lane (`next`) in
12
+ * its LAST continuation — the settle's first read sits on the tick `finish` resolved on, as it did in the one-function
13
+ * body — and returns what it returns. What crosses the boundary is exactly what the settle reads of this lane's work: the
14
+ * ADOPTED cause (`threw`, possibly lifted here) and the compaction outcome (`comp`); the pause / interruption / reconcile
15
+ * facts are consumed here.
16
+ */
17
+ import type { AssistantMessage } from "../../internal/llm.js";
18
+ import type { Model } from "../../internal/llm.js";
19
+ import { type MaybeCompactOptions } from "../auto-compaction.js";
20
+ import type { SessionStore } from "../session.js";
21
+ import type { PushQueue } from "../push-queue.js";
22
+ import { type Brain, type TaskEvent, type TaskSpec } from "../types.js";
23
+ import { type Stats } from "./assemble-result.js";
24
+ import type { EndOfTaskCompaction, Prepared, RunnerDepsSeat, RunState } from "./contracts.js";
25
+ export interface RunTerminalAdoptionInput<R> {
26
+ /** borrowed-readonly — the task spec: the task id the account's notices name; `finish`'s first argument (the end-of-task pass below). */
27
+ spec: TaskSpec;
28
+ /** borrowed-mutable — the run's event queue: the reconcile's synthetic `tool_end` frames and the end-of-task compaction's failure frame. */
29
+ queue: PushQueue<TaskEvent>;
30
+ /** borrowed-mutable — the leg's prepared seat: the guardrail / gate-stop / paused refs are read, the usage ledger charged, the session
31
+ * reconciled and the marker appended, the harness's parkable inputs read and dropped. */
32
+ prepared: Prepared;
33
+ /** borrowed-readonly — the run's mutable state: the platform terminal, the limit flags, the attachment state and the telemetry seat. */
34
+ rs: RunState;
35
+ /** borrowed-readonly — this leg's usage counters (the final charge's cumulative total). */
36
+ stats: Stats;
37
+ /** borrowed-readonly — the run's event-identity mint (the synthetic `tool_end` frames, the compaction failure frame). */
38
+ ident: () => {
39
+ eventId: string;
40
+ parentToolCallId?: string;
41
+ sourceTaskId?: string;
42
+ };
43
+ /** borrowed-readonly — the `message_committed` mint (the reconcile's appended toolResults, the marker). */
44
+ emitCommitted: (entryId: string, role: "user" | "assistant" | "toolResult", toolCallId?: string) => void;
45
+ /** borrowed-readonly — the observed tool-start ids the reconcile splits orphans by (EMPTIED by the reconcile's commit). */
46
+ startedToolCallIds: Set<string>;
47
+ /** borrowed-readonly — the label map the synthetic `tool_end` frames carry. */
48
+ toolLabels: Map<string, string>;
49
+ /** borrowed-readonly — the hard walltime timer: `fired` is one of the interruption facts. */
50
+ timeout: {
51
+ readonly fired: boolean;
52
+ };
53
+ /** borrowed-readonly — the compaction machinery's accounted brain (the end-of-task summary call is accounted too). */
54
+ compactionBrain: Brain;
55
+ /** borrowed-readonly — the shared compaction breaker: an open breaker skips the end-of-task attempt. */
56
+ compactionBreaker: {
57
+ readonly failures: number;
58
+ };
59
+ /** borrowed-readonly — the window-safety builder the end-of-task lane shares with the boundary / PTL lanes. */
60
+ windowSafetyOptions: (mainModel: Model) => Pick<MaybeCompactOptions, "fallbackBudget" | "onWindowSafety">;
61
+ /** borrowed-readonly — the held agent_end account, as the read face on the identity lane's Result (backlog #389). */
62
+ undrainedUserAtEnd: {
63
+ readonly current: {
64
+ steer: number;
65
+ followUp: number;
66
+ } | undefined;
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
+ };
73
+ /** borrowed-readonly — the leg lane's final assistant message (its `stopReason` is one of the interruption facts). */
74
+ final: AssistantMessage | undefined;
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. */
76
+ threw: unknown;
77
+ /** borrowed-readonly — the live-abort flag the leg captured before its release-abort. */
78
+ abortedLive: boolean;
79
+ /** borrowed-readonly — the leg's snapshot of `loopLatch.userInterrupted` (gates the interruption marker). */
80
+ userInterruptedLive: boolean;
81
+ /** borrowed-readonly — the Runner's deployment deps, read LIVE (`onError`, `checkpointStore`, `onNotice`); the one home of why it is
82
+ * a seat and not a captured object is {@link RunnerDepsSeat}. */
83
+ runner: RunnerDepsSeat;
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;
86
+ /** borrowed-readonly — the settle lane, entered in this lane's last continuation with the adopted cause and the compaction outcome. */
87
+ next: (adopted: RunTerminalAdoptionResult) => Promise<R>;
88
+ }
89
+ /** What the settle reads of this lane's work. */
90
+ export interface RunTerminalAdoptionResult {
91
+ /** The ADOPTED terminal cause: the leg's throw, or the typed cause lifted over it here (guardrail stall / gate stop / platform terminal). */
92
+ threw: unknown;
93
+ /** The end-of-task compaction pass's outcome. */
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
+ }
99
+ export declare function runTerminalAdoption<R>(input: RunTerminalAdoptionInput<R>): Promise<R>;