@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,159 @@
1
+ import { remainingBudgetMicroUsd, remainingTokens } from "../checkpoint-store.js";
2
+ import { computeCostMicroUsd, isModelPriced, malformedPricingField, modelCostToPricing } from "../pricing.js";
3
+ import { resolveModel } from "../roles.js";
4
+ import { emitTrace } from "../trace.js";
5
+ import { resolveMaxTurns } from "./clock-and-limits.js";
6
+ import { resolveOutputRetries } from "./synthetic-tools.js";
7
+ import { cacheFamilyOf } from "./usage-accounting.js";
8
+ export function runTelemetryAndBudgetSeats(input) {
9
+ const { spec, prepared, resume, stats, taskIdRef, entryTracer, modelCatalog, runSourceTaskId, rs, runner, sessions } = input;
10
+ prepared.liveSpendRef.get = () => ({ costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) });
11
+ if (resume &&
12
+ resume.cp.suspendedAt !== undefined &&
13
+ (resume.cp.gate.kind === "human" ||
14
+ resume.cp.gate.kind === "irreversible_ask" ||
15
+ resume.cp.gate.kind === "needs_review" ||
16
+ resume.cp.gate.kind === "plan_review")) {
17
+ const waitMs = Math.max(0, prepared.now() - resume.cp.suspendedAt);
18
+ const decision = resume.outcome.gate === "wake" ? undefined : resume.outcome.decision;
19
+ prepared.humanReviewRef.count += 1;
20
+ prepared.humanReviewRef.totalWaitMs += waitMs;
21
+ const gateToolName = "toolName" in resume.cp.gate ? resume.cp.gate.toolName : undefined;
22
+ prepared.humanReviewRef.gates.push({ kind: resume.cp.gate.kind, waitMs, ...(decision !== undefined ? { decision } : {}), ...(gateToolName !== undefined ? { toolName: gateToolName } : {}) });
23
+ }
24
+ if (resume !== undefined && resume.outcome.gate === "plan_review" && resume.outcome.decision === "reject") {
25
+ prepared.planModeRef.active = true;
26
+ }
27
+ rs.telemetry.cacheFamily = cacheFamilyOf(prepared.model);
28
+ rs.telemetry.pricing = runner.deps.pricing?.[prepared.model.id] ?? modelCostToPricing(prepared.model.cost);
29
+ rs.telemetry.pricingConfigured = isModelPriced(prepared.model, runner.deps.pricing);
30
+ if (spec.limits?.degrade) {
31
+ try {
32
+ rs.degrade.degradeToModel = resolveModel(spec.limits.degrade.to, modelCatalog);
33
+ }
34
+ catch (e) {
35
+ runner.deps.onError?.(e, { phase: "degraded", sessionId: prepared.sessionId });
36
+ }
37
+ }
38
+ const famOverride = prepared.model.params?.promptCacheFamily;
39
+ if (famOverride && !["input-includes-cached", "input-excludes-cached", "openai", "anthropic"].includes(famOverride)) {
40
+ runner.deps.onError?.(new Error(`prompt-cache: unrecognized model.params.promptCacheFamily "${famOverride}" — ignored (fell back to model.api inference). Use "input-includes-cached" | "input-excludes-cached" (aliases "openai" | "anthropic").`), { phase: "prompt-cache", sessionId: prepared.sessionId });
41
+ }
42
+ rs.limits.turnsExceeded = false;
43
+ rs.degrade.outputErrorStreak = 0;
44
+ rs.degrade.outputInvalid = false;
45
+ rs.telemetry.cacheBreakReported = false;
46
+ rs.limits.outputRetryCap = resolveOutputRetries(spec.outputRetries);
47
+ rs.limits.effectiveMaxTurns = resolveMaxTurns(spec.limits);
48
+ rs.telemetry.tracer = entryTracer !== undefined ? entryTracer.tracer : (spec.tracer ?? runner.deps.tracer);
49
+ rs.telemetry.taskId = runSourceTaskId;
50
+ rs.telemetry.runId = prepared.runId;
51
+ if (taskIdRef) {
52
+ taskIdRef.current = rs.telemetry.taskId;
53
+ taskIdRef.sessionId = prepared.sessionId;
54
+ }
55
+ rs.telemetry.taskStart = Date.now();
56
+ rs.telemetry.taskStartMonotonic = performance.now();
57
+ const discloseNoteTaskRunFailure = (err) => {
58
+ try {
59
+ runner.deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "config", sessionId: prepared.sessionId });
60
+ }
61
+ catch {
62
+ }
63
+ };
64
+ try {
65
+ void Promise.resolve(sessions.noteTaskRun?.(prepared.sessionId, rs.telemetry.taskId, rs.telemetry.runId)).catch(discloseNoteTaskRunFailure);
66
+ }
67
+ catch (err) {
68
+ discloseNoteTaskRunFailure(err);
69
+ }
70
+ const noteUnevaluablePriceTable = (p) => {
71
+ if (prepared.usageGovernance?.governsCost !== true)
72
+ return;
73
+ if (malformedPricingField(p) !== undefined)
74
+ rs.telemetry.unpricedSpend = true;
75
+ };
76
+ rs.degrade.recordDegraded = (info, toModel) => {
77
+ if (rs.degrade.degraded !== undefined)
78
+ return;
79
+ let m = toModel;
80
+ if (!m) {
81
+ try {
82
+ m = resolveModel(info.to, modelCatalog);
83
+ }
84
+ catch {
85
+ m = undefined;
86
+ }
87
+ }
88
+ if (m) {
89
+ rs.telemetry.pricing = runner.deps.pricing?.[m.id] ?? modelCostToPricing(m.cost);
90
+ rs.telemetry.pricingConfigured = isModelPriced(m, runner.deps.pricing);
91
+ noteUnevaluablePriceTable(rs.telemetry.pricing);
92
+ rs.telemetry.cacheFamily = cacheFamilyOf(m);
93
+ }
94
+ else {
95
+ if (runner.deps.pricing?.[info.to]) {
96
+ rs.telemetry.pricing = runner.deps.pricing[info.to];
97
+ rs.telemetry.pricingConfigured = true;
98
+ noteUnevaluablePriceTable(rs.telemetry.pricing);
99
+ }
100
+ else {
101
+ rs.telemetry.pricingConfigured = false;
102
+ }
103
+ rs.telemetry.cacheFamily = "input-excludes-cached";
104
+ }
105
+ rs.degrade.degraded = info;
106
+ emitTrace(rs.telemetry.tracer, () => ({ kind: "task.degraded", version: 1, taskId: rs.telemetry.taskId, from: info.from, to: info.to, reason: info.reason, atTurn: info.atTurn, ts: Date.now() }));
107
+ try {
108
+ runner.deps.onError?.(new Error(`degraded to a cheaper model: ${info.from} → ${info.to} (${info.reason}) at turn ${info.atTurn}`), { phase: "degraded", sessionId: prepared.sessionId });
109
+ }
110
+ catch {
111
+ }
112
+ };
113
+ const sliceWindowMicroUsd = spec.limits?.maxCostUsd !== undefined ? Math.round(spec.limits.maxCostUsd * 1e6) : undefined;
114
+ rs.budget.remainingMicroUsd = prepared.resourceLedger
115
+ ? remainingBudgetMicroUsd(prepared.resourceLedger)
116
+ : spec.resourceSuspend?.totalBudgetUsd !== undefined
117
+ ? Math.round(spec.resourceSuspend.totalBudgetUsd * 1e6)
118
+ : undefined;
119
+ rs.budget.maxCostMicroUsd =
120
+ sliceWindowMicroUsd !== undefined && rs.budget.remainingMicroUsd !== undefined
121
+ ? Math.min(sliceWindowMicroUsd, rs.budget.remainingMicroUsd)
122
+ : (sliceWindowMicroUsd ?? rs.budget.remainingMicroUsd);
123
+ rs.budget.remainingTokens = prepared.resourceLedger
124
+ ? remainingTokens(prepared.resourceLedger)
125
+ : spec.resourceSuspend?.totalTokens;
126
+ rs.budget.maxTokensWindow =
127
+ spec.limits?.maxTokens !== undefined && rs.budget.remainingTokens !== undefined
128
+ ? Math.min(spec.limits.maxTokens, rs.budget.remainingTokens)
129
+ : (spec.limits?.maxTokens ?? rs.budget.remainingTokens);
130
+ rs.budget.overBudget = () => {
131
+ if (rs.budget.maxTokensWindow !== undefined && stats.tokens > rs.budget.maxTokensWindow)
132
+ return "tokens";
133
+ if (rs.budget.maxCostMicroUsd !== undefined && stats.costMicroUsd > rs.budget.maxCostMicroUsd)
134
+ return "cost";
135
+ return undefined;
136
+ };
137
+ rs.budget.streamCancel =
138
+ (spec.limits?.budgetStreamCancel ?? rs.budget.maxCostMicroUsd !== undefined) && prepared.suspendForResource === undefined;
139
+ rs.budget.callOutputChars = 0;
140
+ rs.budget.lastStreamBudgetCheck = 0;
141
+ rs.budget.projectedOverBudget = () => {
142
+ const estOut = Math.ceil(rs.budget.callOutputChars / 4);
143
+ if (rs.budget.maxTokensWindow !== undefined && stats.tokens + estOut > rs.budget.maxTokensWindow)
144
+ return "tokens";
145
+ if (rs.budget.maxCostMicroUsd !== undefined) {
146
+ const estOutMicro = computeCostMicroUsd({ totalInputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: estOut }, rs.telemetry.pricing);
147
+ if (stats.costMicroUsd + estOutMicro > rs.budget.maxCostMicroUsd)
148
+ return "cost";
149
+ }
150
+ return undefined;
151
+ };
152
+ rs.turn.turnUsageMissing = false;
153
+ rs.counters.repetitionCuts = 0;
154
+ rs.counters.repetitionSpared = 0;
155
+ rs.counters.repetitionEvents = [];
156
+ rs.counters.REPETITION_EVENTS_CAP = 20;
157
+ rs.counters.preemptIgnoredReported = false;
158
+ return { noteUnevaluablePriceTable };
159
+ }
@@ -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
+ }
@@ -0,0 +1,26 @@
1
+ import type { TaskSpec } from "../types.js";
2
+ import type { WriteFamily } from "./turn-attachments.js";
3
+ import type { Prepared, RunState } from "./contracts.js";
4
+ export interface RunToolMountFactsInput {
5
+ /** borrowed-readonly — the task spec: the config overrides the manifest frame reports. */
6
+ spec: TaskSpec;
7
+ /** borrowed-mutable — the leg's prepared seat: `turnSnapshot.onChange` is installed; the roster, the model and the
8
+ * prompt manifest are read. */
9
+ prepared: Prepared;
10
+ /** borrowed-readonly — the run's mutable state: the attachment state's presence gates the mount facts; the
11
+ * telemetry is read for the frames. */
12
+ rs: RunState;
13
+ }
14
+ export interface RunToolMountFactsResult {
15
+ /** Whether TodoWrite is mounted (and the attachment state exists) — the todo reminder's gate. */
16
+ todoToolMounted: boolean;
17
+ /** Whether TaskCreate is mounted (and the attachment state exists) — the task reminder's gate. */
18
+ taskToolsMounted: boolean;
19
+ /** task #51 F2: the alias-aware write-family resolver (mounted name + aliases → canonical family). */
20
+ writeFamilyOf: (name: string) => WriteFamily | undefined;
21
+ /** Per tool-call id: the execution start time (for the tool_end duration). */
22
+ toolStartAt: Map<string, number>;
23
+ /** Every tool-call id whose start was observed this run and whose result has not been committed yet. */
24
+ startedToolCallIds: Set<string>;
25
+ }
26
+ export declare function runToolMountFacts(input: RunToolMountFactsInput): RunToolMountFactsResult;
@@ -0,0 +1,58 @@
1
+ import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
2
+ import { engineVersion } from "../version.js";
3
+ import { emitTrace } from "../trace.js";
4
+ import { writeFamilyOfCanonical } from "./tool-end-body.js";
5
+ export function runToolMountFacts(input) {
6
+ const { spec, prepared, rs } = input;
7
+ const todoToolMounted = rs.attach.attachState !== undefined && prepared.tools.some((t) => t.name === "TodoWrite");
8
+ const taskToolsMounted = rs.attach.attachState !== undefined && prepared.tools.some((t) => t.name === "TaskCreate");
9
+ const writeFamilyByName = new Map();
10
+ if (rs.attach.attachState !== undefined) {
11
+ for (const t of prepared.tools) {
12
+ const family = writeFamilyOfCanonical(t.name);
13
+ if (family !== undefined)
14
+ for (const n of [t.name, ...(t.aliases ?? [])])
15
+ writeFamilyByName.set(n, family);
16
+ }
17
+ }
18
+ const writeFamilyOf = (name) => writeFamilyByName.get(name) ?? writeFamilyOfCanonical(name);
19
+ const toolStartAt = new Map();
20
+ const startedToolCallIds = new Set();
21
+ emitTrace(rs.telemetry.tracer, () => ({ kind: "task.start", version: 1, taskId: rs.telemetry.taskId, runId: rs.telemetry.runId, model: prepared.model.id, engineVersion: engineVersion(), ts: rs.telemetry.taskStart }));
22
+ emitTrace(rs.telemetry.tracer, () => ({
23
+ kind: "prompt.assembled",
24
+ version: 1,
25
+ taskId: rs.telemetry.taskId,
26
+ constitution: prepared.promptManifest.constitution,
27
+ blocks: prepared.promptManifest.blocks,
28
+ ...(prepared.promptManifest.sections ? { sections: prepared.promptManifest.sections } : {}),
29
+ ...(prepared.promptManifest.tools ? { tools: prepared.promptManifest.tools } : {}),
30
+ ...(prepared.promptManifest.snapshot ? { snapshot: prepared.promptManifest.snapshot } : {}),
31
+ ...(prepared.promptManifest.lowering ? { lowering: prepared.promptManifest.lowering } : {}),
32
+ ...(prepared.promptManifest.toolDisclosure ? { toolDisclosure: prepared.promptManifest.toolDisclosure } : {}),
33
+ totalChars: prepared.promptManifest.blocks.reduce((n, b) => n + b.chars, 0),
34
+ ts: rs.telemetry.taskStart,
35
+ }));
36
+ emitTrace(rs.telemetry.tracer, () => {
37
+ const reasons = declarationReasons(spec.configOverrides);
38
+ return {
39
+ kind: "config.assembled",
40
+ version: 1,
41
+ taskId: rs.telemetry.taskId,
42
+ catalogVersion: CONFIG_CATALOG_VERSION,
43
+ fields: resolveEffectiveConfig(spec, { modelMaxTokens: prepared.model.maxTokens }),
44
+ ...(Object.keys(reasons).length > 0 ? { overrideReasons: reasons } : {}),
45
+ ts: rs.telemetry.taskStart,
46
+ };
47
+ });
48
+ if (prepared.turnSnapshot !== undefined) {
49
+ prepared.turnSnapshot.onChange = (snap) => emitTrace(rs.telemetry.tracer, () => ({
50
+ kind: "prompt.snapshot_changed",
51
+ version: 1,
52
+ taskId: rs.telemetry.taskId,
53
+ snapshot: { cacheIdentity: snap.cacheIdentity, elements: { ...snap.elements } },
54
+ ts: Date.now(),
55
+ }));
56
+ }
57
+ return { todoToolMounted, taskToolsMounted, writeFamilyOf, toolStartAt, startedToolCallIds };
58
+ }
@@ -1,41 +1,6 @@
1
1
  import type { Model } from "../../internal/llm.js";
2
2
  import type { Stats } from "./assemble-result.js";
3
3
  import type { Prepared, RunState, TurnBoundaryDeps } from "./contracts.js";
4
- /**
5
- * §17.4 (design/64 GAP-25): stop attempting within-task compaction after this many CONSECUTIVE
6
- * failures in one task — a never-recovering summary gateway must not burn an API call at every turn
7
- * boundary (CC: MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3). Reset on any success; an open breaker also
8
- * skips the end-of-task attempt in `finish()` (teardown unaffected). Per-task scope.
9
- *
10
- * RB-190 — this used to say "a flaky/never-recovering gateway", and the word `flaky` was not true. Because
11
- * any success zeroes the counter, a gateway that alternates never reaches three CONSECUTIVE failures and
12
- * the breaker never opens; it keeps paying for a call at every failed boundary, indefinitely.
13
- *
14
- * The behaviour is right and stays: a gateway succeeding half the time is compacting half the time, and
15
- * opening the breaker on it would disable compaction outright and let the context overflow — trading a
16
- * wasted call for a failed task. What was wrong is the comment claiming a guarantee the code does not
17
- * make. An alternating gateway is deliberately NOT caught here; if that ever needs catching it wants a
18
- * different signal (a failure RATE over a window), not a consecutive counter.
19
- */
20
- export declare const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
21
- /**
22
- * §25.2 anti-thrash (LONGRUN-1b, search [84]): after a compaction, the context must regrow past
23
- * (tokensBefore − freed) × this factor — the post-compaction size in trigger units — before another
24
- * summary call fires. An EFFECTIVE compaction (freed large) leaves the floor far below the threshold,
25
- * so behavior is unchanged; an INEFFECTIVE one (small window, chunky turns, large summary ⇒ freed
26
- * small, still at/over the threshold) raises the floor so attempts space out instead of firing at
27
- * every boundary (measured: 26/39 boundaries, +135% wall). The floor binds only when post-compaction
28
- * size > threshold/1.5 ≈ compaction freed <33% headroom. Degrades gracefully: the retained summary
29
- * chain keeps early facts; only the un-summarized tail falls back to trim.
30
- */
31
- export declare const COMPACTION_REGROWTH_FACTOR = 1.5;
32
- /**
33
- * design/84 Seam C reuse guard (b): a compaction that frees fewer than this many (structural) tokens is
34
- * treated as NO-EFFECTIVE-COMPACTION and does NOT raise the anti-thrash floor. Guards against a reused
35
- * provider summary (or a pathological LLM summary) that reclaims no headroom yet suppresses the next real
36
- * summary by inflating the floor to trigger×factor.
37
- */
38
- export declare const COMPACTION_FREED_EPSILON = 256;
39
4
  /**
40
5
  * What the driver hands the turn-boundary lane, once per leg. The four seats are the four positional
41
6
  * parameters the factory used to take; the Input is a carrier of REFERENCES, not a snapshot — the handler
@@ -13,9 +13,7 @@ import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
13
13
  import { workflowSizeGuidelineChangeNotice } from "../../orchestration/workflow-size-guideline.js";
14
14
  import { awaitChargeWithSlowDisclosure, discloseUnevaluableWindow, ENV_DUE_GOVERNANCE_READ_BUDGET_MS, GOVERNANCE_READ_STALLED, LIMIT_APPROACH_DEFAULT_THRESHOLDS, limitApproachFrames, platformLimitTerminal, raceUntilDeadline } from "./clock-and-limits.js";
15
15
  import { gitRestateOption } from "./git-leg-delivery.js";
16
- export const MAX_CONSECUTIVE_COMPACTION_FAILURES = 3;
17
- export const COMPACTION_REGROWTH_FACTOR = 1.5;
18
- export const COMPACTION_FREED_EPSILON = 256;
16
+ import { COMPACTION_FREED_EPSILON, COMPACTION_REGROWTH_FACTOR, MAX_CONSECUTIVE_COMPACTION_FAILURES } from "./compaction-knobs.js";
19
17
  const BATCH_CONTEXT_MIN_KEEP_BYTES = 160;
20
18
  const BATCH_TRUNCATION_MARKER = "\n…[truncated]";
21
19
  export function createTurnBoundary(input) {
@@ -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()`