@osolmaz/pi-workflows 0.16.0 → 0.16.2

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 (175) hide show
  1. package/README.md +18 -2
  2. package/dist/channels/adapter-entry.d.ts +3 -0
  3. package/dist/channels/adapter-entry.js +203 -0
  4. package/dist/channels/adapter-entry.js.map +1 -0
  5. package/dist/channels/config.d.ts +43 -0
  6. package/dist/channels/config.js +234 -0
  7. package/dist/channels/config.js.map +1 -0
  8. package/dist/channels/protocol.d.ts +94 -0
  9. package/dist/channels/protocol.js +219 -0
  10. package/dist/channels/protocol.js.map +1 -0
  11. package/dist/channels/telegram.d.ts +46 -0
  12. package/dist/channels/telegram.js +285 -0
  13. package/dist/channels/telegram.js.map +1 -0
  14. package/dist/client/client.d.ts +1 -0
  15. package/dist/client/client.js +5 -1
  16. package/dist/client/client.js.map +1 -1
  17. package/dist/client/index.d.ts +1 -1
  18. package/dist/client/protocol.d.ts +1 -1
  19. package/dist/client/protocol.js +11 -5
  20. package/dist/client/protocol.js.map +1 -1
  21. package/dist/client/view.d.ts +48 -16
  22. package/dist/client/view.js +1 -0
  23. package/dist/client/view.js.map +1 -1
  24. package/dist/controllers/index.d.ts +1 -1
  25. package/dist/controllers/index.js.map +1 -1
  26. package/dist/controllers/sqlite.d.ts +20 -165
  27. package/dist/controllers/sqlite.js +178 -436
  28. package/dist/controllers/sqlite.js.map +1 -1
  29. package/dist/extension/index.d.ts +23 -2
  30. package/dist/extension/index.js +637 -534
  31. package/dist/extension/index.js.map +1 -1
  32. package/dist/extension/recorder.d.ts +95 -0
  33. package/dist/extension/recorder.js +530 -0
  34. package/dist/extension/recorder.js.map +1 -0
  35. package/dist/extension/remote-recorder-store.d.ts +25 -0
  36. package/dist/extension/remote-recorder-store.js +81 -0
  37. package/dist/extension/remote-recorder-store.js.map +1 -0
  38. package/dist/extension/session-events.d.ts +134 -0
  39. package/dist/extension/session-events.js +60 -0
  40. package/dist/extension/session-events.js.map +1 -0
  41. package/dist/extension/session-view.d.ts +2 -0
  42. package/dist/extension/session-view.js +60 -2
  43. package/dist/extension/session-view.js.map +1 -1
  44. package/dist/extension/step-message.d.ts +6 -4
  45. package/dist/extension/step-message.js +12 -6
  46. package/dist/extension/step-message.js.map +1 -1
  47. package/dist/extension/widget.d.ts +2 -2
  48. package/dist/extension/widget.js +28 -18
  49. package/dist/extension/widget.js.map +1 -1
  50. package/dist/extension/workflow-message-coordinator.d.ts +28 -0
  51. package/dist/extension/workflow-message-coordinator.js +346 -0
  52. package/dist/extension/workflow-message-coordinator.js.map +1 -0
  53. package/dist/host/channel-effects.d.ts +72 -0
  54. package/dist/host/channel-effects.js +271 -0
  55. package/dist/host/channel-effects.js.map +1 -0
  56. package/dist/host/channel-supervisor.d.ts +21 -0
  57. package/dist/host/channel-supervisor.js +54 -0
  58. package/dist/host/channel-supervisor.js.map +1 -0
  59. package/dist/host/runner.d.ts +50 -8
  60. package/dist/host/runner.js +1632 -341
  61. package/dist/host/runner.js.map +1 -1
  62. package/dist/host/state.d.ts +19 -16
  63. package/dist/host/state.js +251 -63
  64. package/dist/host/state.js.map +1 -1
  65. package/dist/host/view.d.ts +19 -9
  66. package/dist/host/view.js +227 -119
  67. package/dist/host/view.js.map +1 -1
  68. package/dist/host/worker-entry.d.ts +4 -1
  69. package/dist/host/worker-entry.js +23 -24
  70. package/dist/host/worker-entry.js.map +1 -1
  71. package/dist/host/worker-protocol.d.ts +15 -0
  72. package/dist/host/worker-protocol.js.map +1 -1
  73. package/dist/state/prune.js +3 -25
  74. package/dist/state/prune.js.map +1 -1
  75. package/dist/state/schema.js +76 -97
  76. package/dist/state/schema.js.map +1 -1
  77. package/dist/state/workflow-messages.d.ts +98 -0
  78. package/dist/state/workflow-messages.js +366 -0
  79. package/dist/state/workflow-messages.js.map +1 -0
  80. package/dist/viewer/render.js +1 -1
  81. package/dist/viewer/render.js.map +1 -1
  82. package/dist/viewer/tui.d.ts +1 -1
  83. package/dist/viewer/tui.js +65 -8
  84. package/dist/viewer/tui.js.map +1 -1
  85. package/dist/workflows/composition.js +0 -4
  86. package/dist/workflows/composition.js.map +1 -1
  87. package/dist/workflows/definition.js +0 -8
  88. package/dist/workflows/definition.js.map +1 -1
  89. package/dist/workflows/engine.js +4 -5
  90. package/dist/workflows/engine.js.map +1 -1
  91. package/dist/workflows/human-decision.d.ts +2 -0
  92. package/dist/workflows/human-decision.js +8 -2
  93. package/dist/workflows/human-decision.js.map +1 -1
  94. package/dist/workflows/index.d.ts +1 -1
  95. package/dist/workflows/index.js.map +1 -1
  96. package/dist/workflows/schema.js +0 -4
  97. package/dist/workflows/schema.js.map +1 -1
  98. package/dist/workflows/settings.d.ts +1 -8
  99. package/dist/workflows/settings.js.map +1 -1
  100. package/dist/workflows/store.d.ts +20 -19
  101. package/dist/workflows/store.js +163 -354
  102. package/dist/workflows/store.js.map +1 -1
  103. package/dist/workflows/tool-input.d.ts +21 -0
  104. package/dist/workflows/tool-input.js +23 -1
  105. package/dist/workflows/tool-input.js.map +1 -1
  106. package/dist/workflows/types.d.ts +6 -8
  107. package/dist/workflows/workflow-message-content.d.ts +38 -0
  108. package/dist/workflows/workflow-message-content.js +157 -0
  109. package/dist/workflows/workflow-message-content.js.map +1 -0
  110. package/docs/2026-08-18-herdr-piw-plan.md +2 -1
  111. package/docs/2026-08-20-durable-workflow-launch-plan.md +1 -1
  112. package/docs/2026-08-25-workflow-follow-ups.md +26 -50
  113. package/docs/2026-08-30-out-of-process-workflow-host-plan.md +3 -3
  114. package/docs/2026-09-01-restore-session-delivery-controls-plan.md +4 -0
  115. package/docs/2026-09-01-unified-workflow-client-plan.md +5 -1
  116. package/docs/2026-09-02-installed-live-e2e-plan.md +15 -1
  117. package/docs/2026-09-02-unify-workflow-messages-plan.md +562 -0
  118. package/docs/2026-09-04-workflow-run-state-plan.md +363 -0
  119. package/docs/DEFERRED_TURNS.md +66 -294
  120. package/docs/HUMAN_DECISIONS.md +29 -30
  121. package/docs/HUMAN_DECISION_PRESENTATIONS.md +4 -11
  122. package/docs/SQLITE_STATE.md +25 -17
  123. package/docs/WORKFLOW_HOST.md +105 -31
  124. package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
  125. package/docs/live-replay-protocol.md +8 -4
  126. package/docs/tui-viewer.md +3 -1
  127. package/docs/workflows.md +44 -28
  128. package/herdr-plugin.toml +1 -1
  129. package/package.json +1 -1
  130. package/plugins/herdr/viewer.mjs +14 -5
  131. package/src/channels/adapter-entry.ts +220 -0
  132. package/src/channels/config.ts +296 -0
  133. package/src/channels/protocol.ts +333 -0
  134. package/src/channels/telegram.ts +335 -0
  135. package/src/client/client.ts +6 -2
  136. package/src/client/index.ts +2 -1
  137. package/src/client/protocol.ts +11 -5
  138. package/src/client/view.ts +51 -18
  139. package/src/controllers/index.ts +0 -1
  140. package/src/controllers/sqlite.ts +247 -845
  141. package/src/extension/index.ts +712 -647
  142. package/src/extension/recorder.ts +687 -0
  143. package/src/extension/remote-recorder-store.ts +126 -0
  144. package/src/extension/session-events.ts +119 -0
  145. package/src/extension/session-view.ts +86 -3
  146. package/src/extension/step-message.ts +19 -9
  147. package/src/extension/widget.ts +37 -17
  148. package/src/extension/workflow-message-coordinator.ts +398 -0
  149. package/src/host/channel-effects.ts +465 -0
  150. package/src/host/channel-supervisor.ts +73 -0
  151. package/src/host/runner.ts +2163 -525
  152. package/src/host/state.ts +333 -94
  153. package/src/host/view.ts +254 -122
  154. package/src/host/worker-entry.ts +38 -30
  155. package/src/host/worker-protocol.ts +11 -0
  156. package/src/state/prune.ts +3 -31
  157. package/src/state/schema.ts +76 -97
  158. package/src/state/workflow-messages.ts +599 -0
  159. package/src/viewer/render.ts +1 -5
  160. package/src/viewer/tui.ts +71 -6
  161. package/src/workflows/composition.ts +0 -5
  162. package/src/workflows/definition.ts +0 -8
  163. package/src/workflows/engine.ts +4 -6
  164. package/src/workflows/human-decision.ts +18 -2
  165. package/src/workflows/index.ts +0 -1
  166. package/src/workflows/schema.ts +0 -6
  167. package/src/workflows/settings.ts +1 -20
  168. package/src/workflows/store.ts +237 -489
  169. package/src/workflows/tool-input.ts +36 -1
  170. package/src/workflows/types.ts +6 -8
  171. package/src/workflows/workflow-message-content.ts +197 -0
  172. package/dist/extension/decision-channels.d.ts +0 -134
  173. package/dist/extension/decision-channels.js +0 -1307
  174. package/dist/extension/decision-channels.js.map +0 -1
  175. package/src/extension/decision-channels.ts +0 -1826
package/dist/host/view.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
- import { ORIGIN_ACTIVITY_LEASE_MS } from "../client/activity.js";
3
2
  import { RUN_VIEW_SCHEMA, SESSION_VIEW_SCHEMA, } from "../client/view.js";
4
3
  import { canonicalJson, parseJson } from "../state/json.js";
4
+ import { WorkflowMessageStore } from "../state/workflow-messages.js";
5
5
  export const WORKFLOW_PAGE_KINDS = [
6
6
  "steps",
7
7
  "trace",
@@ -18,32 +18,38 @@ const VIEW_PAGE_ITEMS = 256;
18
18
  const CONTENT_CHUNK_BYTES = 192 * 1024;
19
19
  const CONTENT_CACHE_BYTES = 64 * 1024 * 1024;
20
20
  const VIEW_CACHE_ITEMS = 64;
21
+ const TERMINAL_VIEW_RETENTION_MS = 60_000;
21
22
  export class HostViewStore {
22
23
  state;
23
24
  queue;
24
25
  hostState;
25
26
  runs;
26
27
  hasLiveWorker;
27
- activity = new Map();
28
+ hasActiveSessionTurn;
28
29
  contentRecords = new Map();
29
30
  listCache = new Map();
30
31
  runCache = new Map();
31
32
  sessionCache = new Map();
32
33
  contentBytes = 0;
33
- activityRevision = 0;
34
- constructor(state, queue, hostState, runs, hasLiveWorker) {
34
+ originActivityRevision = 0;
35
+ workflowMessages;
36
+ constructor(state, queue, hostState, runs, hasLiveWorker, hasActiveSessionTurn) {
35
37
  this.state = state;
36
38
  this.queue = queue;
37
39
  this.hostState = hostState;
38
40
  this.runs = runs;
39
41
  this.hasLiveWorker = hasLiveWorker;
42
+ this.hasActiveSessionTurn = hasActiveSessionTurn;
43
+ this.workflowMessages = hostState.workflowMessages;
44
+ }
45
+ noteOriginActivityChange() {
46
+ this.originActivityRevision += 1;
40
47
  }
41
48
  list(cursor = 0, limit) {
42
- this.expireActivity();
43
49
  return this.state.readTransaction(() => {
44
50
  const pageSize = Math.min(limit ?? VIEW_PAGE_ITEMS, VIEW_PAGE_ITEMS);
45
51
  const current = this.queue.workflowRunListRevision();
46
- const revision = `${current.revision}:${this.activityRevision}`;
52
+ const revision = `${current.revision}:${this.workflowActivityRevision()}`;
47
53
  const cacheKey = `${cursor}:${pageSize}`;
48
54
  const cached = this.listCache.get(cacheKey);
49
55
  if (cached?.revision === revision) {
@@ -82,7 +88,6 @@ export class HostViewStore {
82
88
  });
83
89
  }
84
90
  run(runId) {
85
- this.expireActivity();
86
91
  return this.state.readTransaction(() => {
87
92
  const version = this.runVersion(runId);
88
93
  const cached = this.runCache.get(runId);
@@ -96,7 +101,6 @@ export class HostViewStore {
96
101
  });
97
102
  }
98
103
  page(runId, request) {
99
- this.expireActivity();
100
104
  return this.state.readTransaction(() => this.readRun(runId, request));
101
105
  }
102
106
  readRun(runId, page) {
@@ -194,38 +198,58 @@ export class HostViewStore {
194
198
  possiblyInterrupted: queue.status === "parked" && display.status !== "paused",
195
199
  };
196
200
  }
197
- session(sessionId) {
198
- this.expireActivity();
201
+ session(sessionId, coordinator = null) {
199
202
  return this.state.readTransaction(() => {
200
- const queue = this.queue.findSessionReservationView(sessionId);
201
- const deliveries = {
202
- notification: this.queue.hasClaimableWorkflowNotification({ targetSessionId: sessionId }),
203
- turn: this.queue.hasClaimableWorkflowTurnIntent({ targetSessionId: sessionId }),
204
- };
205
- const version = [
206
- queue === undefined ? "none" : this.runVersion(queue.runId),
207
- this.pendingSessionRevision(sessionId),
208
- deliveries.notification,
209
- deliveries.turn,
210
- ].join(":");
211
- const cached = this.sessionCache.get(sessionId);
212
- if (cached?.version === version) {
213
- refreshCacheEntry(this.sessionCache, sessionId, cached);
214
- return cached.view;
215
- }
203
+ const activeQueue = this.queue.findSessionReservationView(sessionId);
204
+ const retainedRunId = activeQueue === undefined ? this.retainedTerminalRunId(sessionId) : undefined;
205
+ const runId = activeQueue?.runId ?? retainedRunId;
216
206
  const pending = this.hostState.listPendingInteractions(sessionId);
217
207
  const pendingInteractions = byteBoundedForwardPage(pending, (request) => this.projectRecordField(request.runId, request, "contract"));
218
- const view = {
208
+ const workflowMessages = this.workflowMessages.listSession(sessionId);
209
+ const eligible = workflowMessages.find((message) => this.isMessageEligible(message));
210
+ const next = coordinator === null || (coordinator.active && !coordinator.branchReportRequired)
211
+ ? eligible
212
+ : undefined;
213
+ const open = this.openWorkflowMessage(workflowMessages);
214
+ const openTurn = open === undefined
215
+ ? undefined
216
+ : this.workflowMessages.openTurnForMessage(open.workflowMessageId);
217
+ return {
219
218
  schema: SESSION_VIEW_SCHEMA,
220
219
  sessionId,
221
- run: queue === undefined ? null : this.run(queue.runId),
220
+ run: runId === undefined ? null : this.run(runId),
222
221
  pendingInteractions,
223
222
  pendingInteractionStart: 0,
224
223
  pendingInteractionTotal: pending.length,
225
- deliveries,
224
+ workflowMessages,
225
+ workflowMessageStart: 0,
226
+ workflowMessageTotal: workflowMessages.length,
227
+ workflowMessageWindowComplete: true,
228
+ nextWorkflowMessageId: next?.workflowMessageId ?? null,
229
+ openWorkflowMessageId: open?.workflowMessageId ?? null,
230
+ openWorkflowTurn: openTurn ?? null,
231
+ coordinatorEpoch: coordinator?.epoch ?? null,
232
+ coordinatorActive: coordinator?.active ?? false,
233
+ branchReportRequired: coordinator?.branchReportRequired ?? false,
226
234
  };
227
- rememberCacheEntry(this.sessionCache, sessionId, { version, view });
228
- return view;
235
+ });
236
+ }
237
+ clearTerminal(sessionId, runId, now = Date.now()) {
238
+ return this.state.transaction(() => {
239
+ const retained = this.retainedTerminalRunId(sessionId, now);
240
+ if (retained === undefined)
241
+ return null;
242
+ if (runId !== undefined && retained !== runId) {
243
+ throw new Error(`Retained terminal workflow does not match run ${runId}`);
244
+ }
245
+ this.state.connection
246
+ .prepare(`INSERT INTO session_terminal_views(target_session_id, cleared_run_id, cleared_at)
247
+ VALUES (?, ?, ?)
248
+ ON CONFLICT(target_session_id) DO UPDATE SET
249
+ cleared_run_id = excluded.cleared_run_id, cleared_at = excluded.cleared_at`)
250
+ .run(sessionId, retained, now);
251
+ this.sessionCache.delete(sessionId);
252
+ return retained;
229
253
  });
230
254
  }
231
255
  content(runId, contentPath, offset) {
@@ -435,77 +459,160 @@ export class HostViewStore {
435
459
  sha256: match[1],
436
460
  });
437
461
  }
438
- reportActivity(connectionId, report) {
439
- if (report.deliveryId !== `interaction:${report.requestId}`) {
440
- throw new Error("Origin activity delivery does not match the interactive request");
441
- }
442
- const key = activityKey(connectionId, report.requestId);
443
- const previous = this.activity.get(key);
444
- if (report.state === "settled") {
445
- if (previous === undefined)
446
- return;
447
- if (previous.sessionId !== report.sessionId ||
448
- previous.runId !== report.runId ||
449
- previous.requestId !== report.requestId ||
450
- previous.deliveryId !== report.deliveryId ||
451
- previous.sessionEntryId !== report.sessionEntryId) {
452
- throw new Error("Origin activity identity changed");
462
+ clearConnection(_connectionId) {
463
+ // Coordinator fencing is process-local in the host. Durable turn state is
464
+ // reconciled from the Pi branch after the next coordinator connects.
465
+ }
466
+ retainedTerminalRunId(sessionId, now = Date.now()) {
467
+ const messages = this.workflowMessages
468
+ .listSession(sessionId)
469
+ .filter((message) => message.kind === "terminal")
470
+ .reverse();
471
+ for (const message of messages) {
472
+ const run = this.state.connection
473
+ .prepare("SELECT status FROM runs WHERE run_id = ?")
474
+ .get(message.runId);
475
+ if (!isObjectRecord(run) || !isTerminalStatus(run.status))
476
+ continue;
477
+ const clear = this.state.connection
478
+ .prepare(`SELECT cleared_run_id AS clearedRunId, cleared_at AS clearedAt
479
+ FROM session_terminal_views WHERE target_session_id = ?`)
480
+ .get(sessionId);
481
+ if (isObjectRecord(clear) &&
482
+ clear.clearedRunId === message.runId &&
483
+ typeof clear.clearedAt === "number" &&
484
+ clear.clearedAt >= Date.parse(message.createdAt)) {
485
+ continue;
453
486
  }
454
- if (report.sequence <= previous.sequence) {
455
- throw new Error("Origin activity sequence must increase");
487
+ if (message.status === "pending")
488
+ return message.runId;
489
+ if (message.status !== "sent")
490
+ continue;
491
+ const turn = this.workflowMessages.latestTurnForMessage(message.workflowMessageId);
492
+ if (turn === undefined || turn.state === "started")
493
+ return message.runId;
494
+ if (turn.endedAt !== null && Date.parse(turn.endedAt) + TERMINAL_VIEW_RETENTION_MS > now) {
495
+ return message.runId;
456
496
  }
457
- this.activity.delete(key);
458
- this.activityRevision += 1;
459
- return;
460
497
  }
461
- const request = this.hostState
462
- .listPendingInteractions(report.sessionId)
463
- .find((candidate) => candidate.requestId === report.requestId);
464
- validateActivityRequest(request, report);
465
- if (previous !== undefined) {
466
- if (previous.sessionId !== report.sessionId ||
467
- previous.runId !== report.runId ||
468
- previous.requestId !== report.requestId ||
469
- previous.deliveryId !== report.deliveryId ||
470
- previous.sessionEntryId !== report.sessionEntryId) {
471
- throw new Error("Origin activity identity changed");
472
- }
473
- if (report.sequence <= previous.sequence) {
474
- throw new Error("Origin activity sequence must increase");
475
- }
498
+ return undefined;
499
+ }
500
+ isMessageEligible(message) {
501
+ if (message.status !== "pending")
502
+ return false;
503
+ if (message.kind === "step" || message.kind === "decision") {
504
+ const request = this.state.connection
505
+ .prepare(`SELECT i.status, r.paused FROM interactive_requests i
506
+ JOIN runs r ON r.run_id = i.run_id
507
+ WHERE i.request_id = ? AND i.run_id = ?`)
508
+ .get(message.sourceId, message.runId);
509
+ if (!isObjectRecord(request) || request.status !== "pending")
510
+ return false;
511
+ return message.kind === "decision" || request.paused === 0;
476
512
  }
477
- else if (report.state !== "started") {
478
- throw new Error("Origin activity must start before refresh");
513
+ if (message.kind === "notification")
514
+ return true;
515
+ if (message.kind === "terminal") {
516
+ const run = this.state.connection
517
+ .prepare("SELECT status FROM runs WHERE run_id = ?")
518
+ .get(message.runId);
519
+ return isObjectRecord(run) && isTerminalStatus(run.status);
479
520
  }
480
- this.activity.set(key, {
481
- ...report,
482
- connectionId,
483
- expiresAt: Date.now() + ORIGIN_ACTIVITY_LEASE_MS,
484
- });
485
- if (previous === undefined)
486
- this.activityRevision += 1;
487
- }
488
- clearConnection(connectionId) {
489
- let changed = false;
490
- for (const [key, value] of this.activity) {
491
- if (value.connectionId !== connectionId)
521
+ const source = this.state.connection
522
+ .prepare(`SELECT f.run_id AS runId, f.order_number AS orderNumber, f.status
523
+ FROM workflow_follow_ups f WHERE f.follow_up_id = ?`)
524
+ .get(message.sourceId);
525
+ if (!isObjectRecord(source) ||
526
+ source.status !== "queued" ||
527
+ typeof source.orderNumber !== "number" ||
528
+ typeof source.runId !== "string") {
529
+ return false;
530
+ }
531
+ const leaf = this.terminalChainLeaf(source.runId);
532
+ if (leaf === undefined || leaf.status !== "completed")
533
+ return false;
534
+ const terminal = this.workflowMessages
535
+ .listRun(leaf.runId)
536
+ .filter((candidate) => candidate.kind === "terminal" && candidate.status === "sent")
537
+ .at(-1);
538
+ if (terminal === undefined)
539
+ return false;
540
+ const terminalTurn = this.workflowMessages.latestTurnForMessage(terminal.workflowMessageId);
541
+ if (terminalTurn?.state !== "ended")
542
+ return false;
543
+ const prior = this.state.connection
544
+ .prepare(`SELECT follow_up_id AS followUpId, status FROM workflow_follow_ups
545
+ WHERE run_id = ? AND order_number < ? ORDER BY order_number`)
546
+ .all(source.runId, source.orderNumber);
547
+ for (const item of prior) {
548
+ if (!isObjectRecord(item) ||
549
+ typeof item.followUpId !== "string" ||
550
+ typeof item.status !== "string") {
551
+ return false;
552
+ }
553
+ if (item.status === "removed" || item.status === "cancelled")
492
554
  continue;
493
- this.activity.delete(key);
494
- changed = true;
555
+ const priorMessage = this.workflowMessages.latestForSource("followUp", item.followUpId);
556
+ if (priorMessage === undefined ||
557
+ this.workflowMessages.latestTurnForMessage(priorMessage.workflowMessageId)?.state !==
558
+ "ended") {
559
+ return false;
560
+ }
495
561
  }
496
- if (changed)
497
- this.activityRevision += 1;
562
+ const reservation = this.state.connection
563
+ .prepare(`SELECT 1 AS present FROM run_bindings b JOIN runs r ON r.run_id = b.run_id
564
+ WHERE b.origin_session_id = ? AND r.run_id <> ?
565
+ AND r.status IN ('queued', 'running', 'waiting') LIMIT 1`)
566
+ .get(message.targetSessionId, message.runId);
567
+ return reservation === undefined;
568
+ }
569
+ terminalChainLeaf(runId) {
570
+ const row = this.state.connection
571
+ .prepare(`WITH RECURSIVE chain(run_id, status, depth, created_at) AS (
572
+ SELECT run_id, status, 0, created_at FROM runs WHERE run_id = ?
573
+ UNION ALL
574
+ SELECT child.run_id, child.status, chain.depth + 1, child.created_at
575
+ FROM runs child JOIN chain ON child.parent_run_id = chain.run_id
576
+ )
577
+ SELECT run_id AS runId, status FROM chain
578
+ ORDER BY depth DESC, created_at DESC LIMIT 1`)
579
+ .get(runId);
580
+ return isObjectRecord(row) && typeof row.runId === "string" && typeof row.status === "string"
581
+ ? { runId: row.runId, status: row.status }
582
+ : undefined;
498
583
  }
499
- expireActivity(now = Date.now()) {
500
- let changed = false;
501
- for (const [key, value] of this.activity) {
502
- if (value.expiresAt > now)
584
+ openWorkflowMessage(messages) {
585
+ for (const message of [...messages].reverse()) {
586
+ if (message.status !== "sent")
503
587
  continue;
504
- this.activity.delete(key);
505
- changed = true;
588
+ if (message.kind === "step") {
589
+ const request = this.state.connection
590
+ .prepare(`SELECT i.status, r.paused FROM interactive_requests i
591
+ JOIN runs r ON r.run_id = i.run_id WHERE i.request_id = ?`)
592
+ .get(message.sourceId);
593
+ if (isObjectRecord(request) && request.status === "pending" && request.paused === 0) {
594
+ return message;
595
+ }
596
+ }
597
+ else if (message.kind === "terminal" || message.kind === "followUp") {
598
+ const turn = this.workflowMessages.latestTurnForMessage(message.workflowMessageId);
599
+ if (turn === undefined || turn.state === "started")
600
+ return message;
601
+ }
506
602
  }
507
- if (changed)
508
- this.activityRevision += 1;
603
+ return undefined;
604
+ }
605
+ workflowActivityRevision() {
606
+ const row = this.state.connection
607
+ .prepare(`SELECT
608
+ COALESCE((SELECT max(updated_at) FROM workflow_messages), 0) AS messageUpdatedAt,
609
+ COALESCE((SELECT max(COALESCE(ended_at, started_at)) FROM workflow_turns), 0) AS turnUpdatedAt`)
610
+ .get();
611
+ return isObjectRecord(row) &&
612
+ typeof row.messageUpdatedAt === "number" &&
613
+ typeof row.turnUpdatedAt === "number"
614
+ ? `${row.messageUpdatedAt}:${row.turnUpdatedAt}${this.originActivityRevision === 0 ? "" : `-${this.originActivityRevision}`}`
615
+ : `0:0${this.originActivityRevision === 0 ? "" : `-${this.originActivityRevision}`}`;
509
616
  }
510
617
  display(queue, state) {
511
618
  return reduceWorkflowDisplay({
@@ -520,16 +627,20 @@ export class HostViewStore {
520
627
  });
521
628
  }
522
629
  hasActivity(runId) {
523
- for (const activity of this.activity.values()) {
524
- if (activity.runId === runId)
525
- return true;
526
- }
527
- return false;
630
+ const row = this.state.connection
631
+ .prepare(`SELECT t.target_session_id AS targetSessionId FROM workflow_turns t
632
+ JOIN workflow_messages m ON m.workflow_message_id = t.workflow_message_id
633
+ WHERE t.run_id = ? AND t.state = 'started'
634
+ AND m.kind IN ('step', 'terminal', 'followUp') LIMIT 1`)
635
+ .get(runId);
636
+ return (isObjectRecord(row) &&
637
+ typeof row.targetSessionId === "string" &&
638
+ this.hasActiveSessionTurn(row.targetSessionId));
528
639
  }
529
640
  hasPendingInteraction(runId) {
530
641
  const row = this.state.connection
531
642
  .prepare(`SELECT 1 AS present FROM interactive_requests
532
- WHERE run_id = ? AND status IN ('pending', 'presenting') LIMIT 1`)
643
+ WHERE run_id = ? AND status = 'pending' LIMIT 1`)
533
644
  .get(runId);
534
645
  return row !== undefined;
535
646
  }
@@ -558,7 +669,7 @@ export class HostViewStore {
558
669
  row.presentationRevision,
559
670
  row.runStatus,
560
671
  row.paused,
561
- this.activityRevision,
672
+ this.workflowActivityRevision(),
562
673
  this.hasLiveWorker(runId),
563
674
  this.hasActivity(runId),
564
675
  this.hasPendingInteraction(runId),
@@ -570,7 +681,7 @@ export class HostViewStore {
570
681
  .prepare(`SELECT count(*) AS count, COALESCE(sum(revision), 0) AS revisionSum,
571
682
  COALESCE(max(updated_at), 0) AS updatedAt
572
683
  FROM interactive_requests
573
- WHERE target_session_id = ? AND status IN ('pending', 'presenting')`)
684
+ WHERE target_session_id = ? AND status = 'pending'`)
574
685
  .get(sessionId);
575
686
  if (!isSessionRevisionRow(row))
576
687
  throw new Error("Session view revision is invalid");
@@ -591,6 +702,10 @@ export function reduceWorkflowDisplay(facts) {
591
702
  status = "ambiguous";
592
703
  reason = "An external effect needs explicit recovery.";
593
704
  }
705
+ else if (facts.workerActive || facts.originTurnActive) {
706
+ status = "running";
707
+ activity = facts.workerActive ? "supervised_worker" : "origin_turn";
708
+ }
594
709
  else if (facts.durableStatus === "completed" ||
595
710
  facts.durableStatus === "failed" ||
596
711
  facts.durableStatus === "timed_out" ||
@@ -602,17 +717,16 @@ export function reduceWorkflowDisplay(facts) {
602
717
  status = "paused";
603
718
  reason = "The workflow is durably paused.";
604
719
  }
605
- else if (facts.workerActive || facts.originTurnActive) {
606
- status = "running";
607
- activity = facts.workerActive ? "supervised_worker" : "origin_turn";
608
- }
609
720
  else if (facts.pendingInteraction || facts.durableStatus === "waiting") {
610
721
  status = "waiting";
611
722
  reason = "The workflow is waiting for origin-session input.";
612
723
  }
613
724
  else if (facts.queueStatus === "parked" || facts.queueStatus === "queued") {
614
725
  status = "queued";
615
- reason = facts.queueStatus === "parked" ? "The workflow is ready to resume." : null;
726
+ reason =
727
+ facts.queueStatus === "parked"
728
+ ? (facts.errorMessage ?? "The workflow is ready to resume.")
729
+ : null;
616
730
  }
617
731
  else if (facts.queueStatus === "done") {
618
732
  status = "completed";
@@ -668,6 +782,10 @@ function projectQueue(run) {
668
782
  originSessionId: run.originSessionId,
669
783
  executionMode: run.executionMode,
670
784
  parentRunId: run.parentRunId,
785
+ rootRunId: run.rootRunId,
786
+ lineageKind: run.lineageKind,
787
+ restartNumber: run.restartNumber,
788
+ parentTerminalFingerprint: run.parentTerminalFingerprint,
671
789
  errorCode: run.errorCode,
672
790
  createdAt: run.createdAt,
673
791
  updatedAt: run.updatedAt,
@@ -685,21 +803,11 @@ function workflowRootSource(value) {
685
803
  }
686
804
  return root;
687
805
  }
688
- function validateActivityRequest(request, report) {
689
- if (request === undefined)
690
- throw new Error("Origin activity request is not pending");
691
- if (request.runId !== report.runId || request.targetSessionId !== report.sessionId) {
692
- throw new Error("Origin activity target does not match the interactive request");
693
- }
694
- if (request.presentationSessionEntryId !== report.sessionEntryId) {
695
- throw new Error("Origin activity session entry was not durably presented");
696
- }
697
- if (!Number.isSafeInteger(report.sequence) || report.sequence < 0) {
698
- throw new Error("Origin activity sequence must be a non-negative integer");
699
- }
806
+ function isObjectRecord(value) {
807
+ return typeof value === "object" && value !== null && !Array.isArray(value);
700
808
  }
701
- function activityKey(connectionId, requestId) {
702
- return `${connectionId}\u0000${requestId}`;
809
+ function isTerminalStatus(value) {
810
+ return (value === "completed" || value === "failed" || value === "timed_out" || value === "cancelled");
703
811
  }
704
812
  function contentKey(runId, contentPath) {
705
813
  return `${runId}\u0000${contentPath}`;