@claudexor/orchestrator 3.1.2 → 3.2.1

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 (128) hide show
  1. package/README.md +3 -1
  2. package/dist/attemptFinalize.d.ts +14 -11
  3. package/dist/attemptFinalize.d.ts.map +1 -1
  4. package/dist/attemptFinalize.js +41 -14
  5. package/dist/attemptFinalize.js.map +1 -1
  6. package/dist/attemptTelemetry.d.ts +7 -0
  7. package/dist/attemptTelemetry.d.ts.map +1 -1
  8. package/dist/attemptTelemetry.js +2 -1
  9. package/dist/attemptTelemetry.js.map +1 -1
  10. package/dist/attemptUsageCost.d.ts +17 -1
  11. package/dist/attemptUsageCost.d.ts.map +1 -1
  12. package/dist/attemptUsageCost.js +26 -2
  13. package/dist/attemptUsageCost.js.map +1 -1
  14. package/dist/budgetFailure.js.map +1 -1
  15. package/dist/credential-profile-rotation.d.ts +148 -0
  16. package/dist/credential-profile-rotation.d.ts.map +1 -0
  17. package/dist/credential-profile-rotation.js +322 -0
  18. package/dist/credential-profile-rotation.js.map +1 -0
  19. package/dist/credential-profiles.d.ts +38 -134
  20. package/dist/credential-profiles.d.ts.map +1 -1
  21. package/dist/credential-profiles.js +74 -296
  22. package/dist/credential-profiles.js.map +1 -1
  23. package/dist/decisionBudget.d.ts +2 -2
  24. package/dist/decisionBudget.d.ts.map +1 -1
  25. package/dist/deepScanReducer.d.ts +14 -44
  26. package/dist/deepScanReducer.d.ts.map +1 -1
  27. package/dist/deepScanReducer.js +240 -128
  28. package/dist/deepScanReducer.js.map +1 -1
  29. package/dist/delegationBudgetAuthority.d.ts.map +1 -1
  30. package/dist/delegationBudgetAuthority.js.map +1 -1
  31. package/dist/delegationFailure.d.ts.map +1 -1
  32. package/dist/git-precondition.d.ts +11 -0
  33. package/dist/git-precondition.d.ts.map +1 -0
  34. package/dist/git-precondition.js +69 -0
  35. package/dist/git-precondition.js.map +1 -0
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/interaction.d.ts +14 -6
  41. package/dist/interaction.d.ts.map +1 -1
  42. package/dist/interaction.js +106 -19
  43. package/dist/interaction.js.map +1 -1
  44. package/dist/orchestrator.d.ts +51 -83
  45. package/dist/orchestrator.d.ts.map +1 -1
  46. package/dist/orchestrator.js +558 -704
  47. package/dist/orchestrator.js.map +1 -1
  48. package/dist/planBrief.d.ts +27 -0
  49. package/dist/planBrief.d.ts.map +1 -0
  50. package/dist/planBrief.js +42 -0
  51. package/dist/planBrief.js.map +1 -0
  52. package/dist/planQuestions.js +4 -1
  53. package/dist/planQuestions.js.map +1 -1
  54. package/dist/planRun.d.ts +4 -3
  55. package/dist/planRun.d.ts.map +1 -1
  56. package/dist/planRun.js +33 -33
  57. package/dist/planRun.js.map +1 -1
  58. package/dist/plannerAttempt.d.ts +64 -0
  59. package/dist/plannerAttempt.d.ts.map +1 -0
  60. package/dist/plannerAttempt.js +235 -0
  61. package/dist/plannerAttempt.js.map +1 -0
  62. package/dist/policyFindings.js.map +1 -1
  63. package/dist/requestRequirements.d.ts +28 -1
  64. package/dist/requestRequirements.d.ts.map +1 -1
  65. package/dist/requestRequirements.js +89 -10
  66. package/dist/requestRequirements.js.map +1 -1
  67. package/dist/reviewerPanel.js.map +1 -1
  68. package/dist/root-ledger.d.ts.map +1 -1
  69. package/dist/root-ledger.js +2 -0
  70. package/dist/root-ledger.js.map +1 -1
  71. package/dist/run-bounded.d.ts.map +1 -1
  72. package/dist/run-bounded.js +7 -1
  73. package/dist/run-bounded.js.map +1 -1
  74. package/dist/run-input-resolution.d.ts.map +1 -1
  75. package/dist/run-input-resolution.js +26 -8
  76. package/dist/run-input-resolution.js.map +1 -1
  77. package/dist/runEventLog.d.ts +48 -0
  78. package/dist/runEventLog.d.ts.map +1 -0
  79. package/dist/runEventLog.js +63 -0
  80. package/dist/runEventLog.js.map +1 -0
  81. package/dist/runFacts.d.ts +23 -0
  82. package/dist/runFacts.d.ts.map +1 -0
  83. package/dist/runFacts.js +179 -0
  84. package/dist/runFacts.js.map +1 -0
  85. package/dist/runFactsBuilder.d.ts +5 -0
  86. package/dist/runFactsBuilder.d.ts.map +1 -0
  87. package/dist/runFactsBuilder.js +412 -0
  88. package/dist/runFactsBuilder.js.map +1 -0
  89. package/dist/runFactsDeliverable.d.ts +13 -0
  90. package/dist/runFactsDeliverable.d.ts.map +1 -0
  91. package/dist/runFactsDeliverable.js +94 -0
  92. package/dist/runFactsDeliverable.js.map +1 -0
  93. package/dist/runFactsPresentation.d.ts +7 -0
  94. package/dist/runFactsPresentation.d.ts.map +1 -0
  95. package/dist/runFactsPresentation.js +113 -0
  96. package/dist/runFactsPresentation.js.map +1 -0
  97. package/dist/runFactsReview.d.ts +7 -0
  98. package/dist/runFactsReview.d.ts.map +1 -0
  99. package/dist/runFactsReview.js +63 -0
  100. package/dist/runFactsReview.js.map +1 -0
  101. package/dist/runSupport.d.ts +17 -15
  102. package/dist/runSupport.d.ts.map +1 -1
  103. package/dist/runSupport.js +16 -0
  104. package/dist/runSupport.js.map +1 -1
  105. package/dist/runTerminalContext.d.ts +31 -0
  106. package/dist/runTerminalContext.d.ts.map +1 -0
  107. package/dist/runTerminalContext.js +12 -0
  108. package/dist/runTerminalContext.js.map +1 -0
  109. package/dist/runTerminalResults.d.ts +55 -0
  110. package/dist/runTerminalResults.d.ts.map +1 -0
  111. package/dist/runTerminalResults.js +134 -0
  112. package/dist/runTerminalResults.js.map +1 -0
  113. package/dist/runTerminals.d.ts +4 -80
  114. package/dist/runTerminals.d.ts.map +1 -1
  115. package/dist/runTerminals.js +357 -260
  116. package/dist/runTerminals.js.map +1 -1
  117. package/dist/secretDiff.d.ts +3 -0
  118. package/dist/secretDiff.d.ts.map +1 -1
  119. package/dist/secretDiff.js +7 -0
  120. package/dist/secretDiff.js.map +1 -1
  121. package/dist/structuredOutput.d.ts.map +1 -1
  122. package/dist/structuredOutput.js.map +1 -1
  123. package/dist/task-contract-builder.d.ts +35 -0
  124. package/dist/task-contract-builder.d.ts.map +1 -0
  125. package/dist/task-contract-builder.js +114 -0
  126. package/dist/task-contract-builder.js.map +1 -0
  127. package/dist/transientClassify.js.map +1 -1
  128. package/package.json +17 -17
@@ -8,23 +8,15 @@
8
8
  * forever.
9
9
  */
10
10
  import { join } from "node:path";
11
- import { RunFailureCode } from "@claudexor/schema";
12
- import { redactSecrets } from "@claudexor/util";
11
+ import { makeOutcomeFacts, needsOperatorAttention, RunOutcomeFacts, } from "@claudexor/schema";
12
+ import { readTextSafe, redactSecrets } from "@claudexor/util";
13
13
  import { budgetFailureRecord, classifyBudgetFailure } from "./budgetFailure.js";
14
14
  import { reconcileDecisionBudget, settledBudgetSnapshot, } from "./decisionBudget.js";
15
15
  import { reconcileDecisionTerminal } from "./decisionTerminalReconciliation.js";
16
+ import { prepareRunFactsFailureReceipt, prepareRunFactsReceipt } from "./runFacts.js";
17
+ import { cancelledResult, failTerminally, writeFailure } from "./runTerminalResults.js";
16
18
  import { clearFailureArtifact, reconcileWorkProductTerminal, terminalOutcomeFacts, } from "./terminalOutcome.js";
17
- export function announcedRunContext(identity, ledger, hasDelegateAuthority) {
18
- return {
19
- ...identity,
20
- spend: () => ledger.spend(),
21
- valuation: () => ledger.valuation(),
22
- spendEstimated: () => ledger.estimated(),
23
- valuationKnowledge: () => ledger.valuationKnowledge(),
24
- budgetTerminal: () => ledger.terminal(),
25
- recheckBudgetAfterBarrier: hasDelegateAuthority,
26
- };
27
- }
19
+ export { cancelledResult, failTerminally, writeFailure };
28
20
  function ownsDelegateDrain(context) {
29
21
  try {
30
22
  return context.recheckBudgetAfterBarrier?.() === true;
@@ -64,133 +56,77 @@ function postDrainBudgetFailure(context, terminal, budget, prior) {
64
56
  spendUsd: budget.spendUsd,
65
57
  };
66
58
  }
67
- export function writeFailure(store, paths, failure) {
68
- store.writeYaml(join(paths.finalDir, "failure.yaml"), {
69
- phase: failure.phase,
70
- category: failure.category,
71
- code: failure.code ?? null,
72
- harnessId: failure.harnessId ?? null,
73
- attemptId: failure.attemptId ?? null,
74
- safeMessage: redactSecrets(failure.safeMessage),
75
- rawDetailRef: failure.rawDetailRef ?? null,
76
- logRefs: failure.logRefs ?? [],
77
- eventRefs: failure.eventRefs ?? [],
78
- runDir: failure.runDir ?? paths.root,
79
- nextActions: failure.nextActions ?? [],
80
- });
59
+ function terminalEventTypeFor(_originalType, outcome) {
60
+ if (outcome.lifecycle !== "succeeded")
61
+ return "run.failed";
62
+ if (needsOperatorAttention(outcome, false))
63
+ return "run.blocked";
64
+ // The canonical axes own terminal type. Preserving a producer-authored
65
+ // run.blocked with clean axes creates an unexplainable block (no reason or
66
+ // required action), so it is normalized to completed.
67
+ return "run.completed";
81
68
  }
82
- /**
83
- * Terminal result for a cancelled run: emits run.failed with status
84
- * "cancelled" so every mode ends consistently. `writeTelemetry` carries the
85
- * PARTIAL attempt telemetry collected before the abort — a cancelled run
86
- * must still account for what it spent and observed; it used to be
87
- * written only by convergence.
88
- */
89
- export function cancelledResult(log, runId, taskId, mode, runDir, candidates, writeTelemetry, spendUsd,
90
- /** The abort signal that ended the run: a STRING reason (e.g.
91
- * `wall_clock_exceeded` from the maxSeconds deadline) is surfaced; a plain
92
- * user cancel aborts with a DOMException reason and stays a bare cancel. */
93
- cancelSignal,
94
- /** Materializes the diagnostic summary the output.ready below announces. */
95
- store,
96
- /** A prepared result may already carry independent checks/review/work facts
97
- * when cancellation wins during the Delegate terminal barrier. */
98
- priorFacts) {
99
- if (writeTelemetry) {
100
- try {
101
- writeTelemetry();
102
- }
103
- catch {
104
- /* partial telemetry is best-effort on the cancel path */
69
+ function cancellationFacts(signal, prior) {
70
+ return terminalOutcomeFacts(prior, "cancelled", typeof signal.reason === "string" && signal.reason === "wall_clock_exceeded"
71
+ ? "wall_clock_exceeded"
72
+ : "user_cancelled");
73
+ }
74
+ function terminalOutcomeFromPayload(payload) {
75
+ const parsed = RunOutcomeFacts.safeParse(payload["facts"]);
76
+ if (parsed.success)
77
+ return parsed.data;
78
+ // Older in-process terminal producers (including the Delegate drain
79
+ // contract) supplied only lifecycle. Keep that compatible while treating an
80
+ // explicitly malformed facts object as a contradiction that must fail
81
+ // closed through the terminal-facts path below.
82
+ if (!Object.prototype.hasOwnProperty.call(payload, "facts")) {
83
+ const lifecycle = payload["lifecycle"];
84
+ if (lifecycle === "succeeded" ||
85
+ lifecycle === "failed" ||
86
+ lifecycle === "cancelled" ||
87
+ lifecycle === "interrupted") {
88
+ return makeOutcomeFacts(lifecycle);
105
89
  }
106
90
  }
107
- const cancelReason = typeof cancelSignal?.reason === "string" && cancelSignal.reason
108
- ? cancelSignal.reason
109
- : undefined;
110
- const summaryText = cancelReason === "wall_clock_exceeded"
111
- ? "run cancelled: wall-clock deadline (maxSeconds) exceeded"
112
- : "run cancelled";
113
- // Materialize the diagnostic summary BEFORE announcing it — output.ready must
114
- // point at a file that exists (partial-output honesty), and it must precede
115
- // the terminal in every mode (INV-116).
116
- let summaryWritten = false;
117
- if (store) {
118
- try {
119
- store.writeText(join(runDir, "final", "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: cancelled\n${cancelReason ? `- Reason: ${cancelReason}\n` : ""}\n${summaryText}\n`);
120
- summaryWritten = true;
121
- }
122
- catch {
123
- /* best-effort: a write failure must not mask the cancel terminal */
124
- }
91
+ return RunOutcomeFacts.parse(payload["facts"]);
92
+ }
93
+ function settledSpend(a) {
94
+ try {
95
+ return a.spend ? a.spend() : null;
125
96
  }
126
- // output.ready is EVIDENCE (release wave sol #3): announce the summary only
127
- // when the file actually materialized — a failed write still gets its
128
- // terminal below, just without a pointer to a nonexistent artifact.
129
- if (summaryWritten) {
130
- log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
97
+ catch {
98
+ return null;
131
99
  }
132
- const cancelFacts = terminalOutcomeFacts(priorFacts, "cancelled", cancelReason === "wall_clock_exceeded" ? "wall_clock_exceeded" : "user_cancelled");
133
- log.emit("run.failed", {
134
- lifecycle: "cancelled",
135
- facts: cancelFacts,
136
- reason: cancelFacts.reason,
137
- ...(cancelReason ? { cancel_reason: cancelReason } : {}),
138
- });
139
- return {
140
- runId,
141
- taskId,
142
- mode,
143
- lifecycle: "cancelled",
144
- facts: cancelFacts,
145
- winner: null,
146
- runDir,
147
- summary: summaryText,
148
- candidates,
149
- ...(spendUsd !== undefined ? { spendUsd } : {}),
150
- ...(cancelReason ? { cancelReason } : {}),
151
- };
152
100
  }
153
- /**
154
- * Shared post-announce failure terminal. Unexpected throws use its internal
155
- * defaults; known callers may supply narrow typed provenance. Every run ends
156
- * with failure.yaml + summary + a terminal run.failed event.
157
- */
158
- export function failTerminally(log, store, paths, runId, taskId, mode, phase, err, spendUsd, failureMeta = {}) {
159
- const message = redactSecrets(err instanceof Error ? err.message : String(err));
160
- const parsedCode = RunFailureCode.safeParse(err && typeof err === "object" ? err.code : undefined);
161
- const failFacts = terminalOutcomeFacts(failureMeta.priorFacts, "failed", "harness_failed");
162
- store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: failed\n- Phase: ${phase}\n\n${message}\n`);
163
- writeFailure(store, paths, {
164
- phase,
165
- category: failureMeta.category ?? "internal",
166
- code: parsedCode.success ? parsedCode.data : null,
167
- harnessId: failureMeta.harnessId,
168
- attemptId: failureMeta.attemptId,
169
- safeMessage: message,
170
- rawDetailRef: failureMeta.rawDetailRef,
171
- runDir: paths.root,
172
- nextActions: failureMeta.nextActions ?? ["Open diagnostics", "Retry the run"],
173
- });
174
- log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
175
- log.emit("run.failed", {
176
- lifecycle: "failed",
177
- facts: failFacts,
178
- reason: failFacts.reason,
179
- phase,
180
- error: message,
181
- failure_ref: "final/failure.yaml",
182
- });
101
+ function isTerminalRecoveryRequired(error) {
102
+ return (!!error &&
103
+ typeof error === "object" &&
104
+ "code" in error &&
105
+ error.code === "terminal_recovery_required");
106
+ }
107
+ function committedOutcome(a) {
108
+ const terminal = [...a.log.readAll().events]
109
+ .reverse()
110
+ .find((event) => event.type === "run.completed" ||
111
+ event.type === "run.blocked" ||
112
+ event.type === "run.failed");
113
+ const parsed = RunOutcomeFacts.safeParse(terminal?.payload["facts"]);
114
+ return parsed.success ? parsed.data : null;
115
+ }
116
+ function committedResult(a, outcome, spendUsd) {
117
+ const summary = readTextSafe(join(a.paths.finalDir, "summary.md"))?.trim().slice(0, 400) ??
118
+ `run terminal committed with lifecycle ${outcome.lifecycle}`;
183
119
  return {
184
- runId,
185
- taskId,
186
- mode,
187
- lifecycle: "failed",
188
- facts: failFacts,
120
+ runId: a.runId,
121
+ taskId: a.taskId,
122
+ mode: a.mode,
123
+ lifecycle: outcome.lifecycle,
124
+ facts: outcome,
189
125
  winner: null,
190
- runDir: paths.root,
191
- summary: message,
126
+ runDir: a.paths.root,
127
+ summary,
192
128
  candidates: [],
193
- ...(spendUsd !== undefined ? { spendUsd } : {}),
129
+ spendUsd,
194
130
  };
195
131
  }
196
132
  /**
@@ -213,6 +149,8 @@ beforeTerminal,
213
149
  onSettled) {
214
150
  let announced = null;
215
151
  let preparedResult = null;
152
+ let canonicalOutcome = null;
153
+ let terminalPreparationError = null;
216
154
  let barrierAttempted = false;
217
155
  let barrierFailed = false;
218
156
  const runBarrier = async (context) => {
@@ -228,145 +166,304 @@ onSettled) {
228
166
  }
229
167
  };
230
168
  try {
231
- const result = await body((a) => {
232
- announced = a;
233
- });
234
- preparedResult = result;
235
- const context = announced;
236
- if (context) {
237
- await runBarrier(context);
238
- const budget = settledBudgetSnapshot(context);
239
- const delegateBarrierArmed = ownsDelegateDrain(context);
240
- if (delegateBarrierArmed && signal?.aborted) {
241
- context.log.clearDeferredTerminal();
242
- const cancelFacts = terminalOutcomeFacts(result.facts, "cancelled", signal.reason === "wall_clock_exceeded" ? "wall_clock_exceeded" : "user_cancelled");
243
- const cancelSummary = signal.reason === "wall_clock_exceeded"
244
- ? "run cancelled: wall-clock deadline (maxSeconds) exceeded"
245
- : "run cancelled";
246
- let reconciledCancelFacts = cancelFacts;
247
- try {
248
- reconcileDecisionBudget(context, budget);
249
- reconciledCancelFacts = reconcileDecisionTerminal(context, {
250
- facts: cancelFacts,
251
- why: cancelSummary,
252
- preparedFacts: result.facts,
253
- });
254
- reconcileWorkProductTerminal(context, reconciledCancelFacts);
255
- clearFailureArtifact(context);
169
+ let result;
170
+ try {
171
+ result = await body((a) => {
172
+ announced = a;
173
+ a.log.setBeforeTerminal((originalType, payload) => {
174
+ try {
175
+ // Cancellation wins only until the synchronous terminal commit
176
+ // starts. A later abort cannot rewrite an already-durable success.
177
+ const payloadOutcome = terminalOutcomeFromPayload(payload);
178
+ const terminalOutcome = signal?.aborted
179
+ ? cancellationFacts(signal, payloadOutcome)
180
+ : payloadOutcome;
181
+ const prepared = prepareRunFactsReceipt(a, terminalOutcome);
182
+ canonicalOutcome = prepared.facts.outcome;
183
+ return {
184
+ type: terminalEventTypeFor(originalType, prepared.facts.outcome),
185
+ payload: {
186
+ ...payload,
187
+ lifecycle: prepared.facts.outcome.lifecycle,
188
+ facts: prepared.facts.outcome,
189
+ reason: prepared.facts.outcome.reason,
190
+ run_facts: prepared.facts,
191
+ },
192
+ commit: prepared.commit,
193
+ rollback: () => {
194
+ prepared.rollback();
195
+ canonicalOutcome = null;
196
+ },
197
+ };
198
+ }
199
+ catch (error) {
200
+ // A contradictory terminal projection must fail the run, not throw
201
+ // out of the hook and retry the same contradiction forever.
202
+ const detail = redactSecrets(error instanceof Error ? error.message : String(error));
203
+ terminalPreparationError = `terminal facts preparation failed: ${detail}`;
204
+ const failureOutcome = makeOutcomeFacts("failed", {
205
+ reason: "harness_failed",
206
+ });
207
+ let failureRefWritten = false;
208
+ try {
209
+ a.store.writeText(join(a.paths.finalDir, "summary.md"), `# Run ${a.runId} (${a.mode})\n\n- Lifecycle: failed\n- Phase: terminal_facts\n\n${terminalPreparationError}\n`);
210
+ writeFailure(a.store, a.paths, {
211
+ phase: "terminal_facts",
212
+ category: "internal",
213
+ safeMessage: terminalPreparationError,
214
+ runDir: a.paths.root,
215
+ nextActions: ["Open diagnostics", "Inspect final/run_facts.yaml", "Retry the run"],
216
+ });
217
+ failureRefWritten = true;
218
+ }
219
+ catch {
220
+ /* terminal event remains the fail-closed authority */
221
+ }
222
+ try {
223
+ const prepared = prepareRunFactsFailureReceipt(a, failureOutcome);
224
+ canonicalOutcome = prepared.facts.outcome;
225
+ return {
226
+ type: "run.failed",
227
+ payload: {
228
+ lifecycle: prepared.facts.outcome.lifecycle,
229
+ facts: prepared.facts.outcome,
230
+ reason: prepared.facts.outcome.reason,
231
+ run_facts: prepared.facts,
232
+ phase: "terminal_facts",
233
+ error: terminalPreparationError,
234
+ ...(failureRefWritten ? { failure_ref: "final/failure.yaml" } : {}),
235
+ },
236
+ commit: prepared.commit,
237
+ rollback: () => {
238
+ prepared.rollback();
239
+ canonicalOutcome = null;
240
+ },
241
+ };
242
+ }
243
+ catch {
244
+ // If even the minimal receipt cannot be written, still commit an
245
+ // honest failed terminal. The daemon job state then fails closed.
246
+ canonicalOutcome = failureOutcome;
247
+ return {
248
+ type: "run.failed",
249
+ payload: {
250
+ lifecycle: failureOutcome.lifecycle,
251
+ facts: failureOutcome,
252
+ reason: failureOutcome.reason,
253
+ phase: "terminal_facts",
254
+ error: terminalPreparationError,
255
+ ...(failureRefWritten ? { failure_ref: "final/failure.yaml" } : {}),
256
+ },
257
+ };
258
+ }
259
+ }
260
+ });
261
+ });
262
+ preparedResult = result;
263
+ const context = announced;
264
+ if (context) {
265
+ await runBarrier(context);
266
+ const budget = settledBudgetSnapshot(context);
267
+ const delegateBarrierArmed = ownsDelegateDrain(context);
268
+ if (delegateBarrierArmed && signal?.aborted) {
269
+ context.log.clearDeferredTerminal();
270
+ const cancelFacts = terminalOutcomeFacts(result.facts, "cancelled", signal.reason === "wall_clock_exceeded" ? "wall_clock_exceeded" : "user_cancelled");
271
+ const cancelSummary = signal.reason === "wall_clock_exceeded"
272
+ ? "run cancelled: wall-clock deadline (maxSeconds) exceeded"
273
+ : "run cancelled";
274
+ let reconciledCancelFacts = cancelFacts;
275
+ try {
276
+ reconcileDecisionBudget(context, budget);
277
+ reconciledCancelFacts = reconcileDecisionTerminal(context, {
278
+ facts: cancelFacts,
279
+ why: cancelSummary,
280
+ preparedFacts: result.facts,
281
+ });
282
+ reconcileWorkProductTerminal(context, reconciledCancelFacts);
283
+ clearFailureArtifact(context);
284
+ }
285
+ catch {
286
+ /* the cancellation terminal below remains authoritative */
287
+ }
288
+ const cancelled = cancelledResult(context.log, context.runId, context.taskId, context.mode, context.paths.root, result.candidates, undefined, budget.spendUsd, signal, context.store, reconciledCancelFacts);
289
+ context.log.flushDeferredTerminal();
290
+ result = {
291
+ ...result,
292
+ lifecycle: "cancelled",
293
+ facts: cancelled.facts,
294
+ summary: cancelled.summary,
295
+ spendUsd: cancelled.spendUsd,
296
+ ...(cancelled.cancelReason ? { cancelReason: cancelled.cancelReason } : {}),
297
+ };
256
298
  }
257
- catch {
258
- /* the cancellation terminal below remains authoritative */
299
+ else {
300
+ const budgetTerminal = delegateBarrierArmed ? (context.budgetTerminal?.() ?? null) : null;
301
+ if (budgetTerminal && result.lifecycle === "succeeded") {
302
+ context.log.clearDeferredTerminal();
303
+ result = postDrainBudgetFailure(context, budgetTerminal, budget, result);
304
+ context.log.flushDeferredTerminal();
305
+ }
306
+ else {
307
+ if (delegateBarrierArmed)
308
+ reconcileDecisionBudget(context, budget);
309
+ context.log.flushDeferredTerminal();
310
+ if (budget.spendUsd !== null)
311
+ result = { ...result, spendUsd: budget.spendUsd };
312
+ }
259
313
  }
260
- const cancelled = cancelledResult(context.log, context.runId, context.taskId, context.mode, context.paths.root, result.candidates, undefined, budget.spendUsd, signal, context.store, reconciledCancelFacts);
261
- context.log.flushDeferredTerminal();
262
- return {
263
- ...result,
264
- lifecycle: "cancelled",
265
- facts: cancelled.facts,
266
- summary: cancelled.summary,
267
- spendUsd: cancelled.spendUsd,
268
- ...(cancelled.cancelReason ? { cancelReason: cancelled.cancelReason } : {}),
269
- };
270
- }
271
- const budgetTerminal = delegateBarrierArmed ? (context.budgetTerminal?.() ?? null) : null;
272
- if (budgetTerminal && result.lifecycle === "succeeded") {
273
- context.log.clearDeferredTerminal();
274
- const failed = postDrainBudgetFailure(context, budgetTerminal, budget, result);
275
- context.log.flushDeferredTerminal();
276
- return failed;
277
314
  }
278
- if (delegateBarrierArmed)
279
- reconcileDecisionBudget(context, budget);
280
- context.log.flushDeferredTerminal();
281
- if (budget.spendUsd !== null)
282
- return { ...result, spendUsd: budget.spendUsd };
283
- }
284
- return result;
285
- }
286
- catch (err) {
287
- // TS cannot see the closure assignment; the cast is safe (set-once).
288
- const a = announced;
289
- if (!a)
290
- throw err;
291
- a.log.clearDeferredTerminal();
292
- let terminalError = err;
293
- try {
294
- await runBarrier(a);
295
315
  }
296
- catch (barrierError) {
297
- terminalError = barrierError;
298
- }
299
- // Settled-spend accounting is part of the terminal contract on EVERY
300
- // path (the orchestrate executor aggregates it); a broken spend snapshot
301
- // must not mask the original failure, so it degrades to null loudly-typed.
302
- const budget = settledBudgetSnapshot(a);
303
- const spendUsd = budget.spendUsd;
304
- const delegateBarrierArmed = ownsDelegateDrain(a);
305
- if (signal?.aborted && terminalError === err) {
306
- const priorFacts = preparedResult?.facts;
307
- const cancelFacts = terminalOutcomeFacts(priorFacts, "cancelled", signal.reason === "wall_clock_exceeded" ? "wall_clock_exceeded" : "user_cancelled");
308
- const cancelSummary = signal.reason === "wall_clock_exceeded"
309
- ? "run cancelled: wall-clock deadline (maxSeconds) exceeded"
310
- : "run cancelled";
311
- let reconciledCancelFacts = cancelFacts;
312
- try {
313
- if (delegateBarrierArmed)
314
- reconcileDecisionBudget(a, budget);
315
- reconciledCancelFacts = reconcileDecisionTerminal(a, {
316
- facts: cancelFacts,
317
- why: cancelSummary,
318
- ...(preparedResult ? { preparedFacts: preparedResult.facts } : {}),
319
- });
320
- reconcileWorkProductTerminal(a, reconciledCancelFacts);
321
- clearFailureArtifact(a);
322
- }
323
- catch {
324
- /* the cancellation terminal below remains authoritative */
316
+ catch (err) {
317
+ // TS cannot see the closure assignment; the cast is safe (set-once).
318
+ const a = announced;
319
+ if (!a)
320
+ throw err;
321
+ // The partition journal already owns this terminal. A safety-net emit
322
+ // would hit the poisoned writer, erase the typed recovery signal, and
323
+ // let the daemon persist a false generic failure over durable truth.
324
+ if (isTerminalRecoveryRequired(err))
325
+ throw err;
326
+ if (a.log.terminalCommitted()) {
327
+ const outcome = canonicalOutcome ?? committedOutcome(a);
328
+ if (!outcome)
329
+ throw err;
330
+ const spendUsd = settledSpend(a);
331
+ result = preparedResult
332
+ ? {
333
+ ...preparedResult,
334
+ lifecycle: outcome.lifecycle,
335
+ facts: outcome,
336
+ ...(spendUsd !== null ? { spendUsd } : {}),
337
+ }
338
+ : committedResult(a, outcome, spendUsd);
325
339
  }
326
- const cancelled = cancelledResult(a.log, a.runId, a.taskId, a.mode, a.paths.root, preparedResult?.candidates ?? [], undefined, spendUsd,
327
- // A wall-clock abort surfaced as a throw must keep its reason and
328
- // materialize the diagnostic summary, exactly like the checkpoint paths.
329
- signal, a.store, reconciledCancelFacts);
330
- a.log.flushDeferredTerminal();
331
- return preparedResult
332
- ? {
333
- ...preparedResult,
334
- lifecycle: "cancelled",
335
- facts: cancelled.facts,
336
- summary: cancelled.summary,
337
- spendUsd: cancelled.spendUsd,
338
- ...(cancelled.cancelReason ? { cancelReason: cancelled.cancelReason } : {}),
340
+ else {
341
+ a.log.clearDeferredTerminal();
342
+ let terminalError = err;
343
+ try {
344
+ await runBarrier(a);
345
+ }
346
+ catch (barrierError) {
347
+ terminalError = barrierError;
348
+ }
349
+ // Settled-spend accounting is part of the terminal contract on EVERY
350
+ // path (the orchestrate executor aggregates it); a broken spend snapshot
351
+ // must not mask the original failure, so it degrades to null loudly-typed.
352
+ const budget = settledBudgetSnapshot(a);
353
+ const spendUsd = budget.spendUsd;
354
+ const delegateBarrierArmed = ownsDelegateDrain(a);
355
+ if (signal?.aborted && terminalError === err) {
356
+ const priorFacts = preparedResult?.facts;
357
+ const cancelFacts = terminalOutcomeFacts(priorFacts, "cancelled", signal.reason === "wall_clock_exceeded" ? "wall_clock_exceeded" : "user_cancelled");
358
+ const cancelSummary = signal.reason === "wall_clock_exceeded"
359
+ ? "run cancelled: wall-clock deadline (maxSeconds) exceeded"
360
+ : "run cancelled";
361
+ let reconciledCancelFacts = cancelFacts;
362
+ try {
363
+ if (delegateBarrierArmed)
364
+ reconcileDecisionBudget(a, budget);
365
+ reconciledCancelFacts = reconcileDecisionTerminal(a, {
366
+ facts: cancelFacts,
367
+ why: cancelSummary,
368
+ ...(preparedResult ? { preparedFacts: preparedResult.facts } : {}),
369
+ });
370
+ reconcileWorkProductTerminal(a, reconciledCancelFacts);
371
+ clearFailureArtifact(a);
372
+ }
373
+ catch {
374
+ /* the cancellation terminal below remains authoritative */
375
+ }
376
+ const cancelled = cancelledResult(a.log, a.runId, a.taskId, a.mode, a.paths.root, preparedResult?.candidates ?? [], undefined, spendUsd,
377
+ // A wall-clock abort surfaced as a throw must keep its reason and
378
+ // materialize the diagnostic summary, exactly like the checkpoint paths.
379
+ signal, a.store, reconciledCancelFacts);
380
+ a.log.flushDeferredTerminal();
381
+ result = preparedResult
382
+ ? {
383
+ ...preparedResult,
384
+ lifecycle: "cancelled",
385
+ facts: cancelled.facts,
386
+ summary: cancelled.summary,
387
+ spendUsd: cancelled.spendUsd,
388
+ ...(cancelled.cancelReason ? { cancelReason: cancelled.cancelReason } : {}),
389
+ }
390
+ : cancelled;
391
+ }
392
+ else {
393
+ const priorFacts = preparedResult?.facts;
394
+ const pendingFacts = terminalOutcomeFacts(priorFacts, "failed", "harness_failed");
395
+ const pendingSummary = redactSecrets(terminalError instanceof Error ? terminalError.message : String(terminalError));
396
+ let reconciledFailureFacts = pendingFacts;
397
+ try {
398
+ if (delegateBarrierArmed)
399
+ reconcileDecisionBudget(a, budget);
400
+ reconciledFailureFacts = reconcileDecisionTerminal(a, {
401
+ facts: pendingFacts,
402
+ why: pendingSummary,
403
+ ...(preparedResult ? { preparedFacts: preparedResult.facts } : {}),
404
+ });
405
+ reconcileWorkProductTerminal(a, reconciledFailureFacts);
406
+ }
407
+ catch {
408
+ /* the failure terminal below remains authoritative */
409
+ }
410
+ const failed = failTerminally(a.log, a.store, a.paths, a.runId, a.taskId, a.mode, barrierFailed ? "delegation_drain" : a.phase, terminalError, spendUsd, { priorFacts: reconciledFailureFacts });
411
+ a.log.flushDeferredTerminal();
412
+ result = preparedResult
413
+ ? {
414
+ ...preparedResult,
415
+ lifecycle: "failed",
416
+ facts: failed.facts,
417
+ summary: failed.summary,
418
+ spendUsd: failed.spendUsd,
419
+ }
420
+ : failed;
339
421
  }
340
- : cancelled;
422
+ }
341
423
  }
342
- const priorFacts = preparedResult?.facts;
343
- const pendingFacts = terminalOutcomeFacts(priorFacts, "failed", "harness_failed");
344
- const pendingSummary = redactSecrets(terminalError instanceof Error ? terminalError.message : String(terminalError));
345
- let reconciledFailureFacts = pendingFacts;
346
- try {
347
- if (delegateBarrierArmed)
348
- reconcileDecisionBudget(a, budget);
349
- reconciledFailureFacts = reconcileDecisionTerminal(a, {
350
- facts: pendingFacts,
351
- why: pendingSummary,
352
- ...(preparedResult ? { preparedFacts: preparedResult.facts } : {}),
424
+ // Defensive fallback for a strategy that returned without a terminal emit:
425
+ // materialize/announce a summary when needed, then pass through the same
426
+ // pre-terminal receipt transaction as every normal strategy.
427
+ const a = announced;
428
+ if (a && !a.log.terminalCommitted()) {
429
+ const events = a.log.readAll().events;
430
+ if (!events.some((event) => event.type === "output.ready")) {
431
+ const summaryPath = join(a.paths.finalDir, "summary.md");
432
+ let summaryPresent = readTextSafe(summaryPath) !== null;
433
+ if (!summaryPresent) {
434
+ try {
435
+ a.store.writeText(summaryPath, `# Run ${a.runId} (${a.mode})\n\n${redactSecrets(result.summary)}\n`);
436
+ summaryPresent = true;
437
+ }
438
+ catch {
439
+ /* never point output.ready at a file that did not materialize */
440
+ }
441
+ }
442
+ if (summaryPresent) {
443
+ a.log.emit("output.ready", {
444
+ kind: "summary",
445
+ path: "final/summary.md",
446
+ state: "diagnostic",
447
+ });
448
+ }
449
+ }
450
+ a.log.emit(terminalEventTypeFor("run.completed", result.facts), {
451
+ lifecycle: result.facts.lifecycle,
452
+ facts: result.facts,
453
+ reason: result.facts.reason,
353
454
  });
354
- reconcileWorkProductTerminal(a, reconciledFailureFacts);
355
455
  }
356
- catch {
357
- /* the failure terminal below remains authoritative */
456
+ // TS cannot see assignments performed inside EventLog's hook closure.
457
+ const settledOutcome = canonicalOutcome;
458
+ if (settledOutcome) {
459
+ result = {
460
+ ...result,
461
+ lifecycle: settledOutcome.lifecycle,
462
+ facts: settledOutcome,
463
+ ...(terminalPreparationError ? { winner: null, summary: terminalPreparationError } : {}),
464
+ };
358
465
  }
359
- const failed = failTerminally(a.log, a.store, a.paths, a.runId, a.taskId, a.mode, barrierFailed ? "delegation_drain" : a.phase, terminalError, spendUsd, { priorFacts: reconciledFailureFacts });
360
- a.log.flushDeferredTerminal();
361
- return preparedResult
362
- ? {
363
- ...preparedResult,
364
- lifecycle: "failed",
365
- facts: failed.facts,
366
- summary: failed.summary,
367
- spendUsd: failed.spendUsd,
368
- }
369
- : failed;
466
+ return result;
370
467
  }
371
468
  finally {
372
469
  // Release per-run engine state on EVERY terminal path (normal return, failure