@osolmaz/pi-workflows 0.16.5 → 0.16.7

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 (181) 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 +116 -166
  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/widget.js +1 -1
  21. package/dist/extension/widget.js.map +1 -1
  22. package/dist/extension/workflow-message-coordinator.d.ts +12 -3
  23. package/dist/extension/workflow-message-coordinator.js +108 -94
  24. package/dist/extension/workflow-message-coordinator.js.map +1 -1
  25. package/dist/render/graph-render.js +1 -1
  26. package/dist/resource-managers/index.d.ts +1 -2
  27. package/dist/resource-managers/index.js +1 -2
  28. package/dist/resource-managers/index.js.map +1 -1
  29. package/dist/resource-managers/sqlite.d.ts +2 -295
  30. package/dist/resource-managers/sqlite.js +71 -1528
  31. package/dist/resource-managers/sqlite.js.map +1 -1
  32. package/dist/resource-managers/types.d.ts +2 -7
  33. package/dist/server/resource-runner-entry.js +2 -6
  34. package/dist/server/resource-runner-entry.js.map +1 -1
  35. package/dist/server/rpc-bridge.js +1 -1
  36. package/dist/server/rpc-bridge.js.map +1 -1
  37. package/dist/server/rpc-executor.js +2 -4
  38. package/dist/server/rpc-executor.js.map +1 -1
  39. package/dist/server/server-entry.js +3 -0
  40. package/dist/server/server-entry.js.map +1 -1
  41. package/dist/server/server.d.ts +9 -8
  42. package/dist/server/server.js +364 -541
  43. package/dist/server/server.js.map +1 -1
  44. package/dist/server/state.d.ts +16 -29
  45. package/dist/server/state.js +105 -319
  46. package/dist/server/state.js.map +1 -1
  47. package/dist/server/view.d.ts +6 -5
  48. package/dist/server/view.js +68 -51
  49. package/dist/server/view.js.map +1 -1
  50. package/dist/server/workflow-runner-entry.d.ts +1 -1
  51. package/dist/server/workflow-runner-entry.js +1 -49
  52. package/dist/server/workflow-runner-entry.js.map +1 -1
  53. package/dist/server/workflow-runner-protocol.d.ts +2 -7
  54. package/dist/server/workflow-runner-protocol.js +5 -10
  55. package/dist/server/workflow-runner-protocol.js.map +1 -1
  56. package/dist/server/workflow-runner-store.d.ts +5 -6
  57. package/dist/server/workflow-runner-store.js +7 -14
  58. package/dist/server/workflow-runner-store.js.map +1 -1
  59. package/dist/state/attempt-time.d.ts +21 -0
  60. package/dist/state/attempt-time.js +95 -0
  61. package/dist/state/attempt-time.js.map +1 -0
  62. package/dist/state/json.d.ts +2 -1
  63. package/dist/state/json.js.map +1 -1
  64. package/dist/state/project-store.d.ts +39 -0
  65. package/dist/state/project-store.js +154 -0
  66. package/dist/state/project-store.js.map +1 -0
  67. package/dist/state/prune.js +0 -7
  68. package/dist/state/prune.js.map +1 -1
  69. package/dist/state/schema.js +23 -23
  70. package/dist/state/workflow-messages.d.ts +1 -1
  71. package/dist/state/workflow-messages.js +3 -1
  72. package/dist/state/workflow-messages.js.map +1 -1
  73. package/dist/viewer/backup.js +5 -0
  74. package/dist/viewer/backup.js.map +1 -1
  75. package/dist/workflows/diagnostics.d.ts +8 -0
  76. package/dist/workflows/diagnostics.js +61 -0
  77. package/dist/workflows/diagnostics.js.map +1 -0
  78. package/dist/workflows/engine.d.ts +2 -16
  79. package/dist/workflows/engine.js +128 -252
  80. package/dist/workflows/engine.js.map +1 -1
  81. package/dist/workflows/errors.d.ts +1 -1
  82. package/dist/workflows/errors.js +2 -2
  83. package/dist/workflows/errors.js.map +1 -1
  84. package/dist/workflows/human-decision.d.ts +3 -7
  85. package/dist/workflows/human-decision.js +2 -56
  86. package/dist/workflows/human-decision.js.map +1 -1
  87. package/dist/workflows/index.d.ts +2 -1
  88. package/dist/workflows/index.js +1 -0
  89. package/dist/workflows/index.js.map +1 -1
  90. package/dist/workflows/queue.d.ts +267 -0
  91. package/dist/workflows/queue.js +1248 -0
  92. package/dist/workflows/queue.js.map +1 -0
  93. package/dist/workflows/requests.d.ts +47 -0
  94. package/dist/workflows/requests.js +158 -0
  95. package/dist/workflows/requests.js.map +1 -0
  96. package/dist/workflows/schema.js +2 -4
  97. package/dist/workflows/schema.js.map +1 -1
  98. package/dist/workflows/store.d.ts +11 -16
  99. package/dist/workflows/store.js +177 -174
  100. package/dist/workflows/store.js.map +1 -1
  101. package/dist/workflows/tool-input.d.ts +6 -9
  102. package/dist/workflows/tool-input.js +15 -12
  103. package/dist/workflows/tool-input.js.map +1 -1
  104. package/dist/workflows/transitions.d.ts +38 -0
  105. package/dist/workflows/transitions.js +276 -0
  106. package/dist/workflows/transitions.js.map +1 -0
  107. package/dist/workflows/types.d.ts +9 -42
  108. package/dist/workflows/workflow-message-content.d.ts +7 -1
  109. package/dist/workflows/workflow-message-content.js +22 -4
  110. package/dist/workflows/workflow-message-content.js.map +1 -1
  111. package/docs/2026-08-25-workflow-follow-ups.md +5 -5
  112. package/docs/2026-09-02-unify-workflow-messages-plan.md +2 -0
  113. package/docs/2026-09-04-workflow-run-state-plan.md +2 -0
  114. package/docs/2026-09-06-durable-execution-plan.md +176 -0
  115. package/docs/2026-09-07-workflow-handoff-plan.md +84 -0
  116. package/docs/DEFERRED_TURNS.md +84 -105
  117. package/docs/HUMAN_DECISIONS.md +17 -19
  118. package/docs/HUMAN_DECISION_PRESENTATIONS.md +1 -1
  119. package/docs/SQLITE_STATE.md +68 -29
  120. package/docs/WORKFLOW_SERVER.md +21 -10
  121. package/docs/WORKFLOW_STEP_MESSAGES.md +83 -54
  122. package/docs/plans/2026-08-04-controller-runtime-plan.md +2 -0
  123. package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +2 -0
  124. package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +2 -0
  125. package/docs/workflows.md +111 -45
  126. package/examples/workflows/autoresearch.workflow.ts +0 -2
  127. package/examples/workflows/branch.workflow.ts +0 -4
  128. package/examples/workflows/echo.workflow.ts +0 -2
  129. package/examples/workflows/live-settings.workflow.ts +0 -2
  130. package/examples/workflows/two-turn.workflow.ts +0 -2
  131. package/herdr-plugin.toml +1 -1
  132. package/package.json +1 -1
  133. package/protocol/fixtures/client-v1.json +1 -1
  134. package/skills/pi-workflows/SKILL.md +8 -7
  135. package/src/builtins/autoimplement.workflow.ts +31 -10
  136. package/src/client/client.ts +3 -1
  137. package/src/client/protocol.ts +21 -1
  138. package/src/client/view.ts +9 -5
  139. package/src/extension/index.ts +118 -190
  140. package/src/extension/recorder.ts +2 -2
  141. package/src/extension/step-message.ts +4 -4
  142. package/src/extension/widget.ts +1 -1
  143. package/src/extension/workflow-message-coordinator.ts +133 -95
  144. package/src/render/graph-render.ts +1 -1
  145. package/src/resource-managers/index.ts +1 -9
  146. package/src/resource-managers/sqlite.ts +330 -2655
  147. package/src/resource-managers/types.ts +2 -3
  148. package/src/server/resource-runner-entry.ts +2 -7
  149. package/src/server/rpc-bridge.ts +1 -1
  150. package/src/server/rpc-executor.ts +4 -9
  151. package/src/server/server-entry.ts +3 -0
  152. package/src/server/server.ts +416 -641
  153. package/src/server/state.ts +122 -466
  154. package/src/server/view.ts +73 -58
  155. package/src/server/workflow-runner-entry.ts +2 -55
  156. package/src/server/workflow-runner-protocol.ts +5 -15
  157. package/src/server/workflow-runner-store.ts +12 -27
  158. package/src/state/attempt-time.ts +122 -0
  159. package/src/state/json.ts +2 -1
  160. package/src/state/project-store.ts +218 -0
  161. package/src/state/prune.ts +0 -13
  162. package/src/state/schema.ts +23 -23
  163. package/src/state/workflow-messages.ts +4 -2
  164. package/src/viewer/backup.ts +7 -0
  165. package/src/workflows/diagnostics.ts +78 -0
  166. package/src/workflows/engine.ts +136 -299
  167. package/src/workflows/errors.ts +4 -2
  168. package/src/workflows/human-decision.ts +3 -81
  169. package/src/workflows/index.ts +8 -2
  170. package/src/workflows/queue.ts +2091 -0
  171. package/src/workflows/requests.ts +283 -0
  172. package/src/workflows/schema.ts +2 -6
  173. package/src/workflows/store.ts +226 -236
  174. package/src/workflows/tool-input.ts +20 -12
  175. package/src/workflows/transitions.ts +330 -0
  176. package/src/workflows/types.ts +9 -46
  177. package/src/workflows/workflow-message-content.ts +28 -4
  178. package/dist/resource-managers/runtime.d.ts +0 -59
  179. package/dist/resource-managers/runtime.js +0 -423
  180. package/dist/resource-managers/runtime.js.map +0 -1
  181. package/src/resource-managers/runtime.ts +0 -576
@@ -1,14 +1,17 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { CLIENT_PROTOCOL_SCHEMA, clientRequestFingerprint, } from "../client/protocol.js";
3
+ import { AttemptTime } from "../state/attempt-time.js";
3
4
  import { StateDatabase } from "../state/database.js";
4
5
  import { canonicalJson } from "../state/json.js";
5
6
  import { tokenHash } from "../state/mutation.js";
6
7
  import { WorkflowMessageStore, workflowMessageIdFor, } from "../state/workflow-messages.js";
7
- import { decisionWorkflowMessageContent, stepWorkflowMessageContent, } from "../workflows/workflow-message-content.js";
8
+ import { ensureWorkflowRequest, readWorkflowRequest, recordWorkflowSubmission, } from "../workflows/requests.js";
9
+ import { decisionWorkflowMessageContent, checkpointWorkflowMessageContent, stepWorkflowMessageContent, } from "../workflows/workflow-message-content.js";
8
10
  export class ServerStateStore {
9
11
  state;
10
12
  ownsState;
11
13
  workflowMessages;
14
+ attemptTime;
12
15
  constructor(databasePath, options = {}) {
13
16
  this.ownsState = options.state === undefined;
14
17
  this.state =
@@ -18,6 +21,7 @@ export class ServerStateStore {
18
21
  mode: options.readOnly === true ? "read-only" : "read-write",
19
22
  });
20
23
  this.workflowMessages = new WorkflowMessageStore(this.state);
24
+ this.attemptTime = new AttemptTime(this.state, options.clock);
21
25
  }
22
26
  close() {
23
27
  if (this.ownsState)
@@ -234,48 +238,21 @@ export class ServerStateStore {
234
238
  });
235
239
  }
236
240
  createInteractiveRequest(options) {
237
- const now = Date.now();
238
241
  return this.state.transaction(() => {
239
- const existing = this.interactiveRequest(options.requestId);
240
- if (existing !== undefined) {
241
- if (existing.runId !== options.runId ||
242
- existing.attemptId !== options.attemptId ||
243
- canonicalJson(existing.contract) !== canonicalJson(options.contract)) {
244
- throw new Error(`Interactive request conflicts: ${options.requestId}`);
245
- }
246
- if (existing.status === "settled" && existing.consumedAt === null) {
247
- this.state.connection
248
- .prepare(`UPDATE interactive_requests
249
- SET status = 'pending', accepted_submission_id = NULL,
250
- settled_at = NULL, consumed_at = NULL, revision = revision + 1, updated_at = ?
251
- WHERE request_id = ? AND status = 'settled' AND consumed_at IS NULL`)
252
- .run(now, options.requestId);
253
- const reopened = this.requireInteractiveRequest(options.requestId);
254
- this.ensureInteractionMessage(reopened, "resumed", now);
255
- return reopened;
256
- }
257
- if (existing.status === "pending")
258
- this.ensureInteractionMessage(existing, "initial", now);
259
- return existing;
260
- }
261
- const contractHash = this.state.putJson(options.contract, now);
262
- this.state.connection
263
- .prepare(`INSERT INTO interactive_requests(
264
- request_id, run_id, attempt_id, target_session_id, kind, contract_hash,
265
- revision, status, created_at, updated_at
266
- ) VALUES (?, ?, ?, ?, ?, ?, 1, 'pending', ?, ?)`)
267
- .run(options.requestId, options.runId, options.attemptId, options.targetSessionId, options.kind, contractHash, now, now);
268
- const created = this.requireInteractiveRequest(options.requestId);
269
- this.ensureInteractionMessage(created, "initial", now);
270
- return created;
242
+ const request = ensureWorkflowRequest(this.state, options);
243
+ if (request.status === "pending")
244
+ this.ensureInteractionMessage(request, "initial");
245
+ return request;
271
246
  });
272
247
  }
273
248
  ensureInteractionMessage(request, reason, now = Date.now()) {
274
- const kind = request.kind === "decision" ? "decision" : "step";
249
+ const kind = request.kind === "decision" || request.kind === "checkpoint" ? "decision" : "step";
275
250
  const idempotencyKey = `${request.revision}:${reason}`;
276
251
  const workflowMessageId = workflowMessageIdFor(kind, request.requestId, idempotencyKey);
277
252
  const content = kind === "decision"
278
- ? decisionWorkflowMessageContent({
253
+ ? (request.kind === "checkpoint"
254
+ ? checkpointWorkflowMessageContent
255
+ : decisionWorkflowMessageContent)({
279
256
  workflowMessageId,
280
257
  requestId: request.requestId,
281
258
  runId: request.runId,
@@ -307,7 +284,7 @@ export class ServerStateStore {
307
284
  if (!isRequestIdRow(row))
308
285
  throw new Error("Pending workflow interaction is missing");
309
286
  const current = this.requireInteractiveRequest(row.requestId);
310
- if (current.kind === "decision")
287
+ if (current.kind === "decision" || current.kind === "checkpoint")
311
288
  return current;
312
289
  this.workflowMessages.cancelPendingForSource(current.requestId, "step", now);
313
290
  const changed = this.state.connection
@@ -321,113 +298,77 @@ export class ServerStateStore {
321
298
  return request;
322
299
  });
323
300
  }
324
- beginInteractionModelTurn(requestId, now = Date.now()) {
325
- const row = this.state.connection
326
- .prepare(`SELECT i.attempt_id AS attemptId, i.created_at AS createdAt,
327
- a.started_at AS startedAt, a.deadline_at AS deadlineAt,
328
- (SELECT MAX(t.ended_at)
329
- FROM workflow_messages m
330
- JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
331
- WHERE m.source_id = i.request_id AND m.kind = 'step'
332
- AND t.state = 'ended') AS previousTurnEndedAt
333
- FROM interactive_requests i
334
- JOIN node_attempts a ON a.attempt_id = i.attempt_id
335
- WHERE i.request_id = ? AND i.status = 'pending'`)
336
- .get(requestId);
337
- if (!isInteractionTimeoutRow(row)) {
338
- throw new Error("Pending workflow interaction timeout is missing");
301
+ syncActiveTime() {
302
+ this.attemptTime.sample();
303
+ }
304
+ beginInteractionModelTurn(requestId) {
305
+ const request = this.requireInteractiveRequest(requestId);
306
+ if (request.status !== "pending" || !["agent", "assistant"].includes(request.kind)) {
307
+ throw new Error("Only a pending agent request can start active model time");
339
308
  }
340
- this.shiftInteractionTimeout(row, Math.max(0, now - (row.previousTurnEndedAt ?? row.startedAt ?? row.createdAt)), now);
309
+ const run = this.state.connection
310
+ .prepare("SELECT paused FROM runs WHERE run_id = ?")
311
+ .get(request.runId);
312
+ if (run.paused !== 0)
313
+ throw new Error("Paused workflow cannot start active model time");
314
+ this.attemptTime.start(request.attemptId);
341
315
  }
342
- interactionPauseStartedAt(runId) {
343
- const row = this.state.connection
344
- .prepare(`SELECT r.updated_at AS pausedAt
345
- FROM runs r
346
- JOIN interactive_requests i ON i.run_id = r.run_id AND i.status = 'pending'
347
- WHERE r.run_id = ? AND r.paused = 1 LIMIT 1`)
348
- .get(runId);
349
- return isPausedAtRow(row) ? row.pausedAt : undefined;
316
+ endInteractionModelTurn(requestId) {
317
+ this.attemptTime.stop(this.requireInteractiveRequest(requestId).attemptId);
350
318
  }
351
- resumeInteractionModelTurn(runId, pausedAt, now = Date.now()) {
352
- if (pausedAt === undefined)
353
- return;
354
- const row = this.state.connection
355
- .prepare(`SELECT i.attempt_id AS attemptId, i.created_at AS createdAt,
356
- a.started_at AS startedAt, a.deadline_at AS deadlineAt,
357
- t.ended_at AS previousTurnEndedAt
358
- FROM interactive_requests i
359
- JOIN node_attempts a ON a.attempt_id = i.attempt_id
360
- JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
361
- JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
362
- WHERE i.run_id = ? AND i.status = 'pending' AND t.started_at <= ?
363
- AND (t.ended_at IS NULL OR t.ended_at > ?)
364
- ORDER BY t.started_at DESC LIMIT 1`)
365
- .get(runId, pausedAt, pausedAt);
366
- if (!isInteractionTimeoutRow(row))
367
- return;
368
- const modelTurnStoppedAt = Math.min(now, row.previousTurnEndedAt ?? now);
369
- this.shiftInteractionTimeout(row, Math.max(0, modelTurnStoppedAt - pausedAt), now);
319
+ resumeInteractionModelTurn(runId) {
320
+ const rows = this.state.connection
321
+ .prepare(`SELECT DISTINCT i.attempt_id AS attemptId FROM interactive_requests i
322
+ JOIN runs r ON r.run_id = i.run_id
323
+ JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
324
+ JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id AND t.state = 'started'
325
+ WHERE i.run_id = ? AND i.status = 'pending' AND r.paused = 0`)
326
+ .all(runId);
327
+ for (const row of rows) {
328
+ this.attemptTime.start(row.attemptId);
329
+ this.state.connection
330
+ .prepare("UPDATE node_attempts SET status = 'waiting' WHERE attempt_id = ?")
331
+ .run(row.attemptId);
332
+ }
370
333
  }
371
334
  markSessionModelTurnsInactive(targetSessionId, now = Date.now()) {
372
- this.state.connection
373
- .prepare(`UPDATE node_attempts SET status = 'interrupted', updated_at = ?
374
- WHERE status <> 'interrupted' AND attempt_id IN (
375
- SELECT DISTINCT i.attempt_id
376
- FROM interactive_requests i
377
- JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
378
- JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
379
- AND t.state = 'started'
380
- WHERE i.target_session_id = ? AND i.status = 'pending'
381
- )`)
382
- .run(now, targetSessionId);
383
- }
384
- recoverInactiveModelTurns(previousHeartbeatAt, now = Date.now()) {
385
335
  const rows = this.state.connection
386
- .prepare(`SELECT i.attempt_id AS attemptId, a.status, a.started_at AS startedAt,
387
- a.deadline_at AS deadlineAt, a.updated_at AS updatedAt,
388
- MAX(t.started_at) AS turnStartedAt
389
- FROM interactive_requests i
390
- JOIN node_attempts a ON a.attempt_id = i.attempt_id
391
- JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
392
- JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
393
- AND t.state = 'started'
394
- WHERE i.status = 'pending'
395
- GROUP BY i.attempt_id`)
396
- .all()
397
- .filter(isInactiveInteractionTimeoutRow);
336
+ .prepare(`SELECT DISTINCT i.attempt_id AS attemptId FROM interactive_requests i
337
+ JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
338
+ JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id AND t.state = 'started'
339
+ WHERE i.target_session_id = ? AND i.status = 'pending'`)
340
+ .all(targetSessionId);
398
341
  for (const row of rows) {
399
- const inactiveAt = row.status === "interrupted"
400
- ? row.updatedAt
401
- : previousHeartbeatAt === undefined
402
- ? row.updatedAt
403
- : Math.max(previousHeartbeatAt, row.turnStartedAt);
404
- this.shiftInteractionTimeout(row, Math.max(0, now - inactiveAt), now);
342
+ this.attemptTime.stop(row.attemptId);
405
343
  this.state.connection
406
- .prepare(`UPDATE node_attempts SET status = 'interrupted', updated_at = ?
407
- WHERE attempt_id = ? AND status IN ('pending', 'running', 'waiting', 'interrupted')`)
344
+ .prepare("UPDATE node_attempts SET status = 'interrupted', updated_at = ? WHERE attempt_id = ?")
408
345
  .run(now, row.attemptId);
409
346
  }
410
347
  }
348
+ recoverInactiveModelTurns(now = Date.now()) {
349
+ this.attemptTime.recover();
350
+ this.state.connection
351
+ .prepare(`UPDATE node_attempts SET status = 'interrupted', updated_at = ? WHERE attempt_id IN (
352
+ SELECT i.attempt_id FROM interactive_requests i
353
+ JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
354
+ JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id AND t.state = 'started'
355
+ WHERE i.status = 'pending'
356
+ )`)
357
+ .run(now);
358
+ }
411
359
  resumeSessionModelTurns(targetSessionId, now = Date.now()) {
412
360
  const rows = this.state.connection
413
- .prepare(`SELECT i.attempt_id AS attemptId, a.status, a.started_at AS startedAt,
414
- a.deadline_at AS deadlineAt, a.updated_at AS updatedAt,
415
- MAX(t.started_at) AS turnStartedAt
416
- FROM interactive_requests i
417
- JOIN node_attempts a ON a.attempt_id = i.attempt_id
418
- JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
419
- JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
420
- AND t.state = 'started'
421
- WHERE i.target_session_id = ? AND i.status = 'pending'
422
- AND a.status = 'interrupted'
423
- GROUP BY i.attempt_id`)
424
- .all(targetSessionId)
425
- .filter(isInactiveInteractionTimeoutRow);
361
+ .prepare(`SELECT DISTINCT i.attempt_id AS attemptId FROM interactive_requests i
362
+ JOIN runs r ON r.run_id = i.run_id
363
+ JOIN node_attempts a ON a.attempt_id = i.attempt_id
364
+ JOIN workflow_messages m ON m.source_id = i.request_id AND m.kind = 'step'
365
+ JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id AND t.state = 'started'
366
+ WHERE i.target_session_id = ? AND i.status = 'pending' AND r.paused = 0 AND a.status = 'interrupted'`)
367
+ .all(targetSessionId);
426
368
  for (const row of rows) {
427
- this.shiftInteractionTimeout(row, Math.max(0, now - row.updatedAt), now);
369
+ this.attemptTime.start(row.attemptId);
428
370
  this.state.connection
429
- .prepare(`UPDATE node_attempts SET status = 'waiting', updated_at = ?
430
- WHERE attempt_id = ? AND status = 'interrupted'`)
371
+ .prepare("UPDATE node_attempts SET status = 'waiting', updated_at = ? WHERE attempt_id = ?")
431
372
  .run(now, row.attemptId);
432
373
  }
433
374
  }
@@ -475,55 +416,51 @@ export class ServerStateStore {
475
416
  payload: this.state.readJson(row.payloadHash),
476
417
  };
477
418
  }
478
- validatingInteractionRunIds() {
419
+ readyInteractionRunIds() {
479
420
  return this.state.connection
480
421
  .prepare(`SELECT i.run_id AS runId
481
422
  FROM interactive_requests i
482
423
  JOIN interactive_submissions s ON s.request_id = i.request_id
483
424
  JOIN run_queue q ON q.run_id = i.run_id
484
- WHERE i.status = 'pending' AND s.outcome = 'validating'
425
+ JOIN node_attempts a ON a.attempt_id = i.attempt_id
426
+ WHERE ((i.status = 'pending' AND s.outcome = 'validating')
427
+ OR (i.status = 'settled' AND i.accepted_submission_id = s.submission_id
428
+ AND s.outcome = 'accepted' AND i.consumed_at IS NULL))
429
+ AND a.status IN ('pending', 'running', 'waiting', 'interrupted')
485
430
  AND q.status NOT IN ('done', 'failed', 'cancelled')
486
431
  GROUP BY i.run_id
487
432
  ORDER BY MIN(s.submitted_at), i.run_id`)
488
433
  .all()
489
434
  .flatMap((row) => (isRunIdRow(row) ? [row.runId] : []));
490
435
  }
491
- expiredInteractionRuns(now = Date.now()) {
436
+ expiredInteractionRuns() {
492
437
  return this.state.connection
493
438
  .prepare(`SELECT i.run_id AS runId, i.target_session_id AS targetSessionId
494
- FROM interactive_requests i
495
- JOIN node_attempts a ON a.attempt_id = i.attempt_id
496
- JOIN runs r ON r.run_id = i.run_id
497
- JOIN run_queue q ON q.run_id = i.run_id
498
- WHERE i.status = 'pending' AND r.paused = 0
499
- AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
500
- AND q.status NOT IN ('done', 'failed', 'cancelled')
501
- AND EXISTS (
502
- SELECT 1 FROM workflow_messages m
503
- JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
504
- WHERE m.source_id = i.request_id AND m.kind = 'step' AND t.state = 'started'
505
- )
506
- GROUP BY i.run_id, i.target_session_id
507
- ORDER BY MIN(a.deadline_at), i.run_id`)
508
- .all(now)
439
+ FROM interactive_requests i
440
+ JOIN node_attempts a ON a.attempt_id = i.attempt_id
441
+ JOIN runs r ON r.run_id = i.run_id
442
+ JOIN run_queue q ON q.run_id = i.run_id
443
+ WHERE i.status = 'pending' AND r.paused = 0 AND a.timeout_ms > 0
444
+ AND (SELECT COALESCE(SUM(elapsed_ms), 0) FROM attempt_active_intervals t
445
+ WHERE t.attempt_id = a.attempt_id) >= a.timeout_ms
446
+ AND q.status NOT IN ('done', 'failed', 'cancelled')
447
+ GROUP BY i.run_id, i.target_session_id ORDER BY MIN(a.started_at), i.run_id`)
448
+ .all()
509
449
  .filter(isExpiredInteractionRunRow);
510
450
  }
511
451
  timedOutInteraction(runId) {
512
452
  const row = this.state.connection
513
453
  .prepare(`SELECT i.request_id AS requestId, i.attempt_id AS attemptId, a.node_id AS nodeId
514
- FROM interactive_requests i
515
- JOIN node_attempts a ON a.attempt_id = i.attempt_id
516
- JOIN runs r ON r.run_id = i.run_id
517
- WHERE i.run_id = ? AND i.status = 'cancelled' AND r.status = 'running'
518
- AND r.paused = 0 AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
519
- AND a.status IN ('pending', 'running', 'waiting', 'interrupted', 'timed_out')
520
- AND EXISTS (
521
- SELECT 1 FROM workflow_messages m
522
- JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
523
- WHERE m.source_id = i.request_id AND m.kind = 'step' AND t.state = 'started'
524
- )
525
- ORDER BY a.deadline_at, i.request_id LIMIT 1`)
526
- .get(runId, Date.now());
454
+ FROM interactive_requests i
455
+ JOIN node_attempts a ON a.attempt_id = i.attempt_id
456
+ JOIN runs r ON r.run_id = i.run_id
457
+ WHERE i.run_id = ? AND i.status = 'cancelled' AND r.status = 'running'
458
+ AND r.paused = 0 AND a.timeout_ms > 0
459
+ AND (SELECT COALESCE(SUM(elapsed_ms), 0) FROM attempt_active_intervals t
460
+ WHERE t.attempt_id = a.attempt_id) >= a.timeout_ms
461
+ AND a.status IN ('pending', 'running', 'waiting', 'interrupted', 'timed_out')
462
+ ORDER BY a.started_at, i.request_id LIMIT 1`)
463
+ .get(runId);
527
464
  return isTimedOutInteractionRow(row) ? row : undefined;
528
465
  }
529
466
  interactionSubmission(requestId, submissionId) {
@@ -579,14 +516,6 @@ export class ServerStateStore {
579
516
  return settled;
580
517
  });
581
518
  }
582
- consumeAcceptedInteraction(runId, attemptId, now = Date.now()) {
583
- const changed = this.state.connection
584
- .prepare(`UPDATE interactive_requests SET consumed_at = ?, updated_at = ?
585
- WHERE run_id = ? AND attempt_id = ? AND status = 'settled'
586
- AND accepted_submission_id IS NOT NULL AND consumed_at IS NULL`)
587
- .run(now, now, runId, attemptId);
588
- return changed.changes === 1;
589
- }
590
519
  listPendingInteractions(sessionId) {
591
520
  return this.state.connection
592
521
  .prepare(`SELECT request_id AS requestId FROM interactive_requests
@@ -604,83 +533,24 @@ export class ServerStateStore {
604
533
  .flatMap((row) => isRequestIdRow(row) ? [this.requireInteractiveRequest(row.requestId)] : []);
605
534
  }
606
535
  submitInteraction(options) {
607
- return this.recordInteractionSubmission({
536
+ return recordWorkflowSubmission(this.state, {
608
537
  ...options,
609
538
  outcome: options.accepted ? "accepted" : "rejected",
610
539
  settle: options.accepted,
611
540
  });
612
541
  }
542
+ hasInteractionSubmission(requestId, idempotencyKey) {
543
+ return (this.state.connection
544
+ .prepare("SELECT 1 FROM interactive_submissions WHERE request_id = ? AND idempotency_key = ?")
545
+ .get(requestId, idempotencyKey) !== undefined);
546
+ }
613
547
  beginInteractionValidation(options) {
614
- return this.recordInteractionSubmission({
548
+ return recordWorkflowSubmission(this.state, {
615
549
  ...options,
616
550
  outcome: "validating",
617
551
  settle: false,
618
552
  });
619
553
  }
620
- recordInteractionSubmission(options) {
621
- const now = Date.now();
622
- return this.state.transaction(() => {
623
- const existing = this.state.connection
624
- .prepare(`SELECT submission_id AS submissionId, payload_hash AS payloadHash,
625
- outcome, receipt_hash AS receiptHash
626
- FROM interactive_submissions WHERE request_id = ? AND idempotency_key = ?`)
627
- .get(options.requestId, options.idempotencyKey);
628
- const payloadHash = createHash("sha256").update(canonicalJson(options.payload)).digest();
629
- if (isSubmissionRow(existing)) {
630
- if (!existing.payloadHash.equals(payloadHash)) {
631
- throw new Error("Interactive submission idempotency key conflicts");
632
- }
633
- return {
634
- interaction: this.requireInteractiveRequest(options.requestId),
635
- submissionId: existing.submissionId,
636
- outcome: "adopted",
637
- receipt: existing.receiptHash === null
638
- ? { requestId: options.requestId, submissionId: existing.submissionId }
639
- : this.state.readJson(existing.receiptHash),
640
- };
641
- }
642
- if (options.outcome === "validating") {
643
- const active = this.state.connection
644
- .prepare(`SELECT submission_id AS submissionId FROM interactive_submissions
645
- WHERE request_id = ? AND outcome = 'validating' LIMIT 1`)
646
- .get(options.requestId);
647
- if (isSubmissionIdRow(active)) {
648
- throw new Error("Interactive submission validation is already active");
649
- }
650
- }
651
- const request = this.requireInteractiveRequest(options.requestId);
652
- if (request.revision !== options.expectedRevision || request.status !== "pending") {
653
- throw new Error("Interactive request revision conflict");
654
- }
655
- const savedPayloadHash = this.state.putJson(options.payload, now);
656
- const receiptHash = options.receipt === undefined ? null : this.state.putJson(options.receipt, now);
657
- this.state.connection
658
- .prepare(`INSERT INTO interactive_submissions(
659
- submission_id, request_id, idempotency_key, request_revision,
660
- payload_hash, outcome, receipt_hash, submitted_at
661
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
662
- .run(options.submissionId, options.requestId, options.idempotencyKey, options.expectedRevision, savedPayloadHash, options.outcome, receiptHash, now);
663
- if (options.settle) {
664
- this.state.connection
665
- .prepare(`UPDATE interactive_requests
666
- SET status = 'settled', accepted_submission_id = ?, revision = revision + 1,
667
- updated_at = ?, settled_at = ?
668
- WHERE request_id = ? AND revision = ? AND status = 'pending'`)
669
- .run(options.submissionId, now, now, options.requestId, options.expectedRevision);
670
- this.workflowMessages.cancelPendingForSource(options.requestId, "step", now);
671
- this.workflowMessages.cancelPendingForSource(options.requestId, "decision", now);
672
- }
673
- return {
674
- interaction: this.requireInteractiveRequest(options.requestId),
675
- submissionId: options.submissionId,
676
- outcome: "accepted",
677
- receipt: options.receipt ?? {
678
- requestId: options.requestId,
679
- submissionId: options.submissionId,
680
- },
681
- };
682
- });
683
- }
684
554
  serverRow() {
685
555
  const row = this.state.connection
686
556
  .prepare(`SELECT epoch, host_id AS serverId, token_hash AS tokenHash, pid,
@@ -708,44 +578,7 @@ export class ServerStateStore {
708
578
  };
709
579
  }
710
580
  interactiveRequest(requestId) {
711
- const row = this.state.connection
712
- .prepare(`SELECT request_id AS requestId, run_id AS runId, attempt_id AS attemptId,
713
- target_session_id AS targetSessionId, kind, contract_hash AS contractHash,
714
- revision, status, unproductive_turn_ends AS unproductiveTurnEnds,
715
- accepted_submission_id AS acceptedSubmissionId, created_at AS createdAt,
716
- updated_at AS updatedAt, settled_at AS settledAt, consumed_at AS consumedAt
717
- FROM interactive_requests WHERE request_id = ?`)
718
- .get(requestId);
719
- if (!isInteractiveRequestRow(row))
720
- return undefined;
721
- return {
722
- requestId: row.requestId,
723
- runId: row.runId,
724
- attemptId: row.attemptId,
725
- targetSessionId: row.targetSessionId,
726
- kind: row.kind,
727
- contract: this.state.readJson(row.contractHash),
728
- revision: row.revision,
729
- status: row.status,
730
- unproductiveTurnEnds: row.unproductiveTurnEnds,
731
- acceptedSubmissionId: row.acceptedSubmissionId,
732
- createdAt: new Date(row.createdAt).toISOString(),
733
- updatedAt: new Date(row.updatedAt).toISOString(),
734
- settledAt: iso(row.settledAt),
735
- consumedAt: iso(row.consumedAt),
736
- };
737
- }
738
- shiftInteractionTimeout(row, idleMs, now) {
739
- if (idleMs === 0 || row.deadlineAt === null)
740
- return;
741
- const changed = this.state.connection
742
- .prepare(`UPDATE node_attempts
743
- SET started_at = CASE WHEN started_at IS NULL THEN NULL ELSE started_at + ? END,
744
- deadline_at = deadline_at + ?, updated_at = ?
745
- WHERE attempt_id = ? AND deadline_at = ?`)
746
- .run(idleMs, idleMs, now, row.attemptId, row.deadlineAt);
747
- if (changed.changes !== 1)
748
- throw new Error("Workflow interaction timeout changed concurrently");
581
+ return readWorkflowRequest(this.state, requestId);
749
582
  }
750
583
  requireInteractiveRequest(requestId) {
751
584
  const request = this.interactiveRequest(requestId);
@@ -822,39 +655,9 @@ function isTimedOutInteractionRow(value) {
822
655
  typeof value.attemptId === "string" &&
823
656
  typeof value.nodeId === "string");
824
657
  }
825
- function isInteractionTimeoutRow(value) {
826
- return (isRecord(value) &&
827
- typeof value.attemptId === "string" &&
828
- typeof value.createdAt === "number" &&
829
- nullableNumber(value.startedAt) &&
830
- nullableNumber(value.deadlineAt) &&
831
- nullableNumber(value.previousTurnEndedAt));
832
- }
833
- function isInactiveInteractionTimeoutRow(value) {
834
- return (isRecord(value) &&
835
- typeof value.attemptId === "string" &&
836
- ["pending", "running", "waiting", "interrupted"].includes(value.status) &&
837
- nullableNumber(value.startedAt) &&
838
- nullableNumber(value.deadlineAt) &&
839
- typeof value.updatedAt === "number" &&
840
- typeof value.turnStartedAt === "number");
841
- }
842
658
  function isExpiredInteractionRunRow(value) {
843
659
  return (isRecord(value) && typeof value.runId === "string" && typeof value.targetSessionId === "string");
844
660
  }
845
- function isPausedAtRow(value) {
846
- return isRecord(value) && typeof value.pausedAt === "number";
847
- }
848
- function isSubmissionRow(value) {
849
- return (isRecord(value) &&
850
- typeof value.submissionId === "string" &&
851
- Buffer.isBuffer(value.payloadHash) &&
852
- typeof value.outcome === "string" &&
853
- (value.receiptHash === null || Buffer.isBuffer(value.receiptHash)));
854
- }
855
- function isSubmissionIdRow(value) {
856
- return isRecord(value) && typeof value.submissionId === "string";
857
- }
858
661
  function isSubmissionDetailRow(value) {
859
662
  return (isRecord(value) &&
860
663
  typeof value.requestId === "string" &&
@@ -865,23 +668,6 @@ function isSubmissionDetailRow(value) {
865
668
  (value.receiptHash === null || Buffer.isBuffer(value.receiptHash)) &&
866
669
  typeof value.submittedAt === "number");
867
670
  }
868
- function isInteractiveRequestRow(value) {
869
- return (isRecord(value) &&
870
- typeof value.requestId === "string" &&
871
- typeof value.runId === "string" &&
872
- typeof value.attemptId === "string" &&
873
- typeof value.targetSessionId === "string" &&
874
- ["agent", "assistant", "decision"].includes(value.kind) &&
875
- Buffer.isBuffer(value.contractHash) &&
876
- typeof value.revision === "number" &&
877
- ["pending", "settled", "cancelled"].includes(value.status) &&
878
- typeof value.unproductiveTurnEnds === "number" &&
879
- nullableString(value.acceptedSubmissionId) &&
880
- typeof value.createdAt === "number" &&
881
- typeof value.updatedAt === "number" &&
882
- nullableNumber(value.settledAt) &&
883
- nullableNumber(value.consumedAt));
884
- }
885
671
  function runnerMessageFingerprint(message) {
886
672
  return createHash("sha256").update(canonicalJson(message)).digest();
887
673
  }