@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
@@ -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
 
@@ -146,6 +143,17 @@ export type WorkflowRunDisplayState = {
146
143
  error: string | null;
147
144
  };
148
145
 
146
+ export type WorkflowRunTerminalData = {
147
+ runId: string;
148
+ status: "completed" | "failed" | "timed_out" | "cancelled";
149
+ statusDetail: string | null;
150
+ input: JsonValue;
151
+ finalOutput: JsonValue | null;
152
+ error: string | null;
153
+ presentationInstructions: string;
154
+ restartNumber: number;
155
+ };
156
+
149
157
  export type WorkflowRunStoreOptions = {
150
158
  authorityProvider?: (runId: string) => RunWriteAuthority | undefined;
151
159
  /** Host-owned projection updates that must commit with each run snapshot. */
@@ -156,6 +164,8 @@ export type WorkflowRunStoreOptions = {
156
164
  database: StateDatabase;
157
165
  now: number;
158
166
  }) => void;
167
+ /** The global host may record an attested Pi session after the worker lease ends. */
168
+ allowHostSessionRecording?: boolean;
159
169
  state?: StateDatabase;
160
170
  readOnly?: boolean;
161
171
  };
@@ -352,21 +362,9 @@ type SettingsChangeRow = {
352
362
  acceptedAt: number;
353
363
  };
354
364
 
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
365
  type FollowUpRow = {
367
366
  followUpId: string;
368
367
  resourceId: string;
369
- queueResourceId: string;
370
368
  runId: string;
371
369
  requestId: string;
372
370
  orderNumber: number;
@@ -376,11 +374,9 @@ type FollowUpRow = {
376
374
  sourceType: string;
377
375
  promptHash: Buffer;
378
376
  status: string;
379
- sessionEntryId: string | null;
380
377
  reasonHash: Buffer | null;
381
378
  createdAt: number;
382
379
  updatedAt: number;
383
- sentAt: number | null;
384
380
  revision: number;
385
381
  };
386
382
 
@@ -503,13 +499,16 @@ export class WorkflowRunStore {
503
499
  | ((runId: string) => RunWriteAuthority | undefined)
504
500
  | undefined;
505
501
  private readonly snapshotLifecycle: WorkflowRunStoreOptions["snapshotLifecycle"];
502
+ private readonly allowHostSessionRecording: boolean;
506
503
  private readonly contexts = new Map<string, RunContext>();
507
504
  private readonly ownsState: boolean;
508
505
  private readonly mutations: StateMutationStore;
506
+ private readonly workflowMessages: WorkflowMessageStore;
509
507
 
510
508
  constructor(databasePath: string = workflowStatePath(), options: WorkflowRunStoreOptions = {}) {
511
509
  this.authorityProvider = options.authorityProvider;
512
510
  this.snapshotLifecycle = options.snapshotLifecycle;
511
+ this.allowHostSessionRecording = options.allowHostSessionRecording === true;
513
512
  this.ownsState = options.state === undefined;
514
513
  this.state =
515
514
  options.state ??
@@ -520,6 +519,7 @@ export class WorkflowRunStore {
520
519
  });
521
520
  this.databasePath = this.state.filePath;
522
521
  this.mutations = new StateMutationStore(this.state);
522
+ this.workflowMessages = new WorkflowMessageStore(this.state);
523
523
  }
524
524
 
525
525
  close(): void {
@@ -912,6 +912,18 @@ export class WorkflowRunStore {
912
912
  return;
913
913
  }
914
914
  const definitionHash = this.state.putJson(snapshot, now);
915
+ const parentLineage =
916
+ state.parentRunId === undefined
917
+ ? undefined
918
+ : this.state.connection
919
+ .prepare(
920
+ `SELECT root_run_id AS rootRunId, restart_number AS restartNumber
921
+ FROM runs WHERE run_id = ?`,
922
+ )
923
+ .get(state.parentRunId);
924
+ if (state.parentRunId !== undefined && !isRunLineageRow(parentLineage)) {
925
+ throw new Error(`Workflow parent run is missing: ${state.parentRunId}`);
926
+ }
915
927
  this.state.connection
916
928
  .prepare(
917
929
  `INSERT INTO workflow_definitions(
@@ -940,16 +952,20 @@ export class WorkflowRunStore {
940
952
  this.state.connection
941
953
  .prepare(
942
954
  `INSERT INTO runs(
943
- run_id, resource_id, project_id, parent_run_id, definition_digest,
955
+ run_id, resource_id, project_id, parent_run_id, root_run_id, lineage_kind,
956
+ restart_number, parent_terminal_fingerprint, definition_digest,
944
957
  workflow_ref, launch_options_hash, title, status, paused,
945
958
  status_detail, input_hash, final_output_hash, error_hash,
946
959
  created_at, updated_at, finished_at
947
- ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
960
+ ) VALUES (?, ?, NULL, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
948
961
  )
949
962
  .run(
950
963
  state.runId,
951
964
  resourceId,
952
965
  state.parentRunId ?? null,
966
+ isRunLineageRow(parentLineage) ? parentLineage.rootRunId : state.runId,
967
+ state.parentRunId === undefined ? null : "continuation",
968
+ isRunLineageRow(parentLineage) ? parentLineage.restartNumber : 0,
953
969
  definitionDigest,
954
970
  state.workflowName,
955
971
  launchOptionsHash,
@@ -1287,87 +1303,78 @@ export class WorkflowRunStore {
1287
1303
  if (Buffer.byteLength(request.prompt, "utf8") > 64 * 1024) {
1288
1304
  throw new Error("Workflow follow-up prompt cannot exceed 65536 bytes");
1289
1305
  }
1290
- for (let attempt = 0; attempt < 8; attempt += 1) {
1306
+ return this.state.transaction(() => {
1291
1307
  const prior = this.followUpRowByRequest(request.runId, request.requestId);
1292
1308
  if (prior !== undefined) {
1293
1309
  this.assertSameFollowUp(prior, request);
1294
1310
  return { followUp: this.followUpRecord(prior), adopted: true };
1295
1311
  }
1296
1312
  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;
1313
+ if (this.originSessionId(request.runId) !== request.targetSessionId) {
1314
+ throw new Error("Workflow follow-ups must target the run's origin Pi session");
1368
1315
  }
1369
- }
1370
- throw new StaleResourceError("Workflow follow-up queue kept changing; retry the request");
1316
+ const now = Date.now();
1317
+ const order = this.nextFollowUpOrder(request.runId);
1318
+ const followUpId = followUpIdFor(request.runId, request.requestId);
1319
+ const resourceId = this.mutations.ensureResource("follow_up", followUpId, now);
1320
+ const promptHash = this.state.putText(request.prompt, now);
1321
+ this.state.connection
1322
+ .prepare(
1323
+ `INSERT INTO workflow_follow_ups(
1324
+ follow_up_id, resource_id, run_id, request_id, order_number,
1325
+ target_session_id, actor_type, actor_id, source_type, prompt_hash,
1326
+ status, created_at, updated_at
1327
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?)`,
1328
+ )
1329
+ .run(
1330
+ followUpId,
1331
+ resourceId,
1332
+ request.runId,
1333
+ request.requestId,
1334
+ order,
1335
+ request.targetSessionId,
1336
+ request.actor.type,
1337
+ request.actor.id ?? null,
1338
+ request.source,
1339
+ promptHash,
1340
+ now,
1341
+ now,
1342
+ );
1343
+ const workflowMessageId = workflowMessageIdFor("followUp", followUpId, "initial");
1344
+ this.workflowMessages.create({
1345
+ workflowMessageId,
1346
+ runId: request.runId,
1347
+ targetSessionId: request.targetSessionId,
1348
+ kind: "followUp",
1349
+ sourceId: followUpId,
1350
+ idempotencyKey: "initial",
1351
+ content: followUpWorkflowMessageContent({
1352
+ workflowMessageId,
1353
+ followUpId,
1354
+ runId: request.runId,
1355
+ prompt: request.prompt,
1356
+ }),
1357
+ now,
1358
+ });
1359
+ this.bumpResource(resourceId, 0, now);
1360
+ insertGenericEvent(
1361
+ this.state,
1362
+ resourceId,
1363
+ 1,
1364
+ "follow-up.queued",
1365
+ request.actor.type,
1366
+ request.actor.id ?? null,
1367
+ this.state.putJson({ runId: request.runId, requestId: request.requestId }, now),
1368
+ now,
1369
+ );
1370
+ recordViewerDeltas(
1371
+ this.state,
1372
+ request.runId,
1373
+ this.viewerInspectorTargets(request.runId),
1374
+ now,
1375
+ );
1376
+ return { followUp: this.followUpRecord(this.requireFollowUpRow(followUpId)), adopted: false };
1377
+ });
1371
1378
  }
1372
1379
 
1373
1380
  removeFollowUp(request: WorkflowRemoveFollowUpRequest): WorkflowFollowUpRecord {
@@ -1377,12 +1384,13 @@ export class WorkflowRunStore {
1377
1384
  throw new Error(`Workflow follow-up is not part of run ${request.runId}`);
1378
1385
  }
1379
1386
  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}`);
1387
+ if (row.status === "cancelled") {
1388
+ throw new Error("A cancelled workflow follow-up cannot be removed");
1382
1389
  }
1383
1390
  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");
1391
+ const message = this.workflowMessages.latestForSource("followUp", request.followUpId);
1392
+ if (message?.status === "sent") {
1393
+ throw new Error("A sent workflow follow-up cannot be removed");
1386
1394
  }
1387
1395
  const reason = "Removed before delivery";
1388
1396
  this.mutations.mutate(
@@ -1399,12 +1407,13 @@ export class WorkflowRunStore {
1399
1407
  .prepare(
1400
1408
  `UPDATE workflow_follow_ups
1401
1409
  SET status = 'removed', reason_hash = ?, updated_at = ?
1402
- WHERE follow_up_id = ? AND status IN ('queued', 'pending_presentation', 'ready')`,
1410
+ WHERE follow_up_id = ? AND status = 'queued'`,
1403
1411
  )
1404
1412
  .run(reasonHash, now, request.followUpId);
1405
1413
  if (update.changes !== 1) {
1406
1414
  throw new StaleResourceError("Workflow follow-up changed before removal");
1407
1415
  }
1416
+ this.workflowMessages.cancelPendingForSource(request.followUpId, "followUp", now);
1408
1417
  recordViewerDeltas(
1409
1418
  this.state,
1410
1419
  request.runId,
@@ -1420,10 +1429,8 @@ export class WorkflowRunStore {
1420
1429
  originSessionId(runId: string): string | undefined {
1421
1430
  const row = this.state.connection
1422
1431
  .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
1432
+ `SELECT b.origin_session_id AS originSessionId
1433
+ FROM runs r LEFT JOIN run_bindings b ON b.run_id = r.run_id
1427
1434
  WHERE r.run_id = ?`,
1428
1435
  )
1429
1436
  .get(runId);
@@ -1433,36 +1440,22 @@ export class WorkflowRunStore {
1433
1440
  }
1434
1441
 
1435
1442
  readFollowUpQueue(runId: string): WorkflowFollowUpQueueRecord | undefined {
1436
- const queue = this.followUpQueueRow(runId);
1437
- if (queue === undefined) return undefined;
1443
+ const originSessionId = this.originSessionId(runId);
1444
+ if (originSessionId === undefined) return undefined;
1438
1445
  const followUps = this.state.connection
1439
1446
  .prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? ORDER BY f.order_number`)
1440
1447
  .all(runId)
1441
1448
  .filter(isFollowUpRow)
1442
1449
  .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
- };
1450
+ return { runId, originSessionId, followUps };
1458
1451
  }
1459
1452
 
1460
1453
  private readFollowUpQueueRange(
1461
1454
  runId: string,
1462
1455
  range: WorkflowRunViewRange,
1463
1456
  ): WorkflowFollowUpQueueRecord | null {
1464
- const queue = this.followUpQueueRow(runId);
1465
- if (queue === undefined) return null;
1457
+ const originSessionId = this.originSessionId(runId);
1458
+ if (originSessionId === undefined) return null;
1466
1459
  const followUps = this.state.connection
1467
1460
  .prepare(
1468
1461
  `${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? ORDER BY f.order_number LIMIT ? OFFSET ?`,
@@ -1470,195 +1463,7 @@ export class WorkflowRunStore {
1470
1463
  .all(runId, range.limit, range.start)
1471
1464
  .filter(isFollowUpRow)
1472
1465
  .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);
1466
+ return { runId, originSessionId, followUps };
1662
1467
  }
1663
1468
 
1664
1469
  async prepareRunResume(runId: string): Promise<LoadedWorkflowRun> {
@@ -1670,7 +1475,6 @@ export class WorkflowRunStore {
1670
1475
  this.verifySettingsProjections(runId);
1671
1476
  const revision = this.resourceRevision(runId);
1672
1477
  this.contexts.set(runId, { revision, lock: Promise.resolve() });
1673
- this.finalizeRecordingCaptures(runId, "Workflow host stopped before the run finished");
1674
1478
  this.state.transaction(() => {
1675
1479
  const row = this.requireRunRow(runId);
1676
1480
  const context = this.contextFor(runId);
@@ -1900,7 +1704,7 @@ export class WorkflowRunStore {
1900
1704
  this.state.transaction(() => {
1901
1705
  const run = this.requireRunRow(runId);
1902
1706
  const context = this.contextFor(runId);
1903
- this.assertWriteAuthority(run, context.revision);
1707
+ this.assertSessionWriteAuthority(runId);
1904
1708
  const now = Date.parse(binding.boundAt);
1905
1709
  const resourceId = resourceIdFor("session", segmentId);
1906
1710
  const bindingHash = this.state.putJson(binding, now);
@@ -1947,13 +1751,6 @@ export class WorkflowRunStore {
1947
1751
  ON CONFLICT(run_id) DO NOTHING`,
1948
1752
  )
1949
1753
  .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
1754
  const revision = context.revision + 1;
1958
1755
  const at = new Date(now).toISOString();
1959
1756
  const traceEvent: WorkflowTraceEvent = {
@@ -2396,6 +2193,59 @@ export class WorkflowRunStore {
2396
2193
  };
2397
2194
  }
2398
2195
 
2196
+ readRunInput(runId: string): JsonValue | null {
2197
+ const row = this.readRunRow(runId);
2198
+ return row === undefined ? null : this.state.readJson(row.inputHash);
2199
+ }
2200
+
2201
+ readRunFinalOutput(runId: string): JsonValue | null {
2202
+ const row = this.readRunRow(runId);
2203
+ return row?.finalOutputHash == null ? null : this.state.readJson(row.finalOutputHash);
2204
+ }
2205
+
2206
+ readRunError(runId: string): string | null {
2207
+ const row = this.readRunRow(runId);
2208
+ return row?.errorHash == null ? null : this.readText(row.errorHash);
2209
+ }
2210
+
2211
+ readPresentationInstructions(runId: string): string {
2212
+ const row = this.state.connection
2213
+ .prepare("SELECT presentation_prompt_hash AS hash FROM runs WHERE run_id = ?")
2214
+ .get(runId) as { hash?: Buffer | null } | undefined;
2215
+ if (row?.hash == null) {
2216
+ return "Explain the final workflow result to the user in a normal response.";
2217
+ }
2218
+ return this.readText(row.hash);
2219
+ }
2220
+
2221
+ readTerminalData(runId: string): WorkflowRunTerminalData | null {
2222
+ const row = this.state.connection
2223
+ .prepare(
2224
+ `SELECT status, status_detail AS statusDetail, restart_number AS restartNumber
2225
+ FROM runs WHERE run_id = ?`,
2226
+ )
2227
+ .get(runId) as
2228
+ | { status?: unknown; statusDetail?: unknown; restartNumber?: unknown }
2229
+ | undefined;
2230
+ if (
2231
+ row === undefined ||
2232
+ !["completed", "failed", "timed_out", "cancelled"].includes(String(row.status)) ||
2233
+ typeof row.restartNumber !== "number"
2234
+ ) {
2235
+ return null;
2236
+ }
2237
+ return {
2238
+ runId,
2239
+ status: row.status as WorkflowRunTerminalData["status"],
2240
+ statusDetail: typeof row.statusDetail === "string" ? row.statusDetail : null,
2241
+ input: this.readRunInput(runId) as JsonValue,
2242
+ finalOutput: this.readRunFinalOutput(runId),
2243
+ error: this.readRunError(runId),
2244
+ presentationInstructions: this.readPresentationInstructions(runId),
2245
+ restartNumber: row.restartNumber,
2246
+ };
2247
+ }
2248
+
2399
2249
  readRun(runId: string, options: ReadWorkflowRunOptions = {}): LoadedWorkflowRun | null {
2400
2250
  const row = this.readRunRow(runId);
2401
2251
  if (row === undefined) return null;
@@ -2679,6 +2529,7 @@ export class WorkflowRunStore {
2679
2529
 
2680
2530
  private assertSessionWriteAuthority(runId: string): void {
2681
2531
  const run = this.requireRunRow(runId);
2532
+ if (this.allowHostSessionRecording) return;
2682
2533
  this.assertWriteAuthority(run, this.contextFor(runId).revision);
2683
2534
  }
2684
2535
 
@@ -2902,10 +2753,8 @@ export class WorkflowRunStore {
2902
2753
  order: row.orderNumber,
2903
2754
  state: row.status,
2904
2755
  source: row.sourceType,
2905
- sessionEntryId: row.sessionEntryId,
2906
2756
  }));
2907
2757
  const followUpStart = Math.max(0, followUps.length - VIEWER_PAGE_SIZE);
2908
- const queue = this.requireFollowUpQueueRow(runId);
2909
2758
  return [
2910
2759
  {
2911
2760
  targetType: "inspector",
@@ -2927,12 +2776,7 @@ export class WorkflowRunStore {
2927
2776
  {
2928
2777
  op: "replace",
2929
2778
  path: "/followUpQueue",
2930
- value: parseJson(
2931
- canonicalJson({
2932
- presentationState: queue.presentationState,
2933
- items: followUps.slice(followUpStart),
2934
- }),
2935
- ),
2779
+ value: parseJson(canonicalJson({ items: followUps.slice(followUpStart) })),
2936
2780
  },
2937
2781
  { op: "replace", path: "/followUpStart", value: followUpStart },
2938
2782
  { op: "replace", path: "/followUpTotal", value: followUps.length },
@@ -2946,25 +2790,7 @@ export class WorkflowRunStore {
2946
2790
  initialSettings: InitialWorkflowSettingsScope[],
2947
2791
  now: number,
2948
2792
  ): void {
2949
- const childQueueResourceId = this.ensureFollowUpQueue(state.runId, now);
2950
2793
  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
2794
  this.state.connection
2969
2795
  .prepare(
2970
2796
  `UPDATE workflow_settings SET active_run_id = ?, updated_at = ? WHERE active_run_id = ?`,
@@ -3004,19 +2830,6 @@ export class WorkflowRunStore {
3004
2830
  }
3005
2831
  }
3006
2832
 
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
2833
  private logicalOriginRunId(runId: string): string {
3021
2834
  let current = runId;
3022
2835
  for (let depth = 0; depth < 100; depth += 1) {
@@ -3174,30 +2987,6 @@ export class WorkflowRunStore {
3174
2987
  }
3175
2988
  }
3176
2989
 
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
2990
  private followUpRowByRequest(runId: string, requestId: string): FollowUpRow | undefined {
3202
2991
  const row = this.state.connection
3203
2992
  .prepare(`${FOLLOW_UP_ROW_SELECT} WHERE f.run_id = ? AND f.request_id = ?`)
@@ -3232,11 +3021,9 @@ export class WorkflowRunStore {
3232
3021
  source: row.sourceType,
3233
3022
  prompt: this.readText(row.promptHash),
3234
3023
  state: assertFollowUpState(row.status),
3235
- ...(row.sessionEntryId !== null ? { sessionEntryId: row.sessionEntryId } : {}),
3236
3024
  ...(row.reasonHash !== null ? { reason: this.readText(row.reasonHash) } : {}),
3237
3025
  createdAt: new Date(row.createdAt).toISOString(),
3238
3026
  updatedAt: new Date(row.updatedAt).toISOString(),
3239
- ...(row.sentAt !== null ? { sentAt: new Date(row.sentAt).toISOString() } : {}),
3240
3027
  };
3241
3028
  }
3242
3029
 
@@ -3270,13 +3057,6 @@ export class WorkflowRunStore {
3270
3057
  throw new Error("Workflow follow-up can be removed only by its source or a verified human");
3271
3058
  }
3272
3059
 
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
3060
  private nextFollowUpOrder(runId: string): number {
3281
3061
  const row = this.state.connection
3282
3062
  .prepare(
@@ -3309,55 +3089,51 @@ export class WorkflowRunStore {
3309
3089
 
3310
3090
  private transitionFollowUpsForRunState(
3311
3091
  state: WorkflowRunState,
3312
- event: WorkflowTraceEventDraft,
3092
+ _event: WorkflowTraceEventDraft,
3313
3093
  now: number,
3314
3094
  ): 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
- );
3095
+ if (state.status === "running" || state.status === "waiting" || state.status === "completed") {
3096
+ return;
3330
3097
  }
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
3098
+ const rows = this.state.connection
3339
3099
  .prepare(
3340
- `UPDATE workflow_follow_ups
3341
- SET status = ?, reason_hash = ?, updated_at = ?
3342
- WHERE run_id = ? AND status IN ('queued', 'pending_presentation', 'ready')`,
3100
+ `${FOLLOW_UP_ROW_SELECT}
3101
+ WHERE f.run_id IN (
3102
+ WITH RECURSIVE ancestors(run_id, parent_run_id) AS (
3103
+ SELECT run_id, parent_run_id FROM runs WHERE run_id = ?
3104
+ UNION ALL
3105
+ SELECT parent.run_id, parent.parent_run_id
3106
+ FROM runs parent JOIN ancestors ON ancestors.parent_run_id = parent.run_id
3107
+ )
3108
+ SELECT run_id FROM ancestors
3109
+ ) AND f.status = 'queued'`,
3343
3110
  )
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
- );
3111
+ .all(state.runId)
3112
+ .filter(isFollowUpRow);
3113
+ for (const row of rows) {
3114
+ const reason = state.error ?? `Workflow ended with status ${state.status}`;
3115
+ const reasonHash = this.state.putText(reason, now);
3116
+ const update = this.state.connection
3117
+ .prepare(
3118
+ `UPDATE workflow_follow_ups
3119
+ SET status = 'cancelled', reason_hash = ?, updated_at = ?
3120
+ WHERE follow_up_id = ? AND status = 'queued'`,
3121
+ )
3122
+ .run(reasonHash, now, row.followUpId);
3123
+ if (update.changes !== 1) continue;
3124
+ this.workflowMessages.cancelPendingForSource(row.followUpId, "followUp", now);
3125
+ this.bumpResource(row.resourceId, row.revision, now);
3126
+ insertGenericEvent(
3127
+ this.state,
3128
+ row.resourceId,
3129
+ row.revision + 1,
3130
+ "follow-up.cancelled",
3131
+ "system",
3132
+ null,
3133
+ this.state.putJson({ runId: state.runId, status: state.status, reason }, now),
3134
+ now,
3135
+ );
3136
+ }
3361
3137
  }
3362
3138
 
3363
3139
  private syncNodeAttempts(
@@ -3554,7 +3330,7 @@ export class WorkflowRunStore {
3554
3330
  JOIN session_segments s ON s.segment_id = e.segment_id
3555
3331
  JOIN blobs b ON b.blob_hash = e.entry_hash
3556
3332
  WHERE s.run_id = ?
3557
- AND json_extract(CAST(b.content AS TEXT), '$.customType') = 'pi-workflows-agent-step'
3333
+ AND json_extract(CAST(b.content AS TEXT), '$.customType') = 'pi-workflows-step'
3558
3334
  AND json_extract(CAST(b.content AS TEXT), '$.details.contract.attemptId') = ?
3559
3335
  ORDER BY e.recorded_at DESC LIMIT 1`,
3560
3336
  )
@@ -3766,7 +3542,15 @@ export class WorkflowRunStore {
3766
3542
  ): WorkflowRunState {
3767
3543
  const sources = this.readRunSources(row.runId, snapshot);
3768
3544
  const carriedStepCount = this.carriedStepCount(row.runId);
3769
- const activeAttempt = row.status === "running" ? this.readActiveAttempt(row.runId) : undefined;
3545
+ const activeAttempt =
3546
+ row.status === "running" || row.status === "waiting"
3547
+ ? this.readActiveAttempt(row.runId)
3548
+ : undefined;
3549
+ const runningAttempt = row.status === "running" ? activeAttempt : undefined;
3550
+ const waitingNode =
3551
+ row.status === "waiting"
3552
+ ? (activeAttempt?.nodeId ?? projectionSteps.at(-1)?.nodeId)
3553
+ : undefined;
3770
3554
  const humanDecision = this.readHumanDecisionReceipt(row.runId);
3771
3555
  const outputs: Record<string, unknown> = {};
3772
3556
  const results: Record<string, WorkflowNodeResult> = {};
@@ -3808,7 +3592,7 @@ export class WorkflowRunStore {
3808
3592
  results,
3809
3593
  steps: visibleSteps,
3810
3594
  ...(visibleUpdates.length === 0 ? {} : { updates: visibleUpdates }),
3811
- ...(activeAttempt === undefined ? {} : { currentNode: activeAttempt.nodeId }),
3595
+ ...(runningAttempt === undefined ? {} : { currentNode: runningAttempt.nodeId }),
3812
3596
  ...(activeAttempt === undefined ? {} : { currentAttemptId: activeAttempt.attemptId }),
3813
3597
  ...(activeAttempt?.startedAt === null || activeAttempt === undefined
3814
3598
  ? {}
@@ -3829,9 +3613,7 @@ export class WorkflowRunStore {
3829
3613
  ...(row.statusDetail === null ? {} : { statusDetail: row.statusDetail }),
3830
3614
  ...(humanDecision === undefined ? {} : { humanDecision }),
3831
3615
  ...(row.paused === 0 ? {} : { paused: true }),
3832
- ...(row.status === "waiting" && projectionSteps.at(-1) !== undefined
3833
- ? { waitingOn: projectionSteps.at(-1)?.nodeId as string }
3834
- : {}),
3616
+ ...(waitingNode === undefined ? {} : { waitingOn: waitingNode }),
3835
3617
  ...(row.finalOutputHash === null
3836
3618
  ? {}
3837
3619
  : { finalOutput: this.state.readJson(row.finalOutputHash) }),
@@ -4876,32 +4658,12 @@ function assertActorType(value: string): ActorType {
4876
4658
  }
4877
4659
 
4878
4660
  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
- ) {
4661
+ if (value !== "queued" && value !== "removed" && value !== "cancelled") {
4887
4662
  throw new Error(`Unknown workflow follow-up state: ${value}`);
4888
4663
  }
4889
4664
  return value;
4890
4665
  }
4891
4666
 
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
4667
  function isSettingsScopeRow(value: unknown): value is SettingsScopeRow {
4906
4668
  return (
4907
4669
  isRecord(value) &&
@@ -4955,27 +4717,11 @@ function isSettingsChangeRow(value: unknown): value is SettingsChangeRow {
4955
4717
  );
4956
4718
  }
4957
4719
 
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
4720
  function isFollowUpRow(value: unknown): value is FollowUpRow {
4974
4721
  return (
4975
4722
  isRecord(value) &&
4976
4723
  typeof value.followUpId === "string" &&
4977
4724
  typeof value.resourceId === "string" &&
4978
- typeof value.queueResourceId === "string" &&
4979
4725
  typeof value.runId === "string" &&
4980
4726
  typeof value.requestId === "string" &&
4981
4727
  typeof value.orderNumber === "number" &&
@@ -4985,11 +4731,9 @@ function isFollowUpRow(value: unknown): value is FollowUpRow {
4985
4731
  typeof value.sourceType === "string" &&
4986
4732
  Buffer.isBuffer(value.promptHash) &&
4987
4733
  typeof value.status === "string" &&
4988
- (typeof value.sessionEntryId === "string" || value.sessionEntryId === null) &&
4989
4734
  (Buffer.isBuffer(value.reasonHash) || value.reasonHash === null) &&
4990
4735
  typeof value.createdAt === "number" &&
4991
4736
  typeof value.updatedAt === "number" &&
4992
- (typeof value.sentAt === "number" || value.sentAt === null) &&
4993
4737
  typeof value.revision === "number"
4994
4738
  );
4995
4739
  }
@@ -4998,6 +4742,14 @@ function isParentRunRow(value: unknown): value is { parentRunId: string | null }
4998
4742
  return isRecord(value) && (typeof value.parentRunId === "string" || value.parentRunId === null);
4999
4743
  }
5000
4744
 
4745
+ function isRunLineageRow(value: unknown): value is { rootRunId: string; restartNumber: number } {
4746
+ return (
4747
+ isRecord(value) &&
4748
+ typeof value.rootRunId === "string" &&
4749
+ typeof value.restartNumber === "number"
4750
+ );
4751
+ }
4752
+
5001
4753
  function isStatusRow(value: unknown): value is { status: string } {
5002
4754
  return isRecord(value) && typeof value.status === "string";
5003
4755
  }
@@ -5010,10 +4762,6 @@ function isStepIndexRow(value: unknown): value is { stepIndex: number } {
5010
4762
  return isRecord(value) && typeof value.stepIndex === "number";
5011
4763
  }
5012
4764
 
5013
- function isLeaseExpiryRow(value: unknown): value is { expiresAt: number | null } {
5014
- return isRecord(value) && (typeof value.expiresAt === "number" || value.expiresAt === null);
5015
- }
5016
-
5017
4765
  function isNextOrderRow(value: unknown): value is { nextOrder: number } {
5018
4766
  return isRecord(value) && typeof value.nextOrder === "number";
5019
4767
  }