botmux 2.33.0 → 2.33.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 (281) hide show
  1. package/README.en.md +12 -1
  2. package/README.md +45 -1
  3. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  4. package/dist/adapters/cli/claude-code.js +11 -0
  5. package/dist/adapters/cli/claude-code.js.map +1 -1
  6. package/dist/cli/bots-list-output.d.ts +21 -0
  7. package/dist/cli/bots-list-output.d.ts.map +1 -0
  8. package/dist/cli/bots-list-output.js +23 -0
  9. package/dist/cli/bots-list-output.js.map +1 -0
  10. package/dist/cli/workflow.d.ts +13 -0
  11. package/dist/cli/workflow.d.ts.map +1 -0
  12. package/dist/cli/workflow.js +781 -0
  13. package/dist/cli/workflow.js.map +1 -0
  14. package/dist/cli.js +69 -14
  15. package/dist/cli.js.map +1 -1
  16. package/dist/core/command-handler.d.ts.map +1 -1
  17. package/dist/core/command-handler.js +211 -4
  18. package/dist/core/command-handler.js.map +1 -1
  19. package/dist/core/session-manager.d.ts +6 -1
  20. package/dist/core/session-manager.d.ts.map +1 -1
  21. package/dist/core/session-manager.js +22 -12
  22. package/dist/core/session-manager.js.map +1 -1
  23. package/dist/core/worker-pool.d.ts +13 -0
  24. package/dist/core/worker-pool.d.ts.map +1 -1
  25. package/dist/core/worker-pool.js +100 -6
  26. package/dist/core/worker-pool.js.map +1 -1
  27. package/dist/daemon.d.ts +3 -0
  28. package/dist/daemon.d.ts.map +1 -1
  29. package/dist/daemon.js +884 -3
  30. package/dist/daemon.js.map +1 -1
  31. package/dist/dashboard/auth.d.ts +36 -0
  32. package/dist/dashboard/auth.d.ts.map +1 -1
  33. package/dist/dashboard/auth.js +22 -0
  34. package/dist/dashboard/auth.js.map +1 -1
  35. package/dist/dashboard/web/app.js +20 -1
  36. package/dist/dashboard/web/app.js.map +1 -1
  37. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  38. package/dist/dashboard/web/i18n.js +356 -0
  39. package/dist/dashboard/web/i18n.js.map +1 -1
  40. package/dist/dashboard/web/workflow-catalog.d.ts +2 -0
  41. package/dist/dashboard/web/workflow-catalog.d.ts.map +1 -0
  42. package/dist/dashboard/web/workflow-catalog.js +323 -0
  43. package/dist/dashboard/web/workflow-catalog.js.map +1 -0
  44. package/dist/dashboard/web/workflows.d.ts +2 -0
  45. package/dist/dashboard/web/workflows.d.ts.map +1 -0
  46. package/dist/dashboard/web/workflows.js +1618 -0
  47. package/dist/dashboard/web/workflows.js.map +1 -0
  48. package/dist/dashboard/workflow-api.d.ts +23 -0
  49. package/dist/dashboard/workflow-api.d.ts.map +1 -0
  50. package/dist/dashboard/workflow-api.js +463 -0
  51. package/dist/dashboard/workflow-api.js.map +1 -0
  52. package/dist/dashboard-web/app.js +494 -199
  53. package/dist/dashboard-web/index.html +1 -0
  54. package/dist/dashboard-web/style.css +160 -6
  55. package/dist/dashboard-web/terminal-replay.html +227 -0
  56. package/dist/dashboard.js +29 -12
  57. package/dist/dashboard.js.map +1 -1
  58. package/dist/i18n/en.d.ts.map +1 -1
  59. package/dist/i18n/en.js +12 -0
  60. package/dist/i18n/en.js.map +1 -1
  61. package/dist/i18n/zh.d.ts.map +1 -1
  62. package/dist/i18n/zh.js +12 -0
  63. package/dist/i18n/zh.js.map +1 -1
  64. package/dist/im/lark/card-handler.d.ts +3 -0
  65. package/dist/im/lark/card-handler.d.ts.map +1 -1
  66. package/dist/im/lark/card-handler.js +27 -1
  67. package/dist/im/lark/card-handler.js.map +1 -1
  68. package/dist/im/lark/client.d.ts +19 -2
  69. package/dist/im/lark/client.d.ts.map +1 -1
  70. package/dist/im/lark/client.js +21 -2
  71. package/dist/im/lark/client.js.map +1 -1
  72. package/dist/im/lark/workflow-card-handler.d.ts +50 -0
  73. package/dist/im/lark/workflow-card-handler.d.ts.map +1 -0
  74. package/dist/im/lark/workflow-card-handler.js +152 -0
  75. package/dist/im/lark/workflow-card-handler.js.map +1 -0
  76. package/dist/im/lark/workflow-cards.d.ts +46 -0
  77. package/dist/im/lark/workflow-cards.d.ts.map +1 -0
  78. package/dist/im/lark/workflow-cards.js +226 -0
  79. package/dist/im/lark/workflow-cards.js.map +1 -0
  80. package/dist/im/lark/workflow-progress-card.d.ts +76 -0
  81. package/dist/im/lark/workflow-progress-card.d.ts.map +1 -0
  82. package/dist/im/lark/workflow-progress-card.js +279 -0
  83. package/dist/im/lark/workflow-progress-card.js.map +1 -0
  84. package/dist/im/lark/workflow-slash-command.d.ts +92 -0
  85. package/dist/im/lark/workflow-slash-command.d.ts.map +1 -0
  86. package/dist/im/lark/workflow-slash-command.js +185 -0
  87. package/dist/im/lark/workflow-slash-command.js.map +1 -0
  88. package/dist/services/group-creator.d.ts.map +1 -1
  89. package/dist/services/group-creator.js +17 -4
  90. package/dist/services/group-creator.js.map +1 -1
  91. package/dist/services/groups-store.d.ts +11 -0
  92. package/dist/services/groups-store.d.ts.map +1 -1
  93. package/dist/services/groups-store.js +26 -0
  94. package/dist/services/groups-store.js.map +1 -1
  95. package/dist/services/jsonl-cursor.d.ts +12 -0
  96. package/dist/services/jsonl-cursor.d.ts.map +1 -0
  97. package/dist/services/jsonl-cursor.js +45 -0
  98. package/dist/services/jsonl-cursor.js.map +1 -0
  99. package/dist/services/schedule-store.d.ts +35 -0
  100. package/dist/services/schedule-store.d.ts.map +1 -1
  101. package/dist/services/schedule-store.js +108 -1
  102. package/dist/services/schedule-store.js.map +1 -1
  103. package/dist/skills/definitions.d.ts.map +1 -1
  104. package/dist/skills/definitions.js +399 -0
  105. package/dist/skills/definitions.js.map +1 -1
  106. package/dist/types.d.ts +4 -0
  107. package/dist/types.d.ts.map +1 -1
  108. package/dist/utils/cli-usage-limit.d.ts.map +1 -1
  109. package/dist/utils/cli-usage-limit.js +4 -0
  110. package/dist/utils/cli-usage-limit.js.map +1 -1
  111. package/dist/worker.js +118 -14
  112. package/dist/worker.js.map +1 -1
  113. package/dist/workflows/attempt-resume.d.ts +114 -0
  114. package/dist/workflows/attempt-resume.d.ts.map +1 -0
  115. package/dist/workflows/attempt-resume.js +385 -0
  116. package/dist/workflows/attempt-resume.js.map +1 -0
  117. package/dist/workflows/attempt-terminal.d.ts +21 -0
  118. package/dist/workflows/attempt-terminal.d.ts.map +1 -0
  119. package/dist/workflows/attempt-terminal.js +7 -0
  120. package/dist/workflows/attempt-terminal.js.map +1 -0
  121. package/dist/workflows/blob.d.ts +27 -0
  122. package/dist/workflows/blob.d.ts.map +1 -0
  123. package/dist/workflows/blob.js +39 -0
  124. package/dist/workflows/blob.js.map +1 -0
  125. package/dist/workflows/cancel-run.d.ts +45 -0
  126. package/dist/workflows/cancel-run.d.ts.map +1 -0
  127. package/dist/workflows/cancel-run.js +99 -0
  128. package/dist/workflows/cancel-run.js.map +1 -0
  129. package/dist/workflows/cancel.d.ts +111 -0
  130. package/dist/workflows/cancel.d.ts.map +1 -0
  131. package/dist/workflows/cancel.js +120 -0
  132. package/dist/workflows/cancel.js.map +1 -0
  133. package/dist/workflows/catalog.d.ts +60 -0
  134. package/dist/workflows/catalog.d.ts.map +1 -0
  135. package/dist/workflows/catalog.js +119 -0
  136. package/dist/workflows/catalog.js.map +1 -0
  137. package/dist/workflows/cold-attach.d.ts +30 -0
  138. package/dist/workflows/cold-attach.d.ts.map +1 -0
  139. package/dist/workflows/cold-attach.js +40 -0
  140. package/dist/workflows/cold-attach.js.map +1 -0
  141. package/dist/workflows/cold-scan.d.ts +21 -0
  142. package/dist/workflows/cold-scan.d.ts.map +1 -0
  143. package/dist/workflows/cold-scan.js +70 -0
  144. package/dist/workflows/cold-scan.js.map +1 -0
  145. package/dist/workflows/daemon-spawn.d.ts +117 -0
  146. package/dist/workflows/daemon-spawn.d.ts.map +1 -0
  147. package/dist/workflows/daemon-spawn.js +551 -0
  148. package/dist/workflows/daemon-spawn.js.map +1 -0
  149. package/dist/workflows/definition.d.ts +1309 -0
  150. package/dist/workflows/definition.d.ts.map +1 -0
  151. package/dist/workflows/definition.js +334 -0
  152. package/dist/workflows/definition.js.map +1 -0
  153. package/dist/workflows/effect-input.d.ts +4 -0
  154. package/dist/workflows/effect-input.d.ts.map +1 -0
  155. package/dist/workflows/effect-input.js +18 -0
  156. package/dist/workflows/effect-input.js.map +1 -0
  157. package/dist/workflows/events/append.d.ts +77 -0
  158. package/dist/workflows/events/append.d.ts.map +1 -0
  159. package/dist/workflows/events/append.js +214 -0
  160. package/dist/workflows/events/append.js.map +1 -0
  161. package/dist/workflows/events/idempotency.d.ts +77 -0
  162. package/dist/workflows/events/idempotency.d.ts.map +1 -0
  163. package/dist/workflows/events/idempotency.js +116 -0
  164. package/dist/workflows/events/idempotency.js.map +1 -0
  165. package/dist/workflows/events/index.d.ts +7 -0
  166. package/dist/workflows/events/index.d.ts.map +1 -0
  167. package/dist/workflows/events/index.js +7 -0
  168. package/dist/workflows/events/index.js.map +1 -0
  169. package/dist/workflows/events/payloads.d.ts +917 -0
  170. package/dist/workflows/events/payloads.d.ts.map +1 -0
  171. package/dist/workflows/events/payloads.js +337 -0
  172. package/dist/workflows/events/payloads.js.map +1 -0
  173. package/dist/workflows/events/replay.d.ts +238 -0
  174. package/dist/workflows/events/replay.d.ts.map +1 -0
  175. package/dist/workflows/events/replay.js +608 -0
  176. package/dist/workflows/events/replay.js.map +1 -0
  177. package/dist/workflows/events/schema.d.ts +5242 -0
  178. package/dist/workflows/events/schema.d.ts.map +1 -0
  179. package/dist/workflows/events/schema.js +295 -0
  180. package/dist/workflows/events/schema.js.map +1 -0
  181. package/dist/workflows/events/types.d.ts +34 -0
  182. package/dist/workflows/events/types.d.ts.map +1 -0
  183. package/dist/workflows/events/types.js +2 -0
  184. package/dist/workflows/events/types.js.map +1 -0
  185. package/dist/workflows/fanout.d.ts +36 -0
  186. package/dist/workflows/fanout.d.ts.map +1 -0
  187. package/dist/workflows/fanout.js +114 -0
  188. package/dist/workflows/fanout.js.map +1 -0
  189. package/dist/workflows/hostExecutors/botmux-schedule.d.ts +41 -0
  190. package/dist/workflows/hostExecutors/botmux-schedule.d.ts.map +1 -0
  191. package/dist/workflows/hostExecutors/botmux-schedule.js +121 -0
  192. package/dist/workflows/hostExecutors/botmux-schedule.js.map +1 -0
  193. package/dist/workflows/hostExecutors/feishu-im.d.ts +12 -0
  194. package/dist/workflows/hostExecutors/feishu-im.d.ts.map +1 -0
  195. package/dist/workflows/hostExecutors/feishu-im.js +49 -0
  196. package/dist/workflows/hostExecutors/feishu-im.js.map +1 -0
  197. package/dist/workflows/hostExecutors/feishu-reply.d.ts +24 -0
  198. package/dist/workflows/hostExecutors/feishu-reply.d.ts.map +1 -0
  199. package/dist/workflows/hostExecutors/feishu-reply.js +88 -0
  200. package/dist/workflows/hostExecutors/feishu-reply.js.map +1 -0
  201. package/dist/workflows/hostExecutors/feishu-send.d.ts +23 -0
  202. package/dist/workflows/hostExecutors/feishu-send.d.ts.map +1 -0
  203. package/dist/workflows/hostExecutors/feishu-send.js +124 -0
  204. package/dist/workflows/hostExecutors/feishu-send.js.map +1 -0
  205. package/dist/workflows/hostExecutors/index.d.ts +8 -0
  206. package/dist/workflows/hostExecutors/index.d.ts.map +1 -0
  207. package/dist/workflows/hostExecutors/index.js +8 -0
  208. package/dist/workflows/hostExecutors/index.js.map +1 -0
  209. package/dist/workflows/hostExecutors/protocol.d.ts +42 -0
  210. package/dist/workflows/hostExecutors/protocol.d.ts.map +1 -0
  211. package/dist/workflows/hostExecutors/protocol.js +181 -0
  212. package/dist/workflows/hostExecutors/protocol.js.map +1 -0
  213. package/dist/workflows/hostExecutors/registry.d.ts +10 -0
  214. package/dist/workflows/hostExecutors/registry.d.ts.map +1 -0
  215. package/dist/workflows/hostExecutors/registry.js +36 -0
  216. package/dist/workflows/hostExecutors/registry.js.map +1 -0
  217. package/dist/workflows/hostExecutors/types.d.ts +78 -0
  218. package/dist/workflows/hostExecutors/types.d.ts.map +1 -0
  219. package/dist/workflows/hostExecutors/types.js +2 -0
  220. package/dist/workflows/hostExecutors/types.js.map +1 -0
  221. package/dist/workflows/loader.d.ts +16 -0
  222. package/dist/workflows/loader.d.ts.map +1 -0
  223. package/dist/workflows/loader.js +56 -0
  224. package/dist/workflows/loader.js.map +1 -0
  225. package/dist/workflows/loop.d.ts +50 -0
  226. package/dist/workflows/loop.d.ts.map +1 -0
  227. package/dist/workflows/loop.js +350 -0
  228. package/dist/workflows/loop.js.map +1 -0
  229. package/dist/workflows/ops-projection.d.ts +168 -0
  230. package/dist/workflows/ops-projection.d.ts.map +1 -0
  231. package/dist/workflows/ops-projection.js +707 -0
  232. package/dist/workflows/ops-projection.js.map +1 -0
  233. package/dist/workflows/orchestrator.d.ts +107 -0
  234. package/dist/workflows/orchestrator.d.ts.map +1 -0
  235. package/dist/workflows/orchestrator.js +197 -0
  236. package/dist/workflows/orchestrator.js.map +1 -0
  237. package/dist/workflows/output-binding.d.ts +70 -0
  238. package/dist/workflows/output-binding.d.ts.map +1 -0
  239. package/dist/workflows/output-binding.js +265 -0
  240. package/dist/workflows/output-binding.js.map +1 -0
  241. package/dist/workflows/params.d.ts +61 -0
  242. package/dist/workflows/params.d.ts.map +1 -0
  243. package/dist/workflows/params.js +195 -0
  244. package/dist/workflows/params.js.map +1 -0
  245. package/dist/workflows/resume.d.ts +263 -0
  246. package/dist/workflows/resume.d.ts.map +1 -0
  247. package/dist/workflows/resume.js +808 -0
  248. package/dist/workflows/resume.js.map +1 -0
  249. package/dist/workflows/run-id.d.ts +2 -0
  250. package/dist/workflows/run-id.d.ts.map +1 -0
  251. package/dist/workflows/run-id.js +7 -0
  252. package/dist/workflows/run-id.js.map +1 -0
  253. package/dist/workflows/run-init.d.ts +48 -0
  254. package/dist/workflows/run-init.d.ts.map +1 -0
  255. package/dist/workflows/run-init.js +99 -0
  256. package/dist/workflows/run-init.js.map +1 -0
  257. package/dist/workflows/runs-dir.d.ts +4 -0
  258. package/dist/workflows/runs-dir.d.ts.map +1 -0
  259. package/dist/workflows/runs-dir.js +15 -0
  260. package/dist/workflows/runs-dir.js.map +1 -0
  261. package/dist/workflows/runtime.d.ts +211 -0
  262. package/dist/workflows/runtime.d.ts.map +1 -0
  263. package/dist/workflows/runtime.js +594 -0
  264. package/dist/workflows/runtime.js.map +1 -0
  265. package/dist/workflows/spawn-bot.d.ts +165 -0
  266. package/dist/workflows/spawn-bot.d.ts.map +1 -0
  267. package/dist/workflows/spawn-bot.js +215 -0
  268. package/dist/workflows/spawn-bot.js.map +1 -0
  269. package/dist/workflows/system.d.ts +49 -0
  270. package/dist/workflows/system.d.ts.map +1 -0
  271. package/dist/workflows/system.js +48 -0
  272. package/dist/workflows/system.js.map +1 -0
  273. package/dist/workflows/trigger-run.d.ts +70 -0
  274. package/dist/workflows/trigger-run.d.ts.map +1 -0
  275. package/dist/workflows/trigger-run.js +88 -0
  276. package/dist/workflows/trigger-run.js.map +1 -0
  277. package/dist/workflows/wait.d.ts +120 -0
  278. package/dist/workflows/wait.d.ts.map +1 -0
  279. package/dist/workflows/wait.js +181 -0
  280. package/dist/workflows/wait.js.map +1 -0
  281. package/package.json +3 -3
@@ -0,0 +1,808 @@
1
+ /**
2
+ * Resume + reconcile algorithm (events doc v0.1.2 §4.3 + §4.3.1).
3
+ *
4
+ * Entry point for daemon restart / hand-off. Walks the event log,
5
+ * replays a snapshot, then drives reconcile decisions for each dangling
6
+ * `effectAttempted` and writes terminal events for `pure skill`
7
+ * activities that crashed mid-flight (workerLost path).
8
+ *
9
+ * Step 7 boundaries:
10
+ * - Resume DOES NOT execute activity logic; reconcile uses provider
11
+ * capabilities (`readOnlyLookup` / `idempotentSubmit`) to decide
12
+ * terminal state without re-issuing user-visible work beyond what
13
+ * idempotency guarantees.
14
+ * - Resume DOES NOT decide retry policy. A `freshRetry` decision
15
+ * leaves the attempt dangling — the scheduler (Step 8+) is
16
+ * responsible for spawning the actual replacement attempt.
17
+ * - Dangling waits are left alone (waiting for external signal).
18
+ *
19
+ * Round 1 fixes (codex review of `1d14081`):
20
+ * F1 — replay surfaces the latest reconcileResult per attempt; resume
21
+ * consumes it before re-running the decision tree, so a crash
22
+ * between reconcileResult and the terminal event is recoverable.
23
+ * F2 — reconcilers receive the materialized effect input via the
24
+ * caller-supplied `loadEffectInput` callback. Reconcilers that
25
+ * require input (e.g. Feishu — chatId/rootMessageId/content can't
26
+ * be reconstructed from idempotencyKey alone) fail explicitly
27
+ * when input is unrecoverable.
28
+ * F3 — `retryable` failures from idempotentSubmit do NOT terminate
29
+ * the attempt; the activity stays dangling and is surfaced in
30
+ * `ResumeResult.transientFailures` for the caller to retry.
31
+ * F4 — `resumeStarted` is written ONLY after a preflight validates
32
+ * the log is replayable; bad inputs throw without polluting the
33
+ * run event log.
34
+ */
35
+ import { computeInputHash } from './events/idempotency.js';
36
+ import { replay } from './events/replay.js';
37
+ // ─── Resume orchestrator ────────────────────────────────────────────────────
38
+ export async function resume(ctx) {
39
+ if (ctx.runId !== ctx.log.runId) {
40
+ throw new Error(`resume: ctx.runId (${ctx.runId}) does not match log.runId (${ctx.log.runId})`);
41
+ }
42
+ const now = ctx.now ?? Date.now;
43
+ // F4: Preflight BEFORE writing resumeStarted. Bad logs (empty / no
44
+ // runCreated / cross-runId contamination) throw without polluting the
45
+ // run event log — audit goes to the daemon logger, not the canonical
46
+ // per-run event stream.
47
+ const preEvents = await ctx.log.readAll();
48
+ if (preEvents.length === 0) {
49
+ throw new Error(`resume(${ctx.runId}): cannot resume an empty event log — no runCreated to project from.`);
50
+ }
51
+ if (preEvents[0].type !== 'runCreated') {
52
+ throw new Error(`resume(${ctx.runId}): first event must be runCreated, got ${preEvents[0].type} (corrupt log; not appending resumeStarted).`);
53
+ }
54
+ // We let `replay` enforce cross-runId, but check up front so the
55
+ // diagnostic is colocated with the preflight.
56
+ if (preEvents[0].runId !== ctx.runId) {
57
+ throw new Error(`resume(${ctx.runId}): runCreated.runId is ${preEvents[0].runId}, log/ctx are ${ctx.runId} (corrupt log; not appending resumeStarted).`);
58
+ }
59
+ // Preflight passed — now write the audit entry.
60
+ const resumeStartedEvent = (await ctx.log.append({
61
+ runId: ctx.runId,
62
+ type: 'resumeStarted',
63
+ actor: 'system',
64
+ payload: {
65
+ daemonId: ctx.daemonId,
66
+ lastSeenEventId: preEvents[preEvents.length - 1].eventId,
67
+ },
68
+ }));
69
+ // Re-read so the snapshot includes the resumeStarted (replay treats
70
+ // it as a no-op projection — keeping the read consistent).
71
+ const allEvents = await ctx.log.readAll();
72
+ const snapshot = replay(allEvents);
73
+ // Step 9: cancel recovery — cancelRequested landed but no terminal.
74
+ // Spec §2.5: cancel is the authoritative terminal REASON, but when
75
+ // the cancelled attempt also has a dangling `effectAttempted` we
76
+ // must FIRST run reconcile to capture provider evidence (codex Step 9
77
+ // round 1 finding 1). Skipping reconcile would write activityCanceled
78
+ // without ever observing whether the provider actually performed the
79
+ // side effect — which is the difference between "we cancelled a no-op"
80
+ // and "we cancelled a successful submit", and recovery can't replay
81
+ // that distinction later. We still write `activityCanceled` for the
82
+ // common cases (completedByIdempotentSubmit / freshRetry) so cancel
83
+ // remains the terminal reason; only `manual` reconcile decisions
84
+ // escalate to `activityFailed{manual}` because the provider state is
85
+ // unknown and pretending otherwise would lie about the cancel outcome.
86
+ //
87
+ // Cancel runs first so the subsequent loops can skip its activities.
88
+ const cancelRecoveryOutcomes = [];
89
+ const transientFailures = [];
90
+ const effectAttemptedSet = new Set(snapshot.danglingEffectAttempted);
91
+ for (const activityId of snapshot.danglingCancels) {
92
+ if (effectAttemptedSet.has(activityId)) {
93
+ const result = await recoverCancelWithReconcile(ctx, snapshot, activityId, now());
94
+ if (result.kind === 'outcome')
95
+ cancelRecoveryOutcomes.push(result.outcome);
96
+ else if (result.kind === 'transient')
97
+ transientFailures.push(result.failure);
98
+ // 'skipped' = missing activity; ignore.
99
+ }
100
+ else {
101
+ const cancellation = await recoverCancel(ctx, snapshot, activityId);
102
+ if (cancellation)
103
+ cancelRecoveryOutcomes.push(cancellation);
104
+ }
105
+ }
106
+ // Activities the cancel branch ALREADY terminated (succeeded or escalated
107
+ // to failed) — distinct from activities the cancel branch left dangling
108
+ // because reconcile reported transient.
109
+ const cancelTerminated = new Set(cancelRecoveryOutcomes.map((o) => o.activityId));
110
+ const reconcileOutcomes = [];
111
+ for (const activityId of snapshot.danglingEffectAttempted) {
112
+ if (cancelTerminated.has(activityId))
113
+ continue; // already handled by cancel branch
114
+ // Skip activities that the cancel branch tried but got transient on:
115
+ // we already recorded the transient failure there; running another
116
+ // reconcileOne for the same idempotencyKey would double-write.
117
+ if (snapshot.danglingCancels.includes(activityId)) {
118
+ continue;
119
+ }
120
+ const result = await reconcileOne(ctx, snapshot, activityId, now());
121
+ if (result.kind === 'outcome')
122
+ reconcileOutcomes.push(result.outcome);
123
+ else if (result.kind === 'transient')
124
+ transientFailures.push(result.failure);
125
+ }
126
+ const cancelled = new Set(snapshot.danglingCancels);
127
+ // Step 8: wait recovery — `waitResolved` / `waitDeadlineExceeded`
128
+ // landed but the activity terminal didn't. Materialize the terminal
129
+ // from the recorded resolution so the next replay sees a clean
130
+ // terminal state.
131
+ const waitRecoveryOutcomes = [];
132
+ for (const activityId of snapshot.danglingWaitResolutions) {
133
+ if (cancelled.has(activityId))
134
+ continue;
135
+ const recovery = await recoverWaitResolution(ctx, snapshot, activityId);
136
+ if (recovery)
137
+ waitRecoveryOutcomes.push(recovery);
138
+ }
139
+ // Worker-crashed path: dangling activity, no effectAttempted, no
140
+ // open wait, no recoverable wait resolution → activityFailed{WorkerCrashed, retryable}.
141
+ const workerCrashedOutcomes = [];
142
+ const reconciled = new Set(snapshot.danglingEffectAttempted);
143
+ const waitingActivities = new Set(snapshot.danglingWaits);
144
+ const waitRecovered = new Set(snapshot.danglingWaitResolutions);
145
+ for (const activityId of snapshot.danglingActivities) {
146
+ if (cancelled.has(activityId))
147
+ continue;
148
+ if (reconciled.has(activityId))
149
+ continue;
150
+ if (waitingActivities.has(activityId))
151
+ continue;
152
+ if (waitRecovered.has(activityId))
153
+ continue;
154
+ const activity = snapshot.activities.get(activityId);
155
+ if (!activity)
156
+ continue;
157
+ const latest = activity.attempts[activity.attempts.length - 1];
158
+ if (!latest)
159
+ continue;
160
+ const terminalEvent = (await ctx.log.append({
161
+ runId: ctx.runId,
162
+ type: 'activityFailed',
163
+ actor: 'system',
164
+ payload: {
165
+ activityId,
166
+ attemptId: latest.attemptId,
167
+ error: {
168
+ errorCode: 'WorkerCrashed',
169
+ errorClass: 'retryable',
170
+ errorMessage: 'Worker process exited before the activity reached a terminal state.',
171
+ },
172
+ },
173
+ }));
174
+ workerCrashedOutcomes.push({ activityId, attemptId: latest.attemptId, terminalEvent });
175
+ }
176
+ return {
177
+ resumeStartedEvent,
178
+ snapshot,
179
+ reconcileOutcomes,
180
+ workerCrashedOutcomes,
181
+ transientFailures,
182
+ waitRecoveryOutcomes,
183
+ cancelRecoveryOutcomes,
184
+ };
185
+ }
186
+ // ─── Cancel recovery (Step 9) ──────────────────────────────────────────────
187
+ /**
188
+ * Plain cancel recovery for activities WITHOUT a dangling effectAttempted.
189
+ * Writes `activityCanceled` directly — no provider state to reconcile.
190
+ */
191
+ async function recoverCancel(ctx, snapshot, activityId) {
192
+ const activity = snapshot.activities.get(activityId);
193
+ if (!activity)
194
+ return null;
195
+ const latest = activity.attempts[activity.attempts.length - 1];
196
+ if (!latest?.cancelRequest)
197
+ return null;
198
+ const cr = latest.cancelRequest;
199
+ const terminalEvent = (await ctx.log.append({
200
+ runId: ctx.runId,
201
+ type: 'activityCanceled',
202
+ actor: 'system',
203
+ payload: {
204
+ activityId,
205
+ attemptId: latest.attemptId,
206
+ cancelOriginEventId: cr.cancelOriginEventId,
207
+ },
208
+ }));
209
+ return {
210
+ activityId,
211
+ attemptId: latest.attemptId,
212
+ cancelOriginEventId: cr.cancelOriginEventId,
213
+ delivered: cr.delivered,
214
+ kind: 'cancelled',
215
+ terminalEvent,
216
+ };
217
+ }
218
+ async function recoverCancelWithReconcile(ctx, snapshot, activityId, nowMs) {
219
+ const activity = snapshot.activities.get(activityId);
220
+ if (!activity)
221
+ return { kind: 'skipped' };
222
+ const latest = activity.attempts[activity.attempts.length - 1];
223
+ if (!latest?.cancelRequest || !latest.effectAttempted)
224
+ return { kind: 'skipped' };
225
+ const cr = latest.cancelRequest;
226
+ const evidence = await captureEvidence(ctx, snapshot, activityId, nowMs, {
227
+ cancelContext: {
228
+ cancelOriginEventId: cr.cancelOriginEventId,
229
+ reason: cr.reason,
230
+ requestedBy: cr.requestedBy,
231
+ },
232
+ });
233
+ if (evidence.kind === 'skipped')
234
+ return { kind: 'skipped' };
235
+ if (evidence.kind === 'transient') {
236
+ return { kind: 'transient', failure: evidence.failure };
237
+ }
238
+ const ea = latest.effectAttempted;
239
+ // Decision → terminal mapping for cancel branch.
240
+ if (evidence.kind === 'manual') {
241
+ // Provider state unknown — escalate to activityFailed{manual}. We
242
+ // intentionally do NOT write activityCanceled here: it would
243
+ // misrepresent the cancel as a clean abort even though we can't
244
+ // verify whether the provider performed the side effect. The
245
+ // cancelOriginEventId is preserved in the reconcile evidence for
246
+ // forensics.
247
+ const terminalEvent = (await ctx.log.append({
248
+ runId: ctx.runId,
249
+ type: 'activityFailed',
250
+ actor: 'system',
251
+ payload: {
252
+ activityId,
253
+ attemptId: latest.attemptId,
254
+ error: {
255
+ errorCode: evidence.errorCode,
256
+ errorClass: 'manual',
257
+ errorMessage: `Cancel + reconcile: ${evidence.errorMessage} (cancelOriginEventId=${cr.cancelOriginEventId})`,
258
+ },
259
+ },
260
+ }));
261
+ return {
262
+ kind: 'outcome',
263
+ outcome: {
264
+ activityId,
265
+ attemptId: latest.attemptId,
266
+ cancelOriginEventId: cr.cancelOriginEventId,
267
+ delivered: cr.delivered,
268
+ kind: 'failed',
269
+ reconcileEvent: evidence.reconcileEvent ?? undefined,
270
+ reconcileDecision: 'manual',
271
+ terminalEvent,
272
+ },
273
+ };
274
+ }
275
+ // completedByIdempotentSubmit OR freshRetry: cancel wins as terminal
276
+ // reason. Evidence is preserved in the reconcileResult written by
277
+ // captureEvidence (or referenced via the prior reconcileResult eventId
278
+ // when recovered=true).
279
+ const terminalEvent = (await ctx.log.append({
280
+ runId: ctx.runId,
281
+ type: 'activityCanceled',
282
+ actor: 'system',
283
+ payload: {
284
+ activityId,
285
+ attemptId: latest.attemptId,
286
+ cancelOriginEventId: cr.cancelOriginEventId,
287
+ },
288
+ }));
289
+ void ea;
290
+ return {
291
+ kind: 'outcome',
292
+ outcome: {
293
+ activityId,
294
+ attemptId: latest.attemptId,
295
+ cancelOriginEventId: cr.cancelOriginEventId,
296
+ delivered: cr.delivered,
297
+ kind: 'cancelled',
298
+ reconcileEvent: evidence.reconcileEvent ?? undefined,
299
+ reconcileDecision: evidence.kind,
300
+ terminalEvent,
301
+ },
302
+ };
303
+ }
304
+ // ─── Wait recovery (Step 8) ────────────────────────────────────────────────
305
+ async function recoverWaitResolution(ctx, snapshot, activityId) {
306
+ const activity = snapshot.activities.get(activityId);
307
+ if (!activity)
308
+ return null;
309
+ const latest = activity.attempts[activity.attempts.length - 1];
310
+ if (!latest?.wait?.resolution)
311
+ return null;
312
+ const r = latest.wait.resolution;
313
+ if (r.kind === 'resolved') {
314
+ // approved | external → activitySucceeded.
315
+ // rejected → activityFailed { InputValidationFailed, userFault }.
316
+ if (r.resolution === 'rejected') {
317
+ const terminalEvent = (await ctx.log.append({
318
+ runId: ctx.runId,
319
+ type: 'activityFailed',
320
+ actor: 'system',
321
+ payload: {
322
+ activityId,
323
+ attemptId: latest.attemptId,
324
+ error: {
325
+ errorCode: 'InputValidationFailed',
326
+ errorClass: 'userFault',
327
+ errorMessage: `Recovered wait terminal: rejected by ${r.by}${r.comment ? `: ${r.comment}` : ''}`,
328
+ },
329
+ },
330
+ }));
331
+ return {
332
+ activityId,
333
+ attemptId: latest.attemptId,
334
+ kind: 'failed',
335
+ source: 'resolved',
336
+ terminalEvent,
337
+ };
338
+ }
339
+ // approved | external
340
+ const externalRefs = {
341
+ resolution: r.resolution,
342
+ by: r.by,
343
+ ...(r.comment ? { comment: r.comment } : {}),
344
+ };
345
+ const terminalEvent = await writeRecoverySucceeded(ctx, activityId, latest.attemptId, externalRefs);
346
+ return {
347
+ activityId,
348
+ attemptId: latest.attemptId,
349
+ kind: 'succeeded',
350
+ source: 'resolved',
351
+ terminalEvent,
352
+ };
353
+ }
354
+ // deadlineExceeded
355
+ const policy = latest.wait.onTimeout ?? 'fail';
356
+ if (policy === 'success') {
357
+ const externalRefs = { defaultedToTimeout: true, deadlineAt: r.deadlineAt };
358
+ const terminalEvent = await writeRecoverySucceeded(ctx, activityId, latest.attemptId, externalRefs);
359
+ return {
360
+ activityId,
361
+ attemptId: latest.attemptId,
362
+ kind: 'succeeded',
363
+ source: 'deadlineExceeded',
364
+ terminalEvent,
365
+ };
366
+ }
367
+ const terminalEvent = (await ctx.log.append({
368
+ runId: ctx.runId,
369
+ type: 'activityFailed',
370
+ actor: 'system',
371
+ payload: {
372
+ activityId,
373
+ attemptId: latest.attemptId,
374
+ error: {
375
+ errorCode: 'WaitDeadlineExceeded',
376
+ errorClass: 'userFault',
377
+ errorMessage: `Recovered wait terminal: deadline (${r.deadlineAt}) exceeded at ${r.exceededAtMs}`,
378
+ },
379
+ },
380
+ }));
381
+ return {
382
+ activityId,
383
+ attemptId: latest.attemptId,
384
+ kind: 'failed',
385
+ source: 'deadlineExceeded',
386
+ terminalEvent,
387
+ };
388
+ }
389
+ async function writeRecoverySucceeded(ctx, activityId, attemptId, externalRefs) {
390
+ const outputBuf = Buffer.from(JSON.stringify(externalRefs), 'utf-8');
391
+ const outputHash = await sha256Hex(outputBuf);
392
+ return (await ctx.log.append({
393
+ runId: ctx.runId,
394
+ type: 'activitySucceeded',
395
+ actor: 'system',
396
+ payload: {
397
+ activityId,
398
+ attemptId,
399
+ outputRef: {
400
+ outputHash: `sha256:${outputHash}`,
401
+ outputBytes: outputBuf.length,
402
+ outputSchemaVersion: 1,
403
+ contentType: 'application/json',
404
+ },
405
+ externalRefs,
406
+ },
407
+ }));
408
+ }
409
+ async function reconcileOne(ctx, snapshot, activityId, nowMs) {
410
+ const activity = snapshot.activities.get(activityId);
411
+ if (!activity)
412
+ return { kind: 'skipped' };
413
+ const latest = activity.attempts[activity.attempts.length - 1];
414
+ if (!latest?.effectAttempted)
415
+ return { kind: 'skipped' };
416
+ const ea = latest.effectAttempted;
417
+ const evidence = await captureEvidence(ctx, snapshot, activityId, nowMs);
418
+ if (evidence.kind === 'skipped')
419
+ return { kind: 'skipped' };
420
+ if (evidence.kind === 'transient')
421
+ return { kind: 'transient', failure: evidence.failure };
422
+ return { kind: 'outcome', outcome: await writeRegularTerminal(ctx, latest.attemptId, activityId, ea, evidence) };
423
+ }
424
+ /**
425
+ * Capture reconcile evidence WITHOUT writing the activity terminal.
426
+ * Writes `reconcileResult` (or reuses a prior one — F1 recovery) and
427
+ * returns the decision + auxiliary data the caller needs to write the
428
+ * appropriate terminal.
429
+ *
430
+ * Splitting evidence capture from terminal write lets the cancel path
431
+ * (`recoverCancelWithReconcile`) reuse the decision tree without
432
+ * accidentally fabricating activitySucceeded — codex Step 9 round 1
433
+ * finding 1.
434
+ */
435
+ async function captureEvidence(ctx, snapshot, activityId, nowMs, options) {
436
+ const activity = snapshot.activities.get(activityId);
437
+ if (!activity)
438
+ return { kind: 'skipped' };
439
+ const latest = activity.attempts[activity.attempts.length - 1];
440
+ if (!latest?.effectAttempted)
441
+ return { kind: 'skipped' };
442
+ const ea = latest.effectAttempted;
443
+ const extra = controlExtra(options);
444
+ // F1: recovery path — if a previous resume already wrote a
445
+ // reconcileResult for this attempt but crashed before the terminal,
446
+ // resume the consequences instead of re-running the decision tree.
447
+ // Re-running risks a DIFFERENT decision (TTL crosses, provider state
448
+ // changes), so we honor the recorded choice.
449
+ if (latest.latestReconcileResult) {
450
+ return evidenceFromPriorReconcileResult(latest, ea);
451
+ }
452
+ const reconciler = ctx.reconcilers.get(ea.provider);
453
+ // Case A — unknown provider. No way to confirm; manual/UnknownProvider.
454
+ if (!reconciler) {
455
+ return await writeReconcileResultManual(ctx, activityId, ea, 'none', 'UnknownProviderError', `No reconciler registered for provider "${ea.provider}".`, { reason: 'no_reconciler', ...extra });
456
+ }
457
+ // Case B — TTL boundary. Use the recorded TTL from effectAttempted,
458
+ // not the live reconciler's value: the provider's TTL may have changed
459
+ // between the attempt and this resume, but the contract that was in
460
+ // force at attempt time is what matters.
461
+ const ttlExpired = nowMs - ea.attemptedAtMs > ea.idempotencyTtlMs;
462
+ if (ttlExpired) {
463
+ return await writeReconcileResultManual(ctx, activityId, ea, 'none', 'TtlExpired', `Provider TTL (${ea.idempotencyTtlMs}ms) elapsed before resume could reconcile.`, {
464
+ reason: 'ttl_expired',
465
+ attemptedAtMs: ea.attemptedAtMs,
466
+ nowMs,
467
+ idempotencyTtlMs: ea.idempotencyTtlMs,
468
+ ...extra,
469
+ });
470
+ }
471
+ // F2: materialize effect input via the caller's loader. Some
472
+ // reconcilers can work without it (schedule); others (Feishu) MUST
473
+ // have it.
474
+ let effectInput = undefined;
475
+ let inputLoadError = null;
476
+ if (ctx.loadEffectInput) {
477
+ try {
478
+ effectInput = await ctx.loadEffectInput(activityId, latest.attemptId);
479
+ }
480
+ catch (err) {
481
+ inputLoadError = err instanceof Error ? err : new Error(String(err));
482
+ }
483
+ }
484
+ if (reconciler.requiresEffectInput &&
485
+ (inputLoadError !== null || effectInput === undefined)) {
486
+ return await writeReconcileResultManual(ctx, activityId, ea, 'none', 'InputUnrecoverable', inputLoadError
487
+ ? `Failed to load effect input for reconcile: ${inputLoadError.message}`
488
+ : `Reconciler "${ea.provider}" requires effect input, but ctx.loadEffectInput returned undefined / was not provided.`, { reason: 'input_unrecoverable', hadLoader: !!ctx.loadEffectInput, ...extra });
489
+ }
490
+ // F2.5: inputHash guard. When a sidecar was successfully loaded, the
491
+ // body we're about to hand the reconciler MUST canonicalize to the
492
+ // hash that was recorded on `effectAttempted`. Sidecar tampering,
493
+ // schema drift, or manual edits would otherwise silently produce a
494
+ // re-submit with a different body — Feishu would dedupe by uuid and
495
+ // return the original messageId, but our workflow audit trail would
496
+ // record the tampered input as "successful".
497
+ //
498
+ // Only enforced when the reconciler declares `canonicalInput` so the
499
+ // contract is opt-in per provider. For `requiresEffectInput=true`
500
+ // reconcilers without a `canonicalInput`, fail loud: this is a
501
+ // config error (a Feishu-flavored reconciler that can't canonicalize
502
+ // its own input).
503
+ if (effectInput !== undefined) {
504
+ if (reconciler.canonicalInput) {
505
+ const recomputed = computeInputHash(reconciler.canonicalInput(effectInput));
506
+ if (recomputed !== ea.inputHash) {
507
+ return await writeReconcileResultManual(ctx, activityId, ea, 'none', 'IdempotencyInputMismatch', `Reconciler "${ea.provider}" loaded effect input whose canonical hash (${recomputed}) does not match the recorded effectAttempted.inputHash (${ea.inputHash}). Sidecar tampered or schema drifted; not calling provider.`, {
508
+ reason: 'inputhash_mismatch',
509
+ recordedHash: ea.inputHash,
510
+ recomputedHash: recomputed,
511
+ source: 'hashGuard',
512
+ ...extra,
513
+ });
514
+ }
515
+ }
516
+ else if (reconciler.requiresEffectInput) {
517
+ return await writeReconcileResultManual(ctx, activityId, ea, 'none', 'IdempotencyInputMismatch', `Reconciler "${ea.provider}" declares requiresEffectInput=true but exposes no canonicalInput — cannot verify the loaded sidecar matches effectAttempted.inputHash. Not calling provider.`, { reason: 'no_canonicalInput', source: 'hashGuard', ...extra });
518
+ }
519
+ }
520
+ // Case C — readOnlyLookup available. Prefer it: pure read, no side
521
+ // effect risk. Schedule has it.
522
+ if (reconciler.readOnlyLookup) {
523
+ const lookup = await reconciler.readOnlyLookup(ea.idempotencyKey, effectInput);
524
+ if (lookup.found) {
525
+ return await writeReconcileResultCompleted(ctx, activityId, ea, 'readOnlyLookup', lookup.externalRefs, { ...(lookup.evidence ?? {}), ...extra });
526
+ }
527
+ return await writeReconcileResultFreshRetry(ctx, activityId, ea, 'readOnlyLookup', { ...(lookup.evidence ?? { found: false }), ...extra });
528
+ }
529
+ // Case D — idempotentSubmit only (Feishu).
530
+ if (reconciler.idempotentSubmit) {
531
+ const submit = await reconciler.idempotentSubmit(ea.idempotencyKey, effectInput);
532
+ if (submit.ok) {
533
+ return await writeReconcileResultCompleted(ctx, activityId, ea, 'idempotentSubmit', submit.externalRefs, { ...(submit.evidence ?? {}), ...extra });
534
+ }
535
+ // F3: retryable failures stay dangling — no reconcileResult is
536
+ // written here because the provider's state is in flux. The next
537
+ // resume cycle re-enters the decision tree from scratch.
538
+ if (submit.errorClass === 'retryable') {
539
+ return {
540
+ kind: 'transient',
541
+ failure: {
542
+ activityId,
543
+ attemptId: latest.attemptId,
544
+ provider: ea.provider,
545
+ idempotencyKey: ea.idempotencyKey,
546
+ errorCode: submit.errorCode,
547
+ errorClass: 'retryable',
548
+ errorMessage: submit.errorMessage,
549
+ },
550
+ };
551
+ }
552
+ return await writeReconcileResultManual(ctx, activityId, ea, 'idempotentSubmit', submit.errorCode, submit.errorMessage, { ...(submit.evidence ?? { errorClass: submit.errorClass }), ...extra });
553
+ }
554
+ // Case E — reconciler exists but exposes no capability. Manual.
555
+ return await writeReconcileResultManual(ctx, activityId, ea, 'none', 'UnknownProviderError', `Reconciler for "${ea.provider}" exposes neither readOnlyLookup nor idempotentSubmit.`, { reason: 'no_capability', ...extra });
556
+ }
557
+ function controlExtra(options) {
558
+ if (!options?.cancelContext)
559
+ return {};
560
+ const { cancelOriginEventId, reason, requestedBy } = options.cancelContext;
561
+ return {
562
+ cancelOriginEventId,
563
+ cancelReason: reason,
564
+ cancelRequestedBy: requestedBy,
565
+ };
566
+ }
567
+ /**
568
+ * Write the regular (non-cancel) activity terminal that corresponds to
569
+ * an EvidenceResult. Returns a ReconcileOutcome for the orchestrator.
570
+ *
571
+ * Caller must filter out `transient` and `skipped` before invoking.
572
+ */
573
+ async function writeRegularTerminal(ctx, attemptId, activityId, ea, evidence) {
574
+ switch (evidence.kind) {
575
+ case 'completedByIdempotentSubmit': {
576
+ const outputBuf = Buffer.from(JSON.stringify(evidence.externalRefs), 'utf-8');
577
+ const outputHash = await sha256Hex(outputBuf);
578
+ const terminalEvent = (await ctx.log.append({
579
+ runId: ctx.runId,
580
+ type: 'activitySucceeded',
581
+ actor: 'system',
582
+ payload: {
583
+ activityId,
584
+ attemptId,
585
+ outputRef: {
586
+ outputHash: `sha256:${outputHash}`,
587
+ outputBytes: outputBuf.length,
588
+ outputSchemaVersion: 1,
589
+ contentType: 'application/json',
590
+ },
591
+ externalRefs: evidence.externalRefs,
592
+ },
593
+ }));
594
+ return {
595
+ activityId,
596
+ attemptId,
597
+ idempotencyKey: ea.idempotencyKey,
598
+ provider: ea.provider,
599
+ capability: evidence.capability,
600
+ decision: 'completedByIdempotentSubmit',
601
+ evidence: evidence.evidence,
602
+ terminalEvent,
603
+ reconcileEvent: evidence.reconcileEvent,
604
+ recovered: evidence.recovered,
605
+ };
606
+ }
607
+ case 'freshRetry': {
608
+ return {
609
+ activityId,
610
+ attemptId,
611
+ idempotencyKey: ea.idempotencyKey,
612
+ provider: ea.provider,
613
+ capability: evidence.capability,
614
+ decision: 'freshRetry',
615
+ evidence: evidence.evidence,
616
+ terminalEvent: null,
617
+ reconcileEvent: evidence.reconcileEvent,
618
+ recovered: evidence.recovered,
619
+ };
620
+ }
621
+ case 'manual': {
622
+ const terminalEvent = (await ctx.log.append({
623
+ runId: ctx.runId,
624
+ type: 'activityFailed',
625
+ actor: 'system',
626
+ payload: {
627
+ activityId,
628
+ attemptId,
629
+ error: {
630
+ errorCode: evidence.errorCode,
631
+ errorClass: 'manual',
632
+ errorMessage: evidence.errorMessage,
633
+ },
634
+ },
635
+ }));
636
+ return {
637
+ activityId,
638
+ attemptId,
639
+ idempotencyKey: ea.idempotencyKey,
640
+ provider: ea.provider,
641
+ capability: evidence.capability,
642
+ decision: 'manual',
643
+ evidence: evidence.evidence,
644
+ terminalEvent,
645
+ reconcileEvent: evidence.reconcileEvent,
646
+ recovered: evidence.recovered,
647
+ };
648
+ }
649
+ }
650
+ }
651
+ // ─── F1 recovery: a prior reconcileResult exists, terminal does not ─────────
652
+ /**
653
+ * Re-shape a prior crashed reconcile cycle's reconcileResult into an
654
+ * EvidenceResult so the caller's terminal-write path matches what would
655
+ * have happened originally.
656
+ *
657
+ * Codex Step 7 round 2: corrupt prior decisions (replayed without
658
+ * terminal, or completedByIdempotentSubmit with missing externalRefs)
659
+ * escalate to `manual` with diagnostic evidence rather than fabricate
660
+ * a fake activitySucceeded.
661
+ */
662
+ function evidenceFromPriorReconcileResult(latest, _ea) {
663
+ void _ea;
664
+ const rr = latest.latestReconcileResult;
665
+ switch (rr.decision) {
666
+ case 'completedByIdempotentSubmit': {
667
+ const candidate = rr.evidence.externalRefs;
668
+ if (candidate === undefined ||
669
+ candidate === null ||
670
+ typeof candidate !== 'object' ||
671
+ Array.isArray(candidate)) {
672
+ return {
673
+ kind: 'manual',
674
+ capability: rr.capability,
675
+ errorCode: 'CorruptLog',
676
+ errorMessage: 'Prior reconcileResult{decision=completedByIdempotentSubmit} is missing evidence.externalRefs (or it is not an object) — refusing to fabricate an activitySucceeded from empty refs.',
677
+ evidence: {
678
+ ...rr.evidence,
679
+ corruptReason: 'missing_external_refs',
680
+ originalDecision: 'completedByIdempotentSubmit',
681
+ reconcileEventId: rr.eventId,
682
+ },
683
+ reconcileEvent: null,
684
+ recovered: true,
685
+ };
686
+ }
687
+ return {
688
+ kind: 'completedByIdempotentSubmit',
689
+ capability: rr.capability,
690
+ externalRefs: candidate,
691
+ evidence: rr.evidence,
692
+ reconcileEvent: null,
693
+ recovered: true,
694
+ };
695
+ }
696
+ case 'manual': {
697
+ const errorCode = rr.evidence.errorCode ?? 'UnknownProviderError';
698
+ return {
699
+ kind: 'manual',
700
+ capability: rr.capability,
701
+ errorCode,
702
+ errorMessage: `Recovered from prior crashed reconcile cycle (decision=manual, errorCode=${errorCode}).`,
703
+ evidence: rr.evidence,
704
+ reconcileEvent: null,
705
+ recovered: true,
706
+ };
707
+ }
708
+ case 'freshRetry': {
709
+ return {
710
+ kind: 'freshRetry',
711
+ capability: rr.capability,
712
+ evidence: rr.evidence,
713
+ reconcileEvent: null,
714
+ recovered: true,
715
+ };
716
+ }
717
+ case 'replayed': {
718
+ // Replayed means a terminal already existed when reconcileResult
719
+ // was written. If we landed here, that terminal got lost — log
720
+ // corruption. Surface as manual to flag the inconsistency.
721
+ return {
722
+ kind: 'manual',
723
+ capability: rr.capability,
724
+ errorCode: 'CorruptLog',
725
+ errorMessage: 'Prior reconcileResult decision=replayed but no terminal event present — log inconsistency.',
726
+ evidence: {
727
+ ...rr.evidence,
728
+ originalDecision: 'replayed',
729
+ reconcileEventId: rr.eventId,
730
+ },
731
+ reconcileEvent: null,
732
+ recovered: true,
733
+ };
734
+ }
735
+ }
736
+ }
737
+ // ─── reconcileResult writers (decision-shaped EvidenceResult builders) ──────
738
+ async function writeReconcileResultCompleted(ctx, activityId, ea, capability, externalRefs, evidence) {
739
+ const reconcileEvent = (await ctx.log.append({
740
+ runId: ctx.runId,
741
+ type: 'reconcileResult',
742
+ actor: 'system',
743
+ payload: {
744
+ activityId,
745
+ idempotencyKey: ea.idempotencyKey,
746
+ capability,
747
+ decision: 'completedByIdempotentSubmit',
748
+ evidence: { ...evidence, externalRefs },
749
+ },
750
+ }));
751
+ return {
752
+ kind: 'completedByIdempotentSubmit',
753
+ capability,
754
+ externalRefs,
755
+ evidence,
756
+ reconcileEvent,
757
+ recovered: false,
758
+ };
759
+ }
760
+ async function writeReconcileResultFreshRetry(ctx, activityId, ea, capability, evidence) {
761
+ const reconcileEvent = (await ctx.log.append({
762
+ runId: ctx.runId,
763
+ type: 'reconcileResult',
764
+ actor: 'system',
765
+ payload: {
766
+ activityId,
767
+ idempotencyKey: ea.idempotencyKey,
768
+ capability,
769
+ decision: 'freshRetry',
770
+ evidence,
771
+ },
772
+ }));
773
+ return {
774
+ kind: 'freshRetry',
775
+ capability,
776
+ evidence,
777
+ reconcileEvent,
778
+ recovered: false,
779
+ };
780
+ }
781
+ async function writeReconcileResultManual(ctx, activityId, ea, capability, errorCode, errorMessage, evidence) {
782
+ const reconcileEvent = (await ctx.log.append({
783
+ runId: ctx.runId,
784
+ type: 'reconcileResult',
785
+ actor: 'system',
786
+ payload: {
787
+ activityId,
788
+ idempotencyKey: ea.idempotencyKey,
789
+ capability,
790
+ decision: 'manual',
791
+ evidence: { ...evidence, errorCode },
792
+ },
793
+ }));
794
+ return {
795
+ kind: 'manual',
796
+ capability,
797
+ errorCode,
798
+ errorMessage,
799
+ evidence,
800
+ reconcileEvent,
801
+ recovered: false,
802
+ };
803
+ }
804
+ async function sha256Hex(buf) {
805
+ const { createHash } = await import('node:crypto');
806
+ return createHash('sha256').update(buf).digest('hex');
807
+ }
808
+ //# sourceMappingURL=resume.js.map