@osolmaz/pi-workflows 0.16.4 → 0.16.6

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 (173) hide show
  1. package/README.md +5 -5
  2. package/dist/builtins/autoimplement.workflow.d.ts +5 -1
  3. package/dist/builtins/autoimplement.workflow.js +29 -10
  4. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  5. package/dist/client/client.js +3 -1
  6. package/dist/client/client.js.map +1 -1
  7. package/dist/client/protocol.d.ts +1 -1
  8. package/dist/client/protocol.js +18 -1
  9. package/dist/client/protocol.js.map +1 -1
  10. package/dist/client/view.d.ts +7 -5
  11. package/dist/extension/index.d.ts +2 -1
  12. package/dist/extension/index.js +115 -165
  13. package/dist/extension/index.js.map +1 -1
  14. package/dist/extension/recorder.d.ts +1 -1
  15. package/dist/extension/recorder.js +2 -2
  16. package/dist/extension/recorder.js.map +1 -1
  17. package/dist/extension/step-message.d.ts +1 -1
  18. package/dist/extension/step-message.js +4 -4
  19. package/dist/extension/step-message.js.map +1 -1
  20. package/dist/extension/workflow-message-coordinator.d.ts +12 -1
  21. package/dist/extension/workflow-message-coordinator.js +65 -54
  22. package/dist/extension/workflow-message-coordinator.js.map +1 -1
  23. package/dist/resource-managers/index.d.ts +1 -2
  24. package/dist/resource-managers/index.js +1 -2
  25. package/dist/resource-managers/index.js.map +1 -1
  26. package/dist/resource-managers/sqlite.d.ts +2 -295
  27. package/dist/resource-managers/sqlite.js +71 -1528
  28. package/dist/resource-managers/sqlite.js.map +1 -1
  29. package/dist/resource-managers/types.d.ts +2 -7
  30. package/dist/server/resource-runner-entry.js +2 -6
  31. package/dist/server/resource-runner-entry.js.map +1 -1
  32. package/dist/server/rpc-bridge.js +1 -1
  33. package/dist/server/rpc-bridge.js.map +1 -1
  34. package/dist/server/rpc-executor.js +2 -4
  35. package/dist/server/rpc-executor.js.map +1 -1
  36. package/dist/server/server-entry.js +3 -0
  37. package/dist/server/server-entry.js.map +1 -1
  38. package/dist/server/server.d.ts +22 -8
  39. package/dist/server/server.js +498 -543
  40. package/dist/server/server.js.map +1 -1
  41. package/dist/server/state.d.ts +16 -29
  42. package/dist/server/state.js +105 -319
  43. package/dist/server/state.js.map +1 -1
  44. package/dist/server/view.d.ts +4 -5
  45. package/dist/server/view.js +51 -51
  46. package/dist/server/view.js.map +1 -1
  47. package/dist/server/workflow-runner-entry.d.ts +1 -1
  48. package/dist/server/workflow-runner-entry.js +1 -49
  49. package/dist/server/workflow-runner-entry.js.map +1 -1
  50. package/dist/server/workflow-runner-protocol.d.ts +2 -7
  51. package/dist/server/workflow-runner-protocol.js +5 -10
  52. package/dist/server/workflow-runner-protocol.js.map +1 -1
  53. package/dist/server/workflow-runner-store.d.ts +5 -6
  54. package/dist/server/workflow-runner-store.js +7 -14
  55. package/dist/server/workflow-runner-store.js.map +1 -1
  56. package/dist/state/attempt-time.d.ts +21 -0
  57. package/dist/state/attempt-time.js +95 -0
  58. package/dist/state/attempt-time.js.map +1 -0
  59. package/dist/state/json.d.ts +2 -1
  60. package/dist/state/json.js.map +1 -1
  61. package/dist/state/project-store.d.ts +39 -0
  62. package/dist/state/project-store.js +154 -0
  63. package/dist/state/project-store.js.map +1 -0
  64. package/dist/state/prune.d.ts +28 -0
  65. package/dist/state/prune.js +278 -83
  66. package/dist/state/prune.js.map +1 -1
  67. package/dist/state/schema.js +23 -23
  68. package/dist/state/workflow-messages.d.ts +1 -1
  69. package/dist/state/workflow-messages.js +3 -1
  70. package/dist/state/workflow-messages.js.map +1 -1
  71. package/dist/viewer/backup.js +5 -0
  72. package/dist/viewer/backup.js.map +1 -1
  73. package/dist/workflows/diagnostics.d.ts +8 -0
  74. package/dist/workflows/diagnostics.js +61 -0
  75. package/dist/workflows/diagnostics.js.map +1 -0
  76. package/dist/workflows/engine.d.ts +2 -15
  77. package/dist/workflows/engine.js +125 -245
  78. package/dist/workflows/engine.js.map +1 -1
  79. package/dist/workflows/human-decision.d.ts +3 -7
  80. package/dist/workflows/human-decision.js +2 -56
  81. package/dist/workflows/human-decision.js.map +1 -1
  82. package/dist/workflows/index.d.ts +2 -1
  83. package/dist/workflows/index.js +1 -0
  84. package/dist/workflows/index.js.map +1 -1
  85. package/dist/workflows/queue.d.ts +267 -0
  86. package/dist/workflows/queue.js +1248 -0
  87. package/dist/workflows/queue.js.map +1 -0
  88. package/dist/workflows/requests.d.ts +47 -0
  89. package/dist/workflows/requests.js +158 -0
  90. package/dist/workflows/requests.js.map +1 -0
  91. package/dist/workflows/schema.js +2 -4
  92. package/dist/workflows/schema.js.map +1 -1
  93. package/dist/workflows/store.d.ts +11 -16
  94. package/dist/workflows/store.js +174 -169
  95. package/dist/workflows/store.js.map +1 -1
  96. package/dist/workflows/tool-input.d.ts +6 -9
  97. package/dist/workflows/tool-input.js +15 -12
  98. package/dist/workflows/tool-input.js.map +1 -1
  99. package/dist/workflows/transitions.d.ts +38 -0
  100. package/dist/workflows/transitions.js +276 -0
  101. package/dist/workflows/transitions.js.map +1 -0
  102. package/dist/workflows/types.d.ts +9 -42
  103. package/dist/workflows/workflow-message-content.d.ts +7 -1
  104. package/dist/workflows/workflow-message-content.js +22 -4
  105. package/dist/workflows/workflow-message-content.js.map +1 -1
  106. package/docs/2026-08-25-workflow-follow-ups.md +5 -5
  107. package/docs/2026-09-02-unify-workflow-messages-plan.md +2 -0
  108. package/docs/2026-09-04-workflow-run-state-plan.md +2 -0
  109. package/docs/2026-09-06-durable-execution-plan.md +176 -0
  110. package/docs/DEFERRED_TURNS.md +84 -105
  111. package/docs/HUMAN_DECISIONS.md +17 -19
  112. package/docs/HUMAN_DECISION_PRESENTATIONS.md +1 -1
  113. package/docs/SQLITE_STATE.md +87 -30
  114. package/docs/WORKFLOW_SERVER.md +50 -11
  115. package/docs/WORKFLOW_STEP_MESSAGES.md +72 -54
  116. package/docs/plans/2026-08-04-controller-runtime-plan.md +2 -0
  117. package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +2 -0
  118. package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +2 -0
  119. package/docs/plans/2026-09-04-automatic-state-retention-plan.md +283 -0
  120. package/docs/workflows.md +109 -44
  121. package/examples/workflows/autoresearch.workflow.ts +0 -2
  122. package/examples/workflows/branch.workflow.ts +0 -4
  123. package/examples/workflows/echo.workflow.ts +0 -2
  124. package/examples/workflows/live-settings.workflow.ts +0 -2
  125. package/examples/workflows/two-turn.workflow.ts +0 -2
  126. package/herdr-plugin.toml +1 -1
  127. package/package.json +1 -1
  128. package/protocol/fixtures/client-v1.json +1 -1
  129. package/skills/pi-workflows/SKILL.md +8 -7
  130. package/src/builtins/autoimplement.workflow.ts +31 -10
  131. package/src/client/client.ts +3 -1
  132. package/src/client/protocol.ts +21 -1
  133. package/src/client/view.ts +9 -5
  134. package/src/extension/index.ts +117 -189
  135. package/src/extension/recorder.ts +2 -2
  136. package/src/extension/step-message.ts +4 -4
  137. package/src/extension/workflow-message-coordinator.ts +78 -50
  138. package/src/resource-managers/index.ts +1 -9
  139. package/src/resource-managers/sqlite.ts +330 -2655
  140. package/src/resource-managers/types.ts +2 -3
  141. package/src/server/resource-runner-entry.ts +2 -7
  142. package/src/server/rpc-bridge.ts +1 -1
  143. package/src/server/rpc-executor.ts +4 -9
  144. package/src/server/server-entry.ts +3 -0
  145. package/src/server/server.ts +568 -644
  146. package/src/server/state.ts +122 -466
  147. package/src/server/view.ts +56 -60
  148. package/src/server/workflow-runner-entry.ts +2 -55
  149. package/src/server/workflow-runner-protocol.ts +5 -15
  150. package/src/server/workflow-runner-store.ts +12 -27
  151. package/src/state/attempt-time.ts +122 -0
  152. package/src/state/json.ts +2 -1
  153. package/src/state/project-store.ts +218 -0
  154. package/src/state/prune.ts +408 -95
  155. package/src/state/schema.ts +23 -23
  156. package/src/state/workflow-messages.ts +4 -2
  157. package/src/viewer/backup.ts +7 -0
  158. package/src/workflows/diagnostics.ts +78 -0
  159. package/src/workflows/engine.ts +133 -291
  160. package/src/workflows/human-decision.ts +3 -81
  161. package/src/workflows/index.ts +8 -2
  162. package/src/workflows/queue.ts +2091 -0
  163. package/src/workflows/requests.ts +283 -0
  164. package/src/workflows/schema.ts +2 -6
  165. package/src/workflows/store.ts +223 -231
  166. package/src/workflows/tool-input.ts +20 -12
  167. package/src/workflows/transitions.ts +330 -0
  168. package/src/workflows/types.ts +9 -46
  169. package/src/workflows/workflow-message-content.ts +28 -4
  170. package/dist/resource-managers/runtime.d.ts +0 -59
  171. package/dist/resource-managers/runtime.js +0 -423
  172. package/dist/resource-managers/runtime.js.map +0 -1
  173. package/src/resource-managers/runtime.ts +0 -576
@@ -1,4 +1,5 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
+ import { closeAttemptTime, closeRunTime } from "../state/attempt-time.js";
2
3
  import { StateDatabase, workflowStatePath } from "../state/database.js";
3
4
  import { canonicalJson, parseJson, type JsonValue } from "../state/json.js";
4
5
  import {
@@ -49,7 +50,6 @@ import type {
49
50
  WorkflowEffectReservation,
50
51
  HumanDecisionReceipt,
51
52
  HumanDecisionRequest,
52
- ResolvedHumanDecision,
53
53
  WorkflowNodeDefinition,
54
54
  WorkflowNodeResult,
55
55
  WorkflowNodeSnapshot,
@@ -82,7 +82,6 @@ const STEP_ROW_SELECT = `
82
82
  SELECT s.step_index AS stepIndex, a.attempt_id AS attemptId, a.node_id AS nodeId,
83
83
  a.node_type AS nodeType, a.status,
84
84
  a.prompt_hash AS promptHash, a.output_hash AS outputHash,
85
- s.output_override_hash AS outputOverrideHash,
86
85
  a.receipt_hash AS receiptHash, a.error_hash AS errorHash,
87
86
  prompt_entry.entry_hash AS promptEntryHash,
88
87
  response_entry.entry_hash AS responseEntryHash,
@@ -150,7 +149,6 @@ export type WorkflowRunTerminalData = {
150
149
  input: JsonValue;
151
150
  finalOutput: JsonValue | null;
152
151
  error: string | null;
153
- presentationInstructions: string;
154
152
  restartNumber: number;
155
153
  };
156
154
 
@@ -171,6 +169,18 @@ export type WorkflowRunStoreOptions = {
171
169
  };
172
170
 
173
171
  /** The state boundary used by workflow code, including out-of-process runners. */
172
+ import {
173
+ acceptedRequestOutput,
174
+ ensureWorkflowRequest,
175
+ requestForAttempt,
176
+ type WorkflowCheckpointState,
177
+ } from "./requests.js";
178
+ import {
179
+ applyExecutionTransition,
180
+ executionTransition,
181
+ type WorkflowTransition,
182
+ } from "./transitions.js";
183
+
174
184
  export interface WorkflowExecutionStore {
175
185
  readonly databasePath: string;
176
186
  initializeRun(
@@ -180,14 +190,9 @@ export interface WorkflowExecutionStore {
180
190
  ): Promise<string>;
181
191
  prepareRunResume(runId: string): Promise<WorkflowRunState>;
182
192
  readRunState(runId: string): WorkflowRunState | null | Promise<WorkflowRunState | null>;
183
- writeSnapshot(
184
- runId: string,
185
- state: WorkflowRunState,
186
- event: WorkflowTraceEventDraft,
187
- ): Promise<WorkflowTraceEvent>;
193
+ commitTransition(runId: string, transition: WorkflowTransition): Promise<WorkflowTraceEvent>;
188
194
  publishUpdate(
189
195
  runId: string,
190
- state: WorkflowRunState,
191
196
  nodeId: string,
192
197
  attemptId: string,
193
198
  update: WorkflowUpdateInput,
@@ -208,8 +213,7 @@ export interface WorkflowExecutionStore {
208
213
  scopeId: string,
209
214
  changeNumber: number,
210
215
  ): WorkflowSettingsScopeRecord | undefined | Promise<WorkflowSettingsScopeRecord | undefined>;
211
- createHumanDecisionRequest(request: HumanDecisionRequest): Promise<"created" | "adopted">;
212
- readResolvedHumanDecision(decisionId: string): Promise<ResolvedHumanDecision | null>;
216
+ readCheckpoint(runId: string, attemptId: string): Promise<WorkflowCheckpointState | undefined>;
213
217
  reserveEffect(options: {
214
218
  runId: string;
215
219
  attemptId: string;
@@ -269,7 +273,6 @@ type StepRow = {
269
273
  status: string;
270
274
  promptHash: Buffer | null;
271
275
  outputHash: Buffer | null;
272
- outputOverrideHash: Buffer | null;
273
276
  receiptHash: Buffer | null;
274
277
  promptEntryHash: Buffer | null;
275
278
  responseEntryHash: Buffer | null;
@@ -535,16 +538,25 @@ export class WorkflowRunStore {
535
538
  return revision;
536
539
  }
537
540
 
538
- async createHumanDecisionRequest(request: HumanDecisionRequest): Promise<"created" | "adopted"> {
539
- return await new HumanDecisionStore(this.databasePath, { state: this.state }).createRequest(
540
- request,
541
- );
542
- }
543
-
544
- async readResolvedHumanDecision(decisionId: string): Promise<ResolvedHumanDecision | null> {
545
- return await new HumanDecisionStore(this.databasePath, { state: this.state }).readResolved(
546
- decisionId,
547
- );
541
+ async readCheckpoint(
542
+ runId: string,
543
+ attemptId: string,
544
+ ): Promise<WorkflowCheckpointState | undefined> {
545
+ const request = requestForAttempt(this.state, runId, attemptId);
546
+ if (request === undefined) return undefined;
547
+ if (request.kind !== "checkpoint" && request.kind !== "decision") {
548
+ throw new Error("The attempt does not own a checkpoint request");
549
+ }
550
+ if (request.status !== "settled") return { request };
551
+ const output = acceptedRequestOutput(this.state, request);
552
+ const humanDecision =
553
+ request.kind === "decision"
554
+ ? this.readHumanDecisionReceipt(runId, request.requestId)
555
+ : undefined;
556
+ if (request.kind === "decision" && humanDecision === undefined) {
557
+ throw new Error("The checkpoint has no verified human decision receipt");
558
+ }
559
+ return { request, output, ...(humanDecision === undefined ? {} : { humanDecision }) };
548
560
  }
549
561
 
550
562
  async reserveEffect(options: {
@@ -904,22 +916,12 @@ export class WorkflowRunStore {
904
916
  [...runViewerTargets(state, traceEvent), ...this.viewerInspectorTargets(state.runId)],
905
917
  now,
906
918
  );
907
- this.syncContinuationIdentity(state);
908
919
  context.revision = revision;
909
920
  return;
910
921
  }
911
922
  const definitionHash = this.state.putJson(snapshot, now);
912
- const parentLineage =
913
- state.parentRunId === undefined
914
- ? undefined
915
- : this.state.connection
916
- .prepare(
917
- `SELECT root_run_id AS rootRunId, restart_number AS restartNumber
918
- FROM runs WHERE run_id = ?`,
919
- )
920
- .get(state.parentRunId);
921
- if (state.parentRunId !== undefined && !isRunLineageRow(parentLineage)) {
922
- throw new Error(`Workflow parent run is missing: ${state.parentRunId}`);
923
+ if (state.parentRunId !== undefined) {
924
+ throw new Error("A restart must have an explicit host reservation");
923
925
  }
924
926
  this.state.connection
925
927
  .prepare(
@@ -950,7 +952,7 @@ export class WorkflowRunStore {
950
952
  .prepare(
951
953
  `INSERT INTO runs(
952
954
  run_id, resource_id, project_id, parent_run_id, root_run_id, lineage_kind,
953
- restart_number, parent_terminal_fingerprint, definition_digest,
955
+ restart_number, parent_run_revision, definition_digest,
954
956
  workflow_ref, launch_options_hash, title, status, paused,
955
957
  status_detail, input_hash, final_output_hash, error_hash,
956
958
  created_at, updated_at, finished_at
@@ -959,10 +961,10 @@ export class WorkflowRunStore {
959
961
  .run(
960
962
  state.runId,
961
963
  resourceId,
962
- state.parentRunId ?? null,
963
- isRunLineageRow(parentLineage) ? parentLineage.rootRunId : state.runId,
964
- state.parentRunId === undefined ? null : "continuation",
965
- isRunLineageRow(parentLineage) ? parentLineage.restartNumber : 0,
964
+ null,
965
+ state.runId,
966
+ null,
967
+ 0,
966
968
  definitionDigest,
967
969
  state.workflowName,
968
970
  launchOptionsHash,
@@ -979,7 +981,6 @@ export class WorkflowRunStore {
979
981
  );
980
982
  initializeViewerRun(this.state, state.runId, now);
981
983
  this.insertRunSources(state.runId, source, state.workflowSources ?? []);
982
- this.syncContinuationIdentity(state);
983
984
  insertRunEvent(this.state, resourceId, 1, at, {
984
985
  scope: "run",
985
986
  type: "run_created",
@@ -1010,7 +1011,7 @@ export class WorkflowRunStore {
1010
1011
  const now = Date.now();
1011
1012
  return this.state.transaction(() => {
1012
1013
  this.requireRunAcceptsSettings(options.runId, false);
1013
- const originRunId = this.logicalOriginRunId(options.runId);
1014
+ const originRunId = options.runId;
1014
1015
  const scopeId = workflowSettingsScopeId(originRunId, options.mountPath, options.invocation);
1015
1016
  const existing = this.settingsScopeRow(scopeId);
1016
1017
  if (existing !== undefined) {
@@ -1106,7 +1107,7 @@ export class WorkflowRunStore {
1106
1107
  }
1107
1108
 
1108
1109
  private settingsScopesForRunView(runId: string): WorkflowSettingsScopeRecord[] {
1109
- const originRunId = this.logicalOriginRunId(runId);
1110
+ const originRunId = runId;
1110
1111
  return this.state.connection
1111
1112
  .prepare(
1112
1113
  `SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
@@ -1128,7 +1129,7 @@ export class WorkflowRunStore {
1128
1129
  runId: string,
1129
1130
  range: WorkflowRunViewRange,
1130
1131
  ): WorkflowSettingsScopeRecord[] {
1131
- const originRunId = this.logicalOriginRunId(runId);
1132
+ const originRunId = runId;
1132
1133
  return this.state.connection
1133
1134
  .prepare(
1134
1135
  `SELECT s.scope_id AS scopeId, s.resource_id AS resourceId,
@@ -1464,8 +1465,27 @@ export class WorkflowRunStore {
1464
1465
  }
1465
1466
 
1466
1467
  async prepareRunResume(runId: string): Promise<WorkflowRunState> {
1467
- const state = this.readRunState(runId);
1468
+ let state = this.readRunState(runId);
1468
1469
  if (state === null) throw new Error(`Cannot resume unreadable workflow run: ${runId}`);
1470
+ if (state.status === "waiting" && state.currentAttemptId !== undefined) {
1471
+ const checkpoint = await this.readCheckpoint(runId, state.currentAttemptId);
1472
+ if (checkpoint?.request.status === "settled") {
1473
+ if (state.waitingOn === undefined)
1474
+ throw new Error("Accepted checkpoint has no waiting node");
1475
+ await this.commitTransition(runId, {
1476
+ kind: "resumeInteraction",
1477
+ event: {
1478
+ scope: "node",
1479
+ type: "interaction_accepted",
1480
+ nodeId: state.waitingOn,
1481
+ attemptId: state.currentAttemptId,
1482
+ payload: { requestId: checkpoint.request.requestId },
1483
+ },
1484
+ });
1485
+ state = this.readRunState(runId);
1486
+ if (state === null) throw new Error(`Workflow run became unreadable: ${runId}`);
1487
+ }
1488
+ }
1469
1489
  if (state.status !== "running") {
1470
1490
  throw new Error(`Cannot resume workflow run ${runId} with status ${state.status}`);
1471
1491
  }
@@ -1519,37 +1539,39 @@ export class WorkflowRunStore {
1519
1539
  delete loaded.state.currentNode;
1520
1540
  delete loaded.state.currentAttemptId;
1521
1541
  delete loaded.state.currentNodeStartedAt;
1522
- delete loaded.state.currentNodeDeadlineAt;
1542
+ delete loaded.state.currentNodeTimeoutMs;
1543
+ delete loaded.state.currentNodeElapsedMs;
1523
1544
  delete loaded.state.currentSettingsScopeId;
1524
1545
  delete loaded.state.currentSettingsChangeNumber;
1525
1546
  delete loaded.state.currentSettingsHash;
1526
1547
  delete loaded.state.statusDetail;
1527
1548
  delete loaded.state.paused;
1528
- await this.writeSnapshot(runId, loaded.state, {
1529
- scope: "run",
1530
- type: "run_interrupted",
1531
- payload: { error: reason },
1532
- });
1549
+ await this.commitTransition(
1550
+ runId,
1551
+ executionTransition(loaded.state, {
1552
+ scope: "run",
1553
+ type: "run_interrupted",
1554
+ payload: { error: reason },
1555
+ }),
1556
+ );
1533
1557
  return this.readRun(runId, { includeTrace: true });
1534
1558
  }
1535
1559
 
1536
1560
  async publishUpdate(
1537
1561
  runId: string,
1538
- state: WorkflowRunState,
1539
1562
  nodeId: string,
1540
1563
  attemptId: string,
1541
1564
  update: WorkflowUpdateInput,
1542
1565
  options: { signal?: AbortSignal } = {},
1543
1566
  ): Promise<{ event: WorkflowTraceEvent; record: WorkflowUpdateRecord }> {
1544
1567
  return await this.withRunLock(runId, async () =>
1545
- this.publishUpdateSynchronous(runId, state, nodeId, attemptId, update, options),
1568
+ this.publishUpdateSynchronous(runId, nodeId, attemptId, update, options),
1546
1569
  );
1547
1570
  }
1548
1571
 
1549
1572
  /** Server-only synchronous form for an already serialized command transaction. */
1550
1573
  publishUpdateSynchronous(
1551
1574
  runId: string,
1552
- state: WorkflowRunState,
1553
1575
  nodeId: string,
1554
1576
  attemptId: string,
1555
1577
  update: WorkflowUpdateInput,
@@ -1558,6 +1580,14 @@ export class WorkflowRunStore {
1558
1580
  if (options.signal?.aborted === true) {
1559
1581
  throw options.signal.reason ?? new Error("workflow update attempt is no longer active");
1560
1582
  }
1583
+ const state = this.readRunState(runId);
1584
+ if (
1585
+ state === null ||
1586
+ state.currentAttemptId !== attemptId ||
1587
+ (state.currentNode ?? state.waitingOn) !== nodeId
1588
+ ) {
1589
+ throw new Error("Workflow update does not match the active attempt");
1590
+ }
1561
1591
  const exists = (state.updates ?? []).some(
1562
1592
  (record) => record.type === update.type && record.key === update.key,
1563
1593
  );
@@ -1632,10 +1662,9 @@ export class WorkflowRunStore {
1632
1662
  return { event: acceptedEvent, record: acceptedRecord };
1633
1663
  }
1634
1664
 
1635
- async writeSnapshot(
1665
+ async commitTransition(
1636
1666
  runId: string,
1637
- state: WorkflowRunState,
1638
- event: WorkflowTraceEventDraft,
1667
+ transition: WorkflowTransition,
1639
1668
  ): Promise<WorkflowTraceEvent> {
1640
1669
  return await this.withRunLock(runId, async () => {
1641
1670
  let accepted: WorkflowTraceEvent | undefined;
@@ -1647,6 +1676,79 @@ export class WorkflowRunStore {
1647
1676
  const now = Date.now();
1648
1677
  const at = new Date(now).toISOString();
1649
1678
  const snapshot = this.readDefinition(run.definitionHash);
1679
+ const current = this.materializeRunState(run, snapshot);
1680
+ const state = applyExecutionTransition(current, snapshot, transition, at);
1681
+ if (
1682
+ transition.kind === "finishAttempt" &&
1683
+ transition.step.nodeType === "checkpoint" &&
1684
+ transition.step.outcome === "ok"
1685
+ ) {
1686
+ const request = requestForAttempt(this.state, runId, transition.step.attemptId);
1687
+ if (
1688
+ request === undefined ||
1689
+ request.status !== "settled" ||
1690
+ canonicalJson(acceptedRequestOutput(this.state, request)) !==
1691
+ canonicalJson(transition.step.output)
1692
+ ) {
1693
+ throw new Error("Checkpoint completion requires its exact accepted response");
1694
+ }
1695
+ }
1696
+ if (transition.kind === "waitForInput") {
1697
+ const node = snapshot.nodes[transition.event.nodeId ?? ""];
1698
+ if ((node?.humanDecision !== undefined) !== (transition.requestKind === "decision")) {
1699
+ throw new Error("Checkpoint response authority does not match its definition");
1700
+ }
1701
+ if (transition.requestKind === "decision") {
1702
+ const decision = transition.contract as unknown as HumanDecisionRequest;
1703
+ if (
1704
+ decision.runId !== runId ||
1705
+ decision.attemptId !== transition.event.attemptId ||
1706
+ decision.nodeId !== transition.event.nodeId ||
1707
+ decision.decisionId !== transition.requestId
1708
+ ) {
1709
+ throw new Error("Human decision does not match the active checkpoint");
1710
+ }
1711
+ new HumanDecisionStore(this.databasePath, { state: this.state }).createRequest(
1712
+ decision,
1713
+ );
1714
+ }
1715
+ const binding = this.state.connection
1716
+ .prepare("SELECT origin_session_id AS sessionId FROM run_bindings WHERE run_id = ?")
1717
+ .get(runId) as { sessionId: string | null } | undefined;
1718
+ ensureWorkflowRequest(
1719
+ this.state,
1720
+ {
1721
+ requestId: transition.requestId,
1722
+ runId,
1723
+ attemptId: transition.event.attemptId ?? "",
1724
+ targetSessionId: binding?.sessionId ?? "",
1725
+ kind: transition.requestKind,
1726
+ contract: transition.contract,
1727
+ },
1728
+ now,
1729
+ );
1730
+ }
1731
+ if (
1732
+ transition.kind === "resume" &&
1733
+ transition.attemptId === undefined &&
1734
+ current.currentAttemptId !== undefined
1735
+ ) {
1736
+ const request = this.state.connection
1737
+ .prepare(
1738
+ "SELECT 1 FROM interactive_requests WHERE attempt_id = ? AND status = 'pending'",
1739
+ )
1740
+ .get(current.currentAttemptId);
1741
+ if (request !== undefined)
1742
+ throw new Error("A pending request must resume its exact attempt");
1743
+ const retired = this.state.connection
1744
+ .prepare(
1745
+ `UPDATE node_attempts SET status = 'cancelled', finished_at = ?, updated_at = ?
1746
+ WHERE attempt_id = ? AND run_id = ? AND status = 'interrupted'`,
1747
+ )
1748
+ .run(now, now, current.currentAttemptId, runId);
1749
+ if (retired.changes !== 1) throw new Error("Only an interrupted attempt can be replaced");
1750
+ }
1751
+ const event = transition.event;
1650
1752
  this.assertSettingsRouteCurrent(event, snapshot);
1651
1753
  const traceEvent: WorkflowTraceEvent = { seq: revision, at, runId, ...event };
1652
1754
  state.traceSeq = revision;
@@ -1655,6 +1757,15 @@ export class WorkflowRunStore {
1655
1757
  this.persistRunState(run, state, revision, now);
1656
1758
  this.transitionFollowUpsForRunState(state, event, now);
1657
1759
  this.syncNodeAttempts(state, snapshot, now);
1760
+ if (transition.kind === "finishAttempt") {
1761
+ this.state.connection
1762
+ .prepare(
1763
+ `UPDATE interactive_requests SET consumed_at = ?, updated_at = ?
1764
+ WHERE run_id = ? AND attempt_id = ? AND status = 'settled'
1765
+ AND accepted_submission_id IS NOT NULL AND consumed_at IS NULL`,
1766
+ )
1767
+ .run(now, now, runId, transition.step.attemptId);
1768
+ }
1658
1769
  recordViewerDeltas(this.state, runId, runViewerTargets(state, traceEvent), now);
1659
1770
  this.snapshotLifecycle?.({
1660
1771
  runId,
@@ -2205,16 +2316,6 @@ export class WorkflowRunStore {
2205
2316
  return row?.errorHash == null ? null : this.readText(row.errorHash);
2206
2317
  }
2207
2318
 
2208
- readPresentationInstructions(runId: string): string {
2209
- const row = this.state.connection
2210
- .prepare("SELECT presentation_prompt_hash AS hash FROM runs WHERE run_id = ?")
2211
- .get(runId) as { hash?: Buffer | null } | undefined;
2212
- if (row?.hash == null) {
2213
- return "Explain the final workflow result to the user in a normal response.";
2214
- }
2215
- return this.readText(row.hash);
2216
- }
2217
-
2218
2319
  readTerminalData(runId: string): WorkflowRunTerminalData | null {
2219
2320
  const row = this.state.connection
2220
2321
  .prepare(
@@ -2238,7 +2339,6 @@ export class WorkflowRunStore {
2238
2339
  input: this.readRunInput(runId) as JsonValue,
2239
2340
  finalOutput: this.readRunFinalOutput(runId),
2240
2341
  error: this.readRunError(runId),
2241
- presentationInstructions: this.readPresentationInstructions(runId),
2242
2342
  restartNumber: row.restartNumber,
2243
2343
  };
2244
2344
  }
@@ -2249,6 +2349,17 @@ export class WorkflowRunStore {
2249
2349
  return this.materializeRunState(row, this.readDefinition(row.definitionHash));
2250
2350
  }
2251
2351
 
2352
+ runRevision(runId: string): number {
2353
+ const row = this.state.connection
2354
+ .prepare(
2355
+ `SELECT resources.revision FROM resources
2356
+ JOIN runs ON runs.resource_id = resources.resource_id WHERE runs.run_id = ?`,
2357
+ )
2358
+ .get(runId) as { revision: number } | undefined;
2359
+ if (row === undefined) throw new Error(`Workflow run not found: ${runId}`);
2360
+ return row.revision;
2361
+ }
2362
+
2252
2363
  readRun(runId: string, options: ReadWorkflowRunOptions = {}): LoadedWorkflowRun | null {
2253
2364
  const row = this.readRunRow(runId);
2254
2365
  if (row === undefined) return null;
@@ -2276,7 +2387,7 @@ export class WorkflowRunStore {
2276
2387
  const row = this.readRunRow(runId);
2277
2388
  if (row === undefined) return null;
2278
2389
  const segment = this.segmentRow(segmentIdFor(runId));
2279
- const settingsOrigin = this.logicalOriginRunId(runId);
2390
+ const settingsOrigin = runId;
2280
2391
  const scalar = (sql: string, ...params: unknown[]): number => {
2281
2392
  const count = this.state.connection.prepare(sql).get(...params);
2282
2393
  return isCountRow(count) ? count.count : 0;
@@ -2652,7 +2763,10 @@ export class WorkflowRunStore {
2652
2763
  state.runId,
2653
2764
  );
2654
2765
  if (update.changes !== 1) throw new Error(`Workflow run is missing: ${state.runId}`);
2655
- if (state.status !== "running") {
2766
+ if (state.paused || ["completed", "failed", "cancelled"].includes(state.status)) {
2767
+ closeRunTime(this.state, state.runId);
2768
+ }
2769
+ if (state.status !== "running" && state.status !== run.status) {
2656
2770
  this.enqueueRunSettlementEffect(run.resourceId, expectedRevision, state, now);
2657
2771
  }
2658
2772
  }
@@ -2793,20 +2907,13 @@ export class WorkflowRunStore {
2793
2907
  initialSettings: InitialWorkflowSettingsScope[],
2794
2908
  now: number,
2795
2909
  ): void {
2796
- if (state.parentRunId !== undefined) {
2797
- this.state.connection
2798
- .prepare(
2799
- `UPDATE workflow_settings SET active_run_id = ?, updated_at = ? WHERE active_run_id = ?`,
2800
- )
2801
- .run(state.runId, now, state.parentRunId);
2802
- }
2803
2910
  const existing = this.state.connection
2804
2911
  .prepare("SELECT COUNT(*) AS count FROM workflow_settings WHERE active_run_id = ?")
2805
2912
  .get(state.runId);
2806
2913
  const hasSettings = isCountRow(existing) && existing.count > 0;
2807
2914
  if (hasSettings) return;
2808
2915
  for (const scope of initialSettings) {
2809
- const originRunId = this.logicalOriginRunId(state.runId);
2916
+ const originRunId = state.runId;
2810
2917
  const scopeId = workflowSettingsScopeId(originRunId, scope.mountPath, scope.invocation);
2811
2918
  const resourceId = this.mutations.ensureResource("settings", scopeId, now);
2812
2919
  const settingsHash = this.state.putJson(scope.settings, now);
@@ -2833,19 +2940,6 @@ export class WorkflowRunStore {
2833
2940
  }
2834
2941
  }
2835
2942
 
2836
- private logicalOriginRunId(runId: string): string {
2837
- let current = runId;
2838
- for (let depth = 0; depth < 100; depth += 1) {
2839
- const row = this.state.connection
2840
- .prepare("SELECT parent_run_id AS parentRunId FROM runs WHERE run_id = ?")
2841
- .get(current);
2842
- if (!isParentRunRow(row)) return current;
2843
- if (row.parentRunId === null) return current;
2844
- current = row.parentRunId;
2845
- }
2846
- throw new Error(`Workflow continuation chain is too deep for run ${runId}`);
2847
- }
2848
-
2849
2943
  private requireRunAcceptsSettings(runId: string, allowWaiting: boolean): void {
2850
2944
  const row = this.state.connection
2851
2945
  .prepare("SELECT status FROM runs WHERE run_id = ?")
@@ -3101,15 +3195,7 @@ export class WorkflowRunStore {
3101
3195
  const rows = this.state.connection
3102
3196
  .prepare(
3103
3197
  `${FOLLOW_UP_ROW_SELECT}
3104
- WHERE f.run_id IN (
3105
- WITH RECURSIVE ancestors(run_id, parent_run_id) AS (
3106
- SELECT run_id, parent_run_id FROM runs WHERE run_id = ?
3107
- UNION ALL
3108
- SELECT parent.run_id, parent.parent_run_id
3109
- FROM runs parent JOIN ancestors ON ancestors.parent_run_id = parent.run_id
3110
- )
3111
- SELECT run_id FROM ancestors
3112
- ) AND f.status = 'queued'`,
3198
+ WHERE f.run_id = ? AND f.status = 'queued'`,
3113
3199
  )
3114
3200
  .all(state.runId)
3115
3201
  .filter(isFollowUpRow);
@@ -3144,7 +3230,8 @@ export class WorkflowRunStore {
3144
3230
  snapshot: WorkflowDefinitionSnapshot,
3145
3231
  now: number,
3146
3232
  ): void {
3147
- for (const step of state.steps.slice(state.carriedStepCount ?? 0)) {
3233
+ for (const step of state.steps) {
3234
+ closeAttemptTime(this.state, step.attemptId);
3148
3235
  const promptEntryId = this.findAttemptPromptEntry(state.runId, step.attemptId);
3149
3236
  const promptHash =
3150
3237
  promptEntryId === undefined && step.prompt !== null
@@ -3219,25 +3306,20 @@ export class WorkflowRunStore {
3219
3306
  }
3220
3307
  if (state.currentAttemptId !== undefined && state.currentNode !== undefined) {
3221
3308
  this.ensureAttempt(state, state.currentNode, state.currentAttemptId, now, snapshot);
3222
- if (state.currentNodeDeadlineAt !== undefined) {
3223
- const deadlineAt =
3224
- state.currentNodeDeadlineAt === null ? null : Date.parse(state.currentNodeDeadlineAt);
3225
- if (deadlineAt !== null && !Number.isFinite(deadlineAt)) {
3226
- throw new Error("Workflow node deadline is invalid");
3227
- }
3309
+ if (state.currentNodeTimeoutMs !== undefined) {
3228
3310
  this.state.connection
3229
3311
  .prepare(
3230
- `UPDATE node_attempts SET deadline_at = ?, updated_at = ?
3231
- WHERE attempt_id = ? AND run_id = ?
3312
+ `UPDATE node_attempts SET timeout_ms = ?, updated_at = ?
3313
+ WHERE attempt_id = ? AND run_id = ? AND timeout_ms IS NULL
3232
3314
  AND status IN ('pending', 'running', 'waiting', 'interrupted')`,
3233
3315
  )
3234
- .run(deadlineAt, now, state.currentAttemptId, state.runId);
3316
+ .run(state.currentNodeTimeoutMs ?? 0, now, state.currentAttemptId, state.runId);
3235
3317
  }
3236
3318
  }
3237
- this.syncRunSteps(state, now);
3319
+ this.syncRunSteps(state);
3238
3320
  }
3239
3321
 
3240
- private syncRunSteps(state: WorkflowRunState, now: number): void {
3322
+ private syncRunSteps(state: WorkflowRunState): void {
3241
3323
  const existingRows = this.state.connection
3242
3324
  .prepare(
3243
3325
  `SELECT step_index AS stepIndex, attempt_id AS attemptId
@@ -3257,18 +3339,16 @@ export class WorkflowRunStore {
3257
3339
  const step = state.steps[stepIndex];
3258
3340
  /* istanbul ignore if -- array index follows a checked bound */
3259
3341
  if (step === undefined) throw new Error("Workflow run step became unavailable");
3260
- const attemptOutput = this.readAttemptOutput(step.attemptId);
3261
- const carried = stepIndex < (state.carriedStepCount ?? 0);
3262
- const outputOverrideHash =
3263
- carried && canonicalJson(attemptOutput) !== canonicalJson(step.output)
3264
- ? this.state.putJson(step.output, now)
3265
- : null;
3342
+ const attempt = this.state.connection
3343
+ .prepare("SELECT run_id AS runId FROM node_attempts WHERE attempt_id = ?")
3344
+ .get(step.attemptId) as { runId: string } | undefined;
3345
+ if (attempt?.runId !== state.runId) throw new Error("A run cannot own another run's step");
3266
3346
  this.state.connection
3267
3347
  .prepare(
3268
- `INSERT INTO run_steps(run_id, step_index, attempt_id, output_override_hash)
3269
- VALUES (?, ?, ?, ?)`,
3348
+ `INSERT INTO run_steps(run_id, step_index, attempt_id)
3349
+ VALUES (?, ?, ?)`,
3270
3350
  )
3271
- .run(state.runId, stepIndex, step.attemptId, outputOverrideHash);
3351
+ .run(state.runId, stepIndex, step.attemptId);
3272
3352
  }
3273
3353
  }
3274
3354
 
@@ -3341,31 +3421,6 @@ export class WorkflowRunStore {
3341
3421
  return isEntryIdentityRow(row) ? row.entryId : undefined;
3342
3422
  }
3343
3423
 
3344
- private readAttemptOutput(attemptId: string): unknown {
3345
- const row = this.state.connection
3346
- .prepare(
3347
- `SELECT a.output_hash AS outputHash, a.receipt_hash AS receiptHash,
3348
- e.entry_hash AS responseEntryHash
3349
- FROM node_attempts a
3350
- LEFT JOIN attempt_entries l ON l.attempt_id = a.attempt_id AND l.role = 'response'
3351
- LEFT JOIN session_entries e
3352
- ON e.segment_id = l.segment_id AND e.entry_id = l.entry_id
3353
- WHERE a.attempt_id = ?`,
3354
- )
3355
- .get(attemptId);
3356
- if (!isAttemptValueRow(row)) {
3357
- throw new Error(`Workflow node attempt is missing: ${attemptId}`);
3358
- }
3359
- if (row.outputHash !== null) return this.state.readJson(row.outputHash);
3360
- if (row.responseEntryHash === null) return null;
3361
- const receipt =
3362
- row.receiptHash === null ? {} : this.readJsonAs<StoredAttemptReceipt>(row.receiptHash);
3363
- return assistantOutputFromEntry(
3364
- this.state.readJson(row.responseEntryHash),
3365
- receipt.assistantMessage?.maxChars,
3366
- );
3367
- }
3368
-
3369
3424
  private ensureAttempt(
3370
3425
  state: WorkflowRunState,
3371
3426
  nodeId: string,
@@ -3473,34 +3528,6 @@ export class WorkflowRunStore {
3473
3528
  }
3474
3529
  }
3475
3530
 
3476
- private syncContinuationIdentity(state: WorkflowRunState): void {
3477
- if (state.parentRunId === undefined || state.humanDecision === undefined) return;
3478
- const createdAt = Date.parse(state.humanDecision.acceptedAt);
3479
- this.state.connection
3480
- .prepare(
3481
- `INSERT INTO continuations(
3482
- decision_id, parent_run_id, continuation_run_id, created_at
3483
- ) VALUES (?, ?, ?, ?)
3484
- ON CONFLICT(decision_id) DO NOTHING`,
3485
- )
3486
- .run(state.humanDecision.decisionId, state.parentRunId, state.runId, createdAt);
3487
- const row = this.state.connection
3488
- .prepare(
3489
- `SELECT parent_run_id AS parentRunId, continuation_run_id AS continuationRunId,
3490
- created_at AS createdAt
3491
- FROM continuations WHERE decision_id = ?`,
3492
- )
3493
- .get(state.humanDecision.decisionId);
3494
- if (
3495
- !isContinuationIdentityRow(row) ||
3496
- row.parentRunId !== state.parentRunId ||
3497
- row.continuationRunId !== state.runId ||
3498
- row.createdAt !== createdAt
3499
- ) {
3500
- throw new Error("Immutable human decision continuation conflicts");
3501
- }
3502
- }
3503
-
3504
3531
  private readRunRow(runId: string): RunRow | undefined {
3505
3532
  const row = this.state.connection
3506
3533
  .prepare(
@@ -3544,7 +3571,6 @@ export class WorkflowRunStore {
3544
3571
  visibleUpdates: WorkflowUpdateRecord[],
3545
3572
  ): WorkflowRunState {
3546
3573
  const sources = this.readRunSources(row.runId, snapshot);
3547
- const carriedStepCount = this.carriedStepCount(row.runId);
3548
3574
  const activeAttempt =
3549
3575
  row.status === "running" || row.status === "waiting"
3550
3576
  ? this.readActiveAttempt(row.runId)
@@ -3579,7 +3605,6 @@ export class WorkflowRunStore {
3579
3605
  runId: row.runId,
3580
3606
  workflowName: row.workflowRef,
3581
3607
  ...(row.parentRunId === null ? {} : { parentRunId: row.parentRunId }),
3582
- ...(carriedStepCount === 0 ? {} : { carriedStepCount }),
3583
3608
  ...(row.title === null ? {} : { runTitle: row.title }),
3584
3609
  ...(sources.root === undefined ? {} : { workflowSource: sources.root }),
3585
3610
  ...(sources.mounted.length === 0 ? {} : { workflowSources: sources.mounted }),
@@ -3600,13 +3625,11 @@ export class WorkflowRunStore {
3600
3625
  ...(activeAttempt?.startedAt === null || activeAttempt === undefined
3601
3626
  ? {}
3602
3627
  : { currentNodeStartedAt: new Date(activeAttempt.startedAt).toISOString() }),
3603
- ...(activeAttempt === undefined
3628
+ ...(activeAttempt === undefined || activeAttempt.timeoutMs === null
3604
3629
  ? {}
3605
3630
  : {
3606
- currentNodeDeadlineAt:
3607
- activeAttempt.deadlineAt === null
3608
- ? null
3609
- : new Date(activeAttempt.deadlineAt).toISOString(),
3631
+ currentNodeTimeoutMs: activeAttempt.timeoutMs === 0 ? null : activeAttempt.timeoutMs,
3632
+ currentNodeElapsedMs: activeAttempt.activeElapsedMs,
3610
3633
  }),
3611
3634
  ...savedCurrentSettingsBinding(
3612
3635
  activeAttempt?.settingsScopeId ?? null,
@@ -3693,7 +3716,7 @@ export class WorkflowRunStore {
3693
3716
  : row.promptHash === null
3694
3717
  ? null
3695
3718
  : this.readText(row.promptHash);
3696
- const outputHash = row.outputOverrideHash ?? row.outputHash;
3719
+ const outputHash = row.outputHash;
3697
3720
  const output =
3698
3721
  outputHash !== null
3699
3722
  ? this.state.readJson(outputHash)
@@ -3763,23 +3786,13 @@ export class WorkflowRunStore {
3763
3786
  return { ...(root === undefined ? {} : { root }), mounted };
3764
3787
  }
3765
3788
 
3766
- private carriedStepCount(runId: string): number {
3767
- const row = this.state.connection
3768
- .prepare(
3769
- `SELECT count(*) AS count
3770
- FROM run_steps s JOIN node_attempts a ON a.attempt_id = s.attempt_id
3771
- WHERE s.run_id = ? AND a.run_id <> s.run_id`,
3772
- )
3773
- .get(runId);
3774
- return isCountRow(row) ? row.count : 0;
3775
- }
3776
-
3777
3789
  private readActiveAttempt(runId: string):
3778
3790
  | {
3779
3791
  attemptId: string;
3780
3792
  nodeId: string;
3781
3793
  startedAt: number | null;
3782
- deadlineAt: number | null;
3794
+ timeoutMs: number | null;
3795
+ activeElapsedMs: number;
3783
3796
  settingsScopeId: string | null;
3784
3797
  settingsChangeNumber: number | null;
3785
3798
  settingsHash: Buffer | null;
@@ -3788,7 +3801,10 @@ export class WorkflowRunStore {
3788
3801
  const row = this.state.connection
3789
3802
  .prepare(
3790
3803
  `SELECT attempt_id AS attemptId, node_id AS nodeId, started_at AS startedAt,
3791
- deadline_at AS deadlineAt, settings_scope_id AS settingsScopeId,
3804
+ timeout_ms AS timeoutMs,
3805
+ (SELECT COALESCE(SUM(elapsed_ms), 0) FROM attempt_active_intervals i
3806
+ WHERE i.attempt_id = node_attempts.attempt_id) AS activeElapsedMs,
3807
+ settings_scope_id AS settingsScopeId,
3792
3808
  settings_change_number AS settingsChangeNumber,
3793
3809
  settings_hash AS settingsHash
3794
3810
  FROM node_attempts
@@ -3798,16 +3814,19 @@ export class WorkflowRunStore {
3798
3814
  return isActiveAttemptRow(row) ? row : undefined;
3799
3815
  }
3800
3816
 
3801
- private readHumanDecisionReceipt(runId: string): HumanDecisionReceipt | undefined {
3817
+ private readHumanDecisionReceipt(
3818
+ runId: string,
3819
+ decisionId?: string,
3820
+ ): HumanDecisionReceipt | undefined {
3802
3821
  const row = this.state.connection
3803
3822
  .prepare(
3804
3823
  `SELECT d.request_hash AS requestHash, r.response_hash AS responseHash
3805
- FROM continuations c
3806
- JOIN human_decisions d ON d.decision_id = c.decision_id
3807
- JOIN human_decision_resolutions r ON r.decision_id = c.decision_id
3808
- WHERE c.continuation_run_id = ? AND r.outcome = 'accepted'`,
3824
+ FROM human_decisions d
3825
+ JOIN human_decision_resolutions r ON r.decision_id = d.decision_id
3826
+ WHERE d.run_id = ? AND r.outcome = 'accepted' AND (? IS NULL OR d.decision_id = ?)
3827
+ ORDER BY r.resolved_at DESC, d.decision_id DESC LIMIT 1`,
3809
3828
  )
3810
- .get(runId);
3829
+ .get(runId, decisionId ?? null, decisionId ?? null);
3811
3830
  if (!isDecisionReceiptRow(row)) return undefined;
3812
3831
  const request = this.readJsonAs<Record<string, unknown>>(row.requestHash);
3813
3832
  const decision = this.readJsonAs<Record<string, unknown>>(row.responseHash);
@@ -4210,8 +4229,8 @@ function runViewerTargets(
4210
4229
  },
4211
4230
  {
4212
4231
  op: "add",
4213
- path: "/state/currentNodeDeadlineAt",
4214
- value: state.currentNodeDeadlineAt ?? null,
4232
+ path: "/state/currentNodeTimeoutMs",
4233
+ value: state.currentNodeTimeoutMs ?? null,
4215
4234
  },
4216
4235
  {
4217
4236
  op: "add",
@@ -4741,18 +4760,6 @@ function isFollowUpRow(value: unknown): value is FollowUpRow {
4741
4760
  );
4742
4761
  }
4743
4762
 
4744
- function isParentRunRow(value: unknown): value is { parentRunId: string | null } {
4745
- return isRecord(value) && (typeof value.parentRunId === "string" || value.parentRunId === null);
4746
- }
4747
-
4748
- function isRunLineageRow(value: unknown): value is { rootRunId: string; restartNumber: number } {
4749
- return (
4750
- isRecord(value) &&
4751
- typeof value.rootRunId === "string" &&
4752
- typeof value.restartNumber === "number"
4753
- );
4754
- }
4755
-
4756
4763
  function isStatusRow(value: unknown): value is { status: string } {
4757
4764
  return isRecord(value) && typeof value.status === "string";
4758
4765
  }
@@ -4891,19 +4898,12 @@ function isEntryIdentityRow(value: unknown): value is { entryId: string } {
4891
4898
  return isRecord(value) && typeof value.entryId === "string";
4892
4899
  }
4893
4900
 
4894
- function isAttemptValueRow(value: unknown): value is {
4895
- outputHash: Buffer | null;
4896
- receiptHash: Buffer | null;
4897
- responseEntryHash: Buffer | null;
4898
- } {
4899
- return isRecord(value);
4900
- }
4901
-
4902
4901
  function isActiveAttemptRow(value: unknown): value is {
4903
4902
  attemptId: string;
4904
4903
  nodeId: string;
4905
4904
  startedAt: number | null;
4906
- deadlineAt: number | null;
4905
+ timeoutMs: number | null;
4906
+ activeElapsedMs: number;
4907
4907
  settingsScopeId: string | null;
4908
4908
  settingsChangeNumber: number | null;
4909
4909
  settingsHash: Buffer | null;
@@ -4920,14 +4920,6 @@ function isDecisionReceiptRow(value: unknown): value is {
4920
4920
  );
4921
4921
  }
4922
4922
 
4923
- function isContinuationIdentityRow(value: unknown): value is {
4924
- parentRunId: string;
4925
- continuationRunId: string;
4926
- createdAt: number;
4927
- } {
4928
- return isRecord(value);
4929
- }
4930
-
4931
4923
  function isUpdateRow(value: unknown): value is UpdateRow {
4932
4924
  return isRecord(value);
4933
4925
  }