@osolmaz/pi-workflows 0.15.2 → 0.16.0

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 (124) hide show
  1. package/README.md +6 -6
  2. package/dist/client/activity.d.ts +2 -0
  3. package/dist/client/activity.js +6 -0
  4. package/dist/client/activity.js.map +1 -0
  5. package/dist/client/client.d.ts +101 -0
  6. package/dist/client/client.js +733 -0
  7. package/dist/client/client.js.map +1 -0
  8. package/dist/client/index.d.ts +3 -0
  9. package/dist/client/index.js +3 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/client/materialize.d.ts +7 -0
  12. package/dist/client/materialize.js +177 -0
  13. package/dist/client/materialize.js.map +1 -0
  14. package/dist/client/protocol.d.ts +60 -0
  15. package/dist/client/protocol.js +269 -0
  16. package/dist/client/protocol.js.map +1 -0
  17. package/dist/client/resolver.d.ts +23 -0
  18. package/dist/client/resolver.js +2 -0
  19. package/dist/client/resolver.js.map +1 -0
  20. package/dist/client/view.d.ts +118 -0
  21. package/dist/client/view.js +3 -0
  22. package/dist/client/view.js.map +1 -0
  23. package/dist/controllers/sqlite.d.ts +64 -0
  24. package/dist/controllers/sqlite.js +219 -3
  25. package/dist/controllers/sqlite.js.map +1 -1
  26. package/dist/extension/index.d.ts +1 -0
  27. package/dist/extension/index.js +384 -156
  28. package/dist/extension/index.js.map +1 -1
  29. package/dist/extension/session-delivery.d.ts +6 -0
  30. package/dist/extension/session-delivery.js +80 -25
  31. package/dist/extension/session-delivery.js.map +1 -1
  32. package/dist/extension/session-view.d.ts +21 -0
  33. package/dist/extension/session-view.js +127 -0
  34. package/dist/extension/session-view.js.map +1 -0
  35. package/dist/extension/widget.d.ts +2 -1
  36. package/dist/extension/widget.js +15 -7
  37. package/dist/extension/widget.js.map +1 -1
  38. package/dist/host/child-worker-supervisor.js +1 -1
  39. package/dist/host/child-worker-supervisor.js.map +1 -1
  40. package/dist/host/resolver-entry.d.ts +2 -23
  41. package/dist/host/resolver-entry.js +1 -1
  42. package/dist/host/resolver-entry.js.map +1 -1
  43. package/dist/host/runner.d.ts +12 -0
  44. package/dist/host/runner.js +565 -43
  45. package/dist/host/runner.js.map +1 -1
  46. package/dist/host/state.d.ts +8 -3
  47. package/dist/host/state.js +59 -27
  48. package/dist/host/state.js.map +1 -1
  49. package/dist/host/view.d.ts +73 -0
  50. package/dist/host/view.js +871 -0
  51. package/dist/host/view.js.map +1 -0
  52. package/dist/host/worker-protocol.js +1 -1
  53. package/dist/host/worker-protocol.js.map +1 -1
  54. package/dist/state/database.d.ts +1 -0
  55. package/dist/state/database.js +15 -0
  56. package/dist/state/database.js.map +1 -1
  57. package/dist/state/prune.d.ts +3 -1
  58. package/dist/state/prune.js +6 -8
  59. package/dist/state/prune.js.map +1 -1
  60. package/dist/state/schema.js +12 -1
  61. package/dist/state/schema.js.map +1 -1
  62. package/dist/viewer/backup.d.ts +2 -0
  63. package/dist/viewer/backup.js +28 -0
  64. package/dist/viewer/backup.js.map +1 -0
  65. package/dist/viewer/cli.d.ts +4 -0
  66. package/dist/viewer/cli.js +150 -170
  67. package/dist/viewer/cli.js.map +1 -1
  68. package/dist/viewer/tui.d.ts +5 -7
  69. package/dist/viewer/tui.js +188 -108
  70. package/dist/viewer/tui.js.map +1 -1
  71. package/dist/workflows/store.d.ts +62 -1
  72. package/dist/workflows/store.js +350 -44
  73. package/dist/workflows/store.js.map +1 -1
  74. package/docs/2026-09-01-restore-session-delivery-controls-plan.md +139 -0
  75. package/docs/2026-09-01-unified-workflow-client-plan.md +381 -0
  76. package/docs/2026-09-02-installed-live-e2e-plan.md +225 -0
  77. package/docs/SQLITE_STATE.md +13 -11
  78. package/docs/WORKFLOW_HOST.md +81 -64
  79. package/docs/WORKFLOW_STEP_MESSAGES.md +4 -4
  80. package/docs/development.md +2 -1
  81. package/docs/live-replay-protocol.md +70 -132
  82. package/docs/tui-viewer.md +10 -14
  83. package/docs/workflows.md +56 -3
  84. package/herdr-plugin.toml +1 -1
  85. package/package.json +9 -3
  86. package/protocol/client.v1.schema.json +137 -0
  87. package/protocol/fixtures/client-v1.json +23 -0
  88. package/src/client/activity.ts +6 -0
  89. package/src/client/client.ts +935 -0
  90. package/src/client/index.ts +24 -0
  91. package/src/client/materialize.ts +228 -0
  92. package/src/client/protocol.ts +327 -0
  93. package/src/client/resolver.ts +26 -0
  94. package/src/client/view.ts +138 -0
  95. package/src/controllers/sqlite.ts +342 -3
  96. package/src/extension/index.ts +482 -171
  97. package/src/extension/session-delivery.ts +88 -25
  98. package/src/extension/session-view.ts +154 -0
  99. package/src/extension/widget.ts +18 -9
  100. package/src/host/child-worker-supervisor.ts +1 -1
  101. package/src/host/resolver-entry.ts +11 -26
  102. package/src/host/runner.ts +749 -75
  103. package/src/host/state.ts +82 -44
  104. package/src/host/view.ts +1084 -0
  105. package/src/host/worker-protocol.ts +1 -1
  106. package/src/state/database.ts +13 -0
  107. package/src/state/prune.ts +11 -11
  108. package/src/state/schema.ts +12 -1
  109. package/src/viewer/backup.ts +29 -0
  110. package/src/viewer/cli.ts +171 -185
  111. package/src/viewer/tui.ts +196 -124
  112. package/src/workflows/store.ts +500 -45
  113. package/dist/host/client.d.ts +0 -48
  114. package/dist/host/client.js +0 -216
  115. package/dist/host/client.js.map +0 -1
  116. package/dist/host/protocol.d.ts +0 -38
  117. package/dist/host/protocol.js +0 -156
  118. package/dist/host/protocol.js.map +0 -1
  119. package/dist/viewer/watch.d.ts +0 -6
  120. package/dist/viewer/watch.js +0 -46
  121. package/dist/viewer/watch.js.map +0 -1
  122. package/src/host/client.ts +0 -293
  123. package/src/host/protocol.ts +0 -196
  124. package/src/viewer/watch.ts +0 -51
@@ -0,0 +1,138 @@
1
+ import type { JsonValue } from "../state/json.js";
2
+
3
+ export const RUN_VIEW_SCHEMA = "pi-workflows.run-view.v1" as const;
4
+ export const SESSION_VIEW_SCHEMA = "pi-workflows.session-view.v1" as const;
5
+
6
+ export type WorkflowDisplayStatus =
7
+ | "queued"
8
+ | "running"
9
+ | "waiting"
10
+ | "paused"
11
+ | "completed"
12
+ | "failed"
13
+ | "timed_out"
14
+ | "cancelled"
15
+ | "ambiguous";
16
+
17
+ export type WorkflowDisplay = {
18
+ status: WorkflowDisplayStatus;
19
+ activity: "supervised_worker" | "origin_turn" | null;
20
+ controls: Array<"pause" | "resume" | "cancel" | "answer" | "review">;
21
+ reason: string | null;
22
+ reasonContent?: JsonValue;
23
+ };
24
+
25
+ export type WorkflowRunQueueView = {
26
+ runId: string;
27
+ workflowName: string;
28
+ workflowSourceRef: string;
29
+ initialized: boolean;
30
+ definitionDigest: string;
31
+ status: "queued" | "starting" | "running" | "parked" | "done" | "failed" | "cancelled";
32
+ originSessionId: string | null;
33
+ executionMode: "interactive" | "headless";
34
+ parentRunId: string | null;
35
+ errorCode: string | null;
36
+ createdAt: string;
37
+ updatedAt: string;
38
+ startedAt: string | null;
39
+ finishedAt: string | null;
40
+ };
41
+
42
+ export type WorkflowRunView = {
43
+ schema: typeof RUN_VIEW_SCHEMA;
44
+ runId: string;
45
+ revision: number;
46
+ display: WorkflowDisplay;
47
+ manifest: JsonValue;
48
+ state: JsonValue;
49
+ workflow: JsonValue;
50
+ queue: WorkflowRunQueueView;
51
+ updates: JsonValue[];
52
+ graphSteps: JsonValue[];
53
+ graphStepStart: number;
54
+ graphStepTotal: number;
55
+ takenTransitions: string[];
56
+ graphHistory: JsonValue;
57
+ takenTransitionStart: number;
58
+ takenTransitionTotal: number;
59
+ graphCursor: number;
60
+ stepStart: number;
61
+ stepTotal: number;
62
+ tracePage: JsonValue;
63
+ session: JsonValue;
64
+ settingsScopes: JsonValue[];
65
+ settingsStart: number;
66
+ settingsTotal: number;
67
+ followUpQueue: JsonValue;
68
+ followUpStart: number;
69
+ followUpTotal: number;
70
+ updateStart: number;
71
+ updateTotal: number;
72
+ live: boolean;
73
+ possiblyInterrupted: boolean;
74
+ };
75
+
76
+ export type WorkflowRunListPage = {
77
+ schema: "pi-workflows.run-list-page.v1";
78
+ revision: string;
79
+ start: number;
80
+ total: number;
81
+ items: WorkflowRunSummary[];
82
+ };
83
+
84
+ export type WorkflowRunSummary = {
85
+ runId: string;
86
+ workflowName: string;
87
+ originSessionId: string | null;
88
+ createdAt: string;
89
+ updatedAt: string;
90
+ display: WorkflowDisplay;
91
+ manifest: JsonValue;
92
+ live: boolean;
93
+ possiblyInterrupted: boolean;
94
+ };
95
+
96
+ export type ClientInteractiveRequest = {
97
+ requestId: string;
98
+ runId: string;
99
+ attemptId: string;
100
+ targetSessionId: string;
101
+ kind: "agent" | "assistant" | "decision";
102
+ contract: JsonValue;
103
+ revision: number;
104
+ status: "pending" | "presenting" | "settled" | "cancelled";
105
+ presenterId: string | null;
106
+ presentationClaimExpiresAt: string | null;
107
+ presentationSessionEntryId: string | null;
108
+ acceptedSubmissionId: string | null;
109
+ createdAt: string;
110
+ updatedAt: string;
111
+ settledAt: string | null;
112
+ consumedAt: string | null;
113
+ };
114
+
115
+ export type WorkflowDeliveryAvailability = {
116
+ notification: boolean;
117
+ turn: boolean;
118
+ };
119
+
120
+ export type WorkflowSessionView = {
121
+ schema: typeof SESSION_VIEW_SCHEMA;
122
+ sessionId: string;
123
+ run: WorkflowRunView | null;
124
+ pendingInteractions: JsonValue[];
125
+ pendingInteractionStart: number;
126
+ pendingInteractionTotal: number;
127
+ deliveries: WorkflowDeliveryAvailability;
128
+ };
129
+
130
+ export type OriginActivityReport = {
131
+ sessionId: string;
132
+ runId: string;
133
+ requestId: string;
134
+ deliveryId: string;
135
+ sessionEntryId: string;
136
+ sequence: number;
137
+ state: "started" | "refresh" | "settled";
138
+ };
@@ -62,6 +62,27 @@ export type WorkflowRunClaimOptions = WorkflowRunReservationOptions & {
62
62
  leaseMs: number;
63
63
  };
64
64
 
65
+ export type WorkflowRunQueueViewRecord = {
66
+ runId: string;
67
+ workflowName: string;
68
+ workflowSourceRef: string;
69
+ workflowSource: unknown;
70
+ initialized: boolean;
71
+ definitionDigest: string;
72
+ runStateStatus: string;
73
+ paused: boolean;
74
+ status: WorkflowRunLaunchStatus;
75
+ originSessionId: string | null;
76
+ executionMode: "interactive" | "headless";
77
+ parentRunId: string | null;
78
+ errorCode: string | null;
79
+ errorMessage: string | null;
80
+ createdAt: string;
81
+ updatedAt: string;
82
+ startedAt: string | null;
83
+ finishedAt: string | null;
84
+ };
85
+
65
86
  export type WorkflowRunQueueRecord = {
66
87
  runId: string;
67
88
  workflowName: string;
@@ -191,6 +212,7 @@ type RunRow = {
191
212
  workflowName: string;
192
213
  workflowRef: string;
193
214
  runStatus: string;
215
+ paused: number;
194
216
  definitionDigest: Buffer;
195
217
  definitionHash: Buffer;
196
218
  inputHash: Buffer;
@@ -212,6 +234,34 @@ type RunRow = {
212
234
  ownerId: string | null;
213
235
  claimExpiresAt: number | null;
214
236
  };
237
+ type WorkflowRunViewRow = {
238
+ runId: string;
239
+ workflowName: string;
240
+ workflowRef: string;
241
+ runStatus: string;
242
+ paused: number;
243
+ definitionDigest: Buffer;
244
+ status: WorkflowRunLaunchStatus;
245
+ originSessionId: string | null;
246
+ executionMode: "interactive" | "headless";
247
+ parentRunId: string | null;
248
+ errorCode: string | null;
249
+ errorMessage: string | null;
250
+ createdAt: number;
251
+ updatedAt: number;
252
+ startedAt: number | null;
253
+ finishedAt: number | null;
254
+ sourceType: "builtin" | "file";
255
+ sourceRef: string;
256
+ sourceRevision: string;
257
+ };
258
+
259
+ type RunListRevisionRow = {
260
+ count: number;
261
+ revisionSum: number;
262
+ updatedAt: number;
263
+ };
264
+
215
265
  type RunSourceIdentityRow = {
216
266
  mountPath: string;
217
267
  sourceType: "builtin" | "file";
@@ -1238,6 +1288,40 @@ export class SqliteControllerStore implements ControllerStore {
1238
1288
  return row === undefined ? undefined : this.mapWorkflowRun(row);
1239
1289
  }
1240
1290
 
1291
+ getWorkflowRunView(runId: string): WorkflowRunQueueViewRecord | undefined {
1292
+ const row = this.workflowRunViewRows("WHERE r.run_id = ?", [runId])[0];
1293
+ return row === undefined ? undefined : workflowRunViewRecord(row);
1294
+ }
1295
+
1296
+ workflowRunListRevision(): { total: number; revision: string } {
1297
+ const row = this.state.connection
1298
+ .prepare(
1299
+ `SELECT count(*) AS count, COALESCE(sum(res.revision), 0) AS revisionSum,
1300
+ COALESCE(max(q.updated_at), 0) AS updatedAt
1301
+ FROM runs r JOIN run_queue q ON q.run_id = r.run_id
1302
+ JOIN resources res ON res.resource_id = r.resource_id`,
1303
+ )
1304
+ .get();
1305
+ if (!isRunListRevisionRow(row)) throw new Error("Workflow run list revision is invalid");
1306
+ return {
1307
+ total: row.count,
1308
+ revision: `${row.count}:${row.revisionSum}:${row.updatedAt}`,
1309
+ };
1310
+ }
1311
+
1312
+ listWorkflowRunViews(options: { offset: number; limit: number }): {
1313
+ total: number;
1314
+ revision: string;
1315
+ runs: WorkflowRunQueueViewRecord[];
1316
+ } {
1317
+ const current = this.workflowRunListRevision();
1318
+ const rows = this.workflowRunViewRows("ORDER BY q.created_at DESC LIMIT ? OFFSET ?", [
1319
+ options.limit,
1320
+ options.offset,
1321
+ ]);
1322
+ return { ...current, runs: rows.map(workflowRunViewRecord) };
1323
+ }
1324
+
1241
1325
  workflowRunProjectPath(runId: string): string | undefined {
1242
1326
  const row = this.state.connection
1243
1327
  .prepare(
@@ -1269,11 +1353,11 @@ export class SqliteControllerStore implements ControllerStore {
1269
1353
  clauses.push(`r.run_id NOT IN (${options.excludeRunIds.map(() => "?").join(", ")})`);
1270
1354
  params.push(...options.excludeRunIds);
1271
1355
  }
1272
- params.push(options.limit ?? 100);
1356
+ if (options.limit !== undefined) params.push(options.limit);
1273
1357
  const rows = this.state.connection
1274
1358
  .prepare(
1275
1359
  workflowRunSelect(
1276
- `${clauses.length === 0 ? "" : `WHERE ${clauses.join(" AND ")}`} ORDER BY q.created_at DESC LIMIT ?`,
1360
+ `${clauses.length === 0 ? "" : `WHERE ${clauses.join(" AND ")}`} ORDER BY q.created_at DESC${options.limit === undefined ? "" : " LIMIT ?"}`,
1277
1361
  ),
1278
1362
  )
1279
1363
  .all(...params);
@@ -1291,6 +1375,14 @@ export class SqliteControllerStore implements ControllerStore {
1291
1375
  return isRunRow(row) ? this.mapWorkflowRun(row) : undefined;
1292
1376
  }
1293
1377
 
1378
+ findSessionReservationView(sessionId: string): WorkflowRunQueueViewRecord | undefined {
1379
+ const row = this.workflowRunViewRows(
1380
+ "WHERE b.origin_session_id = ? AND q.status NOT IN ('done', 'failed', 'cancelled') ORDER BY q.created_at DESC LIMIT 1",
1381
+ [sessionId],
1382
+ )[0];
1383
+ return row === undefined ? undefined : workflowRunViewRecord(row);
1384
+ }
1385
+
1294
1386
  claimWorkflowRun(options: {
1295
1387
  runId: string;
1296
1388
  runnerId: string;
@@ -1561,6 +1653,104 @@ export class SqliteControllerStore implements ControllerStore {
1561
1653
  return this.releaseRunClaim(options.runId, options.claimToken, "parked", options.now);
1562
1654
  }
1563
1655
 
1656
+ pauseParkedWorkflowRun(options: { runId: string; now?: string }): boolean {
1657
+ const now = epoch(validTimestamp(options.now));
1658
+ return this.state.transaction(() => {
1659
+ const row = this.workflowRunRow(options.runId);
1660
+ if (row === undefined || row.status !== "parked") return false;
1661
+ const lease = this.requireLease(row.resourceId);
1662
+ if (lease.ownerId !== null && lease.expiresAt !== null && lease.expiresAt > now) return false;
1663
+ const pending = this.state.connection
1664
+ .prepare(
1665
+ `SELECT 1 FROM interactive_requests
1666
+ WHERE run_id = ? AND status IN ('pending', 'presenting') LIMIT 1`,
1667
+ )
1668
+ .get(options.runId);
1669
+ if (pending === undefined) return false;
1670
+ if (row.paused === 1) return true;
1671
+ if (!["running", "waiting"].includes(row.runStatus)) return false;
1672
+ const changed = this.state.connection
1673
+ .prepare(
1674
+ `UPDATE runs SET paused = 1, status_detail = 'paused', updated_at = ?
1675
+ WHERE run_id = ? AND paused = 0 AND status IN ('running', 'waiting')`,
1676
+ )
1677
+ .run(now, options.runId);
1678
+ if (changed.changes !== 1) return false;
1679
+ const revision = this.resourceRevision(row.resourceId);
1680
+ this.bumpResource(row.resourceId, revision, now);
1681
+ this.insertEvent(
1682
+ row.resourceId,
1683
+ revision + 1,
1684
+ "run.paused",
1685
+ "control",
1686
+ null,
1687
+ { status: "parked" },
1688
+ now,
1689
+ );
1690
+ recordViewerDeltas(
1691
+ this.state,
1692
+ options.runId,
1693
+ [{ targetType: "summary" }, { targetType: "replay" }],
1694
+ now,
1695
+ );
1696
+ return true;
1697
+ });
1698
+ }
1699
+
1700
+ resumePausedInteraction(options: { runId: string; now?: string }): boolean {
1701
+ const now = epoch(validTimestamp(options.now));
1702
+ return this.state.transaction(() => {
1703
+ const row = this.workflowRunRow(options.runId);
1704
+ if (
1705
+ row === undefined ||
1706
+ row.status !== "parked" ||
1707
+ row.runStatus !== "waiting" ||
1708
+ row.paused !== 1
1709
+ ) {
1710
+ return false;
1711
+ }
1712
+ const lease = this.requireLease(row.resourceId);
1713
+ if (lease.ownerId !== null && lease.expiresAt !== null && lease.expiresAt > now) return false;
1714
+ const pending = this.state.connection
1715
+ .prepare(
1716
+ `SELECT 1 FROM interactive_requests
1717
+ WHERE run_id = ? AND status IN ('pending', 'presenting') LIMIT 1`,
1718
+ )
1719
+ .get(options.runId);
1720
+ if (pending === undefined) return false;
1721
+ const changed = this.state.connection
1722
+ .prepare(
1723
+ `UPDATE runs
1724
+ SET paused = 0, status_detail = 'waiting for origin-session input', updated_at = ?
1725
+ WHERE run_id = ? AND status = 'waiting' AND paused = 1`,
1726
+ )
1727
+ .run(now, options.runId);
1728
+ if (changed.changes !== 1) return false;
1729
+ const revision = this.resourceRevision(row.resourceId);
1730
+ this.bumpResource(row.resourceId, revision, now);
1731
+ this.insertEvent(
1732
+ row.resourceId,
1733
+ revision + 1,
1734
+ "run.resumed",
1735
+ "control",
1736
+ null,
1737
+ { status: "waiting" },
1738
+ now,
1739
+ );
1740
+ recordViewerDeltas(
1741
+ this.state,
1742
+ options.runId,
1743
+ [{ targetType: "summary" }, { targetType: "replay" }],
1744
+ now,
1745
+ );
1746
+ return true;
1747
+ });
1748
+ }
1749
+
1750
+ isWorkflowRunPaused(runId: string): boolean {
1751
+ return this.workflowRunRow(runId)?.paused === 1;
1752
+ }
1753
+
1564
1754
  parkWorkflowRunForSourceChange(options: {
1565
1755
  runId: string;
1566
1756
  claimToken: string;
@@ -2038,6 +2228,21 @@ export class SqliteControllerStore implements ControllerStore {
2038
2228
  return isTurnIntentRow(row) ? this.mapTurnIntent(row) : undefined;
2039
2229
  }
2040
2230
 
2231
+ isWorkflowTurnIntentClaimLive(options: {
2232
+ intentId: string;
2233
+ targetSessionId: string;
2234
+ claimToken: string;
2235
+ now?: string;
2236
+ }): boolean {
2237
+ const row = this.turnIntentRow(options.intentId);
2238
+ return (
2239
+ row !== undefined &&
2240
+ row.targetSessionId === options.targetSessionId &&
2241
+ row.resolvedAt === null &&
2242
+ this.verifyEffectToken(row.effectId, options.claimToken, options.now)
2243
+ );
2244
+ }
2245
+
2041
2246
  claimWorkflowTurnIntent(options: {
2042
2247
  intentId: string;
2043
2248
  targetSessionId: string;
@@ -2084,6 +2289,24 @@ export class SqliteControllerStore implements ControllerStore {
2084
2289
  return claimed;
2085
2290
  }
2086
2291
 
2292
+ hasClaimableWorkflowTurnIntent(options: { targetSessionId: string; now?: string }): boolean {
2293
+ const now = epoch(validTimestamp(options.now));
2294
+ const row = this.state.connection
2295
+ .prepare(
2296
+ `SELECT 1 AS present
2297
+ FROM turn_intents t
2298
+ JOIN effects e ON e.effect_id = t.effect_id
2299
+ JOIN leases l ON l.resource_id = e.resource_id
2300
+ WHERE t.target_session_id = ? AND t.resolved_at IS NULL
2301
+ AND t.eligible_at IS NOT NULL AND t.eligible_at <= ?
2302
+ AND e.status = 'pending'
2303
+ AND (l.owner_id IS NULL OR l.expires_at IS NULL OR l.expires_at <= ?)
2304
+ LIMIT 1`,
2305
+ )
2306
+ .get(options.targetSessionId, now, now);
2307
+ return row !== undefined;
2308
+ }
2309
+
2087
2310
  makeWorkflowTurnIntentEligible(options: {
2088
2311
  intentId: string;
2089
2312
  fallbackFacts: WorkflowTurnIntentFacts;
@@ -2240,6 +2463,22 @@ export class SqliteControllerStore implements ControllerStore {
2240
2463
  return this.requireNotification(options.runId, options.attemptId, options.notificationIndex);
2241
2464
  }
2242
2465
 
2466
+ hasClaimableWorkflowNotification(options: { targetSessionId: string; now?: string }): boolean {
2467
+ const now = epoch(validTimestamp(options.now));
2468
+ const row = this.state.connection
2469
+ .prepare(
2470
+ `SELECT 1 AS present
2471
+ FROM notifications n
2472
+ JOIN effects e ON e.effect_id = n.effect_id
2473
+ JOIN leases l ON l.resource_id = e.resource_id
2474
+ WHERE n.target_session_id = ? AND e.status = 'pending'
2475
+ AND (l.owner_id IS NULL OR l.expires_at IS NULL OR l.expires_at <= ?)
2476
+ LIMIT 1`,
2477
+ )
2478
+ .get(options.targetSessionId, now);
2479
+ return row !== undefined;
2480
+ }
2481
+
2243
2482
  listPendingWorkflowNotifications(options: {
2244
2483
  targetSessionId: string;
2245
2484
  limit?: number;
@@ -2279,6 +2518,20 @@ export class SqliteControllerStore implements ControllerStore {
2279
2518
  return result;
2280
2519
  }
2281
2520
 
2521
+ isWorkflowNotificationClaimLive(options: {
2522
+ notificationId: string;
2523
+ targetSessionId: string;
2524
+ claimToken: string;
2525
+ now?: string;
2526
+ }): boolean {
2527
+ const row = this.notificationRowById(options.notificationId);
2528
+ return (
2529
+ row !== undefined &&
2530
+ row.targetSessionId === options.targetSessionId &&
2531
+ this.verifyEffectToken(row.effectId, options.claimToken, options.now)
2532
+ );
2533
+ }
2534
+
2282
2535
  markWorkflowNotificationDelivered(options: {
2283
2536
  notificationId: string;
2284
2537
  targetSessionId: string;
@@ -2771,6 +3024,13 @@ export class SqliteControllerStore implements ControllerStore {
2771
3024
  }
2772
3025
  }
2773
3026
 
3027
+ private workflowRunViewRows(clause: string, params: unknown[]): WorkflowRunViewRow[] {
3028
+ return this.state.connection
3029
+ .prepare(workflowRunViewSelect(clause))
3030
+ .all(...params)
3031
+ .filter(isWorkflowRunViewRow);
3032
+ }
3033
+
2774
3034
  /* istanbul ignore next -- pure projection covered by integration tests */
2775
3035
  private mapWorkflowRun(row: RunRow): WorkflowRunQueueRecord {
2776
3036
  return {
@@ -3452,7 +3712,7 @@ function workflowSelect(clause: string): string {
3452
3712
  function workflowRunSelect(clause: string): string {
3453
3713
  return `SELECT r.run_id AS runId, r.resource_id AS resourceId,
3454
3714
  d.workflow_name AS workflowName, r.workflow_ref AS workflowRef, r.status AS runStatus,
3455
- r.definition_digest AS definitionDigest, d.definition_hash AS definitionHash,
3715
+ r.paused, r.definition_digest AS definitionDigest, d.definition_hash AS definitionHash,
3456
3716
  r.input_hash AS inputHash,
3457
3717
  r.launch_options_hash AS launchOptionsHash,
3458
3718
  q.status, q.available_at AS availableAt, q.affinity_runner_id AS affinityRunnerId,
@@ -3468,6 +3728,24 @@ function workflowRunSelect(clause: string): string {
3468
3728
  JOIN leases l ON l.resource_id = r.resource_id ${clause}`;
3469
3729
  }
3470
3730
 
3731
+ function workflowRunViewSelect(clause: string): string {
3732
+ return `SELECT r.run_id AS runId, d.workflow_name AS workflowName,
3733
+ r.workflow_ref AS workflowRef, r.status AS runStatus, r.paused,
3734
+ r.definition_digest AS definitionDigest, q.status,
3735
+ b.origin_session_id AS originSessionId, b.execution_mode AS executionMode,
3736
+ r.parent_run_id AS parentRunId, q.error_code AS errorCode,
3737
+ CAST(error.content AS TEXT) AS errorMessage,
3738
+ q.created_at AS createdAt, q.updated_at AS updatedAt,
3739
+ q.started_at AS startedAt, q.finished_at AS finishedAt,
3740
+ source.source_type AS sourceType, source.source_ref AS sourceRef,
3741
+ source.source_revision AS sourceRevision
3742
+ FROM runs r JOIN workflow_definitions d ON d.definition_digest = r.definition_digest
3743
+ JOIN run_queue q ON q.run_id = r.run_id
3744
+ LEFT JOIN run_bindings b ON b.run_id = r.run_id
3745
+ LEFT JOIN run_sources source ON source.run_id = r.run_id AND source.mount_path = ''
3746
+ LEFT JOIN blobs error ON error.blob_hash = q.error_hash ${clause}`;
3747
+ }
3748
+
3471
3749
  function turnIntentSelect(clause: string): string {
3472
3750
  return `SELECT t.turn_intent_id AS intentId, t.source_event_id AS sourceEventId,
3473
3751
  t.run_id AS runId, t.workflow_ref AS workflowRef,
@@ -3559,6 +3837,67 @@ function isControllerRow(value: unknown): value is ControllerRow {
3559
3837
  function isLeaseRow(value: unknown): value is LeaseRow {
3560
3838
  return isRecord(value);
3561
3839
  }
3840
+ function workflowRunViewRecord(row: WorkflowRunViewRow): WorkflowRunQueueViewRecord {
3841
+ const root =
3842
+ row.sourceType === "builtin"
3843
+ ? { kind: "builtin", id: row.sourceRef, revision: row.sourceRevision }
3844
+ : { kind: "file", path: row.sourceRef, hash: row.sourceRevision };
3845
+ return {
3846
+ runId: row.runId,
3847
+ workflowName: row.workflowName,
3848
+ workflowSourceRef: row.workflowRef,
3849
+ workflowSource: { root, mounted: [] },
3850
+ initialized: row.runStatus !== "queued",
3851
+ definitionDigest: `sha256:${row.definitionDigest.toString("hex")}`,
3852
+ runStateStatus: row.runStatus,
3853
+ paused: row.paused === 1,
3854
+ status: row.status,
3855
+ originSessionId: row.executionMode === "headless" ? null : row.originSessionId,
3856
+ executionMode: row.executionMode,
3857
+ parentRunId: row.parentRunId,
3858
+ errorCode: row.errorCode,
3859
+ errorMessage: row.errorMessage,
3860
+ createdAt: new Date(row.createdAt).toISOString(),
3861
+ updatedAt: new Date(row.updatedAt).toISOString(),
3862
+ startedAt: row.startedAt === null ? null : new Date(row.startedAt).toISOString(),
3863
+ finishedAt: row.finishedAt === null ? null : new Date(row.finishedAt).toISOString(),
3864
+ };
3865
+ }
3866
+
3867
+ function isWorkflowRunViewRow(value: unknown): value is WorkflowRunViewRow {
3868
+ return (
3869
+ isRecord(value) &&
3870
+ typeof value.runId === "string" &&
3871
+ typeof value.workflowName === "string" &&
3872
+ typeof value.workflowRef === "string" &&
3873
+ typeof value.runStatus === "string" &&
3874
+ typeof value.paused === "number" &&
3875
+ Buffer.isBuffer(value.definitionDigest) &&
3876
+ typeof value.status === "string" &&
3877
+ (value.originSessionId === null || typeof value.originSessionId === "string") &&
3878
+ (value.executionMode === "interactive" || value.executionMode === "headless") &&
3879
+ (value.parentRunId === null || typeof value.parentRunId === "string") &&
3880
+ (value.errorCode === null || typeof value.errorCode === "string") &&
3881
+ (value.errorMessage === null || typeof value.errorMessage === "string") &&
3882
+ typeof value.createdAt === "number" &&
3883
+ typeof value.updatedAt === "number" &&
3884
+ (value.startedAt === null || typeof value.startedAt === "number") &&
3885
+ (value.finishedAt === null || typeof value.finishedAt === "number") &&
3886
+ (value.sourceType === "builtin" || value.sourceType === "file") &&
3887
+ typeof value.sourceRef === "string" &&
3888
+ typeof value.sourceRevision === "string"
3889
+ );
3890
+ }
3891
+
3892
+ function isRunListRevisionRow(value: unknown): value is RunListRevisionRow {
3893
+ return (
3894
+ isRecord(value) &&
3895
+ typeof value.count === "number" &&
3896
+ typeof value.revisionSum === "number" &&
3897
+ typeof value.updatedAt === "number"
3898
+ );
3899
+ }
3900
+
3562
3901
  function isRunRow(value: unknown): value is RunRow {
3563
3902
  return isRecord(value);
3564
3903
  }