@sema-agent/core 7.11.2 → 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.
- package/CHANGELOG.md +42 -13
- package/dist/core/auto-mode-arming.d.ts +10 -14
- package/dist/core/auto-mode-arming.js +3 -9
- package/dist/core/auto-mode-defaults.d.ts +0 -2
- package/dist/core/auto-mode-defaults.js +0 -1
- package/dist/core/auto-mode-rebuild.d.ts +6 -13
- package/dist/core/auto-mode-rebuild.js +0 -2
- package/dist/core/auto-mode.d.ts +30 -89
- package/dist/core/auto-mode.js +12 -59
- package/dist/core/checkpoint-store.d.ts +1 -3
- package/dist/core/gate-fold.js +1 -9
- package/dist/core/gate-lanes.js +15 -9
- package/dist/core/hooks.d.ts +6 -0
- package/dist/core/runner/contracts.d.ts +41 -9
- package/dist/core/runner/denial-limit-arms.d.ts +10 -13
- package/dist/core/runner/denial-limit-arms.js +9 -7
- package/dist/core/runner/gate-exit.js +9 -1
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.js +0 -5
- package/dist/core/runner/prepare-suspend-saga.d.ts +0 -2
- package/dist/core/runner/prepare-suspend-saga.js +2 -10
- package/dist/core/runner/prepare-task.js +1 -1
- package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
- package/dist/core/runner/prepare-wiring-manifest.js +1 -8
- package/dist/core/runner/run-attachment-seats.d.ts +4 -2
- package/dist/core/runner/run-attachment-seats.js +2 -2
- package/dist/core/runner/run-identity-wiring.d.ts +14 -33
- package/dist/core/runner/run-identity-wiring.js +4 -3
- package/dist/core/runner/run-leg.d.ts +106 -0
- package/dist/core/runner/run-leg.js +462 -0
- package/dist/core/runner/run-notification-lane.d.ts +55 -0
- package/dist/core/runner/run-notification-lane.js +128 -0
- package/dist/core/runner/run-reasoning-seat.d.ts +5 -5
- package/dist/core/runner/run-reasoning-seat.js +7 -7
- package/dist/core/runner/run-settle-and-teardown.d.ts +109 -0
- package/dist/core/runner/run-settle-and-teardown.js +324 -0
- package/dist/core/runner/run-terminal-adoption.d.ts +99 -0
- package/dist/core/runner/run-terminal-adoption.js +120 -0
- package/dist/core/runner/runtask.d.ts +14 -3
- package/dist/core/runner/runtask.js +55 -1010
- package/dist/core/runner-deps.d.ts +4 -14
- package/dist/core/store-contracts/workflow-journal-store-contract.d.ts +7 -0
- package/dist/core/store-contracts/workflow-journal-store-contract.js +85 -0
- package/dist/core/tool-policy.d.ts +37 -93
- package/dist/core/tool-policy.js +1 -11
- package/dist/core/trace.d.ts +6 -7
- package/dist/core/wiring-manifest.d.ts +5 -22
- package/dist/core/wiring-manifest.js +3 -11
- package/dist/core/workflow-journal-store.d.ts +35 -4
- package/dist/core/workflow-journal-store.js +19 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/orchestration/workflow.js +2 -0
- package/dist/stores/file/workflow-journal-store.d.ts +7 -10
- package/dist/stores/file/workflow-journal-store.js +2 -4
- package/package.json +1 -1
- 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,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`).
|
|
8
|
+
*
|
|
9
|
+
* Async (six first-level awaits, the same awaits at the same positions). It ends by entering the settle lane (`next`) in
|
|
10
|
+
* its LAST continuation — the settle's first read sits on the tick `finish` resolved on, as it did in the one-function
|
|
11
|
+
* body — and returns what it returns. What crosses the boundary is exactly what the settle reads of this lane's work: the
|
|
12
|
+
* ADOPTED cause (`threw`, possibly lifted here) and the compaction outcome (`comp`); the pause / interruption / reconcile
|
|
13
|
+
* facts are consumed here.
|
|
14
|
+
*/
|
|
15
|
+
import type { AssistantMessage } from "../../internal/llm.js";
|
|
16
|
+
import type { Model } from "../../internal/llm.js";
|
|
17
|
+
import type { MaybeCompactOptions } from "../auto-compaction.js";
|
|
18
|
+
import type { PushQueue } from "../push-queue.js";
|
|
19
|
+
import { type Brain, type TaskEvent, type TaskSpec } from "../types.js";
|
|
20
|
+
import { type Stats } from "./assemble-result.js";
|
|
21
|
+
import type { EndOfTaskCompaction, Prepared, RunnerDepsSeat, RunState } from "./contracts.js";
|
|
22
|
+
export interface RunTerminalAdoptionInput<R> {
|
|
23
|
+
/** borrowed-readonly — the task spec: the task id the account's notices name; `finish`'s first argument. */
|
|
24
|
+
spec: TaskSpec;
|
|
25
|
+
/** borrowed-mutable — the run's event queue: the reconcile's synthetic `tool_end` frames and the end-of-task compaction's failure frame. */
|
|
26
|
+
queue: PushQueue<TaskEvent>;
|
|
27
|
+
/** borrowed-mutable — the leg's prepared seat: the guardrail / gate-stop / paused refs are read, the usage ledger charged, the session
|
|
28
|
+
* reconciled and the marker appended, the harness's parkable inputs read and dropped. */
|
|
29
|
+
prepared: Prepared;
|
|
30
|
+
/** borrowed-readonly — the run's mutable state: the platform terminal, the limit flags, the attachment state and the telemetry seat. */
|
|
31
|
+
rs: RunState;
|
|
32
|
+
/** borrowed-readonly — this leg's usage counters (the final charge's cumulative total). */
|
|
33
|
+
stats: Stats;
|
|
34
|
+
/** borrowed-readonly — the run's event-identity mint (the synthetic `tool_end` frames, the compaction failure frame). */
|
|
35
|
+
ident: () => {
|
|
36
|
+
eventId: string;
|
|
37
|
+
parentToolCallId?: string;
|
|
38
|
+
sourceTaskId?: string;
|
|
39
|
+
};
|
|
40
|
+
/** borrowed-readonly — the `message_committed` mint (the reconcile's appended toolResults, the marker). */
|
|
41
|
+
emitCommitted: (entryId: string, role: "user" | "assistant" | "toolResult", toolCallId?: string) => void;
|
|
42
|
+
/** borrowed-readonly — the observed tool-start ids the reconcile splits orphans by (EMPTIED by the reconcile's commit). */
|
|
43
|
+
startedToolCallIds: Set<string>;
|
|
44
|
+
/** borrowed-readonly — the label map the synthetic `tool_end` frames carry. */
|
|
45
|
+
toolLabels: Map<string, string>;
|
|
46
|
+
/** borrowed-readonly — the hard walltime timer: `fired` is one of the interruption facts. */
|
|
47
|
+
timeout: {
|
|
48
|
+
readonly fired: boolean;
|
|
49
|
+
};
|
|
50
|
+
/** borrowed-readonly — the compaction machinery's accounted brain (the end-of-task summary call is accounted too). */
|
|
51
|
+
compactionBrain: Brain;
|
|
52
|
+
/** borrowed-readonly — the shared compaction breaker: an open breaker skips the end-of-task attempt. */
|
|
53
|
+
compactionBreaker: {
|
|
54
|
+
readonly failures: number;
|
|
55
|
+
};
|
|
56
|
+
/** borrowed-readonly — the window-safety builder the end-of-task lane shares with the boundary / PTL lanes. */
|
|
57
|
+
windowSafetyOptions: (mainModel: Model) => Pick<MaybeCompactOptions, "fallbackBudget" | "onWindowSafety">;
|
|
58
|
+
/** borrowed-readonly — the held agent_end account, as the read face on the identity lane's Result (backlog #389). */
|
|
59
|
+
undrainedUserAtEnd: {
|
|
60
|
+
readonly current: {
|
|
61
|
+
steer: number;
|
|
62
|
+
followUp: number;
|
|
63
|
+
} | undefined;
|
|
64
|
+
};
|
|
65
|
+
/** borrowed-readonly — the leg lane's final assistant message (its `stopReason` is one of the interruption facts). */
|
|
66
|
+
final: AssistantMessage | undefined;
|
|
67
|
+
/** 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. */
|
|
68
|
+
threw: unknown;
|
|
69
|
+
/** borrowed-readonly — the live-abort flag the leg captured before its release-abort. */
|
|
70
|
+
abortedLive: boolean;
|
|
71
|
+
/** borrowed-readonly — the leg's snapshot of `loopLatch.userInterrupted` (gates the interruption marker). */
|
|
72
|
+
userInterruptedLive: boolean;
|
|
73
|
+
/** borrowed-readonly — the Runner's deployment deps, read LIVE (`onError`, `checkpointStore`, `onNotice`); the one home of why it is
|
|
74
|
+
* a seat and not a captured object is {@link RunnerDepsSeat}. */
|
|
75
|
+
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>;
|
|
89
|
+
/** borrowed-readonly — the settle lane, entered in this lane's last continuation with the adopted cause and the compaction outcome. */
|
|
90
|
+
next: (adopted: RunTerminalAdoptionResult) => Promise<R>;
|
|
91
|
+
}
|
|
92
|
+
/** What the settle reads of this lane's work. */
|
|
93
|
+
export interface RunTerminalAdoptionResult {
|
|
94
|
+
/** The ADOPTED terminal cause: the leg's throw, or the typed cause lifted over it here (guardrail stall / gate stop / platform terminal). */
|
|
95
|
+
threw: unknown;
|
|
96
|
+
/** The end-of-task compaction pass's outcome. */
|
|
97
|
+
comp: EndOfTaskCompaction;
|
|
98
|
+
}
|
|
99
|
+
export declare function runTerminalAdoption<R>(input: RunTerminalAdoptionInput<R>): Promise<R>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
|
|
2
|
+
import { appendInterruptionMarker, reconcileInterruptedSession } from "../session-reconcile.js";
|
|
3
|
+
import { emitTrace } from "../trace.js";
|
|
4
|
+
import { deliverEngineNotice, undrainedUserInputNotices } from "../types.js";
|
|
5
|
+
import { errorCodeOf } from "./assemble-result.js";
|
|
6
|
+
import { awaitChargeWithSlowDisclosure } from "./clock-and-limits.js";
|
|
7
|
+
import { MAX_CONSECUTIVE_COMPACTION_FAILURES } from "./compaction-knobs.js";
|
|
8
|
+
import { reconciledToolEndBody } from "./tool-end-body.js";
|
|
9
|
+
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;
|
|
11
|
+
let { threw } = input;
|
|
12
|
+
if (threw === undefined && prepared.brainCallGuardrailRef.timedOut !== undefined) {
|
|
13
|
+
threw = prepared.brainCallGuardrailRef.timedOut;
|
|
14
|
+
}
|
|
15
|
+
if (threw === undefined && prepared.gateStopRef.terminal !== undefined) {
|
|
16
|
+
threw = prepared.gateStopRef.terminal;
|
|
17
|
+
}
|
|
18
|
+
if (rs.limits.platformTerminal !== undefined) {
|
|
19
|
+
threw = rs.limits.platformTerminal;
|
|
20
|
+
}
|
|
21
|
+
if (prepared.usageGovernance !== undefined) {
|
|
22
|
+
try {
|
|
23
|
+
await awaitChargeWithSlowDisclosure(prepared.usageGovernance.commit(stats.tokens, rs.telemetry.unpricedSpend ? undefined : stats.costMicroUsd, Date.now()), () => runner.deps.onError?.(new Error("the usage ledger's FINAL charge has not settled after 10s — still waiting (a charge is never abandoned: walking away would double-charge on a later flush). A wedged ledger store wedges this teardown, now visibly."), { phase: "config", sessionId: prepared.sessionId }));
|
|
24
|
+
}
|
|
25
|
+
catch (flushErr) {
|
|
26
|
+
runner.deps.onError?.(flushErr, { phase: "config", sessionId: prepared.sessionId });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const committedPause = prepared.pausedRef.current;
|
|
30
|
+
const durablyPaused = committedPause !== undefined;
|
|
31
|
+
const interrupted = !durablyPaused &&
|
|
32
|
+
(threw !== undefined || timeout.fired || rs.limits.turnsExceeded || rs.limits.budgetHit !== undefined || abortedLive || final?.stopReason === "aborted");
|
|
33
|
+
let orphansClosed = 0;
|
|
34
|
+
let reconcileComplete = false;
|
|
35
|
+
if (interrupted) {
|
|
36
|
+
try {
|
|
37
|
+
const report = await reconcileInterruptedSession(prepared.session, prepared.toolEffects, undefined, startedToolCallIds);
|
|
38
|
+
orphansClosed = report.recovered.length;
|
|
39
|
+
for (const orphan of report.recovered) {
|
|
40
|
+
queue.push({ type: "tool_end", toolCallId: orphan.toolCallId, toolName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), isError: true, ...reconciledToolEndBody(orphan, prepared.structuredProjector), ...ident() });
|
|
41
|
+
emitCommitted(orphan.entryId, "toolResult", orphan.toolCallId);
|
|
42
|
+
}
|
|
43
|
+
reconcileComplete = true;
|
|
44
|
+
}
|
|
45
|
+
catch (reconcileErr) {
|
|
46
|
+
runner.deps.onError?.(reconcileErr instanceof Error ? reconcileErr : new Error(String(reconcileErr)), {
|
|
47
|
+
phase: "interrupt-reconcile",
|
|
48
|
+
sessionId: prepared.sessionId,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
await prepared.harness.flushQueuedSessionWrites();
|
|
53
|
+
}
|
|
54
|
+
catch (flushErr) {
|
|
55
|
+
runner.deps.onError?.(flushErr instanceof Error ? flushErr : new Error(String(flushErr)), {
|
|
56
|
+
phase: "interrupt-reconcile",
|
|
57
|
+
sessionId: prepared.sessionId,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (interrupted && userInterruptedLive && reconcileComplete) {
|
|
62
|
+
try {
|
|
63
|
+
const entryId = await appendInterruptionMarker(prepared.session, { toolUseInFlight: orphansClosed > 0 });
|
|
64
|
+
emitCommitted(entryId, "user");
|
|
65
|
+
}
|
|
66
|
+
catch (markerErr) {
|
|
67
|
+
runner.deps.onError?.(markerErr instanceof Error ? markerErr : new Error(String(markerErr)), {
|
|
68
|
+
phase: "interrupt-reconcile",
|
|
69
|
+
sessionId: prepared.sessionId,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
let migratedParked = { steer: 0, followUp: 0 };
|
|
74
|
+
if (committedPause !== undefined && runner.deps.checkpointStore !== undefined) {
|
|
75
|
+
const parkToken = committedPause.cause.token;
|
|
76
|
+
const parkScope = committedPause.scope;
|
|
77
|
+
const store = runner.deps.checkpointStore;
|
|
78
|
+
const carried = [];
|
|
79
|
+
for (const record of prepared.harness.readParkableUserInputs()) {
|
|
80
|
+
try {
|
|
81
|
+
if (!(await store.setPendingSteer(parkToken, parkScope, record)))
|
|
82
|
+
break;
|
|
83
|
+
carried.push(record);
|
|
84
|
+
}
|
|
85
|
+
catch (parkErr) {
|
|
86
|
+
runner.deps.onError?.(parkErr instanceof Error ? parkErr : new Error(String(parkErr)), {
|
|
87
|
+
phase: "config",
|
|
88
|
+
sessionId: prepared.sessionId,
|
|
89
|
+
});
|
|
90
|
+
const parkCode = errorCodeOf(parkErr);
|
|
91
|
+
if (parkCode === "steering.invalid_content" || parkCode === "steering.queue_full")
|
|
92
|
+
continue;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
migratedParked = prepared.harness.dropParkedUserInputs(carried);
|
|
97
|
+
}
|
|
98
|
+
if (undrainedUserAtEnd.current !== undefined) {
|
|
99
|
+
const remaining = {
|
|
100
|
+
steer: Math.max(0, undrainedUserAtEnd.current.steer - migratedParked.steer),
|
|
101
|
+
followUp: Math.max(0, undrainedUserAtEnd.current.followUp - migratedParked.followUp),
|
|
102
|
+
};
|
|
103
|
+
for (const notice of undrainedUserInputNotices(remaining, spec.taskId ?? prepared.sessionId, prepared.sessionId, prepared.runId)) {
|
|
104
|
+
deliverEngineNotice(runner.deps.onNotice, notice);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (rs.attach.attachState?.postCompactPending === true && (rs.attach.attachmentsCfg?.backgroundTasks ?? eventDefaultOn("background_tasks")) === true) {
|
|
108
|
+
emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.announce_dropped", version: 1, taskId: rs.telemetry.taskId, ts: Date.now() }));
|
|
109
|
+
}
|
|
110
|
+
const comp = await finish(spec, prepared, {
|
|
111
|
+
telemetry: { tracer: rs.telemetry.tracer, taskId: rs.telemetry.taskId },
|
|
112
|
+
skipCompaction: durablyPaused || compactionBreaker.failures >= MAX_CONSECUTIVE_COMPACTION_FAILURES,
|
|
113
|
+
minTokens: rs.counters.compactionFloor,
|
|
114
|
+
brain: compactionBrain,
|
|
115
|
+
windowSafety: windowSafetyOptions(prepared.model),
|
|
116
|
+
onCompactionFailed: (reason) => queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "auto", reason, ...ident() }),
|
|
117
|
+
cancelled: abortedLive,
|
|
118
|
+
});
|
|
119
|
+
return await next({ threw, comp });
|
|
120
|
+
}
|
|
@@ -21,9 +21,6 @@ export declare class Runner implements RunnerSelfSeat {
|
|
|
21
21
|
* authority: a durable resume's checkpoint seed wins on its leg, and a miss (cross-process,
|
|
22
22
|
* evicted) falls back to the transcript-replay rung. See {@link SessionReadFileStates}. */
|
|
23
23
|
private readonly sessionReadStates;
|
|
24
|
-
/** #616 — the per-session breaker read face's record (see {@link RunInternals.autoModeBreakerLedger}). Runner-lived:
|
|
25
|
-
* a trip in one leg is what the session's next leg reports. */
|
|
26
|
-
private readonly autoModeBreakerLedger;
|
|
27
24
|
/** task notifications born BETWEEN turns (run torn down / harness already idle), parked
|
|
28
25
|
* per session and drained into the session's next run at its first boundary. Runner-lived (outlives any
|
|
29
26
|
* single run, like the registry handles that produce into it); bounded + drop-disclosing, see
|
|
@@ -136,6 +133,20 @@ export declare class Runner implements RunnerSelfSeat {
|
|
|
136
133
|
* happen. Iterating just observes events (the buffer is backpressure-free); `.result()` awaits completion. */
|
|
137
134
|
runTaskStream(spec: TaskSpec, resume?: ResumeRun, internals?: RunInternals): TaskStream;
|
|
138
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;
|
|
139
150
|
/**
|
|
140
151
|
* design/100 §E12 — the post-completion prompt-suggestion pass. Never throws (failures route to
|
|
141
152
|
* `onError(phase:"suggestions")` and resolve to `[]`), so it is fire-and-forget safe and `suggestions()`
|