@osolmaz/pi-workflows 0.16.0 → 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 +18 -2
- 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/client.d.ts +1 -0
- package/dist/client/client.js +5 -1
- package/dist/client/client.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/protocol.d.ts +1 -1
- package/dist/client/protocol.js +11 -5
- package/dist/client/protocol.js.map +1 -1
- package/dist/client/view.d.ts +42 -16
- package/dist/client/view.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +12 -165
- package/dist/controllers/sqlite.js +124 -435
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +23 -2
- package/dist/extension/index.js +619 -535
- 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 +2 -0
- package/dist/extension/session-view.js +60 -2
- 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 +2 -2
- package/dist/extension/widget.js +28 -18
- 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/runner.d.ts +43 -8
- package/dist/host/runner.js +1567 -331
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +19 -16
- package/dist/host/state.js +251 -63
- package/dist/host/state.js.map +1 -1
- package/dist/host/view.d.ts +19 -9
- package/dist/host/view.js +222 -118
- package/dist/host/view.js.map +1 -1
- package/dist/state/prune.js +3 -25
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +76 -97
- 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/render.js +1 -1
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/tui.d.ts +1 -1
- package/dist/viewer/tui.js +65 -8
- 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 +5 -19
- package/dist/workflows/store.js +121 -354
- 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 +5 -1
- package/docs/2026-09-02-installed-live-e2e-plan.md +15 -1
- 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 +19 -15
- package/docs/WORKFLOW_HOST.md +93 -28
- package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
- package/docs/live-replay-protocol.md +8 -4
- package/docs/tui-viewer.md +3 -1
- package/docs/workflows.md +34 -27
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/plugins/herdr/viewer.mjs +14 -5
- 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/client.ts +6 -2
- package/src/client/index.ts +2 -1
- package/src/client/protocol.ts +11 -5
- package/src/client/view.ts +43 -18
- package/src/controllers/index.ts +0 -1
- package/src/controllers/sqlite.ts +164 -844
- package/src/extension/index.ts +693 -646
- 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 +86 -3
- package/src/extension/step-message.ts +19 -9
- package/src/extension/widget.ts +37 -17
- 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/runner.ts +2120 -531
- package/src/host/state.ts +333 -94
- package/src/host/view.ts +249 -121
- package/src/state/prune.ts +3 -31
- package/src/state/schema.ts +76 -97
- package/src/state/workflow-messages.ts +548 -0
- package/src/viewer/render.ts +1 -5
- package/src/viewer/tui.ts +71 -6
- 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 +173 -489
- 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/src/extension/decision-channels.ts +0 -1826
|
@@ -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
|
|
|
@@ -75,6 +76,10 @@ export type WorkflowRunQueueViewRecord = {
|
|
|
75
76
|
originSessionId: string | null;
|
|
76
77
|
executionMode: "interactive" | "headless";
|
|
77
78
|
parentRunId: string | null;
|
|
79
|
+
rootRunId: string;
|
|
80
|
+
lineageKind: "continuation" | "restart" | null;
|
|
81
|
+
restartNumber: number;
|
|
82
|
+
parentTerminalFingerprint: string | null;
|
|
78
83
|
errorCode: string | null;
|
|
79
84
|
errorMessage: string | null;
|
|
80
85
|
createdAt: string;
|
|
@@ -101,6 +106,10 @@ export type WorkflowRunQueueRecord = {
|
|
|
101
106
|
originSessionId: string | null;
|
|
102
107
|
executionMode: "interactive" | "headless";
|
|
103
108
|
parentRunId: string | null;
|
|
109
|
+
rootRunId: string;
|
|
110
|
+
lineageKind: "continuation" | "restart" | null;
|
|
111
|
+
restartNumber: number;
|
|
112
|
+
parentTerminalFingerprint: string | null;
|
|
104
113
|
errorCode: string | null;
|
|
105
114
|
errorMessage: string | null;
|
|
106
115
|
createdAt: string;
|
|
@@ -119,62 +128,6 @@ export type WorkflowRunPreparationResult =
|
|
|
119
128
|
run: WorkflowRunQueueRecord;
|
|
120
129
|
};
|
|
121
130
|
|
|
122
|
-
export type WorkflowNotificationRecord = {
|
|
123
|
-
notificationId: string;
|
|
124
|
-
runId: string;
|
|
125
|
-
nodeId: string;
|
|
126
|
-
attemptId: string;
|
|
127
|
-
notificationIndex: number;
|
|
128
|
-
targetSessionId: string;
|
|
129
|
-
kind: "progress" | "final";
|
|
130
|
-
content: string;
|
|
131
|
-
createdAt: string;
|
|
132
|
-
deliveryClaimExpiresAt: string | null;
|
|
133
|
-
deliveredAt: string | null;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
export type WorkflowTurnIntentCause =
|
|
137
|
-
| "agentCancelled"
|
|
138
|
-
| "timedOut"
|
|
139
|
-
| "failed"
|
|
140
|
-
| "launchFailed"
|
|
141
|
-
| "controllerInterrupted"
|
|
142
|
-
| "claimLost"
|
|
143
|
-
| "terminal"
|
|
144
|
-
| "cancelled";
|
|
145
|
-
|
|
146
|
-
export type WorkflowTurnIntentResolution = "workflowPrompt" | "presentation" | "fallback";
|
|
147
|
-
|
|
148
|
-
export type WorkflowTurnIntentFacts = JsonObject & {
|
|
149
|
-
schema: typeof TURN_INTENT_FACTS_SCHEMA;
|
|
150
|
-
workflowName: string;
|
|
151
|
-
runId: string;
|
|
152
|
-
observedState: string;
|
|
153
|
-
cause: WorkflowTurnIntentCause;
|
|
154
|
-
nodeId: string | null;
|
|
155
|
-
attemptId: string | null;
|
|
156
|
-
reason: string | null;
|
|
157
|
-
handoff: boolean;
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
export type WorkflowTurnIntentRecord = {
|
|
161
|
-
intentId: string;
|
|
162
|
-
sourceEventId: string;
|
|
163
|
-
runId: string;
|
|
164
|
-
workflowRef: string;
|
|
165
|
-
targetSessionId: string;
|
|
166
|
-
cause: WorkflowTurnIntentCause;
|
|
167
|
-
nodeId: string | null;
|
|
168
|
-
attemptId: string | null;
|
|
169
|
-
fallbackFacts: WorkflowTurnIntentFacts;
|
|
170
|
-
requestedAt: string;
|
|
171
|
-
eligibleAt: string | null;
|
|
172
|
-
resolvedAt: string | null;
|
|
173
|
-
resolution: WorkflowTurnIntentResolution | null;
|
|
174
|
-
resolutionMessageId: string | null;
|
|
175
|
-
deliveryClaimExpiresAt: string | null;
|
|
176
|
-
};
|
|
177
|
-
|
|
178
131
|
export type RunEventRecord = {
|
|
179
132
|
seq: number;
|
|
180
133
|
recordedAt: string;
|
|
@@ -226,6 +179,10 @@ type RunRow = {
|
|
|
226
179
|
originSessionId: string | null;
|
|
227
180
|
executionMode: "interactive" | "headless";
|
|
228
181
|
parentRunId: string | null;
|
|
182
|
+
rootRunId: string;
|
|
183
|
+
lineageKind: "continuation" | "restart" | null;
|
|
184
|
+
restartNumber: number;
|
|
185
|
+
parentTerminalFingerprint: Buffer | null;
|
|
229
186
|
createdAt: number;
|
|
230
187
|
updatedAt: number;
|
|
231
188
|
startedAt: number | null;
|
|
@@ -245,6 +202,10 @@ type WorkflowRunViewRow = {
|
|
|
245
202
|
originSessionId: string | null;
|
|
246
203
|
executionMode: "interactive" | "headless";
|
|
247
204
|
parentRunId: string | null;
|
|
205
|
+
rootRunId: string;
|
|
206
|
+
lineageKind: "continuation" | "restart" | null;
|
|
207
|
+
restartNumber: number;
|
|
208
|
+
parentTerminalFingerprint: Buffer | null;
|
|
248
209
|
errorCode: string | null;
|
|
249
210
|
errorMessage: string | null;
|
|
250
211
|
createdAt: number;
|
|
@@ -1129,39 +1090,77 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1129
1090
|
if (this.getWorkflowRun(options.runId) !== undefined) {
|
|
1130
1091
|
throw new Error(`Workflow run already reserved: ${options.runId}`);
|
|
1131
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;
|
|
1132
1101
|
if (options.parentRunId !== undefined) {
|
|
1133
1102
|
const parent = this.requireWorkflowRunRow(options.parentRunId);
|
|
1134
1103
|
if (parent.originSessionId !== options.originSessionId) {
|
|
1135
|
-
throw new Error("
|
|
1104
|
+
throw new Error("Workflow parent belongs to another Pi session");
|
|
1136
1105
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
if (
|
|
1140
|
-
|
|
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");
|
|
1141
1121
|
}
|
|
1142
|
-
this.state.connection
|
|
1143
|
-
.prepare(
|
|
1144
|
-
`UPDATE run_queue
|
|
1145
|
-
SET status = 'done', updated_at = ?, finished_at = ?
|
|
1146
|
-
WHERE run_id = ? AND status = 'parked'`,
|
|
1147
|
-
)
|
|
1148
|
-
.run(now, now, parent.runId);
|
|
1149
|
-
const parentRevision = this.resourceRevision(parent.resourceId);
|
|
1150
|
-
this.bumpResource(parent.resourceId, parentRevision, now);
|
|
1151
|
-
this.insertEvent(
|
|
1152
|
-
parent.resourceId,
|
|
1153
|
-
parentRevision + 1,
|
|
1154
|
-
"run.queue_done_for_continuation",
|
|
1155
|
-
"session",
|
|
1156
|
-
options.originSessionId,
|
|
1157
|
-
{ continuationRunId: options.runId },
|
|
1158
|
-
now,
|
|
1159
|
-
);
|
|
1160
|
-
} else if (parent.status === "done") {
|
|
1161
|
-
throw new Error("Continuation parent already has a reserved continuation");
|
|
1162
1122
|
} else {
|
|
1163
|
-
|
|
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
|
+
}
|
|
1164
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");
|
|
1165
1164
|
}
|
|
1166
1165
|
const resourceId = resourceIdFor("run", options.runId);
|
|
1167
1166
|
const definitionHash = this.state.putJson(options.definitionSnapshot, now);
|
|
@@ -1188,16 +1187,21 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1188
1187
|
this.state.connection
|
|
1189
1188
|
.prepare(
|
|
1190
1189
|
`INSERT INTO runs(
|
|
1191
|
-
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,
|
|
1192
1192
|
workflow_ref, launch_options_hash, status, paused,
|
|
1193
1193
|
input_hash, created_at, updated_at
|
|
1194
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, 'queued', 0, ?, ?, ?)`,
|
|
1194
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', 0, ?, ?, ?)`,
|
|
1195
1195
|
)
|
|
1196
1196
|
.run(
|
|
1197
1197
|
options.runId,
|
|
1198
1198
|
resourceId,
|
|
1199
1199
|
this.requireProjectId(),
|
|
1200
1200
|
options.parentRunId ?? null,
|
|
1201
|
+
rootRunId,
|
|
1202
|
+
lineageKind,
|
|
1203
|
+
restartNumber,
|
|
1204
|
+
parentTerminalFingerprint,
|
|
1201
1205
|
definitionDigest,
|
|
1202
1206
|
options.workflowSourceRef,
|
|
1203
1207
|
launchHash,
|
|
@@ -1267,6 +1271,11 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1267
1271
|
runnerId: options.runnerId,
|
|
1268
1272
|
originSessionId: options.originSessionId,
|
|
1269
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 }),
|
|
1270
1279
|
...(options.now === undefined ? {} : { now: options.now }),
|
|
1271
1280
|
});
|
|
1272
1281
|
}
|
|
@@ -1436,6 +1445,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1436
1445
|
|
|
1437
1446
|
beginWorkflowRunInteractionTimeout(options: {
|
|
1438
1447
|
runId: string;
|
|
1448
|
+
targetSessionId: string;
|
|
1439
1449
|
claimToken: string;
|
|
1440
1450
|
now?: string;
|
|
1441
1451
|
}): boolean {
|
|
@@ -1456,18 +1466,24 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1456
1466
|
`SELECT i.request_id AS requestId, i.attempt_id AS attemptId
|
|
1457
1467
|
FROM interactive_requests i
|
|
1458
1468
|
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
1459
|
-
|
|
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
|
|
1460
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
|
+
)
|
|
1461
1478
|
ORDER BY a.deadline_at, i.request_id LIMIT 1`,
|
|
1462
1479
|
)
|
|
1463
|
-
.get(options.runId, now);
|
|
1480
|
+
.get(options.runId, options.targetSessionId, now);
|
|
1464
1481
|
if (!isExpiredInteractionRow(interaction)) return false;
|
|
1465
1482
|
const request = this.state.connection
|
|
1466
1483
|
.prepare(
|
|
1467
1484
|
`UPDATE interactive_requests
|
|
1468
|
-
SET status = 'cancelled',
|
|
1469
|
-
|
|
1470
|
-
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'`,
|
|
1471
1487
|
)
|
|
1472
1488
|
.run(now, interaction.requestId, options.runId);
|
|
1473
1489
|
const run = this.state.connection
|
|
@@ -1489,7 +1505,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1489
1505
|
if (request.changes !== 1 || run.changes !== 1 || queue.changes !== 1) {
|
|
1490
1506
|
throw new Error(`Workflow run ${options.runId} changed during interaction timeout`);
|
|
1491
1507
|
}
|
|
1492
|
-
const error = "Workflow node deadline expired
|
|
1508
|
+
const error = "Workflow node model-turn deadline expired";
|
|
1493
1509
|
const receiptHash = this.state.putJson({ status: "rejected", error }, now);
|
|
1494
1510
|
this.state.connection
|
|
1495
1511
|
.prepare(
|
|
@@ -1539,7 +1555,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1539
1555
|
"(l.owner_id IS NULL OR l.expires_at <= ? OR l.owner_id = ?)",
|
|
1540
1556
|
"(q.affinity_runner_id IS NULL OR q.affinity_runner_id = ? OR q.status IN ('parked', 'starting', 'running'))",
|
|
1541
1557
|
"NOT (q.status = 'parked' AND (r.status = 'waiting' OR r.paused = 1))",
|
|
1542
|
-
"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')",
|
|
1543
1559
|
"(q.error_code IS NULL OR q.error_code <> 'workflowSourceChanged')",
|
|
1544
1560
|
];
|
|
1545
1561
|
const params: unknown[] = [now, now, options.runnerId, options.runnerId];
|
|
@@ -1663,7 +1679,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1663
1679
|
const pending = this.state.connection
|
|
1664
1680
|
.prepare(
|
|
1665
1681
|
`SELECT 1 FROM interactive_requests
|
|
1666
|
-
WHERE run_id = ? AND status
|
|
1682
|
+
WHERE run_id = ? AND status = 'pending' LIMIT 1`,
|
|
1667
1683
|
)
|
|
1668
1684
|
.get(options.runId);
|
|
1669
1685
|
if (pending === undefined) return false;
|
|
@@ -1714,7 +1730,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1714
1730
|
const pending = this.state.connection
|
|
1715
1731
|
.prepare(
|
|
1716
1732
|
`SELECT 1 FROM interactive_requests
|
|
1717
|
-
WHERE run_id = ? AND status
|
|
1733
|
+
WHERE run_id = ? AND status = 'pending' LIMIT 1`,
|
|
1718
1734
|
)
|
|
1719
1735
|
.get(options.runId);
|
|
1720
1736
|
if (pending === undefined) return false;
|
|
@@ -2132,427 +2148,6 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
2132
2148
|
}));
|
|
2133
2149
|
}
|
|
2134
2150
|
|
|
2135
|
-
ensureWorkflowTurnIntent(options: {
|
|
2136
|
-
intentId: string;
|
|
2137
|
-
sourceEventId: string;
|
|
2138
|
-
runId: string;
|
|
2139
|
-
workflowRef: string;
|
|
2140
|
-
targetSessionId: string;
|
|
2141
|
-
cause: WorkflowTurnIntentCause;
|
|
2142
|
-
nodeId?: string | null;
|
|
2143
|
-
attemptId?: string | null;
|
|
2144
|
-
fallbackFacts: WorkflowTurnIntentFacts;
|
|
2145
|
-
eligible: boolean;
|
|
2146
|
-
requestedAt?: string;
|
|
2147
|
-
}): WorkflowTurnIntentRecord {
|
|
2148
|
-
const existing = this.turnIntentBySource(options.sourceEventId);
|
|
2149
|
-
if (existing !== undefined) return existing;
|
|
2150
|
-
const now = epoch(validTimestamp(options.requestedAt));
|
|
2151
|
-
const run = this.requireWorkflowRunRow(options.runId);
|
|
2152
|
-
const intentId = options.intentId;
|
|
2153
|
-
const resourceId = resourceIdFor("turn_intent", intentId);
|
|
2154
|
-
const effectId = `effect-${randomUUID()}`;
|
|
2155
|
-
this.state.transaction(() => {
|
|
2156
|
-
const factsHash = this.state.putJson(options.fallbackFacts, now);
|
|
2157
|
-
this.insertEffectResource(
|
|
2158
|
-
effectId,
|
|
2159
|
-
resourceIdFor("effect", effectId),
|
|
2160
|
-
run.resourceId,
|
|
2161
|
-
this.resourceRevision(run.resourceId),
|
|
2162
|
-
"turn.deliver",
|
|
2163
|
-
intentId,
|
|
2164
|
-
factsHash,
|
|
2165
|
-
"run",
|
|
2166
|
-
now,
|
|
2167
|
-
);
|
|
2168
|
-
this.state.connection
|
|
2169
|
-
.prepare(
|
|
2170
|
-
"INSERT INTO resources(resource_id, resource_type, aggregate_key, revision, created_at, updated_at) VALUES (?, 'turn_intent', ?, 1, ?, ?)",
|
|
2171
|
-
)
|
|
2172
|
-
.run(resourceId, intentId, now, now);
|
|
2173
|
-
this.state.connection
|
|
2174
|
-
.prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
|
|
2175
|
-
.run(resourceId);
|
|
2176
|
-
this.state.connection
|
|
2177
|
-
.prepare(
|
|
2178
|
-
`INSERT INTO turn_intents(
|
|
2179
|
-
turn_intent_id, resource_id, effect_id, source_event_id, run_id,
|
|
2180
|
-
workflow_ref, target_session_id, cause, node_id, attempt_id,
|
|
2181
|
-
facts_hash, requested_at, eligible_at, created_at
|
|
2182
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2183
|
-
)
|
|
2184
|
-
.run(
|
|
2185
|
-
intentId,
|
|
2186
|
-
resourceId,
|
|
2187
|
-
effectId,
|
|
2188
|
-
options.sourceEventId,
|
|
2189
|
-
options.runId,
|
|
2190
|
-
options.workflowRef,
|
|
2191
|
-
options.targetSessionId,
|
|
2192
|
-
options.cause,
|
|
2193
|
-
options.nodeId ?? null,
|
|
2194
|
-
options.attemptId ?? null,
|
|
2195
|
-
factsHash,
|
|
2196
|
-
now,
|
|
2197
|
-
options.eligible ? now : null,
|
|
2198
|
-
now,
|
|
2199
|
-
);
|
|
2200
|
-
this.insertEvent(
|
|
2201
|
-
resourceId,
|
|
2202
|
-
1,
|
|
2203
|
-
"turn.requested",
|
|
2204
|
-
"session",
|
|
2205
|
-
options.targetSessionId,
|
|
2206
|
-
{},
|
|
2207
|
-
now,
|
|
2208
|
-
);
|
|
2209
|
-
});
|
|
2210
|
-
return this.requireTurnIntent(intentId);
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
getWorkflowTurnIntent(intentId: string): WorkflowTurnIntentRecord | undefined {
|
|
2214
|
-
return this.turnIntent(intentId);
|
|
2215
|
-
}
|
|
2216
|
-
|
|
2217
|
-
findPendingWorkflowTurnIntent(options: {
|
|
2218
|
-
runId: string;
|
|
2219
|
-
targetSessionId: string;
|
|
2220
|
-
}): WorkflowTurnIntentRecord | undefined {
|
|
2221
|
-
const row = this.state.connection
|
|
2222
|
-
.prepare(
|
|
2223
|
-
turnIntentSelect(
|
|
2224
|
-
"WHERE t.run_id = ? AND t.target_session_id = ? AND t.resolved_at IS NULL ORDER BY t.requested_at DESC LIMIT 1",
|
|
2225
|
-
),
|
|
2226
|
-
)
|
|
2227
|
-
.get(options.runId, options.targetSessionId);
|
|
2228
|
-
return isTurnIntentRow(row) ? this.mapTurnIntent(row) : undefined;
|
|
2229
|
-
}
|
|
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
|
-
|
|
2246
|
-
claimWorkflowTurnIntent(options: {
|
|
2247
|
-
intentId: string;
|
|
2248
|
-
targetSessionId: string;
|
|
2249
|
-
claimToken: string;
|
|
2250
|
-
leaseMs: number;
|
|
2251
|
-
now?: string;
|
|
2252
|
-
}): WorkflowTurnIntentRecord | undefined {
|
|
2253
|
-
const intent = this.turnIntent(options.intentId);
|
|
2254
|
-
if (intent?.targetSessionId !== options.targetSessionId) return undefined;
|
|
2255
|
-
return this.claimEffectForTurn(
|
|
2256
|
-
options.intentId,
|
|
2257
|
-
options.claimToken,
|
|
2258
|
-
options.leaseMs,
|
|
2259
|
-
options.now,
|
|
2260
|
-
)
|
|
2261
|
-
? this.requireTurnIntent(options.intentId)
|
|
2262
|
-
: undefined;
|
|
2263
|
-
}
|
|
2264
|
-
|
|
2265
|
-
claimEligibleWorkflowTurnIntents(options: {
|
|
2266
|
-
targetSessionId: string;
|
|
2267
|
-
claimToken: string;
|
|
2268
|
-
leaseMs: number;
|
|
2269
|
-
now?: string;
|
|
2270
|
-
limit?: number;
|
|
2271
|
-
}): WorkflowTurnIntentRecord[] {
|
|
2272
|
-
const now = epoch(validTimestamp(options.now));
|
|
2273
|
-
const rows = this.state.connection
|
|
2274
|
-
.prepare(
|
|
2275
|
-
turnIntentSelect(
|
|
2276
|
-
"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 ?",
|
|
2277
|
-
),
|
|
2278
|
-
)
|
|
2279
|
-
.all(options.targetSessionId, now, options.limit ?? 10);
|
|
2280
|
-
const claimed: WorkflowTurnIntentRecord[] = [];
|
|
2281
|
-
for (const row of rows) {
|
|
2282
|
-
if (
|
|
2283
|
-
isTurnIntentRow(row) &&
|
|
2284
|
-
this.claimEffectForTurn(row.intentId, options.claimToken, options.leaseMs, options.now)
|
|
2285
|
-
) {
|
|
2286
|
-
claimed.push(this.requireTurnIntent(row.intentId));
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
return claimed;
|
|
2290
|
-
}
|
|
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
|
-
|
|
2310
|
-
makeWorkflowTurnIntentEligible(options: {
|
|
2311
|
-
intentId: string;
|
|
2312
|
-
fallbackFacts: WorkflowTurnIntentFacts;
|
|
2313
|
-
eligibleAt?: string;
|
|
2314
|
-
now?: string;
|
|
2315
|
-
}): boolean {
|
|
2316
|
-
const eligibleAt = epoch(validTimestamp(options.eligibleAt ?? options.now));
|
|
2317
|
-
const factsHash = this.state.putJson(options.fallbackFacts, eligibleAt);
|
|
2318
|
-
return (
|
|
2319
|
-
this.state.connection
|
|
2320
|
-
.prepare(
|
|
2321
|
-
"UPDATE turn_intents SET eligible_at = ?, facts_hash = ? WHERE turn_intent_id = ? AND resolved_at IS NULL",
|
|
2322
|
-
)
|
|
2323
|
-
.run(eligibleAt, factsHash, options.intentId).changes === 1
|
|
2324
|
-
);
|
|
2325
|
-
}
|
|
2326
|
-
|
|
2327
|
-
resolveWorkflowTurnIntent(options: {
|
|
2328
|
-
intentId: string;
|
|
2329
|
-
targetSessionId: string;
|
|
2330
|
-
claimToken: string;
|
|
2331
|
-
resolution: WorkflowTurnIntentResolution;
|
|
2332
|
-
messageId?: string;
|
|
2333
|
-
now?: string;
|
|
2334
|
-
}): boolean {
|
|
2335
|
-
const now = epoch(validTimestamp(options.now));
|
|
2336
|
-
return this.state.transaction(() => {
|
|
2337
|
-
const row = this.turnIntentRow(options.intentId);
|
|
2338
|
-
if (
|
|
2339
|
-
row === undefined ||
|
|
2340
|
-
row.targetSessionId !== options.targetSessionId ||
|
|
2341
|
-
!this.verifyEffectToken(row.effectId, options.claimToken, options.now)
|
|
2342
|
-
) {
|
|
2343
|
-
return false;
|
|
2344
|
-
}
|
|
2345
|
-
const result = this.state.connection
|
|
2346
|
-
.prepare(
|
|
2347
|
-
`UPDATE turn_intents SET resolved_at = ?, resolution_type = ?, resolution_message_id = ?
|
|
2348
|
-
WHERE turn_intent_id = ? AND resolved_at IS NULL`,
|
|
2349
|
-
)
|
|
2350
|
-
.run(now, options.resolution, options.messageId ?? null, options.intentId);
|
|
2351
|
-
if (result.changes === 1) this.completeEffect(row.effectId, "applied", now);
|
|
2352
|
-
return result.changes === 1;
|
|
2353
|
-
});
|
|
2354
|
-
}
|
|
2355
|
-
|
|
2356
|
-
releaseWorkflowTurnIntentClaim(options: {
|
|
2357
|
-
intentId: string;
|
|
2358
|
-
targetSessionId: string;
|
|
2359
|
-
claimToken: string;
|
|
2360
|
-
}): boolean {
|
|
2361
|
-
const row = this.turnIntentRow(options.intentId);
|
|
2362
|
-
return (
|
|
2363
|
-
row !== undefined &&
|
|
2364
|
-
row.targetSessionId === options.targetSessionId &&
|
|
2365
|
-
this.releaseEffectLease(row.effectId, options.claimToken)
|
|
2366
|
-
);
|
|
2367
|
-
}
|
|
2368
|
-
|
|
2369
|
-
listWorkflowTurnIntents(
|
|
2370
|
-
options: {
|
|
2371
|
-
runId?: string;
|
|
2372
|
-
targetSessionId?: string;
|
|
2373
|
-
unresolvedOnly?: boolean;
|
|
2374
|
-
limit?: number;
|
|
2375
|
-
} = {},
|
|
2376
|
-
): WorkflowTurnIntentRecord[] {
|
|
2377
|
-
const clauses = ["1 = 1"];
|
|
2378
|
-
const params: unknown[] = [];
|
|
2379
|
-
if (options.runId !== undefined) {
|
|
2380
|
-
clauses.push("t.run_id = ?");
|
|
2381
|
-
params.push(options.runId);
|
|
2382
|
-
}
|
|
2383
|
-
if (options.targetSessionId !== undefined) {
|
|
2384
|
-
clauses.push("t.target_session_id = ?");
|
|
2385
|
-
params.push(options.targetSessionId);
|
|
2386
|
-
}
|
|
2387
|
-
if (options.unresolvedOnly === true) clauses.push("t.resolved_at IS NULL");
|
|
2388
|
-
params.push(options.limit ?? 100);
|
|
2389
|
-
const rows = this.state.connection
|
|
2390
|
-
.prepare(
|
|
2391
|
-
turnIntentSelect(`WHERE ${clauses.join(" AND ")} ORDER BY t.requested_at DESC LIMIT ?`),
|
|
2392
|
-
)
|
|
2393
|
-
.all(...params);
|
|
2394
|
-
return rows.filter(isTurnIntentRow).map((row) => this.mapTurnIntent(row));
|
|
2395
|
-
}
|
|
2396
|
-
|
|
2397
|
-
enqueueWorkflowNotification(options: {
|
|
2398
|
-
runId: string;
|
|
2399
|
-
nodeId: string;
|
|
2400
|
-
attemptId: string;
|
|
2401
|
-
notificationIndex: number;
|
|
2402
|
-
targetSessionId: string;
|
|
2403
|
-
kind: "progress" | "final";
|
|
2404
|
-
content: string;
|
|
2405
|
-
now?: string;
|
|
2406
|
-
}): WorkflowNotificationRecord {
|
|
2407
|
-
const existing = this.notification(options.runId, options.attemptId, options.notificationIndex);
|
|
2408
|
-
if (existing !== undefined) return existing;
|
|
2409
|
-
const run = this.requireWorkflowRunRow(options.runId);
|
|
2410
|
-
const now = epoch(validTimestamp(options.now));
|
|
2411
|
-
const notificationId = `notification-${randomUUID()}`;
|
|
2412
|
-
const resourceId = resourceIdFor("notification", notificationId);
|
|
2413
|
-
const effectId = `effect-${randomUUID()}`;
|
|
2414
|
-
this.state.transaction(() => {
|
|
2415
|
-
const contentHash = this.state.putText(options.content, now);
|
|
2416
|
-
this.insertEffectResource(
|
|
2417
|
-
effectId,
|
|
2418
|
-
resourceIdFor("effect", effectId),
|
|
2419
|
-
run.resourceId,
|
|
2420
|
-
this.resourceRevision(run.resourceId),
|
|
2421
|
-
"notification.deliver",
|
|
2422
|
-
notificationId,
|
|
2423
|
-
contentHash,
|
|
2424
|
-
"run",
|
|
2425
|
-
now,
|
|
2426
|
-
);
|
|
2427
|
-
this.state.connection
|
|
2428
|
-
.prepare(
|
|
2429
|
-
"INSERT INTO resources(resource_id, resource_type, aggregate_key, revision, created_at, updated_at) VALUES (?, 'notification', ?, 1, ?, ?)",
|
|
2430
|
-
)
|
|
2431
|
-
.run(resourceId, notificationId, now, now);
|
|
2432
|
-
this.state.connection
|
|
2433
|
-
.prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
|
|
2434
|
-
.run(resourceId);
|
|
2435
|
-
this.state.connection
|
|
2436
|
-
.prepare(
|
|
2437
|
-
`INSERT INTO notifications(
|
|
2438
|
-
notification_id, effect_id, run_id, attempt_id, notification_index,
|
|
2439
|
-
target_session_id, notification_type, content_hash, created_at
|
|
2440
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
2441
|
-
)
|
|
2442
|
-
.run(
|
|
2443
|
-
notificationId,
|
|
2444
|
-
effectId,
|
|
2445
|
-
options.runId,
|
|
2446
|
-
options.attemptId,
|
|
2447
|
-
options.notificationIndex,
|
|
2448
|
-
options.targetSessionId,
|
|
2449
|
-
options.kind,
|
|
2450
|
-
contentHash,
|
|
2451
|
-
now,
|
|
2452
|
-
);
|
|
2453
|
-
this.insertEvent(
|
|
2454
|
-
resourceId,
|
|
2455
|
-
1,
|
|
2456
|
-
"notification.queued",
|
|
2457
|
-
"session",
|
|
2458
|
-
options.targetSessionId,
|
|
2459
|
-
{},
|
|
2460
|
-
now,
|
|
2461
|
-
);
|
|
2462
|
-
});
|
|
2463
|
-
return this.requireNotification(options.runId, options.attemptId, options.notificationIndex);
|
|
2464
|
-
}
|
|
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
|
-
|
|
2482
|
-
listPendingWorkflowNotifications(options: {
|
|
2483
|
-
targetSessionId: string;
|
|
2484
|
-
limit?: number;
|
|
2485
|
-
}): WorkflowNotificationRecord[] {
|
|
2486
|
-
const now = Date.now();
|
|
2487
|
-
return this.notificationRows(options.targetSessionId, options.limit ?? 20)
|
|
2488
|
-
.map((row) => this.mapNotification(row))
|
|
2489
|
-
.filter(
|
|
2490
|
-
(notification) =>
|
|
2491
|
-
notification.deliveryClaimExpiresAt === null ||
|
|
2492
|
-
Date.parse(notification.deliveryClaimExpiresAt) <= now,
|
|
2493
|
-
);
|
|
2494
|
-
}
|
|
2495
|
-
|
|
2496
|
-
claimPendingWorkflowNotifications(options: {
|
|
2497
|
-
targetSessionId: string;
|
|
2498
|
-
claimToken: string;
|
|
2499
|
-
leaseMs: number;
|
|
2500
|
-
now?: string;
|
|
2501
|
-
limit?: number;
|
|
2502
|
-
}): WorkflowNotificationRecord[] {
|
|
2503
|
-
const rows = this.notificationRows(options.targetSessionId, options.limit ?? 20);
|
|
2504
|
-
const result: WorkflowNotificationRecord[] = [];
|
|
2505
|
-
for (const row of rows) {
|
|
2506
|
-
if (
|
|
2507
|
-
this.claimEffect(
|
|
2508
|
-
row.effectId,
|
|
2509
|
-
options.targetSessionId,
|
|
2510
|
-
options.claimToken,
|
|
2511
|
-
options.leaseMs,
|
|
2512
|
-
options.now,
|
|
2513
|
-
)
|
|
2514
|
-
) {
|
|
2515
|
-
result.push(this.mapNotification(row));
|
|
2516
|
-
}
|
|
2517
|
-
}
|
|
2518
|
-
return result;
|
|
2519
|
-
}
|
|
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
|
-
|
|
2535
|
-
markWorkflowNotificationDelivered(options: {
|
|
2536
|
-
notificationId: string;
|
|
2537
|
-
targetSessionId: string;
|
|
2538
|
-
claimToken: string;
|
|
2539
|
-
now?: string;
|
|
2540
|
-
}): boolean {
|
|
2541
|
-
const now = epoch(validTimestamp(options.now));
|
|
2542
|
-
return this.state.transaction(() => {
|
|
2543
|
-
const row = this.notificationRowById(options.notificationId);
|
|
2544
|
-
if (
|
|
2545
|
-
row === undefined ||
|
|
2546
|
-
row.targetSessionId !== options.targetSessionId ||
|
|
2547
|
-
!this.verifyEffectToken(row.effectId, options.claimToken, options.now)
|
|
2548
|
-
) {
|
|
2549
|
-
return false;
|
|
2550
|
-
}
|
|
2551
|
-
this.completeEffect(row.effectId, "applied", now);
|
|
2552
|
-
return true;
|
|
2553
|
-
});
|
|
2554
|
-
}
|
|
2555
|
-
|
|
2556
2151
|
settleRunEffect(runId: string, effectType: "run.park_queue" | "run.settle_queue"): void {
|
|
2557
2152
|
const rows = this.state.connection
|
|
2558
2153
|
.prepare(
|
|
@@ -3007,6 +2602,21 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
3007
2602
|
options: WorkflowRunReservationOptions,
|
|
3008
2603
|
definitionDigest: Buffer,
|
|
3009
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);
|
|
3010
2620
|
const compatible =
|
|
3011
2621
|
row.workflowName === options.workflowName &&
|
|
3012
2622
|
row.workflowRef === options.workflowSourceRef &&
|
|
@@ -3018,7 +2628,14 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
3018
2628
|
canonicalJson(options.launchOptions ?? {}) &&
|
|
3019
2629
|
row.originSessionId === options.originSessionId &&
|
|
3020
2630
|
row.executionMode === (options.executionMode ?? "interactive") &&
|
|
3021
|
-
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)));
|
|
3022
2639
|
if (!compatible) {
|
|
3023
2640
|
throw new Error(`Workflow run preparation conflicts: ${options.runId}`);
|
|
3024
2641
|
}
|
|
@@ -3052,6 +2669,10 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
3052
2669
|
originSessionId: row.executionMode === "headless" ? null : row.originSessionId,
|
|
3053
2670
|
executionMode: row.executionMode,
|
|
3054
2671
|
parentRunId: row.parentRunId,
|
|
2672
|
+
rootRunId: row.rootRunId,
|
|
2673
|
+
lineageKind: row.lineageKind,
|
|
2674
|
+
restartNumber: row.restartNumber,
|
|
2675
|
+
parentTerminalFingerprint: row.parentTerminalFingerprint?.toString("hex") ?? null,
|
|
3055
2676
|
errorCode: row.errorCode,
|
|
3056
2677
|
errorMessage:
|
|
3057
2678
|
row.errorHash === null
|
|
@@ -3092,7 +2713,7 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
3092
2713
|
this.state.connection
|
|
3093
2714
|
.prepare(
|
|
3094
2715
|
`SELECT 1 FROM interactive_requests
|
|
3095
|
-
WHERE run_id = ? AND status
|
|
2716
|
+
WHERE run_id = ? AND status = 'pending' LIMIT 1`,
|
|
3096
2717
|
)
|
|
3097
2718
|
.get(runId) !== undefined
|
|
3098
2719
|
) {
|
|
@@ -3419,262 +3040,12 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
3419
3040
|
this.state.connection
|
|
3420
3041
|
.prepare(
|
|
3421
3042
|
`UPDATE interactive_requests
|
|
3422
|
-
SET status = 'cancelled',
|
|
3423
|
-
|
|
3424
|
-
WHERE run_id = ? AND status IN ('pending', 'presenting')`,
|
|
3043
|
+
SET status = 'cancelled', revision = revision + 1, updated_at = ?
|
|
3044
|
+
WHERE run_id = ? AND status = 'pending'`,
|
|
3425
3045
|
)
|
|
3426
3046
|
.run(now, runId);
|
|
3427
3047
|
}
|
|
3428
3048
|
|
|
3429
|
-
private turnIntent(intentId: string): WorkflowTurnIntentRecord | undefined {
|
|
3430
|
-
const row = this.turnIntentRow(intentId);
|
|
3431
|
-
return row === undefined ? undefined : this.mapTurnIntent(row);
|
|
3432
|
-
}
|
|
3433
|
-
|
|
3434
|
-
private requireTurnIntent(intentId: string): WorkflowTurnIntentRecord {
|
|
3435
|
-
const row = this.turnIntent(intentId);
|
|
3436
|
-
if (row === undefined) throw new Error(`Workflow turn intent not found: ${intentId}`);
|
|
3437
|
-
return row;
|
|
3438
|
-
}
|
|
3439
|
-
|
|
3440
|
-
private turnIntentRow(intentId: string): TurnIntentRow | undefined {
|
|
3441
|
-
const row = this.state.connection
|
|
3442
|
-
.prepare(turnIntentSelect("WHERE t.turn_intent_id = ?"))
|
|
3443
|
-
.get(intentId);
|
|
3444
|
-
return isTurnIntentRow(row) ? row : undefined;
|
|
3445
|
-
}
|
|
3446
|
-
|
|
3447
|
-
private turnIntentBySource(sourceEventId: string): WorkflowTurnIntentRecord | undefined {
|
|
3448
|
-
const row = this.state.connection
|
|
3449
|
-
.prepare(turnIntentSelect("WHERE t.source_event_id = ?"))
|
|
3450
|
-
.get(sourceEventId);
|
|
3451
|
-
return isTurnIntentRow(row) ? this.mapTurnIntent(row) : undefined;
|
|
3452
|
-
}
|
|
3453
|
-
|
|
3454
|
-
/* istanbul ignore next -- pure projection covered by integration tests */
|
|
3455
|
-
private mapTurnIntent(row: TurnIntentRow): WorkflowTurnIntentRecord {
|
|
3456
|
-
return {
|
|
3457
|
-
intentId: row.intentId,
|
|
3458
|
-
sourceEventId: row.sourceEventId,
|
|
3459
|
-
runId: row.runId,
|
|
3460
|
-
workflowRef: row.workflowRef,
|
|
3461
|
-
targetSessionId: row.targetSessionId,
|
|
3462
|
-
cause: row.cause,
|
|
3463
|
-
nodeId: row.nodeId,
|
|
3464
|
-
attemptId: row.attemptId,
|
|
3465
|
-
fallbackFacts: this.state.readJson(row.factsHash) as WorkflowTurnIntentFacts,
|
|
3466
|
-
requestedAt: new Date(row.requestedAt).toISOString(),
|
|
3467
|
-
eligibleAt: row.eligibleAt === null ? null : new Date(row.eligibleAt).toISOString(),
|
|
3468
|
-
resolvedAt: row.resolvedAt === null ? null : new Date(row.resolvedAt).toISOString(),
|
|
3469
|
-
resolution: row.resolution,
|
|
3470
|
-
resolutionMessageId: row.resolutionMessageId,
|
|
3471
|
-
deliveryClaimExpiresAt:
|
|
3472
|
-
row.claimExpiresAt === null ? null : new Date(row.claimExpiresAt).toISOString(),
|
|
3473
|
-
};
|
|
3474
|
-
}
|
|
3475
|
-
|
|
3476
|
-
private claimEffectForTurn(
|
|
3477
|
-
intentId: string,
|
|
3478
|
-
token: string,
|
|
3479
|
-
leaseMs: number,
|
|
3480
|
-
nowValue?: string,
|
|
3481
|
-
): boolean {
|
|
3482
|
-
const row = this.turnIntentRow(intentId);
|
|
3483
|
-
return (
|
|
3484
|
-
row !== undefined &&
|
|
3485
|
-
this.claimEffect(row.effectId, row.targetSessionId, token, leaseMs, nowValue)
|
|
3486
|
-
);
|
|
3487
|
-
}
|
|
3488
|
-
|
|
3489
|
-
private claimEffect(
|
|
3490
|
-
effectId: string,
|
|
3491
|
-
ownerId: string,
|
|
3492
|
-
token: string,
|
|
3493
|
-
leaseMs: number,
|
|
3494
|
-
nowValue?: string,
|
|
3495
|
-
): boolean {
|
|
3496
|
-
const now = epoch(validTimestamp(nowValue));
|
|
3497
|
-
return this.state.transaction(() => {
|
|
3498
|
-
const row = this.state.connection
|
|
3499
|
-
.prepare(
|
|
3500
|
-
"SELECT resource_id AS resourceId FROM effects WHERE effect_id = ? AND status = 'pending'",
|
|
3501
|
-
)
|
|
3502
|
-
.get(effectId);
|
|
3503
|
-
/* istanbul ignore if -- impossible after exact schema and transaction checks */
|
|
3504
|
-
if (!isResourceIdRow(row)) return false;
|
|
3505
|
-
const lease = this.requireLease(row.resourceId);
|
|
3506
|
-
if (lease.ownerId !== null && lease.expiresAt !== null && lease.expiresAt > now) return false;
|
|
3507
|
-
return (
|
|
3508
|
-
this.state.connection
|
|
3509
|
-
.prepare(
|
|
3510
|
-
`UPDATE leases SET generation = ?, owner_type = 'session', owner_id = ?, token_hash = ?,
|
|
3511
|
-
acquired_at = ?, heartbeat_at = ?, expires_at = ?
|
|
3512
|
-
WHERE resource_id = ? AND generation = ?`,
|
|
3513
|
-
)
|
|
3514
|
-
.run(
|
|
3515
|
-
lease.generation + 1,
|
|
3516
|
-
ownerId,
|
|
3517
|
-
tokenHash(token),
|
|
3518
|
-
now,
|
|
3519
|
-
now,
|
|
3520
|
-
now + leaseMs,
|
|
3521
|
-
row.resourceId,
|
|
3522
|
-
lease.generation,
|
|
3523
|
-
).changes === 1
|
|
3524
|
-
);
|
|
3525
|
-
});
|
|
3526
|
-
}
|
|
3527
|
-
|
|
3528
|
-
private verifyEffectToken(effectId: string, token: string, nowValue?: string): boolean {
|
|
3529
|
-
const now = epoch(validTimestamp(nowValue));
|
|
3530
|
-
const row = this.state.connection
|
|
3531
|
-
.prepare(
|
|
3532
|
-
`SELECT l.token_hash AS tokenHash, l.expires_at AS expiresAt
|
|
3533
|
-
FROM effects e JOIN leases l ON l.resource_id = e.resource_id
|
|
3534
|
-
WHERE e.effect_id = ?`,
|
|
3535
|
-
)
|
|
3536
|
-
.get(effectId);
|
|
3537
|
-
return (
|
|
3538
|
-
isEffectTokenRow(row) &&
|
|
3539
|
-
row.tokenHash !== null &&
|
|
3540
|
-
row.tokenHash.equals(tokenHash(token)) &&
|
|
3541
|
-
row.expiresAt !== null &&
|
|
3542
|
-
row.expiresAt > now
|
|
3543
|
-
);
|
|
3544
|
-
}
|
|
3545
|
-
|
|
3546
|
-
private releaseEffectLease(effectId: string, token: string): boolean {
|
|
3547
|
-
return (
|
|
3548
|
-
this.state.connection
|
|
3549
|
-
.prepare(
|
|
3550
|
-
`UPDATE leases SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
|
|
3551
|
-
acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
|
|
3552
|
-
WHERE resource_id = (SELECT resource_id FROM effects WHERE effect_id = ?)
|
|
3553
|
-
AND token_hash = ?`,
|
|
3554
|
-
)
|
|
3555
|
-
.run(effectId, tokenHash(token)).changes === 1
|
|
3556
|
-
);
|
|
3557
|
-
}
|
|
3558
|
-
|
|
3559
|
-
private completeEffect(
|
|
3560
|
-
effectId: string,
|
|
3561
|
-
status: "applied" | "rejected" | "ambiguous",
|
|
3562
|
-
now: number,
|
|
3563
|
-
): void {
|
|
3564
|
-
this.state.connection
|
|
3565
|
-
.prepare("UPDATE effects SET status = ?, updated_at = ?, settled_at = ? WHERE effect_id = ?")
|
|
3566
|
-
.run(status, now, now, effectId);
|
|
3567
|
-
this.state.connection
|
|
3568
|
-
.prepare(
|
|
3569
|
-
`UPDATE leases SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
|
|
3570
|
-
acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
|
|
3571
|
-
WHERE resource_id = (SELECT resource_id FROM effects WHERE effect_id = ?)`,
|
|
3572
|
-
)
|
|
3573
|
-
.run(effectId);
|
|
3574
|
-
}
|
|
3575
|
-
|
|
3576
|
-
private insertEffectResource(
|
|
3577
|
-
effectId: string,
|
|
3578
|
-
resourceId: string,
|
|
3579
|
-
sourceResourceId: string,
|
|
3580
|
-
sourceRevision: number,
|
|
3581
|
-
type: string,
|
|
3582
|
-
key: string,
|
|
3583
|
-
payloadHash: Buffer,
|
|
3584
|
-
scope: "run" | "controller" | "channel" | "system",
|
|
3585
|
-
now: number,
|
|
3586
|
-
): void {
|
|
3587
|
-
this.state.connection
|
|
3588
|
-
.prepare(
|
|
3589
|
-
"INSERT INTO resources(resource_id, resource_type, aggregate_key, revision, created_at, updated_at) VALUES (?, 'effect', ?, 1, ?, ?)",
|
|
3590
|
-
)
|
|
3591
|
-
.run(resourceId, effectId, now, now);
|
|
3592
|
-
this.state.connection
|
|
3593
|
-
.prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
|
|
3594
|
-
.run(resourceId);
|
|
3595
|
-
this.state.connection
|
|
3596
|
-
.prepare(
|
|
3597
|
-
`INSERT INTO effects(
|
|
3598
|
-
effect_id, resource_id, source_resource_id, source_revision, effect_type,
|
|
3599
|
-
idempotency_key, payload_hash, owner_scope, status, attempt_count,
|
|
3600
|
-
created_at, updated_at
|
|
3601
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, ?)`,
|
|
3602
|
-
)
|
|
3603
|
-
.run(
|
|
3604
|
-
effectId,
|
|
3605
|
-
resourceId,
|
|
3606
|
-
sourceResourceId,
|
|
3607
|
-
sourceRevision,
|
|
3608
|
-
type,
|
|
3609
|
-
key,
|
|
3610
|
-
payloadHash,
|
|
3611
|
-
scope,
|
|
3612
|
-
now,
|
|
3613
|
-
now,
|
|
3614
|
-
);
|
|
3615
|
-
this.insertEvent(resourceId, 1, "effect.created", "system", null, {}, now);
|
|
3616
|
-
}
|
|
3617
|
-
|
|
3618
|
-
private notification(
|
|
3619
|
-
runId: string,
|
|
3620
|
-
attemptId: string,
|
|
3621
|
-
index: number,
|
|
3622
|
-
): WorkflowNotificationRecord | undefined {
|
|
3623
|
-
const row = this.state.connection
|
|
3624
|
-
.prepare(
|
|
3625
|
-
notificationSelect("WHERE n.run_id = ? AND n.attempt_id = ? AND n.notification_index = ?"),
|
|
3626
|
-
)
|
|
3627
|
-
.get(runId, attemptId, index);
|
|
3628
|
-
return isNotificationRow(row) ? this.mapNotification(row) : undefined;
|
|
3629
|
-
}
|
|
3630
|
-
|
|
3631
|
-
private requireNotification(
|
|
3632
|
-
runId: string,
|
|
3633
|
-
attemptId: string,
|
|
3634
|
-
index: number,
|
|
3635
|
-
): WorkflowNotificationRecord {
|
|
3636
|
-
const record = this.notification(runId, attemptId, index);
|
|
3637
|
-
/* istanbul ignore if -- impossible after exact schema and transaction checks */
|
|
3638
|
-
if (record === undefined) throw new Error("Workflow notification was not stored");
|
|
3639
|
-
return record;
|
|
3640
|
-
}
|
|
3641
|
-
|
|
3642
|
-
private notificationRows(sessionId: string, limit: number): NotificationRow[] {
|
|
3643
|
-
return this.state.connection
|
|
3644
|
-
.prepare(
|
|
3645
|
-
notificationSelect(
|
|
3646
|
-
"WHERE n.target_session_id = ? AND e.status = 'pending' ORDER BY n.created_at LIMIT ?",
|
|
3647
|
-
),
|
|
3648
|
-
)
|
|
3649
|
-
.all(sessionId, limit)
|
|
3650
|
-
.filter(isNotificationRow);
|
|
3651
|
-
}
|
|
3652
|
-
|
|
3653
|
-
private notificationRowById(id: string): NotificationRow | undefined {
|
|
3654
|
-
const row = this.state.connection
|
|
3655
|
-
.prepare(notificationSelect("WHERE n.notification_id = ?"))
|
|
3656
|
-
.get(id);
|
|
3657
|
-
return isNotificationRow(row) ? row : undefined;
|
|
3658
|
-
}
|
|
3659
|
-
|
|
3660
|
-
/* istanbul ignore next -- pure projection covered by integration tests */
|
|
3661
|
-
private mapNotification(row: NotificationRow): WorkflowNotificationRecord {
|
|
3662
|
-
return {
|
|
3663
|
-
notificationId: row.notificationId,
|
|
3664
|
-
runId: row.runId,
|
|
3665
|
-
nodeId: row.nodeId,
|
|
3666
|
-
attemptId: row.attemptId,
|
|
3667
|
-
notificationIndex: row.notificationIndex,
|
|
3668
|
-
targetSessionId: row.targetSessionId,
|
|
3669
|
-
kind: row.kind,
|
|
3670
|
-
content: this.state.readBlob(row.contentHash)?.content.toString("utf8") ?? "",
|
|
3671
|
-
createdAt: new Date(row.createdAt).toISOString(),
|
|
3672
|
-
deliveryClaimExpiresAt:
|
|
3673
|
-
row.claimExpiresAt === null ? null : new Date(row.claimExpiresAt).toISOString(),
|
|
3674
|
-
deliveredAt: row.deliveredAt === null ? null : new Date(row.deliveredAt).toISOString(),
|
|
3675
|
-
};
|
|
3676
|
-
}
|
|
3677
|
-
|
|
3678
3049
|
private assertWritable(): void {
|
|
3679
3050
|
if (this.state.mode === "read-only") throw new Error("Controller store is read-only");
|
|
3680
3051
|
}
|
|
@@ -3719,6 +3090,9 @@ function workflowRunSelect(clause: string): string {
|
|
|
3719
3090
|
q.consecutive_errors AS consecutiveErrors, q.error_code AS errorCode,
|
|
3720
3091
|
q.error_hash AS errorHash, b.origin_session_id AS originSessionId,
|
|
3721
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,
|
|
3722
3096
|
q.created_at AS createdAt, q.updated_at AS updatedAt,
|
|
3723
3097
|
q.started_at AS startedAt, q.finished_at AS finishedAt,
|
|
3724
3098
|
l.generation AS leaseGeneration, l.owner_id AS ownerId, l.expires_at AS claimExpiresAt
|
|
@@ -3733,8 +3107,10 @@ function workflowRunViewSelect(clause: string): string {
|
|
|
3733
3107
|
r.workflow_ref AS workflowRef, r.status AS runStatus, r.paused,
|
|
3734
3108
|
r.definition_digest AS definitionDigest, q.status,
|
|
3735
3109
|
b.origin_session_id AS originSessionId, b.execution_mode AS executionMode,
|
|
3736
|
-
r.parent_run_id AS parentRunId,
|
|
3737
|
-
|
|
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,
|
|
3738
3114
|
q.created_at AS createdAt, q.updated_at AS updatedAt,
|
|
3739
3115
|
q.started_at AS startedAt, q.finished_at AS finishedAt,
|
|
3740
3116
|
source.source_type AS sourceType, source.source_ref AS sourceRef,
|
|
@@ -3746,31 +3122,6 @@ function workflowRunViewSelect(clause: string): string {
|
|
|
3746
3122
|
LEFT JOIN blobs error ON error.blob_hash = q.error_hash ${clause}`;
|
|
3747
3123
|
}
|
|
3748
3124
|
|
|
3749
|
-
function turnIntentSelect(clause: string): string {
|
|
3750
|
-
return `SELECT t.turn_intent_id AS intentId, t.source_event_id AS sourceEventId,
|
|
3751
|
-
t.run_id AS runId, t.workflow_ref AS workflowRef,
|
|
3752
|
-
t.target_session_id AS targetSessionId, t.cause,
|
|
3753
|
-
t.node_id AS nodeId, t.attempt_id AS attemptId, t.facts_hash AS factsHash,
|
|
3754
|
-
t.requested_at AS requestedAt, t.eligible_at AS eligibleAt,
|
|
3755
|
-
t.resolved_at AS resolvedAt, t.resolution_type AS resolution,
|
|
3756
|
-
t.resolution_message_id AS resolutionMessageId, l.expires_at AS claimExpiresAt,
|
|
3757
|
-
t.effect_id AS effectId
|
|
3758
|
-
FROM turn_intents t JOIN effects e ON e.effect_id = t.effect_id
|
|
3759
|
-
JOIN leases l ON l.resource_id = e.resource_id ${clause}`;
|
|
3760
|
-
}
|
|
3761
|
-
|
|
3762
|
-
function notificationSelect(clause: string): string {
|
|
3763
|
-
return `SELECT n.notification_id AS notificationId, n.effect_id AS effectId,
|
|
3764
|
-
n.run_id AS runId, a.node_id AS nodeId, n.attempt_id AS attemptId,
|
|
3765
|
-
n.notification_index AS notificationIndex, n.target_session_id AS targetSessionId,
|
|
3766
|
-
n.notification_type AS kind, n.content_hash AS contentHash,
|
|
3767
|
-
n.created_at AS createdAt, l.expires_at AS claimExpiresAt,
|
|
3768
|
-
e.settled_at AS deliveredAt
|
|
3769
|
-
FROM notifications n JOIN effects e ON e.effect_id = n.effect_id
|
|
3770
|
-
JOIN leases l ON l.resource_id = e.resource_id
|
|
3771
|
-
JOIN node_attempts a ON a.attempt_id = n.attempt_id ${clause}`;
|
|
3772
|
-
}
|
|
3773
|
-
|
|
3774
3125
|
type QueueListRow = {
|
|
3775
3126
|
controller: string;
|
|
3776
3127
|
resourceKey: string;
|
|
@@ -3795,39 +3146,6 @@ type RunEventRow = {
|
|
|
3795
3146
|
runnerId: string | null;
|
|
3796
3147
|
payloadHash: Buffer | null;
|
|
3797
3148
|
};
|
|
3798
|
-
type TurnIntentRow = {
|
|
3799
|
-
intentId: string;
|
|
3800
|
-
sourceEventId: string;
|
|
3801
|
-
runId: string;
|
|
3802
|
-
workflowRef: string;
|
|
3803
|
-
targetSessionId: string;
|
|
3804
|
-
cause: WorkflowTurnIntentCause;
|
|
3805
|
-
nodeId: string | null;
|
|
3806
|
-
attemptId: string | null;
|
|
3807
|
-
factsHash: Buffer;
|
|
3808
|
-
requestedAt: number;
|
|
3809
|
-
eligibleAt: number | null;
|
|
3810
|
-
resolvedAt: number | null;
|
|
3811
|
-
resolution: WorkflowTurnIntentResolution | null;
|
|
3812
|
-
resolutionMessageId: string | null;
|
|
3813
|
-
claimExpiresAt: number | null;
|
|
3814
|
-
effectId: string;
|
|
3815
|
-
};
|
|
3816
|
-
type NotificationRow = {
|
|
3817
|
-
notificationId: string;
|
|
3818
|
-
effectId: string;
|
|
3819
|
-
runId: string;
|
|
3820
|
-
nodeId: string;
|
|
3821
|
-
attemptId: string;
|
|
3822
|
-
notificationIndex: number;
|
|
3823
|
-
targetSessionId: string;
|
|
3824
|
-
kind: "progress" | "final";
|
|
3825
|
-
contentHash: Buffer;
|
|
3826
|
-
createdAt: number;
|
|
3827
|
-
claimExpiresAt: number | null;
|
|
3828
|
-
deliveredAt: number | null;
|
|
3829
|
-
};
|
|
3830
|
-
|
|
3831
3149
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
3832
3150
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3833
3151
|
}
|
|
@@ -3855,6 +3173,10 @@ function workflowRunViewRecord(row: WorkflowRunViewRow): WorkflowRunQueueViewRec
|
|
|
3855
3173
|
originSessionId: row.executionMode === "headless" ? null : row.originSessionId,
|
|
3856
3174
|
executionMode: row.executionMode,
|
|
3857
3175
|
parentRunId: row.parentRunId,
|
|
3176
|
+
rootRunId: row.rootRunId,
|
|
3177
|
+
lineageKind: row.lineageKind,
|
|
3178
|
+
restartNumber: row.restartNumber,
|
|
3179
|
+
parentTerminalFingerprint: row.parentTerminalFingerprint?.toString("hex") ?? null,
|
|
3858
3180
|
errorCode: row.errorCode,
|
|
3859
3181
|
errorMessage: row.errorMessage,
|
|
3860
3182
|
createdAt: new Date(row.createdAt).toISOString(),
|
|
@@ -3877,6 +3199,13 @@ function isWorkflowRunViewRow(value: unknown): value is WorkflowRunViewRow {
|
|
|
3877
3199
|
(value.originSessionId === null || typeof value.originSessionId === "string") &&
|
|
3878
3200
|
(value.executionMode === "interactive" || value.executionMode === "headless") &&
|
|
3879
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)) &&
|
|
3880
3209
|
(value.errorCode === null || typeof value.errorCode === "string") &&
|
|
3881
3210
|
(value.errorMessage === null || typeof value.errorMessage === "string") &&
|
|
3882
3211
|
typeof value.createdAt === "number" &&
|
|
@@ -3933,12 +3262,6 @@ function isControllerEventRow(value: unknown): value is ControllerEventRow {
|
|
|
3933
3262
|
function isRunEventRow(value: unknown): value is RunEventRow {
|
|
3934
3263
|
return isRecord(value);
|
|
3935
3264
|
}
|
|
3936
|
-
function isTurnIntentRow(value: unknown): value is TurnIntentRow {
|
|
3937
|
-
return isRecord(value);
|
|
3938
|
-
}
|
|
3939
|
-
function isNotificationRow(value: unknown): value is NotificationRow {
|
|
3940
|
-
return isRecord(value);
|
|
3941
|
-
}
|
|
3942
3265
|
function isProjectRow(value: unknown): value is { projectId: string } {
|
|
3943
3266
|
return isRecord(value);
|
|
3944
3267
|
}
|
|
@@ -3963,14 +3286,6 @@ function isSequenceRow(value: unknown): value is { seq: number } {
|
|
|
3963
3286
|
function isKeyRow(value: unknown): value is { key: string } {
|
|
3964
3287
|
return isRecord(value);
|
|
3965
3288
|
}
|
|
3966
|
-
function isResourceIdRow(value: unknown): value is { resourceId: string } {
|
|
3967
|
-
return isRecord(value);
|
|
3968
|
-
}
|
|
3969
|
-
function isEffectTokenRow(
|
|
3970
|
-
value: unknown,
|
|
3971
|
-
): value is { tokenHash: Buffer | null; expiresAt: number | null } {
|
|
3972
|
-
return isRecord(value);
|
|
3973
|
-
}
|
|
3974
3289
|
function isEffectIdentityRow(value: unknown): value is { effectId: string; resourceId: string } {
|
|
3975
3290
|
return isRecord(value);
|
|
3976
3291
|
}
|
|
@@ -3993,6 +3308,11 @@ function digestBuffer(value: string): Buffer {
|
|
|
3993
3308
|
if (!/^[a-f0-9]{64}$/i.test(hex)) throw new Error("Expected a SHA-256 digest");
|
|
3994
3309
|
return Buffer.from(hex, "hex");
|
|
3995
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
|
+
}
|
|
3996
3316
|
type QueuedSource = {
|
|
3997
3317
|
root:
|
|
3998
3318
|
| { kind: "builtin"; id: string; revision: string }
|