@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.
- package/README.md +6 -6
- package/dist/client/activity.d.ts +2 -0
- package/dist/client/activity.js +6 -0
- package/dist/client/activity.js.map +1 -0
- package/dist/client/client.d.ts +101 -0
- package/dist/client/client.js +733 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/materialize.d.ts +7 -0
- package/dist/client/materialize.js +177 -0
- package/dist/client/materialize.js.map +1 -0
- package/dist/client/protocol.d.ts +60 -0
- package/dist/client/protocol.js +269 -0
- package/dist/client/protocol.js.map +1 -0
- package/dist/client/resolver.d.ts +23 -0
- package/dist/client/resolver.js +2 -0
- package/dist/client/resolver.js.map +1 -0
- package/dist/client/view.d.ts +118 -0
- package/dist/client/view.js +3 -0
- package/dist/client/view.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +64 -0
- package/dist/controllers/sqlite.js +219 -3
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +1 -0
- package/dist/extension/index.js +384 -156
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/session-delivery.d.ts +6 -0
- package/dist/extension/session-delivery.js +80 -25
- package/dist/extension/session-delivery.js.map +1 -1
- package/dist/extension/session-view.d.ts +21 -0
- package/dist/extension/session-view.js +127 -0
- package/dist/extension/session-view.js.map +1 -0
- package/dist/extension/widget.d.ts +2 -1
- package/dist/extension/widget.js +15 -7
- package/dist/extension/widget.js.map +1 -1
- package/dist/host/child-worker-supervisor.js +1 -1
- package/dist/host/child-worker-supervisor.js.map +1 -1
- package/dist/host/resolver-entry.d.ts +2 -23
- package/dist/host/resolver-entry.js +1 -1
- package/dist/host/resolver-entry.js.map +1 -1
- package/dist/host/runner.d.ts +12 -0
- package/dist/host/runner.js +565 -43
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +8 -3
- package/dist/host/state.js +59 -27
- package/dist/host/state.js.map +1 -1
- package/dist/host/view.d.ts +73 -0
- package/dist/host/view.js +871 -0
- package/dist/host/view.js.map +1 -0
- package/dist/host/worker-protocol.js +1 -1
- package/dist/host/worker-protocol.js.map +1 -1
- package/dist/state/database.d.ts +1 -0
- package/dist/state/database.js +15 -0
- package/dist/state/database.js.map +1 -1
- package/dist/state/prune.d.ts +3 -1
- package/dist/state/prune.js +6 -8
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +12 -1
- package/dist/state/schema.js.map +1 -1
- package/dist/viewer/backup.d.ts +2 -0
- package/dist/viewer/backup.js +28 -0
- package/dist/viewer/backup.js.map +1 -0
- package/dist/viewer/cli.d.ts +4 -0
- package/dist/viewer/cli.js +150 -170
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/tui.d.ts +5 -7
- package/dist/viewer/tui.js +188 -108
- package/dist/viewer/tui.js.map +1 -1
- package/dist/workflows/store.d.ts +62 -1
- package/dist/workflows/store.js +350 -44
- package/dist/workflows/store.js.map +1 -1
- package/docs/2026-09-01-restore-session-delivery-controls-plan.md +139 -0
- package/docs/2026-09-01-unified-workflow-client-plan.md +381 -0
- package/docs/2026-09-02-installed-live-e2e-plan.md +225 -0
- package/docs/SQLITE_STATE.md +13 -11
- package/docs/WORKFLOW_HOST.md +81 -64
- package/docs/WORKFLOW_STEP_MESSAGES.md +4 -4
- package/docs/development.md +2 -1
- package/docs/live-replay-protocol.md +70 -132
- package/docs/tui-viewer.md +10 -14
- package/docs/workflows.md +56 -3
- package/herdr-plugin.toml +1 -1
- package/package.json +9 -3
- package/protocol/client.v1.schema.json +137 -0
- package/protocol/fixtures/client-v1.json +23 -0
- package/src/client/activity.ts +6 -0
- package/src/client/client.ts +935 -0
- package/src/client/index.ts +24 -0
- package/src/client/materialize.ts +228 -0
- package/src/client/protocol.ts +327 -0
- package/src/client/resolver.ts +26 -0
- package/src/client/view.ts +138 -0
- package/src/controllers/sqlite.ts +342 -3
- package/src/extension/index.ts +482 -171
- package/src/extension/session-delivery.ts +88 -25
- package/src/extension/session-view.ts +154 -0
- package/src/extension/widget.ts +18 -9
- package/src/host/child-worker-supervisor.ts +1 -1
- package/src/host/resolver-entry.ts +11 -26
- package/src/host/runner.ts +749 -75
- package/src/host/state.ts +82 -44
- package/src/host/view.ts +1084 -0
- package/src/host/worker-protocol.ts +1 -1
- package/src/state/database.ts +13 -0
- package/src/state/prune.ts +11 -11
- package/src/state/schema.ts +12 -1
- package/src/viewer/backup.ts +29 -0
- package/src/viewer/cli.ts +171 -185
- package/src/viewer/tui.ts +196 -124
- package/src/workflows/store.ts +500 -45
- package/dist/host/client.d.ts +0 -48
- package/dist/host/client.js +0 -216
- package/dist/host/client.js.map +0 -1
- package/dist/host/protocol.d.ts +0 -38
- package/dist/host/protocol.js +0 -156
- package/dist/host/protocol.js.map +0 -1
- package/dist/viewer/watch.d.ts +0 -6
- package/dist/viewer/watch.js +0 -46
- package/dist/viewer/watch.js.map +0 -1
- package/src/host/client.ts +0 -293
- package/src/host/protocol.ts +0 -196
- 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
|
|
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
|
}
|