@osolmaz/pi-workflows 0.16.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) 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 +42 -16
  22. package/dist/client/view.js.map +1 -1
  23. package/dist/controllers/index.d.ts +1 -1
  24. package/dist/controllers/index.js.map +1 -1
  25. package/dist/controllers/sqlite.d.ts +12 -165
  26. package/dist/controllers/sqlite.js +124 -435
  27. package/dist/controllers/sqlite.js.map +1 -1
  28. package/dist/extension/index.d.ts +23 -2
  29. package/dist/extension/index.js +619 -535
  30. package/dist/extension/index.js.map +1 -1
  31. package/dist/extension/recorder.d.ts +95 -0
  32. package/dist/extension/recorder.js +530 -0
  33. package/dist/extension/recorder.js.map +1 -0
  34. package/dist/extension/remote-recorder-store.d.ts +25 -0
  35. package/dist/extension/remote-recorder-store.js +81 -0
  36. package/dist/extension/remote-recorder-store.js.map +1 -0
  37. package/dist/extension/session-events.d.ts +134 -0
  38. package/dist/extension/session-events.js +60 -0
  39. package/dist/extension/session-events.js.map +1 -0
  40. package/dist/extension/session-view.d.ts +2 -0
  41. package/dist/extension/session-view.js +60 -2
  42. package/dist/extension/session-view.js.map +1 -1
  43. package/dist/extension/step-message.d.ts +6 -4
  44. package/dist/extension/step-message.js +12 -6
  45. package/dist/extension/step-message.js.map +1 -1
  46. package/dist/extension/widget.d.ts +2 -2
  47. package/dist/extension/widget.js +28 -18
  48. package/dist/extension/widget.js.map +1 -1
  49. package/dist/extension/workflow-message-coordinator.d.ts +27 -0
  50. package/dist/extension/workflow-message-coordinator.js +294 -0
  51. package/dist/extension/workflow-message-coordinator.js.map +1 -0
  52. package/dist/host/channel-effects.d.ts +72 -0
  53. package/dist/host/channel-effects.js +271 -0
  54. package/dist/host/channel-effects.js.map +1 -0
  55. package/dist/host/channel-supervisor.d.ts +21 -0
  56. package/dist/host/channel-supervisor.js +54 -0
  57. package/dist/host/channel-supervisor.js.map +1 -0
  58. package/dist/host/runner.d.ts +43 -8
  59. package/dist/host/runner.js +1567 -331
  60. package/dist/host/runner.js.map +1 -1
  61. package/dist/host/state.d.ts +19 -16
  62. package/dist/host/state.js +251 -63
  63. package/dist/host/state.js.map +1 -1
  64. package/dist/host/view.d.ts +19 -9
  65. package/dist/host/view.js +222 -118
  66. package/dist/host/view.js.map +1 -1
  67. package/dist/state/prune.js +3 -25
  68. package/dist/state/prune.js.map +1 -1
  69. package/dist/state/schema.js +76 -97
  70. package/dist/state/schema.js.map +1 -1
  71. package/dist/state/workflow-messages.d.ts +96 -0
  72. package/dist/state/workflow-messages.js +335 -0
  73. package/dist/state/workflow-messages.js.map +1 -0
  74. package/dist/viewer/render.js +1 -1
  75. package/dist/viewer/render.js.map +1 -1
  76. package/dist/viewer/tui.d.ts +1 -1
  77. package/dist/viewer/tui.js +65 -8
  78. package/dist/viewer/tui.js.map +1 -1
  79. package/dist/workflows/human-decision.d.ts +2 -0
  80. package/dist/workflows/human-decision.js +8 -2
  81. package/dist/workflows/human-decision.js.map +1 -1
  82. package/dist/workflows/index.d.ts +1 -1
  83. package/dist/workflows/index.js.map +1 -1
  84. package/dist/workflows/settings.d.ts +1 -8
  85. package/dist/workflows/settings.js.map +1 -1
  86. package/dist/workflows/store.d.ts +5 -19
  87. package/dist/workflows/store.js +121 -354
  88. package/dist/workflows/store.js.map +1 -1
  89. package/dist/workflows/tool-input.d.ts +21 -0
  90. package/dist/workflows/tool-input.js +23 -1
  91. package/dist/workflows/tool-input.js.map +1 -1
  92. package/dist/workflows/types.d.ts +5 -5
  93. package/dist/workflows/workflow-message-content.d.ts +38 -0
  94. package/dist/workflows/workflow-message-content.js +157 -0
  95. package/dist/workflows/workflow-message-content.js.map +1 -0
  96. package/docs/2026-08-18-herdr-piw-plan.md +2 -1
  97. package/docs/2026-08-20-durable-workflow-launch-plan.md +1 -1
  98. package/docs/2026-08-25-workflow-follow-ups.md +26 -50
  99. package/docs/2026-08-30-out-of-process-workflow-host-plan.md +3 -3
  100. package/docs/2026-09-01-restore-session-delivery-controls-plan.md +4 -0
  101. package/docs/2026-09-01-unified-workflow-client-plan.md +5 -1
  102. package/docs/2026-09-02-installed-live-e2e-plan.md +15 -1
  103. package/docs/2026-09-02-unify-workflow-messages-plan.md +562 -0
  104. package/docs/DEFERRED_TURNS.md +66 -294
  105. package/docs/HUMAN_DECISIONS.md +29 -30
  106. package/docs/HUMAN_DECISION_PRESENTATIONS.md +4 -11
  107. package/docs/SQLITE_STATE.md +19 -15
  108. package/docs/WORKFLOW_HOST.md +93 -28
  109. package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
  110. package/docs/live-replay-protocol.md +8 -4
  111. package/docs/tui-viewer.md +3 -1
  112. package/docs/workflows.md +34 -27
  113. package/herdr-plugin.toml +1 -1
  114. package/package.json +1 -1
  115. package/plugins/herdr/viewer.mjs +14 -5
  116. package/src/channels/adapter-entry.ts +220 -0
  117. package/src/channels/config.ts +296 -0
  118. package/src/channels/protocol.ts +333 -0
  119. package/src/channels/telegram.ts +335 -0
  120. package/src/client/client.ts +6 -2
  121. package/src/client/index.ts +2 -1
  122. package/src/client/protocol.ts +11 -5
  123. package/src/client/view.ts +43 -18
  124. package/src/controllers/index.ts +0 -1
  125. package/src/controllers/sqlite.ts +164 -844
  126. package/src/extension/index.ts +693 -646
  127. package/src/extension/recorder.ts +687 -0
  128. package/src/extension/remote-recorder-store.ts +126 -0
  129. package/src/extension/session-events.ts +119 -0
  130. package/src/extension/session-view.ts +86 -3
  131. package/src/extension/step-message.ts +19 -9
  132. package/src/extension/widget.ts +37 -17
  133. package/src/extension/workflow-message-coordinator.ts +334 -0
  134. package/src/host/channel-effects.ts +465 -0
  135. package/src/host/channel-supervisor.ts +73 -0
  136. package/src/host/runner.ts +2120 -531
  137. package/src/host/state.ts +333 -94
  138. package/src/host/view.ts +249 -121
  139. package/src/state/prune.ts +3 -31
  140. package/src/state/schema.ts +76 -97
  141. package/src/state/workflow-messages.ts +548 -0
  142. package/src/viewer/render.ts +1 -5
  143. package/src/viewer/tui.ts +71 -6
  144. package/src/workflows/human-decision.ts +18 -2
  145. package/src/workflows/index.ts +0 -1
  146. package/src/workflows/settings.ts +1 -20
  147. package/src/workflows/store.ts +173 -489
  148. package/src/workflows/tool-input.ts +36 -1
  149. package/src/workflows/types.ts +5 -5
  150. package/src/workflows/workflow-message-content.ts +197 -0
  151. package/dist/extension/decision-channels.d.ts +0 -134
  152. package/dist/extension/decision-channels.js +0 -1307
  153. package/dist/extension/decision-channels.js.map +0 -1
  154. package/src/extension/decision-channels.ts +0 -1826
@@ -7,7 +7,6 @@ import {
7
7
  resourceIdFor,
8
8
  tokenHash,
9
9
  type ActorType,
10
- type LeaseClaim,
11
10
  type MutationActor,
12
11
  type OwnerType,
13
12
  } from "../state/mutation.js";
@@ -18,6 +17,7 @@ import {
18
17
  VIEWER_PAGE_SIZE,
19
18
  type ViewerDeltaDraft,
20
19
  } from "../state/viewer.js";
20
+ import { WorkflowMessageStore, workflowMessageIdFor } from "../state/workflow-messages.js";
21
21
  import { compositionMetadata } from "./composition.js";
22
22
  import { ClaimLostError } from "./errors.js";
23
23
  import { HumanDecisionStore } from "./human-decision.js";
@@ -34,7 +34,6 @@ import {
34
34
  type WorkflowFollowUpQueueRecord,
35
35
  type WorkflowFollowUpRecord,
36
36
  type WorkflowFollowUpState,
37
- type WorkflowPresentationState,
38
37
  type WorkflowQueueFollowUpRequest,
39
38
  type WorkflowRemoveFollowUpRequest,
40
39
  type WorkflowSettingsChangeRecord,
@@ -70,6 +69,7 @@ import type {
70
69
  WorkflowUpdateRecord,
71
70
  } from "./types.js";
72
71
  import { MAX_CURRENT_UPDATES, createUpdateId, updateProjection } from "./updates.js";
72
+ import { followUpWorkflowMessageContent } from "./workflow-message-content.js";
73
73
 
74
74
  export const RUN_STATE_SCHEMA = "pi-workflows.run-state.v1" as const;
75
75
  export const DEFINITION_SNAPSHOT_SCHEMA = "pi-workflows.definition-snapshot.v1" as const;
@@ -110,14 +110,11 @@ const STEP_ROW_SELECT = `
110
110
 
111
111
  const FOLLOW_UP_ROW_SELECT = `
112
112
  SELECT f.follow_up_id AS followUpId, f.resource_id AS resourceId,
113
- f.queue_resource_id AS queueResourceId, f.run_id AS runId,
114
- f.request_id AS requestId, f.order_number AS orderNumber,
113
+ f.run_id AS runId, f.request_id AS requestId, f.order_number AS orderNumber,
115
114
  f.target_session_id AS targetSessionId, f.actor_type AS actorType,
116
115
  f.actor_id AS actorId, f.source_type AS sourceType,
117
- f.prompt_hash AS promptHash, f.status,
118
- f.session_entry_id AS sessionEntryId, f.reason_hash AS reasonHash,
119
- f.created_at AS createdAt, f.updated_at AS updatedAt, f.sent_at AS sentAt,
120
- r.revision
116
+ f.prompt_hash AS promptHash, f.status, f.reason_hash AS reasonHash,
117
+ f.created_at AS createdAt, f.updated_at AS updatedAt, r.revision
121
118
  FROM workflow_follow_ups f
122
119
  JOIN resources r ON r.resource_id = f.resource_id`;
123
120
 
@@ -156,6 +153,8 @@ export type WorkflowRunStoreOptions = {
156
153
  database: StateDatabase;
157
154
  now: number;
158
155
  }) => void;
156
+ /** The global host may record an attested Pi session after the worker lease ends. */
157
+ allowHostSessionRecording?: boolean;
159
158
  state?: StateDatabase;
160
159
  readOnly?: boolean;
161
160
  };
@@ -352,21 +351,9 @@ type SettingsChangeRow = {
352
351
  acceptedAt: number;
353
352
  };
354
353
 
355
- type FollowUpQueueRow = {
356
- runId: string;
357
- resourceId: string;
358
- originSessionId: string | null;
359
- presentationState: string;
360
- presentationEntryId: string | null;
361
- presentationAssistantEntryId: string | null;
362
- presentationReasonHash: Buffer | null;
363
- revision: number;
364
- };
365
-
366
354
  type FollowUpRow = {
367
355
  followUpId: string;
368
356
  resourceId: string;
369
- queueResourceId: string;
370
357
  runId: string;
371
358
  requestId: string;
372
359
  orderNumber: number;
@@ -376,11 +363,9 @@ type FollowUpRow = {
376
363
  sourceType: string;
377
364
  promptHash: Buffer;
378
365
  status: string;
379
- sessionEntryId: string | null;
380
366
  reasonHash: Buffer | null;
381
367
  createdAt: number;
382
368
  updatedAt: number;
383
- sentAt: number | null;
384
369
  revision: number;
385
370
  };
386
371
 
@@ -503,13 +488,16 @@ export class WorkflowRunStore {
503
488
  | ((runId: string) => RunWriteAuthority | undefined)
504
489
  | undefined;
505
490
  private readonly snapshotLifecycle: WorkflowRunStoreOptions["snapshotLifecycle"];
491
+ private readonly allowHostSessionRecording: boolean;
506
492
  private readonly contexts = new Map<string, RunContext>();
507
493
  private readonly ownsState: boolean;
508
494
  private readonly mutations: StateMutationStore;
495
+ private readonly workflowMessages: WorkflowMessageStore;
509
496
 
510
497
  constructor(databasePath: string = workflowStatePath(), options: WorkflowRunStoreOptions = {}) {
511
498
  this.authorityProvider = options.authorityProvider;
512
499
  this.snapshotLifecycle = options.snapshotLifecycle;
500
+ this.allowHostSessionRecording = options.allowHostSessionRecording === true;
513
501
  this.ownsState = options.state === undefined;
514
502
  this.state =
515
503
  options.state ??
@@ -520,6 +508,7 @@ export class WorkflowRunStore {
520
508
  });
521
509
  this.databasePath = this.state.filePath;
522
510
  this.mutations = new StateMutationStore(this.state);
511
+ this.workflowMessages = new WorkflowMessageStore(this.state);
523
512
  }
524
513
 
525
514
  close(): void {
@@ -912,6 +901,18 @@ export class WorkflowRunStore {
912
901
  return;
913
902
  }
914
903
  const definitionHash = this.state.putJson(snapshot, now);
904
+ const parentLineage =
905
+ state.parentRunId === undefined
906
+ ? undefined
907
+ : this.state.connection
908
+ .prepare(
909
+ `SELECT root_run_id AS rootRunId, restart_number AS restartNumber
910
+ FROM runs WHERE run_id = ?`,
911
+ )
912
+ .get(state.parentRunId);
913
+ if (state.parentRunId !== undefined && !isRunLineageRow(parentLineage)) {
914
+ throw new Error(`Workflow parent run is missing: ${state.parentRunId}`);
915
+ }
915
916
  this.state.connection
916
917
  .prepare(
917
918
  `INSERT INTO workflow_definitions(
@@ -940,16 +941,20 @@ export class WorkflowRunStore {
940
941
  this.state.connection
941
942
  .prepare(
942
943
  `INSERT INTO runs(
943
- run_id, resource_id, project_id, parent_run_id, definition_digest,
944
+ run_id, resource_id, project_id, parent_run_id, root_run_id, lineage_kind,
945
+ restart_number, parent_terminal_fingerprint, definition_digest,
944
946
  workflow_ref, launch_options_hash, title, status, paused,
945
947
  status_detail, input_hash, final_output_hash, error_hash,
946
948
  created_at, updated_at, finished_at
947
- ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
949
+ ) VALUES (?, ?, NULL, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
948
950
  )
949
951
  .run(
950
952
  state.runId,
951
953
  resourceId,
952
954
  state.parentRunId ?? null,
955
+ isRunLineageRow(parentLineage) ? parentLineage.rootRunId : state.runId,
956
+ state.parentRunId === undefined ? null : "continuation",
957
+ isRunLineageRow(parentLineage) ? parentLineage.restartNumber : 0,
953
958
  definitionDigest,
954
959
  state.workflowName,
955
960
  launchOptionsHash,
@@ -1287,87 +1292,78 @@ export class WorkflowRunStore {
1287
1292
  if (Buffer.byteLength(request.prompt, "utf8") > 64 * 1024) {
1288
1293
  throw new Error("Workflow follow-up prompt cannot exceed 65536 bytes");
1289
1294
  }
1290
- for (let attempt = 0; attempt < 8; attempt += 1) {
1295
+ return this.state.transaction(() => {
1291
1296
  const prior = this.followUpRowByRequest(request.runId, request.requestId);
1292
1297
  if (prior !== undefined) {
1293
1298
  this.assertSameFollowUp(prior, request);
1294
1299
  return { followUp: this.followUpRecord(prior), adopted: true };
1295
1300
  }
1296
1301
  this.requireRunAcceptsSettings(request.runId, true);
1297
- const queue = this.requireFollowUpQueueRow(request.runId);
1298
- try {
1299
- const result = this.mutations.mutate(
1300
- {
1301
- resourceId: queue.resourceId,
1302
- operation: "follow-up.queue",
1303
- actor: request.actor,
1304
- expectedRevision: queue.revision,
1305
- },
1306
- "follow-up.queued",
1307
- ({ database, now }) => {
1308
- this.requireRunAcceptsSettings(request.runId, true);
1309
- const currentQueue = this.requireFollowUpQueueRow(request.runId);
1310
- if (
1311
- currentQueue.originSessionId !== null &&
1312
- currentQueue.originSessionId !== request.targetSessionId
1313
- ) {
1314
- throw new Error("Workflow follow-ups must target the run's origin Pi session");
1315
- }
1316
- if (currentQueue.originSessionId === null) {
1317
- database.connection
1318
- .prepare(
1319
- `UPDATE workflow_follow_up_queues
1320
- SET origin_session_id = ?, updated_at = ? WHERE run_id = ?`,
1321
- )
1322
- .run(request.targetSessionId, now, request.runId);
1323
- }
1324
- const order = this.nextFollowUpOrder(request.runId);
1325
- const followUpId = followUpIdFor(request.runId, request.requestId);
1326
- const resourceId = this.mutations.ensureResource("follow_up", followUpId, now);
1327
- const promptHash = database.putText(request.prompt, now);
1328
- database.connection
1329
- .prepare(
1330
- `INSERT INTO workflow_follow_ups(
1331
- follow_up_id, resource_id, queue_resource_id, run_id,
1332
- request_id, order_number, target_session_id,
1333
- actor_type, actor_id, source_type, prompt_hash, status,
1334
- created_at, updated_at
1335
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?)`,
1336
- )
1337
- .run(
1338
- followUpId,
1339
- resourceId,
1340
- queue.resourceId,
1341
- request.runId,
1342
- request.requestId,
1343
- order,
1344
- request.targetSessionId,
1345
- request.actor.type,
1346
- request.actor.id ?? null,
1347
- request.source,
1348
- promptHash,
1349
- now,
1350
- now,
1351
- );
1352
- recordViewerDeltas(
1353
- this.state,
1354
- request.runId,
1355
- this.viewerInspectorTargets(request.runId),
1356
- now,
1357
- );
1358
- return followUpId;
1359
- },
1360
- { payload: { runId: request.runId, requestId: request.requestId } },
1361
- );
1362
- return {
1363
- followUp: this.followUpRecord(this.requireFollowUpRow(result.value)),
1364
- adopted: false,
1365
- };
1366
- } catch (error) {
1367
- if (!(error instanceof StaleResourceError)) throw error;
1302
+ if (this.originSessionId(request.runId) !== request.targetSessionId) {
1303
+ throw new Error("Workflow follow-ups must target the run's origin Pi session");
1368
1304
  }
1369
- }
1370
- throw new StaleResourceError("Workflow follow-up queue kept changing; retry the request");
1305
+ const now = Date.now();
1306
+ const order = this.nextFollowUpOrder(request.runId);
1307
+ const followUpId = followUpIdFor(request.runId, request.requestId);
1308
+ const resourceId = this.mutations.ensureResource("follow_up", followUpId, now);
1309
+ const promptHash = this.state.putText(request.prompt, now);
1310
+ this.state.connection
1311
+ .prepare(
1312
+ `INSERT INTO workflow_follow_ups(
1313
+ follow_up_id, resource_id, run_id, request_id, order_number,
1314
+ target_session_id, actor_type, actor_id, source_type, prompt_hash,
1315
+ status, created_at, updated_at
1316
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?)`,
1317
+ )
1318
+ .run(
1319
+ followUpId,
1320
+ resourceId,
1321
+ request.runId,
1322
+ request.requestId,
1323
+ order,
1324
+ request.targetSessionId,
1325
+ request.actor.type,
1326
+ request.actor.id ?? null,
1327
+ request.source,
1328
+ promptHash,
1329
+ now,
1330
+ now,
1331
+ );
1332
+ const workflowMessageId = workflowMessageIdFor("followUp", followUpId, "initial");
1333
+ this.workflowMessages.create({
1334
+ workflowMessageId,
1335
+ runId: request.runId,
1336
+ targetSessionId: request.targetSessionId,
1337
+ kind: "followUp",
1338
+ sourceId: followUpId,
1339
+ idempotencyKey: "initial",
1340
+ content: followUpWorkflowMessageContent({
1341
+ workflowMessageId,
1342
+ followUpId,
1343
+ runId: request.runId,
1344
+ prompt: request.prompt,
1345
+ }),
1346
+ now,
1347
+ });
1348
+ this.bumpResource(resourceId, 0, now);
1349
+ insertGenericEvent(
1350
+ this.state,
1351
+ resourceId,
1352
+ 1,
1353
+ "follow-up.queued",
1354
+ request.actor.type,
1355
+ request.actor.id ?? null,
1356
+ this.state.putJson({ runId: request.runId, requestId: request.requestId }, now),
1357
+ now,
1358
+ );
1359
+ recordViewerDeltas(
1360
+ this.state,
1361
+ request.runId,
1362
+ this.viewerInspectorTargets(request.runId),
1363
+ now,
1364
+ );
1365
+ return { followUp: this.followUpRecord(this.requireFollowUpRow(followUpId)), adopted: false };
1366
+ });
1371
1367
  }
1372
1368
 
1373
1369
  removeFollowUp(request: WorkflowRemoveFollowUpRequest): WorkflowFollowUpRecord {
@@ -1377,12 +1373,13 @@ export class WorkflowRunStore {
1377
1373
  throw new Error(`Workflow follow-up is not part of run ${request.runId}`);
1378
1374
  }
1379
1375
  if (row.status === "removed") return this.followUpRecord(row);
1380
- if (row.status === "sent" || row.status === "cancelled") {
1381
- throw new Error(`Workflow follow-up cannot be removed after it is ${row.status}`);
1376
+ if (row.status === "cancelled") {
1377
+ throw new Error("A cancelled workflow follow-up cannot be removed");
1382
1378
  }
1383
1379
  this.assertFollowUpRemovalAuthority(row, request.actor, request.source);
1384
- if (this.hasLiveLease(row.resourceId)) {
1385
- throw new Error("Workflow follow-up is being delivered and cannot be removed");
1380
+ const message = this.workflowMessages.latestForSource("followUp", request.followUpId);
1381
+ if (message?.status === "sent") {
1382
+ throw new Error("A sent workflow follow-up cannot be removed");
1386
1383
  }
1387
1384
  const reason = "Removed before delivery";
1388
1385
  this.mutations.mutate(
@@ -1399,12 +1396,13 @@ export class WorkflowRunStore {
1399
1396
  .prepare(
1400
1397
  `UPDATE workflow_follow_ups
1401
1398
  SET status = 'removed', reason_hash = ?, updated_at = ?
1402
- WHERE follow_up_id = ? AND status IN ('queued', 'pending_presentation', 'ready')`,
1399
+ WHERE follow_up_id = ? AND status = 'queued'`,
1403
1400
  )
1404
1401
  .run(reasonHash, now, request.followUpId);
1405
1402
  if (update.changes !== 1) {
1406
1403
  throw new StaleResourceError("Workflow follow-up changed before removal");
1407
1404
  }
1405
+ this.workflowMessages.cancelPendingForSource(request.followUpId, "followUp", now);
1408
1406
  recordViewerDeltas(
1409
1407
  this.state,
1410
1408
  request.runId,
@@ -1420,10 +1418,8 @@ export class WorkflowRunStore {
1420
1418
  originSessionId(runId: string): string | undefined {
1421
1419
  const row = this.state.connection
1422
1420
  .prepare(
1423
- `SELECT COALESCE(q.origin_session_id, b.origin_session_id) AS originSessionId
1424
- FROM runs r
1425
- LEFT JOIN workflow_follow_up_queues q ON q.run_id = r.run_id
1426
- LEFT JOIN run_bindings b ON b.run_id = r.run_id
1421
+ `SELECT b.origin_session_id AS originSessionId
1422
+ FROM runs r LEFT JOIN run_bindings b ON b.run_id = r.run_id
1427
1423
  WHERE r.run_id = ?`,
1428
1424
  )
1429
1425
  .get(runId);
@@ -1433,36 +1429,22 @@ export class WorkflowRunStore {
1433
1429
  }
1434
1430
 
1435
1431
  readFollowUpQueue(runId: string): WorkflowFollowUpQueueRecord | undefined {
1436
- const queue = this.followUpQueueRow(runId);
1437
- if (queue === undefined) return undefined;
1432
+ const originSessionId = this.originSessionId(runId);
1433
+ if (originSessionId === undefined) return undefined;
1438
1434
  const followUps = this.state.connection
1439
1435
  .prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? ORDER BY f.order_number`)
1440
1436
  .all(runId)
1441
1437
  .filter(isFollowUpRow)
1442
1438
  .map((row) => this.followUpRecord(row));
1443
- return {
1444
- runId,
1445
- ...(queue.originSessionId !== null ? { originSessionId: queue.originSessionId } : {}),
1446
- presentationState: assertPresentationState(queue.presentationState),
1447
- ...(queue.presentationEntryId !== null
1448
- ? { presentationEntryId: queue.presentationEntryId }
1449
- : {}),
1450
- ...(queue.presentationAssistantEntryId !== null
1451
- ? { presentationAssistantEntryId: queue.presentationAssistantEntryId }
1452
- : {}),
1453
- ...(queue.presentationReasonHash !== null
1454
- ? { presentationReason: this.readText(queue.presentationReasonHash) }
1455
- : {}),
1456
- followUps,
1457
- };
1439
+ return { runId, originSessionId, followUps };
1458
1440
  }
1459
1441
 
1460
1442
  private readFollowUpQueueRange(
1461
1443
  runId: string,
1462
1444
  range: WorkflowRunViewRange,
1463
1445
  ): WorkflowFollowUpQueueRecord | null {
1464
- const queue = this.followUpQueueRow(runId);
1465
- if (queue === undefined) return null;
1446
+ const originSessionId = this.originSessionId(runId);
1447
+ if (originSessionId === undefined) return null;
1466
1448
  const followUps = this.state.connection
1467
1449
  .prepare(
1468
1450
  `${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? ORDER BY f.order_number LIMIT ? OFFSET ?`,
@@ -1470,195 +1452,7 @@ export class WorkflowRunStore {
1470
1452
  .all(runId, range.limit, range.start)
1471
1453
  .filter(isFollowUpRow)
1472
1454
  .map((row) => this.followUpRecord(row));
1473
- return {
1474
- runId,
1475
- ...(queue.originSessionId !== null ? { originSessionId: queue.originSessionId } : {}),
1476
- presentationState: assertPresentationState(queue.presentationState),
1477
- ...(queue.presentationEntryId !== null
1478
- ? { presentationEntryId: queue.presentationEntryId }
1479
- : {}),
1480
- ...(queue.presentationAssistantEntryId !== null
1481
- ? { presentationAssistantEntryId: queue.presentationAssistantEntryId }
1482
- : {}),
1483
- ...(queue.presentationReasonHash !== null
1484
- ? { presentationReason: this.readText(queue.presentationReasonHash) }
1485
- : {}),
1486
- followUps,
1487
- };
1488
- }
1489
-
1490
- listPendingPresentations(targetSessionId: string): string[] {
1491
- return this.state.connection
1492
- .prepare(
1493
- `SELECT run_id AS runId FROM workflow_follow_up_queues
1494
- WHERE origin_session_id = ? AND presentation_state = 'pending'
1495
- ORDER BY created_at`,
1496
- )
1497
- .all(targetSessionId)
1498
- .flatMap((row) => (isRecord(row) && typeof row.runId === "string" ? [row.runId] : []));
1499
- }
1500
-
1501
- settleFollowUpPresentation(options: {
1502
- runId: string;
1503
- state: "settled" | "unavailable";
1504
- presentationEntryId?: string;
1505
- assistantEntryId?: string;
1506
- reason?: string;
1507
- }): WorkflowFollowUpQueueRecord {
1508
- const queue = this.requireFollowUpQueueRow(options.runId);
1509
- if (queue.presentationState === options.state) {
1510
- return this.readFollowUpQueue(options.runId) as WorkflowFollowUpQueueRecord;
1511
- }
1512
- if (queue.presentationState !== "pending") {
1513
- throw new Error(
1514
- `Workflow presentation is ${queue.presentationState}, not pending for ${options.runId}`,
1515
- );
1516
- }
1517
- if (
1518
- options.state === "settled" &&
1519
- (options.presentationEntryId === undefined || options.assistantEntryId === undefined)
1520
- ) {
1521
- throw new Error("Settled workflow presentation requires both session entry IDs");
1522
- }
1523
- if (options.state === "unavailable" && !options.reason?.trim()) {
1524
- throw new Error("Unavailable workflow presentation requires a reason");
1525
- }
1526
- try {
1527
- this.mutations.mutate(
1528
- {
1529
- resourceId: queue.resourceId,
1530
- operation: "follow-up.presentation",
1531
- actor: {
1532
- type: "session",
1533
- ...(queue.originSessionId !== null ? { id: queue.originSessionId } : {}),
1534
- },
1535
- expectedRevision: queue.revision,
1536
- },
1537
- `follow-up.presentation-${options.state}`,
1538
- ({ database, now }) => {
1539
- const reasonHash =
1540
- options.state === "unavailable"
1541
- ? database.putText(options.reason as string, now)
1542
- : null;
1543
- database.connection
1544
- .prepare(
1545
- `UPDATE workflow_follow_up_queues
1546
- SET presentation_state = ?, presentation_entry_id = ?,
1547
- presentation_assistant_entry_id = ?, presentation_reason_hash = ?,
1548
- presentation_updated_at = ?, updated_at = ?
1549
- WHERE run_id = ? AND presentation_state = 'pending'`,
1550
- )
1551
- .run(
1552
- options.state,
1553
- options.presentationEntryId ?? null,
1554
- options.assistantEntryId ?? null,
1555
- reasonHash,
1556
- now,
1557
- now,
1558
- options.runId,
1559
- );
1560
- database.connection
1561
- .prepare(
1562
- `UPDATE workflow_follow_ups SET status = 'ready', updated_at = ?
1563
- WHERE run_id = ? AND status = 'pending_presentation'`,
1564
- )
1565
- .run(now, options.runId);
1566
- recordViewerDeltas(
1567
- this.state,
1568
- options.runId,
1569
- this.viewerInspectorTargets(options.runId),
1570
- now,
1571
- );
1572
- },
1573
- { payload: { runId: options.runId, state: options.state } },
1574
- );
1575
- } catch (error) {
1576
- if (error instanceof StaleResourceError) {
1577
- const current = this.requireFollowUpQueueRow(options.runId);
1578
- if (current.presentationState === options.state) {
1579
- return this.readFollowUpQueue(options.runId) as WorkflowFollowUpQueueRecord;
1580
- }
1581
- }
1582
- throw error;
1583
- }
1584
- return this.readFollowUpQueue(options.runId) as WorkflowFollowUpQueueRecord;
1585
- }
1586
-
1587
- claimNextFollowUp(
1588
- targetSessionId: string,
1589
- ownerId: string,
1590
- leaseMs = 30_000,
1591
- ): { followUp: WorkflowFollowUpRecord; claim: LeaseClaim } | undefined {
1592
- const rows = this.state.connection
1593
- .prepare(
1594
- `${FOLLOW_UP_ROW_SELECT}
1595
- WHERE f.target_session_id = ? AND f.status = 'ready'
1596
- ORDER BY f.created_at, f.order_number LIMIT 16`,
1597
- )
1598
- .all(targetSessionId)
1599
- .filter(isFollowUpRow);
1600
- for (const row of rows) {
1601
- let claim: LeaseClaim | undefined;
1602
- try {
1603
- claim = this.mutations.claim({
1604
- resourceId: row.resourceId,
1605
- ownerType: "session",
1606
- ownerId,
1607
- expectedRevision: row.revision,
1608
- leaseMs,
1609
- });
1610
- } catch (error) {
1611
- if (error instanceof StaleResourceError) continue;
1612
- throw error;
1613
- }
1614
- if (claim === undefined) continue;
1615
- const current = this.requireFollowUpRow(row.followUpId);
1616
- if (current.status !== "ready") {
1617
- this.mutations.release(claim, claim.resourceRevision);
1618
- continue;
1619
- }
1620
- return { followUp: this.followUpRecord(current), claim };
1621
- }
1622
- return undefined;
1623
- }
1624
-
1625
- markFollowUpSent(
1626
- followUpId: string,
1627
- claim: LeaseClaim,
1628
- sessionEntryId: string,
1629
- ): WorkflowFollowUpRecord {
1630
- const row = this.requireFollowUpRow(followUpId);
1631
- if (row.status === "sent") return this.followUpRecord(row);
1632
- this.mutations.mutate(
1633
- {
1634
- resourceId: row.resourceId,
1635
- operation: "follow-up.sent",
1636
- actor: { type: "session", id: claim.ownerId },
1637
- expectedRevision: claim.resourceRevision,
1638
- lease: claim,
1639
- },
1640
- "follow-up.sent",
1641
- ({ database, now }) => {
1642
- const update = database.connection
1643
- .prepare(
1644
- `UPDATE workflow_follow_ups
1645
- SET status = 'sent', session_entry_id = ?, sent_at = ?, updated_at = ?
1646
- WHERE follow_up_id = ? AND status = 'ready'`,
1647
- )
1648
- .run(sessionEntryId, now, now, followUpId);
1649
- if (update.changes !== 1) {
1650
- throw new StaleResourceError("Workflow follow-up changed before delivery completed");
1651
- }
1652
- recordViewerDeltas(this.state, row.runId, this.viewerInspectorTargets(row.runId), now);
1653
- },
1654
- { payload: { followUpId, sessionEntryId } },
1655
- );
1656
- return this.followUpRecord(this.requireFollowUpRow(followUpId));
1657
- }
1658
-
1659
- releaseFollowUpClaim(claim: LeaseClaim): void {
1660
- const revision = this.requireResourceRevision(claim.resourceId);
1661
- this.mutations.release(claim, revision);
1455
+ return { runId, originSessionId, followUps };
1662
1456
  }
1663
1457
 
1664
1458
  async prepareRunResume(runId: string): Promise<LoadedWorkflowRun> {
@@ -1670,7 +1464,6 @@ export class WorkflowRunStore {
1670
1464
  this.verifySettingsProjections(runId);
1671
1465
  const revision = this.resourceRevision(runId);
1672
1466
  this.contexts.set(runId, { revision, lock: Promise.resolve() });
1673
- this.finalizeRecordingCaptures(runId, "Workflow host stopped before the run finished");
1674
1467
  this.state.transaction(() => {
1675
1468
  const row = this.requireRunRow(runId);
1676
1469
  const context = this.contextFor(runId);
@@ -1900,7 +1693,7 @@ export class WorkflowRunStore {
1900
1693
  this.state.transaction(() => {
1901
1694
  const run = this.requireRunRow(runId);
1902
1695
  const context = this.contextFor(runId);
1903
- this.assertWriteAuthority(run, context.revision);
1696
+ this.assertSessionWriteAuthority(runId);
1904
1697
  const now = Date.parse(binding.boundAt);
1905
1698
  const resourceId = resourceIdFor("session", segmentId);
1906
1699
  const bindingHash = this.state.putJson(binding, now);
@@ -1947,13 +1740,6 @@ export class WorkflowRunStore {
1947
1740
  ON CONFLICT(run_id) DO NOTHING`,
1948
1741
  )
1949
1742
  .run(runId, binding.piSessionId, now);
1950
- this.state.connection
1951
- .prepare(
1952
- `UPDATE workflow_follow_up_queues
1953
- SET origin_session_id = COALESCE(origin_session_id, ?), updated_at = ?
1954
- WHERE run_id = ?`,
1955
- )
1956
- .run(binding.piSessionId, now, runId);
1957
1743
  const revision = context.revision + 1;
1958
1744
  const at = new Date(now).toISOString();
1959
1745
  const traceEvent: WorkflowTraceEvent = {
@@ -2679,6 +2465,7 @@ export class WorkflowRunStore {
2679
2465
 
2680
2466
  private assertSessionWriteAuthority(runId: string): void {
2681
2467
  const run = this.requireRunRow(runId);
2468
+ if (this.allowHostSessionRecording) return;
2682
2469
  this.assertWriteAuthority(run, this.contextFor(runId).revision);
2683
2470
  }
2684
2471
 
@@ -2902,10 +2689,8 @@ export class WorkflowRunStore {
2902
2689
  order: row.orderNumber,
2903
2690
  state: row.status,
2904
2691
  source: row.sourceType,
2905
- sessionEntryId: row.sessionEntryId,
2906
2692
  }));
2907
2693
  const followUpStart = Math.max(0, followUps.length - VIEWER_PAGE_SIZE);
2908
- const queue = this.requireFollowUpQueueRow(runId);
2909
2694
  return [
2910
2695
  {
2911
2696
  targetType: "inspector",
@@ -2927,12 +2712,7 @@ export class WorkflowRunStore {
2927
2712
  {
2928
2713
  op: "replace",
2929
2714
  path: "/followUpQueue",
2930
- value: parseJson(
2931
- canonicalJson({
2932
- presentationState: queue.presentationState,
2933
- items: followUps.slice(followUpStart),
2934
- }),
2935
- ),
2715
+ value: parseJson(canonicalJson({ items: followUps.slice(followUpStart) })),
2936
2716
  },
2937
2717
  { op: "replace", path: "/followUpStart", value: followUpStart },
2938
2718
  { op: "replace", path: "/followUpTotal", value: followUps.length },
@@ -2946,25 +2726,7 @@ export class WorkflowRunStore {
2946
2726
  initialSettings: InitialWorkflowSettingsScope[],
2947
2727
  now: number,
2948
2728
  ): void {
2949
- const childQueueResourceId = this.ensureFollowUpQueue(state.runId, now);
2950
2729
  if (state.parentRunId !== undefined) {
2951
- const parentQueue = this.followUpQueueRow(state.parentRunId);
2952
- if (parentQueue !== undefined) {
2953
- this.state.connection
2954
- .prepare(
2955
- `UPDATE workflow_follow_up_queues
2956
- SET origin_session_id = COALESCE(origin_session_id, ?), updated_at = ?
2957
- WHERE run_id = ?`,
2958
- )
2959
- .run(parentQueue.originSessionId, now, state.runId);
2960
- this.state.connection
2961
- .prepare(
2962
- `UPDATE workflow_follow_ups
2963
- SET run_id = ?, queue_resource_id = ?, updated_at = ?
2964
- WHERE run_id = ? AND status = 'queued'`,
2965
- )
2966
- .run(state.runId, childQueueResourceId, now, state.parentRunId);
2967
- }
2968
2730
  this.state.connection
2969
2731
  .prepare(
2970
2732
  `UPDATE workflow_settings SET active_run_id = ?, updated_at = ? WHERE active_run_id = ?`,
@@ -3004,19 +2766,6 @@ export class WorkflowRunStore {
3004
2766
  }
3005
2767
  }
3006
2768
 
3007
- private ensureFollowUpQueue(runId: string, now: number): string {
3008
- const resourceId = this.mutations.ensureResource("follow_up_queue", runId, now);
3009
- this.state.connection
3010
- .prepare(
3011
- `INSERT INTO workflow_follow_up_queues(
3012
- run_id, resource_id, created_at, updated_at
3013
- ) VALUES (?, ?, ?, ?)
3014
- ON CONFLICT(run_id) DO NOTHING`,
3015
- )
3016
- .run(runId, resourceId, now, now);
3017
- return resourceId;
3018
- }
3019
-
3020
2769
  private logicalOriginRunId(runId: string): string {
3021
2770
  let current = runId;
3022
2771
  for (let depth = 0; depth < 100; depth += 1) {
@@ -3174,30 +2923,6 @@ export class WorkflowRunStore {
3174
2923
  }
3175
2924
  }
3176
2925
 
3177
- private followUpQueueRow(runId: string): FollowUpQueueRow | undefined {
3178
- const row = this.state.connection
3179
- .prepare(
3180
- `SELECT q.run_id AS runId, q.resource_id AS resourceId,
3181
- q.origin_session_id AS originSessionId,
3182
- q.presentation_state AS presentationState,
3183
- q.presentation_entry_id AS presentationEntryId,
3184
- q.presentation_assistant_entry_id AS presentationAssistantEntryId,
3185
- q.presentation_reason_hash AS presentationReasonHash,
3186
- r.revision
3187
- FROM workflow_follow_up_queues q
3188
- JOIN resources r ON r.resource_id = q.resource_id
3189
- WHERE q.run_id = ?`,
3190
- )
3191
- .get(runId);
3192
- return isFollowUpQueueRow(row) ? row : undefined;
3193
- }
3194
-
3195
- private requireFollowUpQueueRow(runId: string): FollowUpQueueRow {
3196
- const row = this.followUpQueueRow(runId);
3197
- if (row === undefined) throw new Error(`Workflow follow-up queue not found: ${runId}`);
3198
- return row;
3199
- }
3200
-
3201
2926
  private followUpRowByRequest(runId: string, requestId: string): FollowUpRow | undefined {
3202
2927
  const row = this.state.connection
3203
2928
  .prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? AND f.request_id = ?`)
@@ -3232,11 +2957,9 @@ export class WorkflowRunStore {
3232
2957
  source: row.sourceType,
3233
2958
  prompt: this.readText(row.promptHash),
3234
2959
  state: assertFollowUpState(row.status),
3235
- ...(row.sessionEntryId !== null ? { sessionEntryId: row.sessionEntryId } : {}),
3236
2960
  ...(row.reasonHash !== null ? { reason: this.readText(row.reasonHash) } : {}),
3237
2961
  createdAt: new Date(row.createdAt).toISOString(),
3238
2962
  updatedAt: new Date(row.updatedAt).toISOString(),
3239
- ...(row.sentAt !== null ? { sentAt: new Date(row.sentAt).toISOString() } : {}),
3240
2963
  };
3241
2964
  }
3242
2965
 
@@ -3270,13 +2993,6 @@ export class WorkflowRunStore {
3270
2993
  throw new Error("Workflow follow-up can be removed only by its source or a verified human");
3271
2994
  }
3272
2995
 
3273
- private hasLiveLease(resourceId: string): boolean {
3274
- const row = this.state.connection
3275
- .prepare("SELECT expires_at AS expiresAt FROM leases WHERE resource_id = ?")
3276
- .get(resourceId);
3277
- return isLeaseExpiryRow(row) && row.expiresAt !== null && row.expiresAt > Date.now();
3278
- }
3279
-
3280
2996
  private nextFollowUpOrder(runId: string): number {
3281
2997
  const row = this.state.connection
3282
2998
  .prepare(
@@ -3309,55 +3025,51 @@ export class WorkflowRunStore {
3309
3025
 
3310
3026
  private transitionFollowUpsForRunState(
3311
3027
  state: WorkflowRunState,
3312
- event: WorkflowTraceEventDraft,
3028
+ _event: WorkflowTraceEventDraft,
3313
3029
  now: number,
3314
3030
  ): void {
3315
- if (state.status === "running" || state.status === "waiting") return;
3316
- const queue = this.followUpQueueRow(state.runId);
3317
- if (queue === undefined || queue.presentationState !== "none") return;
3318
- let presentationState: WorkflowPresentationState = "not-needed";
3319
- let followUpState: WorkflowFollowUpState = "cancelled";
3320
- let reasonHash: Buffer | null = null;
3321
- if (state.status === "completed") {
3322
- const required = event.payload.presentationRequired === true;
3323
- presentationState = required ? "pending" : "not-needed";
3324
- followUpState = required ? "pending_presentation" : "ready";
3325
- } else {
3326
- reasonHash = this.state.putText(
3327
- state.error ?? `Workflow ended with status ${state.status}`,
3328
- now,
3329
- );
3031
+ if (state.status === "running" || state.status === "waiting" || state.status === "completed") {
3032
+ return;
3330
3033
  }
3331
- this.state.connection
3332
- .prepare(
3333
- `UPDATE workflow_follow_up_queues
3334
- SET presentation_state = ?, presentation_updated_at = ?, updated_at = ?
3335
- WHERE run_id = ? AND presentation_state = 'none'`,
3336
- )
3337
- .run(presentationState, now, now, state.runId);
3338
- this.state.connection
3034
+ const rows = this.state.connection
3339
3035
  .prepare(
3340
- `UPDATE workflow_follow_ups
3341
- SET status = ?, reason_hash = ?, updated_at = ?
3342
- WHERE run_id = ? AND status IN ('queued', 'pending_presentation', 'ready')`,
3036
+ `${FOLLOW_UP_ROW_SELECT}
3037
+ WHERE f.run_id IN (
3038
+ WITH RECURSIVE ancestors(run_id, parent_run_id) AS (
3039
+ SELECT run_id, parent_run_id FROM runs WHERE run_id = ?
3040
+ UNION ALL
3041
+ SELECT parent.run_id, parent.parent_run_id
3042
+ FROM runs parent JOIN ancestors ON ancestors.parent_run_id = parent.run_id
3043
+ )
3044
+ SELECT run_id FROM ancestors
3045
+ ) AND f.status = 'queued'`,
3343
3046
  )
3344
- .run(followUpState, reasonHash, now, state.runId);
3345
- const revision = this.requireResourceRevision(queue.resourceId);
3346
- this.bumpResource(queue.resourceId, revision, now);
3347
- const payloadHash = this.state.putJson(
3348
- { runId: state.runId, state: presentationState, followUps: followUpState },
3349
- now,
3350
- );
3351
- insertGenericEvent(
3352
- this.state,
3353
- queue.resourceId,
3354
- revision + 1,
3355
- "follow-up.run-finished",
3356
- "system",
3357
- null,
3358
- payloadHash,
3359
- now,
3360
- );
3047
+ .all(state.runId)
3048
+ .filter(isFollowUpRow);
3049
+ for (const row of rows) {
3050
+ const reason = state.error ?? `Workflow ended with status ${state.status}`;
3051
+ const reasonHash = this.state.putText(reason, now);
3052
+ const update = this.state.connection
3053
+ .prepare(
3054
+ `UPDATE workflow_follow_ups
3055
+ SET status = 'cancelled', reason_hash = ?, updated_at = ?
3056
+ WHERE follow_up_id = ? AND status = 'queued'`,
3057
+ )
3058
+ .run(reasonHash, now, row.followUpId);
3059
+ if (update.changes !== 1) continue;
3060
+ this.workflowMessages.cancelPendingForSource(row.followUpId, "followUp", now);
3061
+ this.bumpResource(row.resourceId, row.revision, now);
3062
+ insertGenericEvent(
3063
+ this.state,
3064
+ row.resourceId,
3065
+ row.revision + 1,
3066
+ "follow-up.cancelled",
3067
+ "system",
3068
+ null,
3069
+ this.state.putJson({ runId: state.runId, status: state.status, reason }, now),
3070
+ now,
3071
+ );
3072
+ }
3361
3073
  }
3362
3074
 
3363
3075
  private syncNodeAttempts(
@@ -3554,7 +3266,7 @@ export class WorkflowRunStore {
3554
3266
  JOIN session_segments s ON s.segment_id = e.segment_id
3555
3267
  JOIN blobs b ON b.blob_hash = e.entry_hash
3556
3268
  WHERE s.run_id = ?
3557
- AND json_extract(CAST(b.content AS TEXT), '$.customType') = 'pi-workflows-agent-step'
3269
+ AND json_extract(CAST(b.content AS TEXT), '$.customType') = 'pi-workflows-step'
3558
3270
  AND json_extract(CAST(b.content AS TEXT), '$.details.contract.attemptId') = ?
3559
3271
  ORDER BY e.recorded_at DESC LIMIT 1`,
3560
3272
  )
@@ -3766,7 +3478,15 @@ export class WorkflowRunStore {
3766
3478
  ): WorkflowRunState {
3767
3479
  const sources = this.readRunSources(row.runId, snapshot);
3768
3480
  const carriedStepCount = this.carriedStepCount(row.runId);
3769
- const activeAttempt = row.status === "running" ? this.readActiveAttempt(row.runId) : undefined;
3481
+ const activeAttempt =
3482
+ row.status === "running" || row.status === "waiting"
3483
+ ? this.readActiveAttempt(row.runId)
3484
+ : undefined;
3485
+ const runningAttempt = row.status === "running" ? activeAttempt : undefined;
3486
+ const waitingNode =
3487
+ row.status === "waiting"
3488
+ ? (activeAttempt?.nodeId ?? projectionSteps.at(-1)?.nodeId)
3489
+ : undefined;
3770
3490
  const humanDecision = this.readHumanDecisionReceipt(row.runId);
3771
3491
  const outputs: Record<string, unknown> = {};
3772
3492
  const results: Record<string, WorkflowNodeResult> = {};
@@ -3808,7 +3528,7 @@ export class WorkflowRunStore {
3808
3528
  results,
3809
3529
  steps: visibleSteps,
3810
3530
  ...(visibleUpdates.length === 0 ? {} : { updates: visibleUpdates }),
3811
- ...(activeAttempt === undefined ? {} : { currentNode: activeAttempt.nodeId }),
3531
+ ...(runningAttempt === undefined ? {} : { currentNode: runningAttempt.nodeId }),
3812
3532
  ...(activeAttempt === undefined ? {} : { currentAttemptId: activeAttempt.attemptId }),
3813
3533
  ...(activeAttempt?.startedAt === null || activeAttempt === undefined
3814
3534
  ? {}
@@ -3829,9 +3549,7 @@ export class WorkflowRunStore {
3829
3549
  ...(row.statusDetail === null ? {} : { statusDetail: row.statusDetail }),
3830
3550
  ...(humanDecision === undefined ? {} : { humanDecision }),
3831
3551
  ...(row.paused === 0 ? {} : { paused: true }),
3832
- ...(row.status === "waiting" && projectionSteps.at(-1) !== undefined
3833
- ? { waitingOn: projectionSteps.at(-1)?.nodeId as string }
3834
- : {}),
3552
+ ...(waitingNode === undefined ? {} : { waitingOn: waitingNode }),
3835
3553
  ...(row.finalOutputHash === null
3836
3554
  ? {}
3837
3555
  : { finalOutput: this.state.readJson(row.finalOutputHash) }),
@@ -4876,32 +4594,12 @@ function assertActorType(value: string): ActorType {
4876
4594
  }
4877
4595
 
4878
4596
  function assertFollowUpState(value: string): WorkflowFollowUpState {
4879
- if (
4880
- value !== "queued" &&
4881
- value !== "pending_presentation" &&
4882
- value !== "ready" &&
4883
- value !== "sent" &&
4884
- value !== "removed" &&
4885
- value !== "cancelled"
4886
- ) {
4597
+ if (value !== "queued" && value !== "removed" && value !== "cancelled") {
4887
4598
  throw new Error(`Unknown workflow follow-up state: ${value}`);
4888
4599
  }
4889
4600
  return value;
4890
4601
  }
4891
4602
 
4892
- function assertPresentationState(value: string): WorkflowPresentationState {
4893
- if (
4894
- value !== "none" &&
4895
- value !== "not-needed" &&
4896
- value !== "pending" &&
4897
- value !== "settled" &&
4898
- value !== "unavailable"
4899
- ) {
4900
- throw new Error(`Unknown workflow presentation state: ${value}`);
4901
- }
4902
- return value;
4903
- }
4904
-
4905
4603
  function isSettingsScopeRow(value: unknown): value is SettingsScopeRow {
4906
4604
  return (
4907
4605
  isRecord(value) &&
@@ -4955,27 +4653,11 @@ function isSettingsChangeRow(value: unknown): value is SettingsChangeRow {
4955
4653
  );
4956
4654
  }
4957
4655
 
4958
- function isFollowUpQueueRow(value: unknown): value is FollowUpQueueRow {
4959
- return (
4960
- isRecord(value) &&
4961
- typeof value.runId === "string" &&
4962
- typeof value.resourceId === "string" &&
4963
- (typeof value.originSessionId === "string" || value.originSessionId === null) &&
4964
- typeof value.presentationState === "string" &&
4965
- (typeof value.presentationEntryId === "string" || value.presentationEntryId === null) &&
4966
- (typeof value.presentationAssistantEntryId === "string" ||
4967
- value.presentationAssistantEntryId === null) &&
4968
- (Buffer.isBuffer(value.presentationReasonHash) || value.presentationReasonHash === null) &&
4969
- typeof value.revision === "number"
4970
- );
4971
- }
4972
-
4973
4656
  function isFollowUpRow(value: unknown): value is FollowUpRow {
4974
4657
  return (
4975
4658
  isRecord(value) &&
4976
4659
  typeof value.followUpId === "string" &&
4977
4660
  typeof value.resourceId === "string" &&
4978
- typeof value.queueResourceId === "string" &&
4979
4661
  typeof value.runId === "string" &&
4980
4662
  typeof value.requestId === "string" &&
4981
4663
  typeof value.orderNumber === "number" &&
@@ -4985,11 +4667,9 @@ function isFollowUpRow(value: unknown): value is FollowUpRow {
4985
4667
  typeof value.sourceType === "string" &&
4986
4668
  Buffer.isBuffer(value.promptHash) &&
4987
4669
  typeof value.status === "string" &&
4988
- (typeof value.sessionEntryId === "string" || value.sessionEntryId === null) &&
4989
4670
  (Buffer.isBuffer(value.reasonHash) || value.reasonHash === null) &&
4990
4671
  typeof value.createdAt === "number" &&
4991
4672
  typeof value.updatedAt === "number" &&
4992
- (typeof value.sentAt === "number" || value.sentAt === null) &&
4993
4673
  typeof value.revision === "number"
4994
4674
  );
4995
4675
  }
@@ -4998,6 +4678,14 @@ function isParentRunRow(value: unknown): value is { parentRunId: string | null }
4998
4678
  return isRecord(value) && (typeof value.parentRunId === "string" || value.parentRunId === null);
4999
4679
  }
5000
4680
 
4681
+ function isRunLineageRow(value: unknown): value is { rootRunId: string; restartNumber: number } {
4682
+ return (
4683
+ isRecord(value) &&
4684
+ typeof value.rootRunId === "string" &&
4685
+ typeof value.restartNumber === "number"
4686
+ );
4687
+ }
4688
+
5001
4689
  function isStatusRow(value: unknown): value is { status: string } {
5002
4690
  return isRecord(value) && typeof value.status === "string";
5003
4691
  }
@@ -5010,10 +4698,6 @@ function isStepIndexRow(value: unknown): value is { stepIndex: number } {
5010
4698
  return isRecord(value) && typeof value.stepIndex === "number";
5011
4699
  }
5012
4700
 
5013
- function isLeaseExpiryRow(value: unknown): value is { expiresAt: number | null } {
5014
- return isRecord(value) && (typeof value.expiresAt === "number" || value.expiresAt === null);
5015
- }
5016
-
5017
4701
  function isNextOrderRow(value: unknown): value is { nextOrder: number } {
5018
4702
  return isRecord(value) && typeof value.nextOrder === "number";
5019
4703
  }