@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 @@
1
+ {"version":3,"file":"resolver.js","sourceRoot":"","sources":["../../src/client/resolver.ts"],"names":[],"mappings":""}
@@ -0,0 +1,118 @@
1
+ import type { JsonValue } from "../state/json.js";
2
+ export declare const RUN_VIEW_SCHEMA: "pi-workflows.run-view.v1";
3
+ export declare const SESSION_VIEW_SCHEMA: "pi-workflows.session-view.v1";
4
+ export type WorkflowDisplayStatus = "queued" | "running" | "waiting" | "paused" | "completed" | "failed" | "timed_out" | "cancelled" | "ambiguous";
5
+ export type WorkflowDisplay = {
6
+ status: WorkflowDisplayStatus;
7
+ activity: "supervised_worker" | "origin_turn" | null;
8
+ controls: Array<"pause" | "resume" | "cancel" | "answer" | "review">;
9
+ reason: string | null;
10
+ reasonContent?: JsonValue;
11
+ };
12
+ export type WorkflowRunQueueView = {
13
+ runId: string;
14
+ workflowName: string;
15
+ workflowSourceRef: string;
16
+ initialized: boolean;
17
+ definitionDigest: string;
18
+ status: "queued" | "starting" | "running" | "parked" | "done" | "failed" | "cancelled";
19
+ originSessionId: string | null;
20
+ executionMode: "interactive" | "headless";
21
+ parentRunId: string | null;
22
+ errorCode: string | null;
23
+ createdAt: string;
24
+ updatedAt: string;
25
+ startedAt: string | null;
26
+ finishedAt: string | null;
27
+ };
28
+ export type WorkflowRunView = {
29
+ schema: typeof RUN_VIEW_SCHEMA;
30
+ runId: string;
31
+ revision: number;
32
+ display: WorkflowDisplay;
33
+ manifest: JsonValue;
34
+ state: JsonValue;
35
+ workflow: JsonValue;
36
+ queue: WorkflowRunQueueView;
37
+ updates: JsonValue[];
38
+ graphSteps: JsonValue[];
39
+ graphStepStart: number;
40
+ graphStepTotal: number;
41
+ takenTransitions: string[];
42
+ graphHistory: JsonValue;
43
+ takenTransitionStart: number;
44
+ takenTransitionTotal: number;
45
+ graphCursor: number;
46
+ stepStart: number;
47
+ stepTotal: number;
48
+ tracePage: JsonValue;
49
+ session: JsonValue;
50
+ settingsScopes: JsonValue[];
51
+ settingsStart: number;
52
+ settingsTotal: number;
53
+ followUpQueue: JsonValue;
54
+ followUpStart: number;
55
+ followUpTotal: number;
56
+ updateStart: number;
57
+ updateTotal: number;
58
+ live: boolean;
59
+ possiblyInterrupted: boolean;
60
+ };
61
+ export type WorkflowRunListPage = {
62
+ schema: "pi-workflows.run-list-page.v1";
63
+ revision: string;
64
+ start: number;
65
+ total: number;
66
+ items: WorkflowRunSummary[];
67
+ };
68
+ export type WorkflowRunSummary = {
69
+ runId: string;
70
+ workflowName: string;
71
+ originSessionId: string | null;
72
+ createdAt: string;
73
+ updatedAt: string;
74
+ display: WorkflowDisplay;
75
+ manifest: JsonValue;
76
+ live: boolean;
77
+ possiblyInterrupted: boolean;
78
+ };
79
+ export type ClientInteractiveRequest = {
80
+ requestId: string;
81
+ runId: string;
82
+ attemptId: string;
83
+ targetSessionId: string;
84
+ kind: "agent" | "assistant" | "decision";
85
+ contract: JsonValue;
86
+ revision: number;
87
+ status: "pending" | "presenting" | "settled" | "cancelled";
88
+ presenterId: string | null;
89
+ presentationClaimExpiresAt: string | null;
90
+ presentationSessionEntryId: string | null;
91
+ acceptedSubmissionId: string | null;
92
+ createdAt: string;
93
+ updatedAt: string;
94
+ settledAt: string | null;
95
+ consumedAt: string | null;
96
+ };
97
+ export type WorkflowDeliveryAvailability = {
98
+ notification: boolean;
99
+ turn: boolean;
100
+ };
101
+ export type WorkflowSessionView = {
102
+ schema: typeof SESSION_VIEW_SCHEMA;
103
+ sessionId: string;
104
+ run: WorkflowRunView | null;
105
+ pendingInteractions: JsonValue[];
106
+ pendingInteractionStart: number;
107
+ pendingInteractionTotal: number;
108
+ deliveries: WorkflowDeliveryAvailability;
109
+ };
110
+ export type OriginActivityReport = {
111
+ sessionId: string;
112
+ runId: string;
113
+ requestId: string;
114
+ deliveryId: string;
115
+ sessionEntryId: string;
116
+ sequence: number;
117
+ state: "started" | "refresh" | "settled";
118
+ };
@@ -0,0 +1,3 @@
1
+ export const RUN_VIEW_SCHEMA = "pi-workflows.run-view.v1";
2
+ export const SESSION_VIEW_SCHEMA = "pi-workflows.session-view.v1";
3
+ //# sourceMappingURL=view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view.js","sourceRoot":"","sources":["../../src/client/view.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,eAAe,GAAG,0BAAmC,CAAC;AACnE,MAAM,CAAC,MAAM,mBAAmB,GAAG,8BAAuC,CAAC"}
@@ -22,6 +22,26 @@ export type WorkflowRunClaimOptions = WorkflowRunReservationOptions & {
22
22
  claimToken: string;
23
23
  leaseMs: number;
24
24
  };
25
+ export type WorkflowRunQueueViewRecord = {
26
+ runId: string;
27
+ workflowName: string;
28
+ workflowSourceRef: string;
29
+ workflowSource: unknown;
30
+ initialized: boolean;
31
+ definitionDigest: string;
32
+ runStateStatus: string;
33
+ paused: boolean;
34
+ status: WorkflowRunLaunchStatus;
35
+ originSessionId: string | null;
36
+ executionMode: "interactive" | "headless";
37
+ parentRunId: string | null;
38
+ errorCode: string | null;
39
+ errorMessage: string | null;
40
+ createdAt: string;
41
+ updatedAt: string;
42
+ startedAt: string | null;
43
+ finishedAt: string | null;
44
+ };
25
45
  export type WorkflowRunQueueRecord = {
26
46
  runId: string;
27
47
  workflowName: string;
@@ -212,6 +232,19 @@ export declare class SqliteControllerStore implements ControllerStore {
212
232
  prepareOrAdoptWorkflowRun(options: WorkflowRunClaimOptions): WorkflowRunPreparationResult;
213
233
  enqueueWorkflowRun(options: WorkflowRunClaimOptions): WorkflowRunQueueRecord;
214
234
  getWorkflowRun(runId: string): WorkflowRunQueueRecord | undefined;
235
+ getWorkflowRunView(runId: string): WorkflowRunQueueViewRecord | undefined;
236
+ workflowRunListRevision(): {
237
+ total: number;
238
+ revision: string;
239
+ };
240
+ listWorkflowRunViews(options: {
241
+ offset: number;
242
+ limit: number;
243
+ }): {
244
+ total: number;
245
+ revision: string;
246
+ runs: WorkflowRunQueueViewRecord[];
247
+ };
215
248
  workflowRunProjectPath(runId: string): string | undefined;
216
249
  listWorkflowRuns(options?: {
217
250
  statuses?: WorkflowRunLaunchStatus[];
@@ -219,6 +252,7 @@ export declare class SqliteControllerStore implements ControllerStore {
219
252
  limit?: number;
220
253
  }): WorkflowRunQueueRecord[];
221
254
  findSessionReservation(sessionId: string): WorkflowRunQueueRecord | undefined;
255
+ findSessionReservationView(sessionId: string): WorkflowRunQueueViewRecord | undefined;
222
256
  claimWorkflowRun(options: {
223
257
  runId: string;
224
258
  runnerId: string;
@@ -287,6 +321,15 @@ export declare class SqliteControllerStore implements ControllerStore {
287
321
  claimToken: string;
288
322
  now?: string;
289
323
  }): boolean;
324
+ pauseParkedWorkflowRun(options: {
325
+ runId: string;
326
+ now?: string;
327
+ }): boolean;
328
+ resumePausedInteraction(options: {
329
+ runId: string;
330
+ now?: string;
331
+ }): boolean;
332
+ isWorkflowRunPaused(runId: string): boolean;
290
333
  parkWorkflowRunForSourceChange(options: {
291
334
  runId: string;
292
335
  claimToken: string;
@@ -357,6 +400,12 @@ export declare class SqliteControllerStore implements ControllerStore {
357
400
  runId: string;
358
401
  targetSessionId: string;
359
402
  }): WorkflowTurnIntentRecord | undefined;
403
+ isWorkflowTurnIntentClaimLive(options: {
404
+ intentId: string;
405
+ targetSessionId: string;
406
+ claimToken: string;
407
+ now?: string;
408
+ }): boolean;
360
409
  claimWorkflowTurnIntent(options: {
361
410
  intentId: string;
362
411
  targetSessionId: string;
@@ -371,6 +420,10 @@ export declare class SqliteControllerStore implements ControllerStore {
371
420
  now?: string;
372
421
  limit?: number;
373
422
  }): WorkflowTurnIntentRecord[];
423
+ hasClaimableWorkflowTurnIntent(options: {
424
+ targetSessionId: string;
425
+ now?: string;
426
+ }): boolean;
374
427
  makeWorkflowTurnIntentEligible(options: {
375
428
  intentId: string;
376
429
  fallbackFacts: WorkflowTurnIntentFacts;
@@ -406,6 +459,10 @@ export declare class SqliteControllerStore implements ControllerStore {
406
459
  content: string;
407
460
  now?: string;
408
461
  }): WorkflowNotificationRecord;
462
+ hasClaimableWorkflowNotification(options: {
463
+ targetSessionId: string;
464
+ now?: string;
465
+ }): boolean;
409
466
  listPendingWorkflowNotifications(options: {
410
467
  targetSessionId: string;
411
468
  limit?: number;
@@ -417,6 +474,12 @@ export declare class SqliteControllerStore implements ControllerStore {
417
474
  now?: string;
418
475
  limit?: number;
419
476
  }): WorkflowNotificationRecord[];
477
+ isWorkflowNotificationClaimLive(options: {
478
+ notificationId: string;
479
+ targetSessionId: string;
480
+ claimToken: string;
481
+ now?: string;
482
+ }): boolean;
420
483
  markWorkflowNotificationDelivered(options: {
421
484
  notificationId: string;
422
485
  targetSessionId: string;
@@ -457,6 +520,7 @@ export declare class SqliteControllerStore implements ControllerStore {
457
520
  private requireWorkflowRunRow;
458
521
  private requireWorkflowRun;
459
522
  private assertWorkflowRunPreparationCompatible;
523
+ private workflowRunViewRows;
460
524
  private mapWorkflowRun;
461
525
  private claimRun;
462
526
  private claimRunInTransaction;
@@ -640,6 +640,32 @@ export class SqliteControllerStore {
640
640
  const row = this.workflowRunRow(runId);
641
641
  return row === undefined ? undefined : this.mapWorkflowRun(row);
642
642
  }
643
+ getWorkflowRunView(runId) {
644
+ const row = this.workflowRunViewRows("WHERE r.run_id = ?", [runId])[0];
645
+ return row === undefined ? undefined : workflowRunViewRecord(row);
646
+ }
647
+ workflowRunListRevision() {
648
+ const row = this.state.connection
649
+ .prepare(`SELECT count(*) AS count, COALESCE(sum(res.revision), 0) AS revisionSum,
650
+ COALESCE(max(q.updated_at), 0) AS updatedAt
651
+ FROM runs r JOIN run_queue q ON q.run_id = r.run_id
652
+ JOIN resources res ON res.resource_id = r.resource_id`)
653
+ .get();
654
+ if (!isRunListRevisionRow(row))
655
+ throw new Error("Workflow run list revision is invalid");
656
+ return {
657
+ total: row.count,
658
+ revision: `${row.count}:${row.revisionSum}:${row.updatedAt}`,
659
+ };
660
+ }
661
+ listWorkflowRunViews(options) {
662
+ const current = this.workflowRunListRevision();
663
+ const rows = this.workflowRunViewRows("ORDER BY q.created_at DESC LIMIT ? OFFSET ?", [
664
+ options.limit,
665
+ options.offset,
666
+ ]);
667
+ return { ...current, runs: rows.map(workflowRunViewRecord) };
668
+ }
643
669
  workflowRunProjectPath(runId) {
644
670
  const row = this.state.connection
645
671
  .prepare(`SELECT p.canonical_path AS canonicalPath
@@ -662,9 +688,10 @@ export class SqliteControllerStore {
662
688
  clauses.push(`r.run_id NOT IN (${options.excludeRunIds.map(() => "?").join(", ")})`);
663
689
  params.push(...options.excludeRunIds);
664
690
  }
665
- params.push(options.limit ?? 100);
691
+ if (options.limit !== undefined)
692
+ params.push(options.limit);
666
693
  const rows = this.state.connection
667
- .prepare(workflowRunSelect(`${clauses.length === 0 ? "" : `WHERE ${clauses.join(" AND ")}`} ORDER BY q.created_at DESC LIMIT ?`))
694
+ .prepare(workflowRunSelect(`${clauses.length === 0 ? "" : `WHERE ${clauses.join(" AND ")}`} ORDER BY q.created_at DESC${options.limit === undefined ? "" : " LIMIT ?"}`))
668
695
  .all(...params);
669
696
  return rows.filter(isRunRow).map((row) => this.mapWorkflowRun(row));
670
697
  }
@@ -674,6 +701,10 @@ export class SqliteControllerStore {
674
701
  .get(sessionId);
675
702
  return isRunRow(row) ? this.mapWorkflowRun(row) : undefined;
676
703
  }
704
+ findSessionReservationView(sessionId) {
705
+ const row = this.workflowRunViewRows("WHERE b.origin_session_id = ? AND q.status NOT IN ('done', 'failed', 'cancelled') ORDER BY q.created_at DESC LIMIT 1", [sessionId])[0];
706
+ return row === undefined ? undefined : workflowRunViewRecord(row);
707
+ }
677
708
  claimWorkflowRun(options) {
678
709
  const now = epoch(validTimestamp(options.now));
679
710
  return this.claimRun(options.runId, options.runnerId, options.claimToken, options.leaseMs, now);
@@ -832,6 +863,74 @@ export class SqliteControllerStore {
832
863
  parkWorkflowRun(options) {
833
864
  return this.releaseRunClaim(options.runId, options.claimToken, "parked", options.now);
834
865
  }
866
+ pauseParkedWorkflowRun(options) {
867
+ const now = epoch(validTimestamp(options.now));
868
+ return this.state.transaction(() => {
869
+ const row = this.workflowRunRow(options.runId);
870
+ if (row === undefined || row.status !== "parked")
871
+ return false;
872
+ const lease = this.requireLease(row.resourceId);
873
+ if (lease.ownerId !== null && lease.expiresAt !== null && lease.expiresAt > now)
874
+ return false;
875
+ const pending = this.state.connection
876
+ .prepare(`SELECT 1 FROM interactive_requests
877
+ WHERE run_id = ? AND status IN ('pending', 'presenting') LIMIT 1`)
878
+ .get(options.runId);
879
+ if (pending === undefined)
880
+ return false;
881
+ if (row.paused === 1)
882
+ return true;
883
+ if (!["running", "waiting"].includes(row.runStatus))
884
+ return false;
885
+ const changed = this.state.connection
886
+ .prepare(`UPDATE runs SET paused = 1, status_detail = 'paused', updated_at = ?
887
+ WHERE run_id = ? AND paused = 0 AND status IN ('running', 'waiting')`)
888
+ .run(now, options.runId);
889
+ if (changed.changes !== 1)
890
+ return false;
891
+ const revision = this.resourceRevision(row.resourceId);
892
+ this.bumpResource(row.resourceId, revision, now);
893
+ this.insertEvent(row.resourceId, revision + 1, "run.paused", "control", null, { status: "parked" }, now);
894
+ recordViewerDeltas(this.state, options.runId, [{ targetType: "summary" }, { targetType: "replay" }], now);
895
+ return true;
896
+ });
897
+ }
898
+ resumePausedInteraction(options) {
899
+ const now = epoch(validTimestamp(options.now));
900
+ return this.state.transaction(() => {
901
+ const row = this.workflowRunRow(options.runId);
902
+ if (row === undefined ||
903
+ row.status !== "parked" ||
904
+ row.runStatus !== "waiting" ||
905
+ row.paused !== 1) {
906
+ return false;
907
+ }
908
+ const lease = this.requireLease(row.resourceId);
909
+ if (lease.ownerId !== null && lease.expiresAt !== null && lease.expiresAt > now)
910
+ return false;
911
+ const pending = this.state.connection
912
+ .prepare(`SELECT 1 FROM interactive_requests
913
+ WHERE run_id = ? AND status IN ('pending', 'presenting') LIMIT 1`)
914
+ .get(options.runId);
915
+ if (pending === undefined)
916
+ return false;
917
+ const changed = this.state.connection
918
+ .prepare(`UPDATE runs
919
+ SET paused = 0, status_detail = 'waiting for origin-session input', updated_at = ?
920
+ WHERE run_id = ? AND status = 'waiting' AND paused = 1`)
921
+ .run(now, options.runId);
922
+ if (changed.changes !== 1)
923
+ return false;
924
+ const revision = this.resourceRevision(row.resourceId);
925
+ this.bumpResource(row.resourceId, revision, now);
926
+ this.insertEvent(row.resourceId, revision + 1, "run.resumed", "control", null, { status: "waiting" }, now);
927
+ recordViewerDeltas(this.state, options.runId, [{ targetType: "summary" }, { targetType: "replay" }], now);
928
+ return true;
929
+ });
930
+ }
931
+ isWorkflowRunPaused(runId) {
932
+ return this.workflowRunRow(runId)?.paused === 1;
933
+ }
835
934
  parkWorkflowRunForSourceChange(options) {
836
935
  const now = epoch(validTimestamp(options.now));
837
936
  return this.state.transaction(() => {
@@ -1109,6 +1208,13 @@ export class SqliteControllerStore {
1109
1208
  .get(options.runId, options.targetSessionId);
1110
1209
  return isTurnIntentRow(row) ? this.mapTurnIntent(row) : undefined;
1111
1210
  }
1211
+ isWorkflowTurnIntentClaimLive(options) {
1212
+ const row = this.turnIntentRow(options.intentId);
1213
+ return (row !== undefined &&
1214
+ row.targetSessionId === options.targetSessionId &&
1215
+ row.resolvedAt === null &&
1216
+ this.verifyEffectToken(row.effectId, options.claimToken, options.now));
1217
+ }
1112
1218
  claimWorkflowTurnIntent(options) {
1113
1219
  const intent = this.turnIntent(options.intentId);
1114
1220
  if (intent?.targetSessionId !== options.targetSessionId)
@@ -1131,6 +1237,21 @@ export class SqliteControllerStore {
1131
1237
  }
1132
1238
  return claimed;
1133
1239
  }
1240
+ hasClaimableWorkflowTurnIntent(options) {
1241
+ const now = epoch(validTimestamp(options.now));
1242
+ const row = this.state.connection
1243
+ .prepare(`SELECT 1 AS present
1244
+ FROM turn_intents t
1245
+ JOIN effects e ON e.effect_id = t.effect_id
1246
+ JOIN leases l ON l.resource_id = e.resource_id
1247
+ WHERE t.target_session_id = ? AND t.resolved_at IS NULL
1248
+ AND t.eligible_at IS NOT NULL AND t.eligible_at <= ?
1249
+ AND e.status = 'pending'
1250
+ AND (l.owner_id IS NULL OR l.expires_at IS NULL OR l.expires_at <= ?)
1251
+ LIMIT 1`)
1252
+ .get(options.targetSessionId, now, now);
1253
+ return row !== undefined;
1254
+ }
1134
1255
  makeWorkflowTurnIntentEligible(options) {
1135
1256
  const eligibleAt = epoch(validTimestamp(options.eligibleAt ?? options.now));
1136
1257
  const factsHash = this.state.putJson(options.fallbackFacts, eligibleAt);
@@ -1209,6 +1330,19 @@ export class SqliteControllerStore {
1209
1330
  });
1210
1331
  return this.requireNotification(options.runId, options.attemptId, options.notificationIndex);
1211
1332
  }
1333
+ hasClaimableWorkflowNotification(options) {
1334
+ const now = epoch(validTimestamp(options.now));
1335
+ const row = this.state.connection
1336
+ .prepare(`SELECT 1 AS present
1337
+ FROM notifications n
1338
+ JOIN effects e ON e.effect_id = n.effect_id
1339
+ JOIN leases l ON l.resource_id = e.resource_id
1340
+ WHERE n.target_session_id = ? AND e.status = 'pending'
1341
+ AND (l.owner_id IS NULL OR l.expires_at IS NULL OR l.expires_at <= ?)
1342
+ LIMIT 1`)
1343
+ .get(options.targetSessionId, now);
1344
+ return row !== undefined;
1345
+ }
1212
1346
  listPendingWorkflowNotifications(options) {
1213
1347
  const now = Date.now();
1214
1348
  return this.notificationRows(options.targetSessionId, options.limit ?? 20)
@@ -1226,6 +1360,12 @@ export class SqliteControllerStore {
1226
1360
  }
1227
1361
  return result;
1228
1362
  }
1363
+ isWorkflowNotificationClaimLive(options) {
1364
+ const row = this.notificationRowById(options.notificationId);
1365
+ return (row !== undefined &&
1366
+ row.targetSessionId === options.targetSessionId &&
1367
+ this.verifyEffectToken(row.effectId, options.claimToken, options.now));
1368
+ }
1229
1369
  markWorkflowNotificationDelivered(options) {
1230
1370
  const now = epoch(validTimestamp(options.now));
1231
1371
  return this.state.transaction(() => {
@@ -1597,6 +1737,12 @@ export class SqliteControllerStore {
1597
1737
  throw new Error(`Workflow run preparation conflicts: ${options.runId}`);
1598
1738
  }
1599
1739
  }
1740
+ workflowRunViewRows(clause, params) {
1741
+ return this.state.connection
1742
+ .prepare(workflowRunViewSelect(clause))
1743
+ .all(...params)
1744
+ .filter(isWorkflowRunViewRow);
1745
+ }
1600
1746
  /* istanbul ignore next -- pure projection covered by integration tests */
1601
1747
  mapWorkflowRun(row) {
1602
1748
  return {
@@ -2024,7 +2170,7 @@ function workflowSelect(clause) {
2024
2170
  function workflowRunSelect(clause) {
2025
2171
  return `SELECT r.run_id AS runId, r.resource_id AS resourceId,
2026
2172
  d.workflow_name AS workflowName, r.workflow_ref AS workflowRef, r.status AS runStatus,
2027
- r.definition_digest AS definitionDigest, d.definition_hash AS definitionHash,
2173
+ r.paused, r.definition_digest AS definitionDigest, d.definition_hash AS definitionHash,
2028
2174
  r.input_hash AS inputHash,
2029
2175
  r.launch_options_hash AS launchOptionsHash,
2030
2176
  q.status, q.available_at AS availableAt, q.affinity_runner_id AS affinityRunnerId,
@@ -2039,6 +2185,23 @@ function workflowRunSelect(clause) {
2039
2185
  LEFT JOIN run_bindings b ON b.run_id = r.run_id
2040
2186
  JOIN leases l ON l.resource_id = r.resource_id ${clause}`;
2041
2187
  }
2188
+ function workflowRunViewSelect(clause) {
2189
+ return `SELECT r.run_id AS runId, d.workflow_name AS workflowName,
2190
+ r.workflow_ref AS workflowRef, r.status AS runStatus, r.paused,
2191
+ r.definition_digest AS definitionDigest, q.status,
2192
+ b.origin_session_id AS originSessionId, b.execution_mode AS executionMode,
2193
+ r.parent_run_id AS parentRunId, q.error_code AS errorCode,
2194
+ CAST(error.content AS TEXT) AS errorMessage,
2195
+ q.created_at AS createdAt, q.updated_at AS updatedAt,
2196
+ q.started_at AS startedAt, q.finished_at AS finishedAt,
2197
+ source.source_type AS sourceType, source.source_ref AS sourceRef,
2198
+ source.source_revision AS sourceRevision
2199
+ FROM runs r JOIN workflow_definitions d ON d.definition_digest = r.definition_digest
2200
+ JOIN run_queue q ON q.run_id = r.run_id
2201
+ LEFT JOIN run_bindings b ON b.run_id = r.run_id
2202
+ LEFT JOIN run_sources source ON source.run_id = r.run_id AND source.mount_path = ''
2203
+ LEFT JOIN blobs error ON error.blob_hash = q.error_hash ${clause}`;
2204
+ }
2042
2205
  function turnIntentSelect(clause) {
2043
2206
  return `SELECT t.turn_intent_id AS intentId, t.source_event_id AS sourceEventId,
2044
2207
  t.run_id AS runId, t.workflow_ref AS workflowRef,
@@ -2071,6 +2234,59 @@ function isControllerRow(value) {
2071
2234
  function isLeaseRow(value) {
2072
2235
  return isRecord(value);
2073
2236
  }
2237
+ function workflowRunViewRecord(row) {
2238
+ const root = row.sourceType === "builtin"
2239
+ ? { kind: "builtin", id: row.sourceRef, revision: row.sourceRevision }
2240
+ : { kind: "file", path: row.sourceRef, hash: row.sourceRevision };
2241
+ return {
2242
+ runId: row.runId,
2243
+ workflowName: row.workflowName,
2244
+ workflowSourceRef: row.workflowRef,
2245
+ workflowSource: { root, mounted: [] },
2246
+ initialized: row.runStatus !== "queued",
2247
+ definitionDigest: `sha256:${row.definitionDigest.toString("hex")}`,
2248
+ runStateStatus: row.runStatus,
2249
+ paused: row.paused === 1,
2250
+ status: row.status,
2251
+ originSessionId: row.executionMode === "headless" ? null : row.originSessionId,
2252
+ executionMode: row.executionMode,
2253
+ parentRunId: row.parentRunId,
2254
+ errorCode: row.errorCode,
2255
+ errorMessage: row.errorMessage,
2256
+ createdAt: new Date(row.createdAt).toISOString(),
2257
+ updatedAt: new Date(row.updatedAt).toISOString(),
2258
+ startedAt: row.startedAt === null ? null : new Date(row.startedAt).toISOString(),
2259
+ finishedAt: row.finishedAt === null ? null : new Date(row.finishedAt).toISOString(),
2260
+ };
2261
+ }
2262
+ function isWorkflowRunViewRow(value) {
2263
+ return (isRecord(value) &&
2264
+ typeof value.runId === "string" &&
2265
+ typeof value.workflowName === "string" &&
2266
+ typeof value.workflowRef === "string" &&
2267
+ typeof value.runStatus === "string" &&
2268
+ typeof value.paused === "number" &&
2269
+ Buffer.isBuffer(value.definitionDigest) &&
2270
+ typeof value.status === "string" &&
2271
+ (value.originSessionId === null || typeof value.originSessionId === "string") &&
2272
+ (value.executionMode === "interactive" || value.executionMode === "headless") &&
2273
+ (value.parentRunId === null || typeof value.parentRunId === "string") &&
2274
+ (value.errorCode === null || typeof value.errorCode === "string") &&
2275
+ (value.errorMessage === null || typeof value.errorMessage === "string") &&
2276
+ typeof value.createdAt === "number" &&
2277
+ typeof value.updatedAt === "number" &&
2278
+ (value.startedAt === null || typeof value.startedAt === "number") &&
2279
+ (value.finishedAt === null || typeof value.finishedAt === "number") &&
2280
+ (value.sourceType === "builtin" || value.sourceType === "file") &&
2281
+ typeof value.sourceRef === "string" &&
2282
+ typeof value.sourceRevision === "string");
2283
+ }
2284
+ function isRunListRevisionRow(value) {
2285
+ return (isRecord(value) &&
2286
+ typeof value.count === "number" &&
2287
+ typeof value.revisionSum === "number" &&
2288
+ typeof value.updatedAt === "number");
2289
+ }
2074
2290
  function isRunRow(value) {
2075
2291
  return isRecord(value);
2076
2292
  }