@osolmaz/pi-workflows 0.15.3 → 0.16.1
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 +24 -8
- package/dist/channels/adapter-entry.d.ts +3 -0
- package/dist/channels/adapter-entry.js +203 -0
- package/dist/channels/adapter-entry.js.map +1 -0
- package/dist/channels/config.d.ts +43 -0
- package/dist/channels/config.js +234 -0
- package/dist/channels/config.js.map +1 -0
- package/dist/channels/protocol.d.ts +94 -0
- package/dist/channels/protocol.js +219 -0
- package/dist/channels/protocol.js.map +1 -0
- package/dist/channels/telegram.d.ts +46 -0
- package/dist/channels/telegram.js +285 -0
- package/dist/channels/telegram.js.map +1 -0
- 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 +102 -0
- package/dist/client/client.js +737 -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 +275 -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 +144 -0
- package/dist/client/view.js +3 -0
- package/dist/client/view.js.map +1 -0
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +47 -157
- package/dist/controllers/sqlite.js +230 -406
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +23 -1
- package/dist/extension/index.js +734 -555
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +95 -0
- package/dist/extension/recorder.js +530 -0
- package/dist/extension/recorder.js.map +1 -0
- package/dist/extension/remote-recorder-store.d.ts +25 -0
- package/dist/extension/remote-recorder-store.js +81 -0
- package/dist/extension/remote-recorder-store.js.map +1 -0
- package/dist/extension/session-events.d.ts +134 -0
- package/dist/extension/session-events.js +60 -0
- package/dist/extension/session-events.js.map +1 -0
- package/dist/extension/session-view.d.ts +9 -2
- package/dist/extension/session-view.js +117 -51
- package/dist/extension/session-view.js.map +1 -1
- package/dist/extension/step-message.d.ts +6 -4
- package/dist/extension/step-message.js +12 -6
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.d.ts +3 -2
- package/dist/extension/widget.js +41 -24
- package/dist/extension/widget.js.map +1 -1
- package/dist/extension/workflow-message-coordinator.d.ts +27 -0
- package/dist/extension/workflow-message-coordinator.js +294 -0
- package/dist/extension/workflow-message-coordinator.js.map +1 -0
- package/dist/host/channel-effects.d.ts +72 -0
- package/dist/host/channel-effects.js +271 -0
- package/dist/host/channel-effects.js.map +1 -0
- package/dist/host/channel-supervisor.d.ts +21 -0
- package/dist/host/channel-supervisor.js +54 -0
- package/dist/host/channel-supervisor.js.map +1 -0
- 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 +54 -8
- package/dist/host/runner.js +1997 -305
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +25 -19
- package/dist/host/state.js +303 -89
- package/dist/host/state.js.map +1 -1
- package/dist/host/view.d.ts +83 -0
- package/dist/host/view.js +975 -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 +9 -33
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +88 -98
- package/dist/state/schema.js.map +1 -1
- package/dist/state/workflow-messages.d.ts +96 -0
- package/dist/state/workflow-messages.js +335 -0
- package/dist/state/workflow-messages.js.map +1 -0
- 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/render.js +1 -1
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/tui.d.ts +5 -7
- package/dist/viewer/tui.js +245 -108
- package/dist/viewer/tui.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +2 -0
- package/dist/workflows/human-decision.js +8 -2
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/settings.d.ts +1 -8
- package/dist/workflows/settings.js.map +1 -1
- package/dist/workflows/store.d.ts +67 -20
- package/dist/workflows/store.js +451 -378
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +21 -0
- package/dist/workflows/tool-input.js +23 -1
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +5 -5
- package/dist/workflows/workflow-message-content.d.ts +38 -0
- package/dist/workflows/workflow-message-content.js +157 -0
- package/dist/workflows/workflow-message-content.js.map +1 -0
- package/docs/2026-08-18-herdr-piw-plan.md +2 -1
- package/docs/2026-08-20-durable-workflow-launch-plan.md +1 -1
- package/docs/2026-08-25-workflow-follow-ups.md +26 -50
- package/docs/2026-08-30-out-of-process-workflow-host-plan.md +3 -3
- package/docs/2026-09-01-restore-session-delivery-controls-plan.md +4 -0
- package/docs/2026-09-01-unified-workflow-client-plan.md +385 -0
- package/docs/2026-09-02-installed-live-e2e-plan.md +239 -0
- package/docs/2026-09-02-unify-workflow-messages-plan.md +562 -0
- package/docs/DEFERRED_TURNS.md +66 -294
- package/docs/HUMAN_DECISIONS.md +29 -30
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +4 -11
- package/docs/SQLITE_STATE.md +31 -25
- package/docs/WORKFLOW_HOST.md +155 -77
- package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
- package/docs/development.md +2 -1
- package/docs/live-replay-protocol.md +74 -132
- package/docs/tui-viewer.md +12 -14
- package/docs/workflows.md +78 -28
- package/herdr-plugin.toml +1 -1
- package/package.json +9 -3
- package/plugins/herdr/viewer.mjs +14 -5
- package/protocol/client.v1.schema.json +137 -0
- package/protocol/fixtures/client-v1.json +23 -0
- package/src/channels/adapter-entry.ts +220 -0
- package/src/channels/config.ts +296 -0
- package/src/channels/protocol.ts +333 -0
- package/src/channels/telegram.ts +335 -0
- package/src/client/activity.ts +6 -0
- package/src/client/client.ts +939 -0
- package/src/client/index.ts +25 -0
- package/src/client/materialize.ts +228 -0
- package/src/client/protocol.ts +333 -0
- package/src/client/resolver.ts +26 -0
- package/src/client/view.ts +163 -0
- package/src/controllers/index.ts +0 -1
- package/src/controllers/sqlite.ts +339 -808
- package/src/extension/index.ts +853 -665
- package/src/extension/recorder.ts +687 -0
- package/src/extension/remote-recorder-store.ts +126 -0
- package/src/extension/session-events.ts +119 -0
- package/src/extension/session-view.ts +157 -51
- package/src/extension/step-message.ts +19 -9
- package/src/extension/widget.ts +53 -25
- package/src/extension/workflow-message-coordinator.ts +334 -0
- package/src/host/channel-effects.ts +465 -0
- package/src/host/channel-supervisor.ts +73 -0
- package/src/host/child-worker-supervisor.ts +1 -1
- package/src/host/resolver-entry.ts +11 -26
- package/src/host/runner.ts +2639 -450
- package/src/host/state.ts +404 -137
- package/src/host/view.ts +1212 -0
- package/src/host/worker-protocol.ts +1 -1
- package/src/state/database.ts +13 -0
- package/src/state/prune.ts +14 -42
- package/src/state/schema.ts +88 -98
- package/src/state/workflow-messages.ts +548 -0
- package/src/viewer/backup.ts +29 -0
- package/src/viewer/cli.ts +171 -185
- package/src/viewer/render.ts +1 -5
- package/src/viewer/tui.ts +261 -124
- package/src/workflows/human-decision.ts +18 -2
- package/src/workflows/index.ts +0 -1
- package/src/workflows/settings.ts +1 -20
- package/src/workflows/store.ts +650 -511
- package/src/workflows/tool-input.ts +36 -1
- package/src/workflows/types.ts +5 -5
- package/src/workflows/workflow-message-content.ts +197 -0
- package/dist/extension/decision-channels.d.ts +0 -134
- package/dist/extension/decision-channels.js +0 -1307
- package/dist/extension/decision-channels.js.map +0 -1
- 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/extension/decision-channels.ts +0 -1826
- package/src/host/client.ts +0 -293
- package/src/host/protocol.ts +0 -196
- package/src/viewer/watch.ts +0 -51
|
@@ -30,8 +30,6 @@ import type {
|
|
|
30
30
|
JsonObject,
|
|
31
31
|
} from "./types.js";
|
|
32
32
|
|
|
33
|
-
const TURN_INTENT_FACTS_SCHEMA = "pi-workflows.deferred-turn-facts.v1";
|
|
34
|
-
|
|
35
33
|
export type WorkflowRunLaunchStatus =
|
|
36
34
|
| "queued"
|
|
37
35
|
| "starting"
|
|
@@ -54,6 +52,9 @@ export type WorkflowRunReservationOptions = {
|
|
|
54
52
|
originSessionId: string;
|
|
55
53
|
executionMode?: "interactive" | "headless";
|
|
56
54
|
parentRunId?: string;
|
|
55
|
+
lineageKind?: "continuation" | "restart";
|
|
56
|
+
restartNumber?: number;
|
|
57
|
+
parentTerminalFingerprint?: string;
|
|
57
58
|
now?: string;
|
|
58
59
|
};
|
|
59
60
|
|
|
@@ -62,6 +63,31 @@ export type WorkflowRunClaimOptions = WorkflowRunReservationOptions & {
|
|
|
62
63
|
leaseMs: number;
|
|
63
64
|
};
|
|
64
65
|
|
|
66
|
+
export type WorkflowRunQueueViewRecord = {
|
|
67
|
+
runId: string;
|
|
68
|
+
workflowName: string;
|
|
69
|
+
workflowSourceRef: string;
|
|
70
|
+
workflowSource: unknown;
|
|
71
|
+
initialized: boolean;
|
|
72
|
+
definitionDigest: string;
|
|
73
|
+
runStateStatus: string;
|
|
74
|
+
paused: boolean;
|
|
75
|
+
status: WorkflowRunLaunchStatus;
|
|
76
|
+
originSessionId: string | null;
|
|
77
|
+
executionMode: "interactive" | "headless";
|
|
78
|
+
parentRunId: string | null;
|
|
79
|
+
rootRunId: string;
|
|
80
|
+
lineageKind: "continuation" | "restart" | null;
|
|
81
|
+
restartNumber: number;
|
|
82
|
+
parentTerminalFingerprint: string | null;
|
|
83
|
+
errorCode: string | null;
|
|
84
|
+
errorMessage: string | null;
|
|
85
|
+
createdAt: string;
|
|
86
|
+
updatedAt: string;
|
|
87
|
+
startedAt: string | null;
|
|
88
|
+
finishedAt: string | null;
|
|
89
|
+
};
|
|
90
|
+
|
|
65
91
|
export type WorkflowRunQueueRecord = {
|
|
66
92
|
runId: string;
|
|
67
93
|
workflowName: string;
|
|
@@ -80,6 +106,10 @@ export type WorkflowRunQueueRecord = {
|
|
|
80
106
|
originSessionId: string | null;
|
|
81
107
|
executionMode: "interactive" | "headless";
|
|
82
108
|
parentRunId: string | null;
|
|
109
|
+
rootRunId: string;
|
|
110
|
+
lineageKind: "continuation" | "restart" | null;
|
|
111
|
+
restartNumber: number;
|
|
112
|
+
parentTerminalFingerprint: string | null;
|
|
83
113
|
errorCode: string | null;
|
|
84
114
|
errorMessage: string | null;
|
|
85
115
|
createdAt: string;
|
|
@@ -98,62 +128,6 @@ export type WorkflowRunPreparationResult =
|
|
|
98
128
|
run: WorkflowRunQueueRecord;
|
|
99
129
|
};
|
|
100
130
|
|
|
101
|
-
export type WorkflowNotificationRecord = {
|
|
102
|
-
notificationId: string;
|
|
103
|
-
runId: string;
|
|
104
|
-
nodeId: string;
|
|
105
|
-
attemptId: string;
|
|
106
|
-
notificationIndex: number;
|
|
107
|
-
targetSessionId: string;
|
|
108
|
-
kind: "progress" | "final";
|
|
109
|
-
content: string;
|
|
110
|
-
createdAt: string;
|
|
111
|
-
deliveryClaimExpiresAt: string | null;
|
|
112
|
-
deliveredAt: string | null;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
export type WorkflowTurnIntentCause =
|
|
116
|
-
| "agentCancelled"
|
|
117
|
-
| "timedOut"
|
|
118
|
-
| "failed"
|
|
119
|
-
| "launchFailed"
|
|
120
|
-
| "controllerInterrupted"
|
|
121
|
-
| "claimLost"
|
|
122
|
-
| "terminal"
|
|
123
|
-
| "cancelled";
|
|
124
|
-
|
|
125
|
-
export type WorkflowTurnIntentResolution = "workflowPrompt" | "presentation" | "fallback";
|
|
126
|
-
|
|
127
|
-
export type WorkflowTurnIntentFacts = JsonObject & {
|
|
128
|
-
schema: typeof TURN_INTENT_FACTS_SCHEMA;
|
|
129
|
-
workflowName: string;
|
|
130
|
-
runId: string;
|
|
131
|
-
observedState: string;
|
|
132
|
-
cause: WorkflowTurnIntentCause;
|
|
133
|
-
nodeId: string | null;
|
|
134
|
-
attemptId: string | null;
|
|
135
|
-
reason: string | null;
|
|
136
|
-
handoff: boolean;
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
export type WorkflowTurnIntentRecord = {
|
|
140
|
-
intentId: string;
|
|
141
|
-
sourceEventId: string;
|
|
142
|
-
runId: string;
|
|
143
|
-
workflowRef: string;
|
|
144
|
-
targetSessionId: string;
|
|
145
|
-
cause: WorkflowTurnIntentCause;
|
|
146
|
-
nodeId: string | null;
|
|
147
|
-
attemptId: string | null;
|
|
148
|
-
fallbackFacts: WorkflowTurnIntentFacts;
|
|
149
|
-
requestedAt: string;
|
|
150
|
-
eligibleAt: string | null;
|
|
151
|
-
resolvedAt: string | null;
|
|
152
|
-
resolution: WorkflowTurnIntentResolution | null;
|
|
153
|
-
resolutionMessageId: string | null;
|
|
154
|
-
deliveryClaimExpiresAt: string | null;
|
|
155
|
-
};
|
|
156
|
-
|
|
157
131
|
export type RunEventRecord = {
|
|
158
132
|
seq: number;
|
|
159
133
|
recordedAt: string;
|
|
@@ -205,6 +179,10 @@ type RunRow = {
|
|
|
205
179
|
originSessionId: string | null;
|
|
206
180
|
executionMode: "interactive" | "headless";
|
|
207
181
|
parentRunId: string | null;
|
|
182
|
+
rootRunId: string;
|
|
183
|
+
lineageKind: "continuation" | "restart" | null;
|
|
184
|
+
restartNumber: number;
|
|
185
|
+
parentTerminalFingerprint: Buffer | null;
|
|
208
186
|
createdAt: number;
|
|
209
187
|
updatedAt: number;
|
|
210
188
|
startedAt: number | null;
|
|
@@ -213,6 +191,38 @@ type RunRow = {
|
|
|
213
191
|
ownerId: string | null;
|
|
214
192
|
claimExpiresAt: number | null;
|
|
215
193
|
};
|
|
194
|
+
type WorkflowRunViewRow = {
|
|
195
|
+
runId: string;
|
|
196
|
+
workflowName: string;
|
|
197
|
+
workflowRef: string;
|
|
198
|
+
runStatus: string;
|
|
199
|
+
paused: number;
|
|
200
|
+
definitionDigest: Buffer;
|
|
201
|
+
status: WorkflowRunLaunchStatus;
|
|
202
|
+
originSessionId: string | null;
|
|
203
|
+
executionMode: "interactive" | "headless";
|
|
204
|
+
parentRunId: string | null;
|
|
205
|
+
rootRunId: string;
|
|
206
|
+
lineageKind: "continuation" | "restart" | null;
|
|
207
|
+
restartNumber: number;
|
|
208
|
+
parentTerminalFingerprint: Buffer | null;
|
|
209
|
+
errorCode: string | null;
|
|
210
|
+
errorMessage: string | null;
|
|
211
|
+
createdAt: number;
|
|
212
|
+
updatedAt: number;
|
|
213
|
+
startedAt: number | null;
|
|
214
|
+
finishedAt: number | null;
|
|
215
|
+
sourceType: "builtin" | "file";
|
|
216
|
+
sourceRef: string;
|
|
217
|
+
sourceRevision: string;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
type RunListRevisionRow = {
|
|
221
|
+
count: number;
|
|
222
|
+
revisionSum: number;
|
|
223
|
+
updatedAt: number;
|
|
224
|
+
};
|
|
225
|
+
|
|
216
226
|
type RunSourceIdentityRow = {
|
|
217
227
|
mountPath: string;
|
|
218
228
|
sourceType: "builtin" | "file";
|
|
@@ -1080,39 +1090,77 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1080
1090
|
if (this.getWorkflowRun(options.runId) !== undefined) {
|
|
1081
1091
|
throw new Error(`Workflow run already reserved: ${options.runId}`);
|
|
1082
1092
|
}
|
|
1093
|
+
const lineageKind =
|
|
1094
|
+
options.parentRunId === undefined ? null : (options.lineageKind ?? "continuation");
|
|
1095
|
+
if (options.parentRunId === undefined && options.lineageKind !== undefined) {
|
|
1096
|
+
throw new Error("A root workflow run cannot declare lineage");
|
|
1097
|
+
}
|
|
1098
|
+
let rootRunId = options.runId;
|
|
1099
|
+
let restartNumber = 0;
|
|
1100
|
+
let parentTerminalFingerprint: Buffer | null = null;
|
|
1083
1101
|
if (options.parentRunId !== undefined) {
|
|
1084
1102
|
const parent = this.requireWorkflowRunRow(options.parentRunId);
|
|
1085
1103
|
if (parent.originSessionId !== options.originSessionId) {
|
|
1086
|
-
throw new Error("
|
|
1104
|
+
throw new Error("Workflow parent belongs to another Pi session");
|
|
1087
1105
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
if (
|
|
1091
|
-
|
|
1106
|
+
rootRunId = parent.rootRunId;
|
|
1107
|
+
if (lineageKind === "restart") {
|
|
1108
|
+
if (
|
|
1109
|
+
!["completed", "failed", "timed_out", "cancelled"].includes(parent.runStatus) ||
|
|
1110
|
+
!["done", "failed", "cancelled"].includes(parent.status)
|
|
1111
|
+
) {
|
|
1112
|
+
throw new Error("Restart parent is not terminal");
|
|
1113
|
+
}
|
|
1114
|
+
if (options.parentTerminalFingerprint === undefined) {
|
|
1115
|
+
throw new Error("Restart requires the parent terminal fingerprint");
|
|
1116
|
+
}
|
|
1117
|
+
parentTerminalFingerprint = terminalFingerprintBuffer(options.parentTerminalFingerprint);
|
|
1118
|
+
restartNumber = options.restartNumber ?? parent.restartNumber + 1;
|
|
1119
|
+
if (restartNumber !== parent.restartNumber + 1) {
|
|
1120
|
+
throw new Error("Restart number does not follow its parent");
|
|
1092
1121
|
}
|
|
1093
|
-
this.state.connection
|
|
1094
|
-
.prepare(
|
|
1095
|
-
`UPDATE run_queue
|
|
1096
|
-
SET status = 'done', updated_at = ?, finished_at = ?
|
|
1097
|
-
WHERE run_id = ? AND status = 'parked'`,
|
|
1098
|
-
)
|
|
1099
|
-
.run(now, now, parent.runId);
|
|
1100
|
-
const parentRevision = this.resourceRevision(parent.resourceId);
|
|
1101
|
-
this.bumpResource(parent.resourceId, parentRevision, now);
|
|
1102
|
-
this.insertEvent(
|
|
1103
|
-
parent.resourceId,
|
|
1104
|
-
parentRevision + 1,
|
|
1105
|
-
"run.queue_done_for_continuation",
|
|
1106
|
-
"session",
|
|
1107
|
-
options.originSessionId,
|
|
1108
|
-
{ continuationRunId: options.runId },
|
|
1109
|
-
now,
|
|
1110
|
-
);
|
|
1111
|
-
} else if (parent.status === "done") {
|
|
1112
|
-
throw new Error("Continuation parent already has a reserved continuation");
|
|
1113
1122
|
} else {
|
|
1114
|
-
|
|
1123
|
+
if (
|
|
1124
|
+
options.parentTerminalFingerprint !== undefined ||
|
|
1125
|
+
(options.restartNumber !== undefined && options.restartNumber !== parent.restartNumber)
|
|
1126
|
+
) {
|
|
1127
|
+
throw new Error("Continuation lineage conflicts with restart metadata");
|
|
1128
|
+
}
|
|
1129
|
+
restartNumber = parent.restartNumber;
|
|
1130
|
+
if (parent.status === "parked") {
|
|
1131
|
+
const parentLease = this.requireLease(parent.resourceId);
|
|
1132
|
+
if (parentLease.ownerId !== null) {
|
|
1133
|
+
throw new Error("Continuation parent still has an active owner");
|
|
1134
|
+
}
|
|
1135
|
+
this.state.connection
|
|
1136
|
+
.prepare(
|
|
1137
|
+
`UPDATE run_queue
|
|
1138
|
+
SET status = 'done', updated_at = ?, finished_at = ?
|
|
1139
|
+
WHERE run_id = ? AND status = 'parked'`,
|
|
1140
|
+
)
|
|
1141
|
+
.run(now, now, parent.runId);
|
|
1142
|
+
const parentRevision = this.resourceRevision(parent.resourceId);
|
|
1143
|
+
this.bumpResource(parent.resourceId, parentRevision, now);
|
|
1144
|
+
this.insertEvent(
|
|
1145
|
+
parent.resourceId,
|
|
1146
|
+
parentRevision + 1,
|
|
1147
|
+
"run.queue_done_for_continuation",
|
|
1148
|
+
"session",
|
|
1149
|
+
options.originSessionId,
|
|
1150
|
+
{ continuationRunId: options.runId },
|
|
1151
|
+
now,
|
|
1152
|
+
);
|
|
1153
|
+
} else if (parent.status === "done") {
|
|
1154
|
+
throw new Error("Continuation parent already has a reserved continuation");
|
|
1155
|
+
} else {
|
|
1156
|
+
throw new Error(`Continuation parent queue is ${parent.status}`);
|
|
1157
|
+
}
|
|
1115
1158
|
}
|
|
1159
|
+
} else if (
|
|
1160
|
+
options.restartNumber !== undefined ||
|
|
1161
|
+
options.parentTerminalFingerprint !== undefined
|
|
1162
|
+
) {
|
|
1163
|
+
throw new Error("A root workflow run cannot declare restart metadata");
|
|
1116
1164
|
}
|
|
1117
1165
|
const resourceId = resourceIdFor("run", options.runId);
|
|
1118
1166
|
const definitionHash = this.state.putJson(options.definitionSnapshot, now);
|
|
@@ -1139,16 +1187,21 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1139
1187
|
this.state.connection
|
|
1140
1188
|
.prepare(
|
|
1141
1189
|
`INSERT INTO runs(
|
|
1142
|
-
run_id, resource_id, project_id, parent_run_id,
|
|
1190
|
+
run_id, resource_id, project_id, parent_run_id, root_run_id, lineage_kind,
|
|
1191
|
+
restart_number, parent_terminal_fingerprint, definition_digest,
|
|
1143
1192
|
workflow_ref, launch_options_hash, status, paused,
|
|
1144
1193
|
input_hash, created_at, updated_at
|
|
1145
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, 'queued', 0, ?, ?, ?)`,
|
|
1194
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', 0, ?, ?, ?)`,
|
|
1146
1195
|
)
|
|
1147
1196
|
.run(
|
|
1148
1197
|
options.runId,
|
|
1149
1198
|
resourceId,
|
|
1150
1199
|
this.requireProjectId(),
|
|
1151
1200
|
options.parentRunId ?? null,
|
|
1201
|
+
rootRunId,
|
|
1202
|
+
lineageKind,
|
|
1203
|
+
restartNumber,
|
|
1204
|
+
parentTerminalFingerprint,
|
|
1152
1205
|
definitionDigest,
|
|
1153
1206
|
options.workflowSourceRef,
|
|
1154
1207
|
launchHash,
|
|
@@ -1218,6 +1271,11 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1218
1271
|
runnerId: options.runnerId,
|
|
1219
1272
|
originSessionId: options.originSessionId,
|
|
1220
1273
|
...(options.parentRunId === undefined ? {} : { parentRunId: options.parentRunId }),
|
|
1274
|
+
...(options.lineageKind === undefined ? {} : { lineageKind: options.lineageKind }),
|
|
1275
|
+
...(options.restartNumber === undefined ? {} : { restartNumber: options.restartNumber }),
|
|
1276
|
+
...(options.parentTerminalFingerprint === undefined
|
|
1277
|
+
? {}
|
|
1278
|
+
: { parentTerminalFingerprint: options.parentTerminalFingerprint }),
|
|
1221
1279
|
...(options.now === undefined ? {} : { now: options.now }),
|
|
1222
1280
|
});
|
|
1223
1281
|
}
|
|
@@ -1239,6 +1297,40 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1239
1297
|
return row === undefined ? undefined : this.mapWorkflowRun(row);
|
|
1240
1298
|
}
|
|
1241
1299
|
|
|
1300
|
+
getWorkflowRunView(runId: string): WorkflowRunQueueViewRecord | undefined {
|
|
1301
|
+
const row = this.workflowRunViewRows("WHERE r.run_id = ?", [runId])[0];
|
|
1302
|
+
return row === undefined ? undefined : workflowRunViewRecord(row);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
workflowRunListRevision(): { total: number; revision: string } {
|
|
1306
|
+
const row = this.state.connection
|
|
1307
|
+
.prepare(
|
|
1308
|
+
`SELECT count(*) AS count, COALESCE(sum(res.revision), 0) AS revisionSum,
|
|
1309
|
+
COALESCE(max(q.updated_at), 0) AS updatedAt
|
|
1310
|
+
FROM runs r JOIN run_queue q ON q.run_id = r.run_id
|
|
1311
|
+
JOIN resources res ON res.resource_id = r.resource_id`,
|
|
1312
|
+
)
|
|
1313
|
+
.get();
|
|
1314
|
+
if (!isRunListRevisionRow(row)) throw new Error("Workflow run list revision is invalid");
|
|
1315
|
+
return {
|
|
1316
|
+
total: row.count,
|
|
1317
|
+
revision: `${row.count}:${row.revisionSum}:${row.updatedAt}`,
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
listWorkflowRunViews(options: { offset: number; limit: number }): {
|
|
1322
|
+
total: number;
|
|
1323
|
+
revision: string;
|
|
1324
|
+
runs: WorkflowRunQueueViewRecord[];
|
|
1325
|
+
} {
|
|
1326
|
+
const current = this.workflowRunListRevision();
|
|
1327
|
+
const rows = this.workflowRunViewRows("ORDER BY q.created_at DESC LIMIT ? OFFSET ?", [
|
|
1328
|
+
options.limit,
|
|
1329
|
+
options.offset,
|
|
1330
|
+
]);
|
|
1331
|
+
return { ...current, runs: rows.map(workflowRunViewRecord) };
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1242
1334
|
workflowRunProjectPath(runId: string): string | undefined {
|
|
1243
1335
|
const row = this.state.connection
|
|
1244
1336
|
.prepare(
|
|
@@ -1270,11 +1362,11 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1270
1362
|
clauses.push(`r.run_id NOT IN (${options.excludeRunIds.map(() => "?").join(", ")})`);
|
|
1271
1363
|
params.push(...options.excludeRunIds);
|
|
1272
1364
|
}
|
|
1273
|
-
params.push(options.limit
|
|
1365
|
+
if (options.limit !== undefined) params.push(options.limit);
|
|
1274
1366
|
const rows = this.state.connection
|
|
1275
1367
|
.prepare(
|
|
1276
1368
|
workflowRunSelect(
|
|
1277
|
-
`${clauses.length === 0 ? "" : `WHERE ${clauses.join(" AND ")}`} ORDER BY q.created_at DESC LIMIT
|
|
1369
|
+
`${clauses.length === 0 ? "" : `WHERE ${clauses.join(" AND ")}`} ORDER BY q.created_at DESC${options.limit === undefined ? "" : " LIMIT ?"}`,
|
|
1278
1370
|
),
|
|
1279
1371
|
)
|
|
1280
1372
|
.all(...params);
|
|
@@ -1292,6 +1384,14 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1292
1384
|
return isRunRow(row) ? this.mapWorkflowRun(row) : undefined;
|
|
1293
1385
|
}
|
|
1294
1386
|
|
|
1387
|
+
findSessionReservationView(sessionId: string): WorkflowRunQueueViewRecord | undefined {
|
|
1388
|
+
const row = this.workflowRunViewRows(
|
|
1389
|
+
"WHERE b.origin_session_id = ? AND q.status NOT IN ('done', 'failed', 'cancelled') ORDER BY q.created_at DESC LIMIT 1",
|
|
1390
|
+
[sessionId],
|
|
1391
|
+
)[0];
|
|
1392
|
+
return row === undefined ? undefined : workflowRunViewRecord(row);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1295
1395
|
claimWorkflowRun(options: {
|
|
1296
1396
|
runId: string;
|
|
1297
1397
|
runnerId: string;
|
|
@@ -1345,6 +1445,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1345
1445
|
|
|
1346
1446
|
beginWorkflowRunInteractionTimeout(options: {
|
|
1347
1447
|
runId: string;
|
|
1448
|
+
targetSessionId: string;
|
|
1348
1449
|
claimToken: string;
|
|
1349
1450
|
now?: string;
|
|
1350
1451
|
}): boolean {
|
|
@@ -1365,18 +1466,24 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1365
1466
|
`SELECT i.request_id AS requestId, i.attempt_id AS attemptId
|
|
1366
1467
|
FROM interactive_requests i
|
|
1367
1468
|
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
1368
|
-
|
|
1469
|
+
JOIN runs r ON r.run_id = i.run_id
|
|
1470
|
+
WHERE i.run_id = ? AND i.target_session_id = ?
|
|
1471
|
+
AND i.status = 'pending' AND r.paused = 0
|
|
1369
1472
|
AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
|
|
1473
|
+
AND EXISTS (
|
|
1474
|
+
SELECT 1 FROM workflow_messages m
|
|
1475
|
+
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
|
|
1476
|
+
WHERE m.source_id = i.request_id AND m.kind = 'step' AND t.state = 'started'
|
|
1477
|
+
)
|
|
1370
1478
|
ORDER BY a.deadline_at, i.request_id LIMIT 1`,
|
|
1371
1479
|
)
|
|
1372
|
-
.get(options.runId, now);
|
|
1480
|
+
.get(options.runId, options.targetSessionId, now);
|
|
1373
1481
|
if (!isExpiredInteractionRow(interaction)) return false;
|
|
1374
1482
|
const request = this.state.connection
|
|
1375
1483
|
.prepare(
|
|
1376
1484
|
`UPDATE interactive_requests
|
|
1377
|
-
SET status = 'cancelled',
|
|
1378
|
-
|
|
1379
|
-
WHERE request_id = ? AND run_id = ? AND status IN ('pending', 'presenting')`,
|
|
1485
|
+
SET status = 'cancelled', revision = revision + 1, updated_at = ?
|
|
1486
|
+
WHERE request_id = ? AND run_id = ? AND status = 'pending'`,
|
|
1380
1487
|
)
|
|
1381
1488
|
.run(now, interaction.requestId, options.runId);
|
|
1382
1489
|
const run = this.state.connection
|
|
@@ -1398,7 +1505,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1398
1505
|
if (request.changes !== 1 || run.changes !== 1 || queue.changes !== 1) {
|
|
1399
1506
|
throw new Error(`Workflow run ${options.runId} changed during interaction timeout`);
|
|
1400
1507
|
}
|
|
1401
|
-
const error = "Workflow node deadline expired
|
|
1508
|
+
const error = "Workflow node model-turn deadline expired";
|
|
1402
1509
|
const receiptHash = this.state.putJson({ status: "rejected", error }, now);
|
|
1403
1510
|
this.state.connection
|
|
1404
1511
|
.prepare(
|
|
@@ -1448,7 +1555,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1448
1555
|
"(l.owner_id IS NULL OR l.expires_at <= ? OR l.owner_id = ?)",
|
|
1449
1556
|
"(q.affinity_runner_id IS NULL OR q.affinity_runner_id = ? OR q.status IN ('parked', 'starting', 'running'))",
|
|
1450
1557
|
"NOT (q.status = 'parked' AND (r.status = 'waiting' OR r.paused = 1))",
|
|
1451
|
-
"NOT EXISTS (SELECT 1 FROM interactive_requests i WHERE i.run_id = r.run_id AND i.status
|
|
1558
|
+
"NOT EXISTS (SELECT 1 FROM interactive_requests i WHERE i.run_id = r.run_id AND i.status = 'pending')",
|
|
1452
1559
|
"(q.error_code IS NULL OR q.error_code <> 'workflowSourceChanged')",
|
|
1453
1560
|
];
|
|
1454
1561
|
const params: unknown[] = [now, now, options.runnerId, options.runnerId];
|
|
@@ -1572,7 +1679,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1572
1679
|
const pending = this.state.connection
|
|
1573
1680
|
.prepare(
|
|
1574
1681
|
`SELECT 1 FROM interactive_requests
|
|
1575
|
-
WHERE run_id = ? AND status
|
|
1682
|
+
WHERE run_id = ? AND status = 'pending' LIMIT 1`,
|
|
1576
1683
|
)
|
|
1577
1684
|
.get(options.runId);
|
|
1578
1685
|
if (pending === undefined) return false;
|
|
@@ -1623,7 +1730,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1623
1730
|
const pending = this.state.connection
|
|
1624
1731
|
.prepare(
|
|
1625
1732
|
`SELECT 1 FROM interactive_requests
|
|
1626
|
-
WHERE run_id = ? AND status
|
|
1733
|
+
WHERE run_id = ? AND status = 'pending' LIMIT 1`,
|
|
1627
1734
|
)
|
|
1628
1735
|
.get(options.runId);
|
|
1629
1736
|
if (pending === undefined) return false;
|
|
@@ -2041,393 +2148,6 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
2041
2148
|
}));
|
|
2042
2149
|
}
|
|
2043
2150
|
|
|
2044
|
-
ensureWorkflowTurnIntent(options: {
|
|
2045
|
-
intentId: string;
|
|
2046
|
-
sourceEventId: string;
|
|
2047
|
-
runId: string;
|
|
2048
|
-
workflowRef: string;
|
|
2049
|
-
targetSessionId: string;
|
|
2050
|
-
cause: WorkflowTurnIntentCause;
|
|
2051
|
-
nodeId?: string | null;
|
|
2052
|
-
attemptId?: string | null;
|
|
2053
|
-
fallbackFacts: WorkflowTurnIntentFacts;
|
|
2054
|
-
eligible: boolean;
|
|
2055
|
-
requestedAt?: string;
|
|
2056
|
-
}): WorkflowTurnIntentRecord {
|
|
2057
|
-
const existing = this.turnIntentBySource(options.sourceEventId);
|
|
2058
|
-
if (existing !== undefined) return existing;
|
|
2059
|
-
const now = epoch(validTimestamp(options.requestedAt));
|
|
2060
|
-
const run = this.requireWorkflowRunRow(options.runId);
|
|
2061
|
-
const intentId = options.intentId;
|
|
2062
|
-
const resourceId = resourceIdFor("turn_intent", intentId);
|
|
2063
|
-
const effectId = `effect-${randomUUID()}`;
|
|
2064
|
-
this.state.transaction(() => {
|
|
2065
|
-
const factsHash = this.state.putJson(options.fallbackFacts, now);
|
|
2066
|
-
this.insertEffectResource(
|
|
2067
|
-
effectId,
|
|
2068
|
-
resourceIdFor("effect", effectId),
|
|
2069
|
-
run.resourceId,
|
|
2070
|
-
this.resourceRevision(run.resourceId),
|
|
2071
|
-
"turn.deliver",
|
|
2072
|
-
intentId,
|
|
2073
|
-
factsHash,
|
|
2074
|
-
"run",
|
|
2075
|
-
now,
|
|
2076
|
-
);
|
|
2077
|
-
this.state.connection
|
|
2078
|
-
.prepare(
|
|
2079
|
-
"INSERT INTO resources(resource_id, resource_type, aggregate_key, revision, created_at, updated_at) VALUES (?, 'turn_intent', ?, 1, ?, ?)",
|
|
2080
|
-
)
|
|
2081
|
-
.run(resourceId, intentId, now, now);
|
|
2082
|
-
this.state.connection
|
|
2083
|
-
.prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
|
|
2084
|
-
.run(resourceId);
|
|
2085
|
-
this.state.connection
|
|
2086
|
-
.prepare(
|
|
2087
|
-
`INSERT INTO turn_intents(
|
|
2088
|
-
turn_intent_id, resource_id, effect_id, source_event_id, run_id,
|
|
2089
|
-
workflow_ref, target_session_id, cause, node_id, attempt_id,
|
|
2090
|
-
facts_hash, requested_at, eligible_at, created_at
|
|
2091
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2092
|
-
)
|
|
2093
|
-
.run(
|
|
2094
|
-
intentId,
|
|
2095
|
-
resourceId,
|
|
2096
|
-
effectId,
|
|
2097
|
-
options.sourceEventId,
|
|
2098
|
-
options.runId,
|
|
2099
|
-
options.workflowRef,
|
|
2100
|
-
options.targetSessionId,
|
|
2101
|
-
options.cause,
|
|
2102
|
-
options.nodeId ?? null,
|
|
2103
|
-
options.attemptId ?? null,
|
|
2104
|
-
factsHash,
|
|
2105
|
-
now,
|
|
2106
|
-
options.eligible ? now : null,
|
|
2107
|
-
now,
|
|
2108
|
-
);
|
|
2109
|
-
this.insertEvent(
|
|
2110
|
-
resourceId,
|
|
2111
|
-
1,
|
|
2112
|
-
"turn.requested",
|
|
2113
|
-
"session",
|
|
2114
|
-
options.targetSessionId,
|
|
2115
|
-
{},
|
|
2116
|
-
now,
|
|
2117
|
-
);
|
|
2118
|
-
});
|
|
2119
|
-
return this.requireTurnIntent(intentId);
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
getWorkflowTurnIntent(intentId: string): WorkflowTurnIntentRecord | undefined {
|
|
2123
|
-
return this.turnIntent(intentId);
|
|
2124
|
-
}
|
|
2125
|
-
|
|
2126
|
-
findPendingWorkflowTurnIntent(options: {
|
|
2127
|
-
runId: string;
|
|
2128
|
-
targetSessionId: string;
|
|
2129
|
-
}): WorkflowTurnIntentRecord | undefined {
|
|
2130
|
-
const row = this.state.connection
|
|
2131
|
-
.prepare(
|
|
2132
|
-
turnIntentSelect(
|
|
2133
|
-
"WHERE t.run_id = ? AND t.target_session_id = ? AND t.resolved_at IS NULL ORDER BY t.requested_at DESC LIMIT 1",
|
|
2134
|
-
),
|
|
2135
|
-
)
|
|
2136
|
-
.get(options.runId, options.targetSessionId);
|
|
2137
|
-
return isTurnIntentRow(row) ? this.mapTurnIntent(row) : undefined;
|
|
2138
|
-
}
|
|
2139
|
-
|
|
2140
|
-
isWorkflowTurnIntentClaimLive(options: {
|
|
2141
|
-
intentId: string;
|
|
2142
|
-
targetSessionId: string;
|
|
2143
|
-
claimToken: string;
|
|
2144
|
-
now?: string;
|
|
2145
|
-
}): boolean {
|
|
2146
|
-
const row = this.turnIntentRow(options.intentId);
|
|
2147
|
-
return (
|
|
2148
|
-
row !== undefined &&
|
|
2149
|
-
row.targetSessionId === options.targetSessionId &&
|
|
2150
|
-
row.resolvedAt === null &&
|
|
2151
|
-
this.verifyEffectToken(row.effectId, options.claimToken, options.now)
|
|
2152
|
-
);
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
claimWorkflowTurnIntent(options: {
|
|
2156
|
-
intentId: string;
|
|
2157
|
-
targetSessionId: string;
|
|
2158
|
-
claimToken: string;
|
|
2159
|
-
leaseMs: number;
|
|
2160
|
-
now?: string;
|
|
2161
|
-
}): WorkflowTurnIntentRecord | undefined {
|
|
2162
|
-
const intent = this.turnIntent(options.intentId);
|
|
2163
|
-
if (intent?.targetSessionId !== options.targetSessionId) return undefined;
|
|
2164
|
-
return this.claimEffectForTurn(
|
|
2165
|
-
options.intentId,
|
|
2166
|
-
options.claimToken,
|
|
2167
|
-
options.leaseMs,
|
|
2168
|
-
options.now,
|
|
2169
|
-
)
|
|
2170
|
-
? this.requireTurnIntent(options.intentId)
|
|
2171
|
-
: undefined;
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
claimEligibleWorkflowTurnIntents(options: {
|
|
2175
|
-
targetSessionId: string;
|
|
2176
|
-
claimToken: string;
|
|
2177
|
-
leaseMs: number;
|
|
2178
|
-
now?: string;
|
|
2179
|
-
limit?: number;
|
|
2180
|
-
}): WorkflowTurnIntentRecord[] {
|
|
2181
|
-
const now = epoch(validTimestamp(options.now));
|
|
2182
|
-
const rows = this.state.connection
|
|
2183
|
-
.prepare(
|
|
2184
|
-
turnIntentSelect(
|
|
2185
|
-
"WHERE t.target_session_id = ? AND t.resolved_at IS NULL AND t.eligible_at IS NOT NULL AND t.eligible_at <= ? ORDER BY t.eligible_at LIMIT ?",
|
|
2186
|
-
),
|
|
2187
|
-
)
|
|
2188
|
-
.all(options.targetSessionId, now, options.limit ?? 10);
|
|
2189
|
-
const claimed: WorkflowTurnIntentRecord[] = [];
|
|
2190
|
-
for (const row of rows) {
|
|
2191
|
-
if (
|
|
2192
|
-
isTurnIntentRow(row) &&
|
|
2193
|
-
this.claimEffectForTurn(row.intentId, options.claimToken, options.leaseMs, options.now)
|
|
2194
|
-
) {
|
|
2195
|
-
claimed.push(this.requireTurnIntent(row.intentId));
|
|
2196
|
-
}
|
|
2197
|
-
}
|
|
2198
|
-
return claimed;
|
|
2199
|
-
}
|
|
2200
|
-
|
|
2201
|
-
makeWorkflowTurnIntentEligible(options: {
|
|
2202
|
-
intentId: string;
|
|
2203
|
-
fallbackFacts: WorkflowTurnIntentFacts;
|
|
2204
|
-
eligibleAt?: string;
|
|
2205
|
-
now?: string;
|
|
2206
|
-
}): boolean {
|
|
2207
|
-
const eligibleAt = epoch(validTimestamp(options.eligibleAt ?? options.now));
|
|
2208
|
-
const factsHash = this.state.putJson(options.fallbackFacts, eligibleAt);
|
|
2209
|
-
return (
|
|
2210
|
-
this.state.connection
|
|
2211
|
-
.prepare(
|
|
2212
|
-
"UPDATE turn_intents SET eligible_at = ?, facts_hash = ? WHERE turn_intent_id = ? AND resolved_at IS NULL",
|
|
2213
|
-
)
|
|
2214
|
-
.run(eligibleAt, factsHash, options.intentId).changes === 1
|
|
2215
|
-
);
|
|
2216
|
-
}
|
|
2217
|
-
|
|
2218
|
-
resolveWorkflowTurnIntent(options: {
|
|
2219
|
-
intentId: string;
|
|
2220
|
-
targetSessionId: string;
|
|
2221
|
-
claimToken: string;
|
|
2222
|
-
resolution: WorkflowTurnIntentResolution;
|
|
2223
|
-
messageId?: string;
|
|
2224
|
-
now?: string;
|
|
2225
|
-
}): boolean {
|
|
2226
|
-
const now = epoch(validTimestamp(options.now));
|
|
2227
|
-
return this.state.transaction(() => {
|
|
2228
|
-
const row = this.turnIntentRow(options.intentId);
|
|
2229
|
-
if (
|
|
2230
|
-
row === undefined ||
|
|
2231
|
-
row.targetSessionId !== options.targetSessionId ||
|
|
2232
|
-
!this.verifyEffectToken(row.effectId, options.claimToken, options.now)
|
|
2233
|
-
) {
|
|
2234
|
-
return false;
|
|
2235
|
-
}
|
|
2236
|
-
const result = this.state.connection
|
|
2237
|
-
.prepare(
|
|
2238
|
-
`UPDATE turn_intents SET resolved_at = ?, resolution_type = ?, resolution_message_id = ?
|
|
2239
|
-
WHERE turn_intent_id = ? AND resolved_at IS NULL`,
|
|
2240
|
-
)
|
|
2241
|
-
.run(now, options.resolution, options.messageId ?? null, options.intentId);
|
|
2242
|
-
if (result.changes === 1) this.completeEffect(row.effectId, "applied", now);
|
|
2243
|
-
return result.changes === 1;
|
|
2244
|
-
});
|
|
2245
|
-
}
|
|
2246
|
-
|
|
2247
|
-
releaseWorkflowTurnIntentClaim(options: {
|
|
2248
|
-
intentId: string;
|
|
2249
|
-
targetSessionId: string;
|
|
2250
|
-
claimToken: string;
|
|
2251
|
-
}): boolean {
|
|
2252
|
-
const row = this.turnIntentRow(options.intentId);
|
|
2253
|
-
return (
|
|
2254
|
-
row !== undefined &&
|
|
2255
|
-
row.targetSessionId === options.targetSessionId &&
|
|
2256
|
-
this.releaseEffectLease(row.effectId, options.claimToken)
|
|
2257
|
-
);
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
listWorkflowTurnIntents(
|
|
2261
|
-
options: {
|
|
2262
|
-
runId?: string;
|
|
2263
|
-
targetSessionId?: string;
|
|
2264
|
-
unresolvedOnly?: boolean;
|
|
2265
|
-
limit?: number;
|
|
2266
|
-
} = {},
|
|
2267
|
-
): WorkflowTurnIntentRecord[] {
|
|
2268
|
-
const clauses = ["1 = 1"];
|
|
2269
|
-
const params: unknown[] = [];
|
|
2270
|
-
if (options.runId !== undefined) {
|
|
2271
|
-
clauses.push("t.run_id = ?");
|
|
2272
|
-
params.push(options.runId);
|
|
2273
|
-
}
|
|
2274
|
-
if (options.targetSessionId !== undefined) {
|
|
2275
|
-
clauses.push("t.target_session_id = ?");
|
|
2276
|
-
params.push(options.targetSessionId);
|
|
2277
|
-
}
|
|
2278
|
-
if (options.unresolvedOnly === true) clauses.push("t.resolved_at IS NULL");
|
|
2279
|
-
params.push(options.limit ?? 100);
|
|
2280
|
-
const rows = this.state.connection
|
|
2281
|
-
.prepare(
|
|
2282
|
-
turnIntentSelect(`WHERE ${clauses.join(" AND ")} ORDER BY t.requested_at DESC LIMIT ?`),
|
|
2283
|
-
)
|
|
2284
|
-
.all(...params);
|
|
2285
|
-
return rows.filter(isTurnIntentRow).map((row) => this.mapTurnIntent(row));
|
|
2286
|
-
}
|
|
2287
|
-
|
|
2288
|
-
enqueueWorkflowNotification(options: {
|
|
2289
|
-
runId: string;
|
|
2290
|
-
nodeId: string;
|
|
2291
|
-
attemptId: string;
|
|
2292
|
-
notificationIndex: number;
|
|
2293
|
-
targetSessionId: string;
|
|
2294
|
-
kind: "progress" | "final";
|
|
2295
|
-
content: string;
|
|
2296
|
-
now?: string;
|
|
2297
|
-
}): WorkflowNotificationRecord {
|
|
2298
|
-
const existing = this.notification(options.runId, options.attemptId, options.notificationIndex);
|
|
2299
|
-
if (existing !== undefined) return existing;
|
|
2300
|
-
const run = this.requireWorkflowRunRow(options.runId);
|
|
2301
|
-
const now = epoch(validTimestamp(options.now));
|
|
2302
|
-
const notificationId = `notification-${randomUUID()}`;
|
|
2303
|
-
const resourceId = resourceIdFor("notification", notificationId);
|
|
2304
|
-
const effectId = `effect-${randomUUID()}`;
|
|
2305
|
-
this.state.transaction(() => {
|
|
2306
|
-
const contentHash = this.state.putText(options.content, now);
|
|
2307
|
-
this.insertEffectResource(
|
|
2308
|
-
effectId,
|
|
2309
|
-
resourceIdFor("effect", effectId),
|
|
2310
|
-
run.resourceId,
|
|
2311
|
-
this.resourceRevision(run.resourceId),
|
|
2312
|
-
"notification.deliver",
|
|
2313
|
-
notificationId,
|
|
2314
|
-
contentHash,
|
|
2315
|
-
"run",
|
|
2316
|
-
now,
|
|
2317
|
-
);
|
|
2318
|
-
this.state.connection
|
|
2319
|
-
.prepare(
|
|
2320
|
-
"INSERT INTO resources(resource_id, resource_type, aggregate_key, revision, created_at, updated_at) VALUES (?, 'notification', ?, 1, ?, ?)",
|
|
2321
|
-
)
|
|
2322
|
-
.run(resourceId, notificationId, now, now);
|
|
2323
|
-
this.state.connection
|
|
2324
|
-
.prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
|
|
2325
|
-
.run(resourceId);
|
|
2326
|
-
this.state.connection
|
|
2327
|
-
.prepare(
|
|
2328
|
-
`INSERT INTO notifications(
|
|
2329
|
-
notification_id, effect_id, run_id, attempt_id, notification_index,
|
|
2330
|
-
target_session_id, notification_type, content_hash, created_at
|
|
2331
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2332
|
-
)
|
|
2333
|
-
.run(
|
|
2334
|
-
notificationId,
|
|
2335
|
-
effectId,
|
|
2336
|
-
options.runId,
|
|
2337
|
-
options.attemptId,
|
|
2338
|
-
options.notificationIndex,
|
|
2339
|
-
options.targetSessionId,
|
|
2340
|
-
options.kind,
|
|
2341
|
-
contentHash,
|
|
2342
|
-
now,
|
|
2343
|
-
);
|
|
2344
|
-
this.insertEvent(
|
|
2345
|
-
resourceId,
|
|
2346
|
-
1,
|
|
2347
|
-
"notification.queued",
|
|
2348
|
-
"session",
|
|
2349
|
-
options.targetSessionId,
|
|
2350
|
-
{},
|
|
2351
|
-
now,
|
|
2352
|
-
);
|
|
2353
|
-
});
|
|
2354
|
-
return this.requireNotification(options.runId, options.attemptId, options.notificationIndex);
|
|
2355
|
-
}
|
|
2356
|
-
|
|
2357
|
-
listPendingWorkflowNotifications(options: {
|
|
2358
|
-
targetSessionId: string;
|
|
2359
|
-
limit?: number;
|
|
2360
|
-
}): WorkflowNotificationRecord[] {
|
|
2361
|
-
const now = Date.now();
|
|
2362
|
-
return this.notificationRows(options.targetSessionId, options.limit ?? 20)
|
|
2363
|
-
.map((row) => this.mapNotification(row))
|
|
2364
|
-
.filter(
|
|
2365
|
-
(notification) =>
|
|
2366
|
-
notification.deliveryClaimExpiresAt === null ||
|
|
2367
|
-
Date.parse(notification.deliveryClaimExpiresAt) <= now,
|
|
2368
|
-
);
|
|
2369
|
-
}
|
|
2370
|
-
|
|
2371
|
-
claimPendingWorkflowNotifications(options: {
|
|
2372
|
-
targetSessionId: string;
|
|
2373
|
-
claimToken: string;
|
|
2374
|
-
leaseMs: number;
|
|
2375
|
-
now?: string;
|
|
2376
|
-
limit?: number;
|
|
2377
|
-
}): WorkflowNotificationRecord[] {
|
|
2378
|
-
const rows = this.notificationRows(options.targetSessionId, options.limit ?? 20);
|
|
2379
|
-
const result: WorkflowNotificationRecord[] = [];
|
|
2380
|
-
for (const row of rows) {
|
|
2381
|
-
if (
|
|
2382
|
-
this.claimEffect(
|
|
2383
|
-
row.effectId,
|
|
2384
|
-
options.targetSessionId,
|
|
2385
|
-
options.claimToken,
|
|
2386
|
-
options.leaseMs,
|
|
2387
|
-
options.now,
|
|
2388
|
-
)
|
|
2389
|
-
) {
|
|
2390
|
-
result.push(this.mapNotification(row));
|
|
2391
|
-
}
|
|
2392
|
-
}
|
|
2393
|
-
return result;
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2396
|
-
isWorkflowNotificationClaimLive(options: {
|
|
2397
|
-
notificationId: string;
|
|
2398
|
-
targetSessionId: string;
|
|
2399
|
-
claimToken: string;
|
|
2400
|
-
now?: string;
|
|
2401
|
-
}): boolean {
|
|
2402
|
-
const row = this.notificationRowById(options.notificationId);
|
|
2403
|
-
return (
|
|
2404
|
-
row !== undefined &&
|
|
2405
|
-
row.targetSessionId === options.targetSessionId &&
|
|
2406
|
-
this.verifyEffectToken(row.effectId, options.claimToken, options.now)
|
|
2407
|
-
);
|
|
2408
|
-
}
|
|
2409
|
-
|
|
2410
|
-
markWorkflowNotificationDelivered(options: {
|
|
2411
|
-
notificationId: string;
|
|
2412
|
-
targetSessionId: string;
|
|
2413
|
-
claimToken: string;
|
|
2414
|
-
now?: string;
|
|
2415
|
-
}): boolean {
|
|
2416
|
-
const now = epoch(validTimestamp(options.now));
|
|
2417
|
-
return this.state.transaction(() => {
|
|
2418
|
-
const row = this.notificationRowById(options.notificationId);
|
|
2419
|
-
if (
|
|
2420
|
-
row === undefined ||
|
|
2421
|
-
row.targetSessionId !== options.targetSessionId ||
|
|
2422
|
-
!this.verifyEffectToken(row.effectId, options.claimToken, options.now)
|
|
2423
|
-
) {
|
|
2424
|
-
return false;
|
|
2425
|
-
}
|
|
2426
|
-
this.completeEffect(row.effectId, "applied", now);
|
|
2427
|
-
return true;
|
|
2428
|
-
});
|
|
2429
|
-
}
|
|
2430
|
-
|
|
2431
2151
|
settleRunEffect(runId: string, effectType: "run.park_queue" | "run.settle_queue"): void {
|
|
2432
2152
|
const rows = this.state.connection
|
|
2433
2153
|
.prepare(
|
|
@@ -2882,6 +2602,21 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
2882
2602
|
options: WorkflowRunReservationOptions,
|
|
2883
2603
|
definitionDigest: Buffer,
|
|
2884
2604
|
): void {
|
|
2605
|
+
const expectedLineageKind =
|
|
2606
|
+
options.parentRunId === undefined ? null : (options.lineageKind ?? "continuation");
|
|
2607
|
+
const parent =
|
|
2608
|
+
options.parentRunId === undefined
|
|
2609
|
+
? undefined
|
|
2610
|
+
: this.requireWorkflowRunRow(options.parentRunId);
|
|
2611
|
+
const expectedRootRunId = parent?.rootRunId ?? options.runId;
|
|
2612
|
+
const expectedRestartNumber =
|
|
2613
|
+
expectedLineageKind === "restart"
|
|
2614
|
+
? (options.restartNumber ?? (parent?.restartNumber ?? -1) + 1)
|
|
2615
|
+
: (parent?.restartNumber ?? 0);
|
|
2616
|
+
const expectedParentTerminalFingerprint =
|
|
2617
|
+
options.parentTerminalFingerprint === undefined
|
|
2618
|
+
? null
|
|
2619
|
+
: terminalFingerprintBuffer(options.parentTerminalFingerprint);
|
|
2885
2620
|
const compatible =
|
|
2886
2621
|
row.workflowName === options.workflowName &&
|
|
2887
2622
|
row.workflowRef === options.workflowSourceRef &&
|
|
@@ -2893,12 +2628,26 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
2893
2628
|
canonicalJson(options.launchOptions ?? {}) &&
|
|
2894
2629
|
row.originSessionId === options.originSessionId &&
|
|
2895
2630
|
row.executionMode === (options.executionMode ?? "interactive") &&
|
|
2896
|
-
row.parentRunId === (options.parentRunId ?? null)
|
|
2631
|
+
row.parentRunId === (options.parentRunId ?? null) &&
|
|
2632
|
+
row.rootRunId === expectedRootRunId &&
|
|
2633
|
+
row.lineageKind === expectedLineageKind &&
|
|
2634
|
+
row.restartNumber === expectedRestartNumber &&
|
|
2635
|
+
((row.parentTerminalFingerprint === null && expectedParentTerminalFingerprint === null) ||
|
|
2636
|
+
(row.parentTerminalFingerprint !== null &&
|
|
2637
|
+
expectedParentTerminalFingerprint !== null &&
|
|
2638
|
+
row.parentTerminalFingerprint.equals(expectedParentTerminalFingerprint)));
|
|
2897
2639
|
if (!compatible) {
|
|
2898
2640
|
throw new Error(`Workflow run preparation conflicts: ${options.runId}`);
|
|
2899
2641
|
}
|
|
2900
2642
|
}
|
|
2901
2643
|
|
|
2644
|
+
private workflowRunViewRows(clause: string, params: unknown[]): WorkflowRunViewRow[] {
|
|
2645
|
+
return this.state.connection
|
|
2646
|
+
.prepare(workflowRunViewSelect(clause))
|
|
2647
|
+
.all(...params)
|
|
2648
|
+
.filter(isWorkflowRunViewRow);
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2902
2651
|
/* istanbul ignore next -- pure projection covered by integration tests */
|
|
2903
2652
|
private mapWorkflowRun(row: RunRow): WorkflowRunQueueRecord {
|
|
2904
2653
|
return {
|
|
@@ -2920,6 +2669,10 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
2920
2669
|
originSessionId: row.executionMode === "headless" ? null : row.originSessionId,
|
|
2921
2670
|
executionMode: row.executionMode,
|
|
2922
2671
|
parentRunId: row.parentRunId,
|
|
2672
|
+
rootRunId: row.rootRunId,
|
|
2673
|
+
lineageKind: row.lineageKind,
|
|
2674
|
+
restartNumber: row.restartNumber,
|
|
2675
|
+
parentTerminalFingerprint: row.parentTerminalFingerprint?.toString("hex") ?? null,
|
|
2923
2676
|
errorCode: row.errorCode,
|
|
2924
2677
|
errorMessage:
|
|
2925
2678
|
row.errorHash === null
|
|
@@ -2960,7 +2713,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
2960
2713
|
this.state.connection
|
|
2961
2714
|
.prepare(
|
|
2962
2715
|
`SELECT 1 FROM interactive_requests
|
|
2963
|
-
WHERE run_id = ? AND status
|
|
2716
|
+
WHERE run_id = ? AND status = 'pending' LIMIT 1`,
|
|
2964
2717
|
)
|
|
2965
2718
|
.get(runId) !== undefined
|
|
2966
2719
|
) {
|
|
@@ -3287,262 +3040,12 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
3287
3040
|
this.state.connection
|
|
3288
3041
|
.prepare(
|
|
3289
3042
|
`UPDATE interactive_requests
|
|
3290
|
-
SET status = 'cancelled',
|
|
3291
|
-
|
|
3292
|
-
WHERE run_id = ? AND status IN ('pending', 'presenting')`,
|
|
3043
|
+
SET status = 'cancelled', revision = revision + 1, updated_at = ?
|
|
3044
|
+
WHERE run_id = ? AND status = 'pending'`,
|
|
3293
3045
|
)
|
|
3294
3046
|
.run(now, runId);
|
|
3295
3047
|
}
|
|
3296
3048
|
|
|
3297
|
-
private turnIntent(intentId: string): WorkflowTurnIntentRecord | undefined {
|
|
3298
|
-
const row = this.turnIntentRow(intentId);
|
|
3299
|
-
return row === undefined ? undefined : this.mapTurnIntent(row);
|
|
3300
|
-
}
|
|
3301
|
-
|
|
3302
|
-
private requireTurnIntent(intentId: string): WorkflowTurnIntentRecord {
|
|
3303
|
-
const row = this.turnIntent(intentId);
|
|
3304
|
-
if (row === undefined) throw new Error(`Workflow turn intent not found: ${intentId}`);
|
|
3305
|
-
return row;
|
|
3306
|
-
}
|
|
3307
|
-
|
|
3308
|
-
private turnIntentRow(intentId: string): TurnIntentRow | undefined {
|
|
3309
|
-
const row = this.state.connection
|
|
3310
|
-
.prepare(turnIntentSelect("WHERE t.turn_intent_id = ?"))
|
|
3311
|
-
.get(intentId);
|
|
3312
|
-
return isTurnIntentRow(row) ? row : undefined;
|
|
3313
|
-
}
|
|
3314
|
-
|
|
3315
|
-
private turnIntentBySource(sourceEventId: string): WorkflowTurnIntentRecord | undefined {
|
|
3316
|
-
const row = this.state.connection
|
|
3317
|
-
.prepare(turnIntentSelect("WHERE t.source_event_id = ?"))
|
|
3318
|
-
.get(sourceEventId);
|
|
3319
|
-
return isTurnIntentRow(row) ? this.mapTurnIntent(row) : undefined;
|
|
3320
|
-
}
|
|
3321
|
-
|
|
3322
|
-
/* istanbul ignore next -- pure projection covered by integration tests */
|
|
3323
|
-
private mapTurnIntent(row: TurnIntentRow): WorkflowTurnIntentRecord {
|
|
3324
|
-
return {
|
|
3325
|
-
intentId: row.intentId,
|
|
3326
|
-
sourceEventId: row.sourceEventId,
|
|
3327
|
-
runId: row.runId,
|
|
3328
|
-
workflowRef: row.workflowRef,
|
|
3329
|
-
targetSessionId: row.targetSessionId,
|
|
3330
|
-
cause: row.cause,
|
|
3331
|
-
nodeId: row.nodeId,
|
|
3332
|
-
attemptId: row.attemptId,
|
|
3333
|
-
fallbackFacts: this.state.readJson(row.factsHash) as WorkflowTurnIntentFacts,
|
|
3334
|
-
requestedAt: new Date(row.requestedAt).toISOString(),
|
|
3335
|
-
eligibleAt: row.eligibleAt === null ? null : new Date(row.eligibleAt).toISOString(),
|
|
3336
|
-
resolvedAt: row.resolvedAt === null ? null : new Date(row.resolvedAt).toISOString(),
|
|
3337
|
-
resolution: row.resolution,
|
|
3338
|
-
resolutionMessageId: row.resolutionMessageId,
|
|
3339
|
-
deliveryClaimExpiresAt:
|
|
3340
|
-
row.claimExpiresAt === null ? null : new Date(row.claimExpiresAt).toISOString(),
|
|
3341
|
-
};
|
|
3342
|
-
}
|
|
3343
|
-
|
|
3344
|
-
private claimEffectForTurn(
|
|
3345
|
-
intentId: string,
|
|
3346
|
-
token: string,
|
|
3347
|
-
leaseMs: number,
|
|
3348
|
-
nowValue?: string,
|
|
3349
|
-
): boolean {
|
|
3350
|
-
const row = this.turnIntentRow(intentId);
|
|
3351
|
-
return (
|
|
3352
|
-
row !== undefined &&
|
|
3353
|
-
this.claimEffect(row.effectId, row.targetSessionId, token, leaseMs, nowValue)
|
|
3354
|
-
);
|
|
3355
|
-
}
|
|
3356
|
-
|
|
3357
|
-
private claimEffect(
|
|
3358
|
-
effectId: string,
|
|
3359
|
-
ownerId: string,
|
|
3360
|
-
token: string,
|
|
3361
|
-
leaseMs: number,
|
|
3362
|
-
nowValue?: string,
|
|
3363
|
-
): boolean {
|
|
3364
|
-
const now = epoch(validTimestamp(nowValue));
|
|
3365
|
-
return this.state.transaction(() => {
|
|
3366
|
-
const row = this.state.connection
|
|
3367
|
-
.prepare(
|
|
3368
|
-
"SELECT resource_id AS resourceId FROM effects WHERE effect_id = ? AND status = 'pending'",
|
|
3369
|
-
)
|
|
3370
|
-
.get(effectId);
|
|
3371
|
-
/* istanbul ignore if -- impossible after exact schema and transaction checks */
|
|
3372
|
-
if (!isResourceIdRow(row)) return false;
|
|
3373
|
-
const lease = this.requireLease(row.resourceId);
|
|
3374
|
-
if (lease.ownerId !== null && lease.expiresAt !== null && lease.expiresAt > now) return false;
|
|
3375
|
-
return (
|
|
3376
|
-
this.state.connection
|
|
3377
|
-
.prepare(
|
|
3378
|
-
`UPDATE leases SET generation = ?, owner_type = 'session', owner_id = ?, token_hash = ?,
|
|
3379
|
-
acquired_at = ?, heartbeat_at = ?, expires_at = ?
|
|
3380
|
-
WHERE resource_id = ? AND generation = ?`,
|
|
3381
|
-
)
|
|
3382
|
-
.run(
|
|
3383
|
-
lease.generation + 1,
|
|
3384
|
-
ownerId,
|
|
3385
|
-
tokenHash(token),
|
|
3386
|
-
now,
|
|
3387
|
-
now,
|
|
3388
|
-
now + leaseMs,
|
|
3389
|
-
row.resourceId,
|
|
3390
|
-
lease.generation,
|
|
3391
|
-
).changes === 1
|
|
3392
|
-
);
|
|
3393
|
-
});
|
|
3394
|
-
}
|
|
3395
|
-
|
|
3396
|
-
private verifyEffectToken(effectId: string, token: string, nowValue?: string): boolean {
|
|
3397
|
-
const now = epoch(validTimestamp(nowValue));
|
|
3398
|
-
const row = this.state.connection
|
|
3399
|
-
.prepare(
|
|
3400
|
-
`SELECT l.token_hash AS tokenHash, l.expires_at AS expiresAt
|
|
3401
|
-
FROM effects e JOIN leases l ON l.resource_id = e.resource_id
|
|
3402
|
-
WHERE e.effect_id = ?`,
|
|
3403
|
-
)
|
|
3404
|
-
.get(effectId);
|
|
3405
|
-
return (
|
|
3406
|
-
isEffectTokenRow(row) &&
|
|
3407
|
-
row.tokenHash !== null &&
|
|
3408
|
-
row.tokenHash.equals(tokenHash(token)) &&
|
|
3409
|
-
row.expiresAt !== null &&
|
|
3410
|
-
row.expiresAt > now
|
|
3411
|
-
);
|
|
3412
|
-
}
|
|
3413
|
-
|
|
3414
|
-
private releaseEffectLease(effectId: string, token: string): boolean {
|
|
3415
|
-
return (
|
|
3416
|
-
this.state.connection
|
|
3417
|
-
.prepare(
|
|
3418
|
-
`UPDATE leases SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
|
|
3419
|
-
acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
|
|
3420
|
-
WHERE resource_id = (SELECT resource_id FROM effects WHERE effect_id = ?)
|
|
3421
|
-
AND token_hash = ?`,
|
|
3422
|
-
)
|
|
3423
|
-
.run(effectId, tokenHash(token)).changes === 1
|
|
3424
|
-
);
|
|
3425
|
-
}
|
|
3426
|
-
|
|
3427
|
-
private completeEffect(
|
|
3428
|
-
effectId: string,
|
|
3429
|
-
status: "applied" | "rejected" | "ambiguous",
|
|
3430
|
-
now: number,
|
|
3431
|
-
): void {
|
|
3432
|
-
this.state.connection
|
|
3433
|
-
.prepare("UPDATE effects SET status = ?, updated_at = ?, settled_at = ? WHERE effect_id = ?")
|
|
3434
|
-
.run(status, now, now, effectId);
|
|
3435
|
-
this.state.connection
|
|
3436
|
-
.prepare(
|
|
3437
|
-
`UPDATE leases SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
|
|
3438
|
-
acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
|
|
3439
|
-
WHERE resource_id = (SELECT resource_id FROM effects WHERE effect_id = ?)`,
|
|
3440
|
-
)
|
|
3441
|
-
.run(effectId);
|
|
3442
|
-
}
|
|
3443
|
-
|
|
3444
|
-
private insertEffectResource(
|
|
3445
|
-
effectId: string,
|
|
3446
|
-
resourceId: string,
|
|
3447
|
-
sourceResourceId: string,
|
|
3448
|
-
sourceRevision: number,
|
|
3449
|
-
type: string,
|
|
3450
|
-
key: string,
|
|
3451
|
-
payloadHash: Buffer,
|
|
3452
|
-
scope: "run" | "controller" | "channel" | "system",
|
|
3453
|
-
now: number,
|
|
3454
|
-
): void {
|
|
3455
|
-
this.state.connection
|
|
3456
|
-
.prepare(
|
|
3457
|
-
"INSERT INTO resources(resource_id, resource_type, aggregate_key, revision, created_at, updated_at) VALUES (?, 'effect', ?, 1, ?, ?)",
|
|
3458
|
-
)
|
|
3459
|
-
.run(resourceId, effectId, now, now);
|
|
3460
|
-
this.state.connection
|
|
3461
|
-
.prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
|
|
3462
|
-
.run(resourceId);
|
|
3463
|
-
this.state.connection
|
|
3464
|
-
.prepare(
|
|
3465
|
-
`INSERT INTO effects(
|
|
3466
|
-
effect_id, resource_id, source_resource_id, source_revision, effect_type,
|
|
3467
|
-
idempotency_key, payload_hash, owner_scope, status, attempt_count,
|
|
3468
|
-
created_at, updated_at
|
|
3469
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, ?)`,
|
|
3470
|
-
)
|
|
3471
|
-
.run(
|
|
3472
|
-
effectId,
|
|
3473
|
-
resourceId,
|
|
3474
|
-
sourceResourceId,
|
|
3475
|
-
sourceRevision,
|
|
3476
|
-
type,
|
|
3477
|
-
key,
|
|
3478
|
-
payloadHash,
|
|
3479
|
-
scope,
|
|
3480
|
-
now,
|
|
3481
|
-
now,
|
|
3482
|
-
);
|
|
3483
|
-
this.insertEvent(resourceId, 1, "effect.created", "system", null, {}, now);
|
|
3484
|
-
}
|
|
3485
|
-
|
|
3486
|
-
private notification(
|
|
3487
|
-
runId: string,
|
|
3488
|
-
attemptId: string,
|
|
3489
|
-
index: number,
|
|
3490
|
-
): WorkflowNotificationRecord | undefined {
|
|
3491
|
-
const row = this.state.connection
|
|
3492
|
-
.prepare(
|
|
3493
|
-
notificationSelect("WHERE n.run_id = ? AND n.attempt_id = ? AND n.notification_index = ?"),
|
|
3494
|
-
)
|
|
3495
|
-
.get(runId, attemptId, index);
|
|
3496
|
-
return isNotificationRow(row) ? this.mapNotification(row) : undefined;
|
|
3497
|
-
}
|
|
3498
|
-
|
|
3499
|
-
private requireNotification(
|
|
3500
|
-
runId: string,
|
|
3501
|
-
attemptId: string,
|
|
3502
|
-
index: number,
|
|
3503
|
-
): WorkflowNotificationRecord {
|
|
3504
|
-
const record = this.notification(runId, attemptId, index);
|
|
3505
|
-
/* istanbul ignore if -- impossible after exact schema and transaction checks */
|
|
3506
|
-
if (record === undefined) throw new Error("Workflow notification was not stored");
|
|
3507
|
-
return record;
|
|
3508
|
-
}
|
|
3509
|
-
|
|
3510
|
-
private notificationRows(sessionId: string, limit: number): NotificationRow[] {
|
|
3511
|
-
return this.state.connection
|
|
3512
|
-
.prepare(
|
|
3513
|
-
notificationSelect(
|
|
3514
|
-
"WHERE n.target_session_id = ? AND e.status = 'pending' ORDER BY n.created_at LIMIT ?",
|
|
3515
|
-
),
|
|
3516
|
-
)
|
|
3517
|
-
.all(sessionId, limit)
|
|
3518
|
-
.filter(isNotificationRow);
|
|
3519
|
-
}
|
|
3520
|
-
|
|
3521
|
-
private notificationRowById(id: string): NotificationRow | undefined {
|
|
3522
|
-
const row = this.state.connection
|
|
3523
|
-
.prepare(notificationSelect("WHERE n.notification_id = ?"))
|
|
3524
|
-
.get(id);
|
|
3525
|
-
return isNotificationRow(row) ? row : undefined;
|
|
3526
|
-
}
|
|
3527
|
-
|
|
3528
|
-
/* istanbul ignore next -- pure projection covered by integration tests */
|
|
3529
|
-
private mapNotification(row: NotificationRow): WorkflowNotificationRecord {
|
|
3530
|
-
return {
|
|
3531
|
-
notificationId: row.notificationId,
|
|
3532
|
-
runId: row.runId,
|
|
3533
|
-
nodeId: row.nodeId,
|
|
3534
|
-
attemptId: row.attemptId,
|
|
3535
|
-
notificationIndex: row.notificationIndex,
|
|
3536
|
-
targetSessionId: row.targetSessionId,
|
|
3537
|
-
kind: row.kind,
|
|
3538
|
-
content: this.state.readBlob(row.contentHash)?.content.toString("utf8") ?? "",
|
|
3539
|
-
createdAt: new Date(row.createdAt).toISOString(),
|
|
3540
|
-
deliveryClaimExpiresAt:
|
|
3541
|
-
row.claimExpiresAt === null ? null : new Date(row.claimExpiresAt).toISOString(),
|
|
3542
|
-
deliveredAt: row.deliveredAt === null ? null : new Date(row.deliveredAt).toISOString(),
|
|
3543
|
-
};
|
|
3544
|
-
}
|
|
3545
|
-
|
|
3546
3049
|
private assertWritable(): void {
|
|
3547
3050
|
if (this.state.mode === "read-only") throw new Error("Controller store is read-only");
|
|
3548
3051
|
}
|
|
@@ -3587,6 +3090,9 @@ function workflowRunSelect(clause: string): string {
|
|
|
3587
3090
|
q.consecutive_errors AS consecutiveErrors, q.error_code AS errorCode,
|
|
3588
3091
|
q.error_hash AS errorHash, b.origin_session_id AS originSessionId,
|
|
3589
3092
|
b.execution_mode AS executionMode, r.parent_run_id AS parentRunId,
|
|
3093
|
+
r.root_run_id AS rootRunId, r.lineage_kind AS lineageKind,
|
|
3094
|
+
r.restart_number AS restartNumber,
|
|
3095
|
+
r.parent_terminal_fingerprint AS parentTerminalFingerprint,
|
|
3590
3096
|
q.created_at AS createdAt, q.updated_at AS updatedAt,
|
|
3591
3097
|
q.started_at AS startedAt, q.finished_at AS finishedAt,
|
|
3592
3098
|
l.generation AS leaseGeneration, l.owner_id AS ownerId, l.expires_at AS claimExpiresAt
|
|
@@ -3596,29 +3102,24 @@ function workflowRunSelect(clause: string): string {
|
|
|
3596
3102
|
JOIN leases l ON l.resource_id = r.resource_id ${clause}`;
|
|
3597
3103
|
}
|
|
3598
3104
|
|
|
3599
|
-
function
|
|
3600
|
-
return `SELECT
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
n.created_at AS createdAt, l.expires_at AS claimExpiresAt,
|
|
3618
|
-
e.settled_at AS deliveredAt
|
|
3619
|
-
FROM notifications n JOIN effects e ON e.effect_id = n.effect_id
|
|
3620
|
-
JOIN leases l ON l.resource_id = e.resource_id
|
|
3621
|
-
JOIN node_attempts a ON a.attempt_id = n.attempt_id ${clause}`;
|
|
3105
|
+
function workflowRunViewSelect(clause: string): string {
|
|
3106
|
+
return `SELECT r.run_id AS runId, d.workflow_name AS workflowName,
|
|
3107
|
+
r.workflow_ref AS workflowRef, r.status AS runStatus, r.paused,
|
|
3108
|
+
r.definition_digest AS definitionDigest, q.status,
|
|
3109
|
+
b.origin_session_id AS originSessionId, b.execution_mode AS executionMode,
|
|
3110
|
+
r.parent_run_id AS parentRunId, r.root_run_id AS rootRunId,
|
|
3111
|
+
r.lineage_kind AS lineageKind, r.restart_number AS restartNumber,
|
|
3112
|
+
r.parent_terminal_fingerprint AS parentTerminalFingerprint,
|
|
3113
|
+
q.error_code AS errorCode, CAST(error.content AS TEXT) AS errorMessage,
|
|
3114
|
+
q.created_at AS createdAt, q.updated_at AS updatedAt,
|
|
3115
|
+
q.started_at AS startedAt, q.finished_at AS finishedAt,
|
|
3116
|
+
source.source_type AS sourceType, source.source_ref AS sourceRef,
|
|
3117
|
+
source.source_revision AS sourceRevision
|
|
3118
|
+
FROM runs r JOIN workflow_definitions d ON d.definition_digest = r.definition_digest
|
|
3119
|
+
JOIN run_queue q ON q.run_id = r.run_id
|
|
3120
|
+
LEFT JOIN run_bindings b ON b.run_id = r.run_id
|
|
3121
|
+
LEFT JOIN run_sources source ON source.run_id = r.run_id AND source.mount_path = ''
|
|
3122
|
+
LEFT JOIN blobs error ON error.blob_hash = q.error_hash ${clause}`;
|
|
3622
3123
|
}
|
|
3623
3124
|
|
|
3624
3125
|
type QueueListRow = {
|
|
@@ -3645,39 +3146,6 @@ type RunEventRow = {
|
|
|
3645
3146
|
runnerId: string | null;
|
|
3646
3147
|
payloadHash: Buffer | null;
|
|
3647
3148
|
};
|
|
3648
|
-
type TurnIntentRow = {
|
|
3649
|
-
intentId: string;
|
|
3650
|
-
sourceEventId: string;
|
|
3651
|
-
runId: string;
|
|
3652
|
-
workflowRef: string;
|
|
3653
|
-
targetSessionId: string;
|
|
3654
|
-
cause: WorkflowTurnIntentCause;
|
|
3655
|
-
nodeId: string | null;
|
|
3656
|
-
attemptId: string | null;
|
|
3657
|
-
factsHash: Buffer;
|
|
3658
|
-
requestedAt: number;
|
|
3659
|
-
eligibleAt: number | null;
|
|
3660
|
-
resolvedAt: number | null;
|
|
3661
|
-
resolution: WorkflowTurnIntentResolution | null;
|
|
3662
|
-
resolutionMessageId: string | null;
|
|
3663
|
-
claimExpiresAt: number | null;
|
|
3664
|
-
effectId: string;
|
|
3665
|
-
};
|
|
3666
|
-
type NotificationRow = {
|
|
3667
|
-
notificationId: string;
|
|
3668
|
-
effectId: string;
|
|
3669
|
-
runId: string;
|
|
3670
|
-
nodeId: string;
|
|
3671
|
-
attemptId: string;
|
|
3672
|
-
notificationIndex: number;
|
|
3673
|
-
targetSessionId: string;
|
|
3674
|
-
kind: "progress" | "final";
|
|
3675
|
-
contentHash: Buffer;
|
|
3676
|
-
createdAt: number;
|
|
3677
|
-
claimExpiresAt: number | null;
|
|
3678
|
-
deliveredAt: number | null;
|
|
3679
|
-
};
|
|
3680
|
-
|
|
3681
3149
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
3682
3150
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3683
3151
|
}
|
|
@@ -3687,6 +3155,78 @@ function isControllerRow(value: unknown): value is ControllerRow {
|
|
|
3687
3155
|
function isLeaseRow(value: unknown): value is LeaseRow {
|
|
3688
3156
|
return isRecord(value);
|
|
3689
3157
|
}
|
|
3158
|
+
function workflowRunViewRecord(row: WorkflowRunViewRow): WorkflowRunQueueViewRecord {
|
|
3159
|
+
const root =
|
|
3160
|
+
row.sourceType === "builtin"
|
|
3161
|
+
? { kind: "builtin", id: row.sourceRef, revision: row.sourceRevision }
|
|
3162
|
+
: { kind: "file", path: row.sourceRef, hash: row.sourceRevision };
|
|
3163
|
+
return {
|
|
3164
|
+
runId: row.runId,
|
|
3165
|
+
workflowName: row.workflowName,
|
|
3166
|
+
workflowSourceRef: row.workflowRef,
|
|
3167
|
+
workflowSource: { root, mounted: [] },
|
|
3168
|
+
initialized: row.runStatus !== "queued",
|
|
3169
|
+
definitionDigest: `sha256:${row.definitionDigest.toString("hex")}`,
|
|
3170
|
+
runStateStatus: row.runStatus,
|
|
3171
|
+
paused: row.paused === 1,
|
|
3172
|
+
status: row.status,
|
|
3173
|
+
originSessionId: row.executionMode === "headless" ? null : row.originSessionId,
|
|
3174
|
+
executionMode: row.executionMode,
|
|
3175
|
+
parentRunId: row.parentRunId,
|
|
3176
|
+
rootRunId: row.rootRunId,
|
|
3177
|
+
lineageKind: row.lineageKind,
|
|
3178
|
+
restartNumber: row.restartNumber,
|
|
3179
|
+
parentTerminalFingerprint: row.parentTerminalFingerprint?.toString("hex") ?? null,
|
|
3180
|
+
errorCode: row.errorCode,
|
|
3181
|
+
errorMessage: row.errorMessage,
|
|
3182
|
+
createdAt: new Date(row.createdAt).toISOString(),
|
|
3183
|
+
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
3184
|
+
startedAt: row.startedAt === null ? null : new Date(row.startedAt).toISOString(),
|
|
3185
|
+
finishedAt: row.finishedAt === null ? null : new Date(row.finishedAt).toISOString(),
|
|
3186
|
+
};
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
function isWorkflowRunViewRow(value: unknown): value is WorkflowRunViewRow {
|
|
3190
|
+
return (
|
|
3191
|
+
isRecord(value) &&
|
|
3192
|
+
typeof value.runId === "string" &&
|
|
3193
|
+
typeof value.workflowName === "string" &&
|
|
3194
|
+
typeof value.workflowRef === "string" &&
|
|
3195
|
+
typeof value.runStatus === "string" &&
|
|
3196
|
+
typeof value.paused === "number" &&
|
|
3197
|
+
Buffer.isBuffer(value.definitionDigest) &&
|
|
3198
|
+
typeof value.status === "string" &&
|
|
3199
|
+
(value.originSessionId === null || typeof value.originSessionId === "string") &&
|
|
3200
|
+
(value.executionMode === "interactive" || value.executionMode === "headless") &&
|
|
3201
|
+
(value.parentRunId === null || typeof value.parentRunId === "string") &&
|
|
3202
|
+
typeof value.rootRunId === "string" &&
|
|
3203
|
+
(value.lineageKind === null ||
|
|
3204
|
+
value.lineageKind === "continuation" ||
|
|
3205
|
+
value.lineageKind === "restart") &&
|
|
3206
|
+
typeof value.restartNumber === "number" &&
|
|
3207
|
+
(value.parentTerminalFingerprint === null ||
|
|
3208
|
+
Buffer.isBuffer(value.parentTerminalFingerprint)) &&
|
|
3209
|
+
(value.errorCode === null || typeof value.errorCode === "string") &&
|
|
3210
|
+
(value.errorMessage === null || typeof value.errorMessage === "string") &&
|
|
3211
|
+
typeof value.createdAt === "number" &&
|
|
3212
|
+
typeof value.updatedAt === "number" &&
|
|
3213
|
+
(value.startedAt === null || typeof value.startedAt === "number") &&
|
|
3214
|
+
(value.finishedAt === null || typeof value.finishedAt === "number") &&
|
|
3215
|
+
(value.sourceType === "builtin" || value.sourceType === "file") &&
|
|
3216
|
+
typeof value.sourceRef === "string" &&
|
|
3217
|
+
typeof value.sourceRevision === "string"
|
|
3218
|
+
);
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
function isRunListRevisionRow(value: unknown): value is RunListRevisionRow {
|
|
3222
|
+
return (
|
|
3223
|
+
isRecord(value) &&
|
|
3224
|
+
typeof value.count === "number" &&
|
|
3225
|
+
typeof value.revisionSum === "number" &&
|
|
3226
|
+
typeof value.updatedAt === "number"
|
|
3227
|
+
);
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3690
3230
|
function isRunRow(value: unknown): value is RunRow {
|
|
3691
3231
|
return isRecord(value);
|
|
3692
3232
|
}
|
|
@@ -3722,12 +3262,6 @@ function isControllerEventRow(value: unknown): value is ControllerEventRow {
|
|
|
3722
3262
|
function isRunEventRow(value: unknown): value is RunEventRow {
|
|
3723
3263
|
return isRecord(value);
|
|
3724
3264
|
}
|
|
3725
|
-
function isTurnIntentRow(value: unknown): value is TurnIntentRow {
|
|
3726
|
-
return isRecord(value);
|
|
3727
|
-
}
|
|
3728
|
-
function isNotificationRow(value: unknown): value is NotificationRow {
|
|
3729
|
-
return isRecord(value);
|
|
3730
|
-
}
|
|
3731
3265
|
function isProjectRow(value: unknown): value is { projectId: string } {
|
|
3732
3266
|
return isRecord(value);
|
|
3733
3267
|
}
|
|
@@ -3752,14 +3286,6 @@ function isSequenceRow(value: unknown): value is { seq: number } {
|
|
|
3752
3286
|
function isKeyRow(value: unknown): value is { key: string } {
|
|
3753
3287
|
return isRecord(value);
|
|
3754
3288
|
}
|
|
3755
|
-
function isResourceIdRow(value: unknown): value is { resourceId: string } {
|
|
3756
|
-
return isRecord(value);
|
|
3757
|
-
}
|
|
3758
|
-
function isEffectTokenRow(
|
|
3759
|
-
value: unknown,
|
|
3760
|
-
): value is { tokenHash: Buffer | null; expiresAt: number | null } {
|
|
3761
|
-
return isRecord(value);
|
|
3762
|
-
}
|
|
3763
3289
|
function isEffectIdentityRow(value: unknown): value is { effectId: string; resourceId: string } {
|
|
3764
3290
|
return isRecord(value);
|
|
3765
3291
|
}
|
|
@@ -3782,6 +3308,11 @@ function digestBuffer(value: string): Buffer {
|
|
|
3782
3308
|
if (!/^[a-f0-9]{64}$/i.test(hex)) throw new Error("Expected a SHA-256 digest");
|
|
3783
3309
|
return Buffer.from(hex, "hex");
|
|
3784
3310
|
}
|
|
3311
|
+
function terminalFingerprintBuffer(value: string): Buffer {
|
|
3312
|
+
const hex = value.startsWith("sha256:") ? value.slice(7) : value;
|
|
3313
|
+
if (!/^[a-f0-9]{64}$/i.test(hex)) throw new Error("Expected a terminal fingerprint");
|
|
3314
|
+
return Buffer.from(hex, "hex");
|
|
3315
|
+
}
|
|
3785
3316
|
type QueuedSource = {
|
|
3786
3317
|
root:
|
|
3787
3318
|
| { kind: "builtin"; id: string; revision: string }
|