@osolmaz/pi-workflows 0.15.3 → 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 (118) 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 +43 -0
  24. package/dist/controllers/sqlite.js +137 -2
  25. package/dist/controllers/sqlite.js.map +1 -1
  26. package/dist/extension/index.d.ts +1 -0
  27. package/dist/extension/index.js +278 -183
  28. package/dist/extension/index.js.map +1 -1
  29. package/dist/extension/session-view.d.ts +7 -2
  30. package/dist/extension/session-view.js +60 -52
  31. package/dist/extension/session-view.js.map +1 -1
  32. package/dist/extension/widget.d.ts +2 -1
  33. package/dist/extension/widget.js +14 -7
  34. package/dist/extension/widget.js.map +1 -1
  35. package/dist/host/child-worker-supervisor.js +1 -1
  36. package/dist/host/child-worker-supervisor.js.map +1 -1
  37. package/dist/host/resolver-entry.d.ts +2 -23
  38. package/dist/host/resolver-entry.js +1 -1
  39. package/dist/host/resolver-entry.js.map +1 -1
  40. package/dist/host/runner.d.ts +11 -0
  41. package/dist/host/runner.js +473 -17
  42. package/dist/host/runner.js.map +1 -1
  43. package/dist/host/state.d.ts +6 -3
  44. package/dist/host/state.js +52 -26
  45. package/dist/host/state.js.map +1 -1
  46. package/dist/host/view.d.ts +73 -0
  47. package/dist/host/view.js +871 -0
  48. package/dist/host/view.js.map +1 -0
  49. package/dist/host/worker-protocol.js +1 -1
  50. package/dist/host/worker-protocol.js.map +1 -1
  51. package/dist/state/database.d.ts +1 -0
  52. package/dist/state/database.js +15 -0
  53. package/dist/state/database.js.map +1 -1
  54. package/dist/state/prune.d.ts +3 -1
  55. package/dist/state/prune.js +6 -8
  56. package/dist/state/prune.js.map +1 -1
  57. package/dist/state/schema.js +12 -1
  58. package/dist/state/schema.js.map +1 -1
  59. package/dist/viewer/backup.d.ts +2 -0
  60. package/dist/viewer/backup.js +28 -0
  61. package/dist/viewer/backup.js.map +1 -0
  62. package/dist/viewer/cli.d.ts +4 -0
  63. package/dist/viewer/cli.js +150 -170
  64. package/dist/viewer/cli.js.map +1 -1
  65. package/dist/viewer/tui.d.ts +5 -7
  66. package/dist/viewer/tui.js +188 -108
  67. package/dist/viewer/tui.js.map +1 -1
  68. package/dist/workflows/store.d.ts +62 -1
  69. package/dist/workflows/store.js +350 -44
  70. package/dist/workflows/store.js.map +1 -1
  71. package/docs/2026-09-01-unified-workflow-client-plan.md +381 -0
  72. package/docs/2026-09-02-installed-live-e2e-plan.md +225 -0
  73. package/docs/SQLITE_STATE.md +13 -11
  74. package/docs/WORKFLOW_HOST.md +74 -61
  75. package/docs/development.md +2 -1
  76. package/docs/live-replay-protocol.md +70 -132
  77. package/docs/tui-viewer.md +10 -14
  78. package/docs/workflows.md +44 -1
  79. package/herdr-plugin.toml +1 -1
  80. package/package.json +9 -3
  81. package/protocol/client.v1.schema.json +137 -0
  82. package/protocol/fixtures/client-v1.json +23 -0
  83. package/src/client/activity.ts +6 -0
  84. package/src/client/client.ts +935 -0
  85. package/src/client/index.ts +24 -0
  86. package/src/client/materialize.ts +228 -0
  87. package/src/client/protocol.ts +327 -0
  88. package/src/client/resolver.ts +26 -0
  89. package/src/client/view.ts +138 -0
  90. package/src/controllers/sqlite.ts +213 -2
  91. package/src/extension/index.ts +349 -208
  92. package/src/extension/session-view.ts +73 -50
  93. package/src/extension/widget.ts +16 -8
  94. package/src/host/child-worker-supervisor.ts +1 -1
  95. package/src/host/resolver-entry.ts +11 -26
  96. package/src/host/runner.ts +648 -48
  97. package/src/host/state.ts +71 -43
  98. package/src/host/view.ts +1084 -0
  99. package/src/host/worker-protocol.ts +1 -1
  100. package/src/state/database.ts +13 -0
  101. package/src/state/prune.ts +11 -11
  102. package/src/state/schema.ts +12 -1
  103. package/src/viewer/backup.ts +29 -0
  104. package/src/viewer/cli.ts +171 -185
  105. package/src/viewer/tui.ts +196 -124
  106. package/src/workflows/store.ts +500 -45
  107. package/dist/host/client.d.ts +0 -48
  108. package/dist/host/client.js +0 -216
  109. package/dist/host/client.js.map +0 -1
  110. package/dist/host/protocol.d.ts +0 -38
  111. package/dist/host/protocol.js +0 -156
  112. package/dist/host/protocol.js.map +0 -1
  113. package/dist/viewer/watch.d.ts +0 -6
  114. package/dist/viewer/watch.js +0 -46
  115. package/dist/viewer/watch.js.map +0 -1
  116. package/src/host/client.ts +0 -293
  117. package/src/host/protocol.ts +0 -196
  118. 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;
@@ -213,6 +234,34 @@ type RunRow = {
213
234
  ownerId: string | null;
214
235
  claimExpiresAt: number | null;
215
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
+
216
265
  type RunSourceIdentityRow = {
217
266
  mountPath: string;
218
267
  sourceType: "builtin" | "file";
@@ -1239,6 +1288,40 @@ export class SqliteControllerStore implements ControllerStore {
1239
1288
  return row === undefined ? undefined : this.mapWorkflowRun(row);
1240
1289
  }
1241
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
+
1242
1325
  workflowRunProjectPath(runId: string): string | undefined {
1243
1326
  const row = this.state.connection
1244
1327
  .prepare(
@@ -1270,11 +1353,11 @@ export class SqliteControllerStore implements ControllerStore {
1270
1353
  clauses.push(`r.run_id NOT IN (${options.excludeRunIds.map(() => "?").join(", ")})`);
1271
1354
  params.push(...options.excludeRunIds);
1272
1355
  }
1273
- params.push(options.limit ?? 100);
1356
+ if (options.limit !== undefined) params.push(options.limit);
1274
1357
  const rows = this.state.connection
1275
1358
  .prepare(
1276
1359
  workflowRunSelect(
1277
- `${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 ?"}`,
1278
1361
  ),
1279
1362
  )
1280
1363
  .all(...params);
@@ -1292,6 +1375,14 @@ export class SqliteControllerStore implements ControllerStore {
1292
1375
  return isRunRow(row) ? this.mapWorkflowRun(row) : undefined;
1293
1376
  }
1294
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
+
1295
1386
  claimWorkflowRun(options: {
1296
1387
  runId: string;
1297
1388
  runnerId: string;
@@ -2198,6 +2289,24 @@ export class SqliteControllerStore implements ControllerStore {
2198
2289
  return claimed;
2199
2290
  }
2200
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
+
2201
2310
  makeWorkflowTurnIntentEligible(options: {
2202
2311
  intentId: string;
2203
2312
  fallbackFacts: WorkflowTurnIntentFacts;
@@ -2354,6 +2463,22 @@ export class SqliteControllerStore implements ControllerStore {
2354
2463
  return this.requireNotification(options.runId, options.attemptId, options.notificationIndex);
2355
2464
  }
2356
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
+
2357
2482
  listPendingWorkflowNotifications(options: {
2358
2483
  targetSessionId: string;
2359
2484
  limit?: number;
@@ -2899,6 +3024,13 @@ export class SqliteControllerStore implements ControllerStore {
2899
3024
  }
2900
3025
  }
2901
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
+
2902
3034
  /* istanbul ignore next -- pure projection covered by integration tests */
2903
3035
  private mapWorkflowRun(row: RunRow): WorkflowRunQueueRecord {
2904
3036
  return {
@@ -3596,6 +3728,24 @@ function workflowRunSelect(clause: string): string {
3596
3728
  JOIN leases l ON l.resource_id = r.resource_id ${clause}`;
3597
3729
  }
3598
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
+
3599
3749
  function turnIntentSelect(clause: string): string {
3600
3750
  return `SELECT t.turn_intent_id AS intentId, t.source_event_id AS sourceEventId,
3601
3751
  t.run_id AS runId, t.workflow_ref AS workflowRef,
@@ -3687,6 +3837,67 @@ function isControllerRow(value: unknown): value is ControllerRow {
3687
3837
  function isLeaseRow(value: unknown): value is LeaseRow {
3688
3838
  return isRecord(value);
3689
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
+
3690
3901
  function isRunRow(value: unknown): value is RunRow {
3691
3902
  return isRecord(value);
3692
3903
  }