@osolmaz/pi-workflows 0.16.0 → 0.16.2
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 +48 -16
- package/dist/client/view.js +1 -0
- 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 +20 -165
- package/dist/controllers/sqlite.js +178 -436
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +23 -2
- package/dist/extension/index.js +637 -534
- 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 +28 -0
- package/dist/extension/workflow-message-coordinator.js +346 -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 +50 -8
- package/dist/host/runner.js +1632 -341
- 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 +227 -119
- package/dist/host/view.js.map +1 -1
- package/dist/host/worker-entry.d.ts +4 -1
- package/dist/host/worker-entry.js +23 -24
- package/dist/host/worker-entry.js.map +1 -1
- package/dist/host/worker-protocol.d.ts +15 -0
- package/dist/host/worker-protocol.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 +98 -0
- package/dist/state/workflow-messages.js +366 -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/composition.js +0 -4
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/definition.js +0 -8
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.js +4 -5
- package/dist/workflows/engine.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/schema.js +0 -4
- package/dist/workflows/schema.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 +20 -19
- package/dist/workflows/store.js +163 -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 +6 -8
- 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/2026-09-04-workflow-run-state-plan.md +363 -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 +25 -17
- package/docs/WORKFLOW_HOST.md +105 -31
- 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 +44 -28
- 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 +51 -18
- package/src/controllers/index.ts +0 -1
- package/src/controllers/sqlite.ts +247 -845
- package/src/extension/index.ts +712 -647
- 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 +398 -0
- package/src/host/channel-effects.ts +465 -0
- package/src/host/channel-supervisor.ts +73 -0
- package/src/host/runner.ts +2163 -525
- package/src/host/state.ts +333 -94
- package/src/host/view.ts +254 -122
- package/src/host/worker-entry.ts +38 -30
- package/src/host/worker-protocol.ts +11 -0
- package/src/state/prune.ts +3 -31
- package/src/state/schema.ts +76 -97
- package/src/state/workflow-messages.ts +599 -0
- package/src/viewer/render.ts +1 -5
- package/src/viewer/tui.ts +71 -6
- package/src/workflows/composition.ts +0 -5
- package/src/workflows/definition.ts +0 -8
- package/src/workflows/engine.ts +4 -6
- package/src/workflows/human-decision.ts +18 -2
- package/src/workflows/index.ts +0 -1
- package/src/workflows/schema.ts +0 -6
- package/src/workflows/settings.ts +1 -20
- package/src/workflows/store.ts +237 -489
- package/src/workflows/tool-input.ts +36 -1
- package/src/workflows/types.ts +6 -8
- 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
|
@@ -5,11 +5,12 @@ import { StateDatabase, workflowStatePath } from "../state/database.js";
|
|
|
5
5
|
import { canonicalJson } from "../state/json.js";
|
|
6
6
|
import { resourceIdFor, tokenHash } from "../state/mutation.js";
|
|
7
7
|
import { initializeViewerRun, recordViewerDeltas } from "../state/viewer.js";
|
|
8
|
+
import { WorkflowMessageStore } from "../state/workflow-messages.js";
|
|
8
9
|
import { EffectRequestConflictError, ResourceConflictError, ResourceNotFoundError, WorkflowRequestConflictError, } from "./errors.js";
|
|
9
|
-
const TURN_INTENT_FACTS_SCHEMA = "pi-workflows.deferred-turn-facts.v1";
|
|
10
10
|
export class SqliteControllerStore {
|
|
11
11
|
filePath;
|
|
12
12
|
state;
|
|
13
|
+
workflowMessages;
|
|
13
14
|
ownsState;
|
|
14
15
|
projectId;
|
|
15
16
|
closed = false;
|
|
@@ -23,6 +24,7 @@ export class SqliteControllerStore {
|
|
|
23
24
|
checkLegacyState: filePath === workflowStatePath(),
|
|
24
25
|
});
|
|
25
26
|
this.filePath = this.state.filePath;
|
|
27
|
+
this.workflowMessages = new WorkflowMessageStore(this.state);
|
|
26
28
|
if (options.global === true) {
|
|
27
29
|
this.projectId = null;
|
|
28
30
|
}
|
|
@@ -520,32 +522,65 @@ export class SqliteControllerStore {
|
|
|
520
522
|
if (this.getWorkflowRun(options.runId) !== undefined) {
|
|
521
523
|
throw new Error(`Workflow run already reserved: ${options.runId}`);
|
|
522
524
|
}
|
|
525
|
+
const lineageKind = options.parentRunId === undefined ? null : (options.lineageKind ?? "continuation");
|
|
526
|
+
if (options.parentRunId === undefined && options.lineageKind !== undefined) {
|
|
527
|
+
throw new Error("A root workflow run cannot declare lineage");
|
|
528
|
+
}
|
|
529
|
+
let rootRunId = options.runId;
|
|
530
|
+
let restartNumber = 0;
|
|
531
|
+
let parentTerminalFingerprint = null;
|
|
523
532
|
if (options.parentRunId !== undefined) {
|
|
524
533
|
const parent = this.requireWorkflowRunRow(options.parentRunId);
|
|
525
534
|
if (parent.originSessionId !== options.originSessionId) {
|
|
526
|
-
throw new Error("
|
|
535
|
+
throw new Error("Workflow parent belongs to another Pi session");
|
|
527
536
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if (
|
|
531
|
-
|
|
537
|
+
rootRunId = parent.rootRunId;
|
|
538
|
+
if (lineageKind === "restart") {
|
|
539
|
+
if (!["completed", "failed", "timed_out", "cancelled"].includes(parent.runStatus) ||
|
|
540
|
+
!["done", "failed", "cancelled"].includes(parent.status)) {
|
|
541
|
+
throw new Error("Restart parent is not terminal");
|
|
542
|
+
}
|
|
543
|
+
if (options.parentTerminalFingerprint === undefined) {
|
|
544
|
+
throw new Error("Restart requires the parent terminal fingerprint");
|
|
545
|
+
}
|
|
546
|
+
parentTerminalFingerprint = terminalFingerprintBuffer(options.parentTerminalFingerprint);
|
|
547
|
+
restartNumber = options.restartNumber ?? parent.restartNumber + 1;
|
|
548
|
+
if (restartNumber !== parent.restartNumber + 1) {
|
|
549
|
+
throw new Error("Restart number does not follow its parent");
|
|
532
550
|
}
|
|
533
|
-
this.state.connection
|
|
534
|
-
.prepare(`UPDATE run_queue
|
|
535
|
-
SET status = 'done', updated_at = ?, finished_at = ?
|
|
536
|
-
WHERE run_id = ? AND status = 'parked'`)
|
|
537
|
-
.run(now, now, parent.runId);
|
|
538
|
-
const parentRevision = this.resourceRevision(parent.resourceId);
|
|
539
|
-
this.bumpResource(parent.resourceId, parentRevision, now);
|
|
540
|
-
this.insertEvent(parent.resourceId, parentRevision + 1, "run.queue_done_for_continuation", "session", options.originSessionId, { continuationRunId: options.runId }, now);
|
|
541
|
-
}
|
|
542
|
-
else if (parent.status === "done") {
|
|
543
|
-
throw new Error("Continuation parent already has a reserved continuation");
|
|
544
551
|
}
|
|
545
552
|
else {
|
|
546
|
-
|
|
553
|
+
if (options.parentTerminalFingerprint !== undefined ||
|
|
554
|
+
(options.restartNumber !== undefined && options.restartNumber !== parent.restartNumber)) {
|
|
555
|
+
throw new Error("Continuation lineage conflicts with restart metadata");
|
|
556
|
+
}
|
|
557
|
+
restartNumber = parent.restartNumber;
|
|
558
|
+
if (parent.status === "parked") {
|
|
559
|
+
const parentLease = this.requireLease(parent.resourceId);
|
|
560
|
+
if (parentLease.ownerId !== null) {
|
|
561
|
+
throw new Error("Continuation parent still has an active owner");
|
|
562
|
+
}
|
|
563
|
+
this.state.connection
|
|
564
|
+
.prepare(`UPDATE run_queue
|
|
565
|
+
SET status = 'done', updated_at = ?, finished_at = ?
|
|
566
|
+
WHERE run_id = ? AND status = 'parked'`)
|
|
567
|
+
.run(now, now, parent.runId);
|
|
568
|
+
const parentRevision = this.resourceRevision(parent.resourceId);
|
|
569
|
+
this.bumpResource(parent.resourceId, parentRevision, now);
|
|
570
|
+
this.insertEvent(parent.resourceId, parentRevision + 1, "run.queue_done_for_continuation", "session", options.originSessionId, { continuationRunId: options.runId }, now);
|
|
571
|
+
}
|
|
572
|
+
else if (parent.status === "done") {
|
|
573
|
+
throw new Error("Continuation parent already has a reserved continuation");
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
throw new Error(`Continuation parent queue is ${parent.status}`);
|
|
577
|
+
}
|
|
547
578
|
}
|
|
548
579
|
}
|
|
580
|
+
else if (options.restartNumber !== undefined ||
|
|
581
|
+
options.parentTerminalFingerprint !== undefined) {
|
|
582
|
+
throw new Error("A root workflow run cannot declare restart metadata");
|
|
583
|
+
}
|
|
549
584
|
const resourceId = resourceIdFor("run", options.runId);
|
|
550
585
|
const definitionHash = this.state.putJson(options.definitionSnapshot, now);
|
|
551
586
|
const queuedSource = queuedWorkflowSource(options.workflowSource);
|
|
@@ -566,11 +601,12 @@ export class SqliteControllerStore {
|
|
|
566
601
|
.run(resourceId);
|
|
567
602
|
this.state.connection
|
|
568
603
|
.prepare(`INSERT INTO runs(
|
|
569
|
-
run_id, resource_id, project_id, parent_run_id,
|
|
604
|
+
run_id, resource_id, project_id, parent_run_id, root_run_id, lineage_kind,
|
|
605
|
+
restart_number, parent_terminal_fingerprint, definition_digest,
|
|
570
606
|
workflow_ref, launch_options_hash, status, paused,
|
|
571
607
|
input_hash, created_at, updated_at
|
|
572
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, 'queued', 0, ?, ?, ?)`)
|
|
573
|
-
.run(options.runId, resourceId, this.requireProjectId(), options.parentRunId ?? null, definitionDigest, options.workflowSourceRef, launchHash, inputHash, now, now);
|
|
608
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', 0, ?, ?, ?)`)
|
|
609
|
+
.run(options.runId, resourceId, this.requireProjectId(), options.parentRunId ?? null, rootRunId, lineageKind, restartNumber, parentTerminalFingerprint, definitionDigest, options.workflowSourceRef, launchHash, inputHash, now, now);
|
|
574
610
|
initializeViewerRun(this.state, options.runId, now);
|
|
575
611
|
insertQueuedRunSources(this.state, options.runId, queuedSource);
|
|
576
612
|
this.state.connection
|
|
@@ -621,6 +657,11 @@ export class SqliteControllerStore {
|
|
|
621
657
|
runnerId: options.runnerId,
|
|
622
658
|
originSessionId: options.originSessionId,
|
|
623
659
|
...(options.parentRunId === undefined ? {} : { parentRunId: options.parentRunId }),
|
|
660
|
+
...(options.lineageKind === undefined ? {} : { lineageKind: options.lineageKind }),
|
|
661
|
+
...(options.restartNumber === undefined ? {} : { restartNumber: options.restartNumber }),
|
|
662
|
+
...(options.parentTerminalFingerprint === undefined
|
|
663
|
+
? {}
|
|
664
|
+
: { parentTerminalFingerprint: options.parentTerminalFingerprint }),
|
|
624
665
|
...(options.now === undefined ? {} : { now: options.now }),
|
|
625
666
|
});
|
|
626
667
|
}
|
|
@@ -736,17 +777,23 @@ export class SqliteControllerStore {
|
|
|
736
777
|
.prepare(`SELECT i.request_id AS requestId, i.attempt_id AS attemptId
|
|
737
778
|
FROM interactive_requests i
|
|
738
779
|
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
739
|
-
|
|
780
|
+
JOIN runs r ON r.run_id = i.run_id
|
|
781
|
+
WHERE i.run_id = ? AND i.target_session_id = ?
|
|
782
|
+
AND i.status = 'pending' AND r.paused = 0
|
|
740
783
|
AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
|
|
784
|
+
AND EXISTS (
|
|
785
|
+
SELECT 1 FROM workflow_messages m
|
|
786
|
+
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
|
|
787
|
+
WHERE m.source_id = i.request_id AND m.kind = 'step' AND t.state = 'started'
|
|
788
|
+
)
|
|
741
789
|
ORDER BY a.deadline_at, i.request_id LIMIT 1`)
|
|
742
|
-
.get(options.runId, now);
|
|
790
|
+
.get(options.runId, options.targetSessionId, now);
|
|
743
791
|
if (!isExpiredInteractionRow(interaction))
|
|
744
792
|
return false;
|
|
745
793
|
const request = this.state.connection
|
|
746
794
|
.prepare(`UPDATE interactive_requests
|
|
747
|
-
SET status = 'cancelled',
|
|
748
|
-
|
|
749
|
-
WHERE request_id = ? AND run_id = ? AND status IN ('pending', 'presenting')`)
|
|
795
|
+
SET status = 'cancelled', revision = revision + 1, updated_at = ?
|
|
796
|
+
WHERE request_id = ? AND run_id = ? AND status = 'pending'`)
|
|
750
797
|
.run(now, interaction.requestId, options.runId);
|
|
751
798
|
const run = this.state.connection
|
|
752
799
|
.prepare(`UPDATE runs
|
|
@@ -763,7 +810,7 @@ export class SqliteControllerStore {
|
|
|
763
810
|
if (request.changes !== 1 || run.changes !== 1 || queue.changes !== 1) {
|
|
764
811
|
throw new Error(`Workflow run ${options.runId} changed during interaction timeout`);
|
|
765
812
|
}
|
|
766
|
-
const error = "Workflow node deadline expired
|
|
813
|
+
const error = "Workflow node model-turn deadline expired";
|
|
767
814
|
const receiptHash = this.state.putJson({ status: "rejected", error }, now);
|
|
768
815
|
this.state.connection
|
|
769
816
|
.prepare(`UPDATE interactive_submissions SET outcome = 'rejected', receipt_hash = ?
|
|
@@ -788,8 +835,8 @@ export class SqliteControllerStore {
|
|
|
788
835
|
"(l.owner_id IS NULL OR l.expires_at <= ? OR l.owner_id = ?)",
|
|
789
836
|
"(q.affinity_runner_id IS NULL OR q.affinity_runner_id = ? OR q.status IN ('parked', 'starting', 'running'))",
|
|
790
837
|
"NOT (q.status = 'parked' AND (r.status = 'waiting' OR r.paused = 1))",
|
|
791
|
-
"NOT EXISTS (SELECT 1 FROM interactive_requests i WHERE i.run_id = r.run_id AND i.status
|
|
792
|
-
"(q.error_code IS NULL OR q.error_code
|
|
838
|
+
"NOT EXISTS (SELECT 1 FROM interactive_requests i WHERE i.run_id = r.run_id AND i.status = 'pending')",
|
|
839
|
+
"(q.error_code IS NULL OR q.error_code NOT IN ('workflowSourceChanged', 'workerNoProgress'))",
|
|
793
840
|
];
|
|
794
841
|
const params = [now, now, options.runnerId, options.runnerId];
|
|
795
842
|
if (this.projectId !== null) {
|
|
@@ -863,6 +910,50 @@ export class SqliteControllerStore {
|
|
|
863
910
|
parkWorkflowRun(options) {
|
|
864
911
|
return this.releaseRunClaim(options.runId, options.claimToken, "parked", options.now);
|
|
865
912
|
}
|
|
913
|
+
parkWorkflowRunForWorkerNoProgress(options) {
|
|
914
|
+
const now = epoch(validTimestamp(options.now));
|
|
915
|
+
return this.state.transaction(() => {
|
|
916
|
+
if (!this.verifyWorkflowRunClaim({
|
|
917
|
+
runId: options.runId,
|
|
918
|
+
claimToken: options.claimToken,
|
|
919
|
+
now: new Date(now).toISOString(),
|
|
920
|
+
})) {
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
const row = this.requireWorkflowRunRow(options.runId);
|
|
924
|
+
const lease = this.requireLease(row.resourceId);
|
|
925
|
+
const detail = options.detail.slice(0, 8_192);
|
|
926
|
+
const errorHash = this.state.putText(detail, now);
|
|
927
|
+
const run = this.state.connection
|
|
928
|
+
.prepare(`UPDATE runs SET status_detail = ?, updated_at = ?, finished_at = NULL
|
|
929
|
+
WHERE run_id = ? AND status NOT IN ('completed', 'failed', 'timed_out', 'cancelled')`)
|
|
930
|
+
.run(detail, now, options.runId);
|
|
931
|
+
const queue = this.state.connection
|
|
932
|
+
.prepare(`UPDATE run_queue
|
|
933
|
+
SET status = 'parked', error_code = 'workerNoProgress', error_hash = ?,
|
|
934
|
+
available_at = ?, updated_at = ?, finished_at = NULL
|
|
935
|
+
WHERE run_id = ? AND status IN ('queued', 'starting', 'running', 'parked')`)
|
|
936
|
+
.run(errorHash, now, now, options.runId);
|
|
937
|
+
if (run.changes !== 1 || queue.changes !== 1) {
|
|
938
|
+
throw new Error(`Workflow run ${options.runId} changed during worker recovery`);
|
|
939
|
+
}
|
|
940
|
+
const released = this.state.connection
|
|
941
|
+
.prepare(`UPDATE leases
|
|
942
|
+
SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
|
|
943
|
+
acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
|
|
944
|
+
WHERE resource_id = ? AND token_hash = ? AND generation = ? AND expires_at > ?`)
|
|
945
|
+
.run(row.resourceId, tokenHash(options.claimToken), lease.generation, now);
|
|
946
|
+
/* istanbul ignore if -- the exact live claim is stable in this transaction */
|
|
947
|
+
if (released.changes !== 1) {
|
|
948
|
+
throw new Error(`Workflow run ${options.runId} claim changed during worker recovery`);
|
|
949
|
+
}
|
|
950
|
+
const revision = this.resourceRevision(row.resourceId);
|
|
951
|
+
this.bumpResource(row.resourceId, revision, now);
|
|
952
|
+
this.insertEvent(row.resourceId, revision + 1, "run.worker_no_progress", lease.ownerType ?? "system", lease.ownerId, { status: "parked", code: "workerNoProgress" }, now, lease.generation || undefined);
|
|
953
|
+
recordViewerDeltas(this.state, options.runId, [{ targetType: "summary" }, { targetType: "replay" }], now);
|
|
954
|
+
return true;
|
|
955
|
+
});
|
|
956
|
+
}
|
|
866
957
|
pauseParkedWorkflowRun(options) {
|
|
867
958
|
const now = epoch(validTimestamp(options.now));
|
|
868
959
|
return this.state.transaction(() => {
|
|
@@ -874,7 +965,7 @@ export class SqliteControllerStore {
|
|
|
874
965
|
return false;
|
|
875
966
|
const pending = this.state.connection
|
|
876
967
|
.prepare(`SELECT 1 FROM interactive_requests
|
|
877
|
-
WHERE run_id = ? AND status
|
|
968
|
+
WHERE run_id = ? AND status = 'pending' LIMIT 1`)
|
|
878
969
|
.get(options.runId);
|
|
879
970
|
if (pending === undefined)
|
|
880
971
|
return false;
|
|
@@ -910,7 +1001,7 @@ export class SqliteControllerStore {
|
|
|
910
1001
|
return false;
|
|
911
1002
|
const pending = this.state.connection
|
|
912
1003
|
.prepare(`SELECT 1 FROM interactive_requests
|
|
913
|
-
WHERE run_id = ? AND status
|
|
1004
|
+
WHERE run_id = ? AND status = 'pending' LIMIT 1`)
|
|
914
1005
|
.get(options.runId);
|
|
915
1006
|
if (pending === undefined)
|
|
916
1007
|
return false;
|
|
@@ -1081,6 +1172,7 @@ export class SqliteControllerStore {
|
|
|
1081
1172
|
if (run.changes !== 1) {
|
|
1082
1173
|
throw new Error(`Workflow run ${options.runId} has inconsistent durable state`);
|
|
1083
1174
|
}
|
|
1175
|
+
this.settleWorkflowRunMessages(options.runId, now);
|
|
1084
1176
|
this.cancelWorkflowRunDependents(options.runId, controlId, errorHash, now);
|
|
1085
1177
|
const released = this.state.connection
|
|
1086
1178
|
.prepare(`UPDATE leases
|
|
@@ -1170,215 +1262,6 @@ export class SqliteControllerStore {
|
|
|
1170
1262
|
payload: row.payloadHash === null ? {} : this.state.readJson(row.payloadHash),
|
|
1171
1263
|
}));
|
|
1172
1264
|
}
|
|
1173
|
-
ensureWorkflowTurnIntent(options) {
|
|
1174
|
-
const existing = this.turnIntentBySource(options.sourceEventId);
|
|
1175
|
-
if (existing !== undefined)
|
|
1176
|
-
return existing;
|
|
1177
|
-
const now = epoch(validTimestamp(options.requestedAt));
|
|
1178
|
-
const run = this.requireWorkflowRunRow(options.runId);
|
|
1179
|
-
const intentId = options.intentId;
|
|
1180
|
-
const resourceId = resourceIdFor("turn_intent", intentId);
|
|
1181
|
-
const effectId = `effect-${randomUUID()}`;
|
|
1182
|
-
this.state.transaction(() => {
|
|
1183
|
-
const factsHash = this.state.putJson(options.fallbackFacts, now);
|
|
1184
|
-
this.insertEffectResource(effectId, resourceIdFor("effect", effectId), run.resourceId, this.resourceRevision(run.resourceId), "turn.deliver", intentId, factsHash, "run", now);
|
|
1185
|
-
this.state.connection
|
|
1186
|
-
.prepare("INSERT INTO resources(resource_id, resource_type, aggregate_key, revision, created_at, updated_at) VALUES (?, 'turn_intent', ?, 1, ?, ?)")
|
|
1187
|
-
.run(resourceId, intentId, now, now);
|
|
1188
|
-
this.state.connection
|
|
1189
|
-
.prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
|
|
1190
|
-
.run(resourceId);
|
|
1191
|
-
this.state.connection
|
|
1192
|
-
.prepare(`INSERT INTO turn_intents(
|
|
1193
|
-
turn_intent_id, resource_id, effect_id, source_event_id, run_id,
|
|
1194
|
-
workflow_ref, target_session_id, cause, node_id, attempt_id,
|
|
1195
|
-
facts_hash, requested_at, eligible_at, created_at
|
|
1196
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1197
|
-
.run(intentId, resourceId, effectId, options.sourceEventId, options.runId, options.workflowRef, options.targetSessionId, options.cause, options.nodeId ?? null, options.attemptId ?? null, factsHash, now, options.eligible ? now : null, now);
|
|
1198
|
-
this.insertEvent(resourceId, 1, "turn.requested", "session", options.targetSessionId, {}, now);
|
|
1199
|
-
});
|
|
1200
|
-
return this.requireTurnIntent(intentId);
|
|
1201
|
-
}
|
|
1202
|
-
getWorkflowTurnIntent(intentId) {
|
|
1203
|
-
return this.turnIntent(intentId);
|
|
1204
|
-
}
|
|
1205
|
-
findPendingWorkflowTurnIntent(options) {
|
|
1206
|
-
const row = this.state.connection
|
|
1207
|
-
.prepare(turnIntentSelect("WHERE t.run_id = ? AND t.target_session_id = ? AND t.resolved_at IS NULL ORDER BY t.requested_at DESC LIMIT 1"))
|
|
1208
|
-
.get(options.runId, options.targetSessionId);
|
|
1209
|
-
return isTurnIntentRow(row) ? this.mapTurnIntent(row) : undefined;
|
|
1210
|
-
}
|
|
1211
|
-
isWorkflowTurnIntentClaimLive(options) {
|
|
1212
|
-
const row = this.turnIntentRow(options.intentId);
|
|
1213
|
-
return (row !== undefined &&
|
|
1214
|
-
row.targetSessionId === options.targetSessionId &&
|
|
1215
|
-
row.resolvedAt === null &&
|
|
1216
|
-
this.verifyEffectToken(row.effectId, options.claimToken, options.now));
|
|
1217
|
-
}
|
|
1218
|
-
claimWorkflowTurnIntent(options) {
|
|
1219
|
-
const intent = this.turnIntent(options.intentId);
|
|
1220
|
-
if (intent?.targetSessionId !== options.targetSessionId)
|
|
1221
|
-
return undefined;
|
|
1222
|
-
return this.claimEffectForTurn(options.intentId, options.claimToken, options.leaseMs, options.now)
|
|
1223
|
-
? this.requireTurnIntent(options.intentId)
|
|
1224
|
-
: undefined;
|
|
1225
|
-
}
|
|
1226
|
-
claimEligibleWorkflowTurnIntents(options) {
|
|
1227
|
-
const now = epoch(validTimestamp(options.now));
|
|
1228
|
-
const rows = this.state.connection
|
|
1229
|
-
.prepare(turnIntentSelect("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 ?"))
|
|
1230
|
-
.all(options.targetSessionId, now, options.limit ?? 10);
|
|
1231
|
-
const claimed = [];
|
|
1232
|
-
for (const row of rows) {
|
|
1233
|
-
if (isTurnIntentRow(row) &&
|
|
1234
|
-
this.claimEffectForTurn(row.intentId, options.claimToken, options.leaseMs, options.now)) {
|
|
1235
|
-
claimed.push(this.requireTurnIntent(row.intentId));
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
return claimed;
|
|
1239
|
-
}
|
|
1240
|
-
hasClaimableWorkflowTurnIntent(options) {
|
|
1241
|
-
const now = epoch(validTimestamp(options.now));
|
|
1242
|
-
const row = this.state.connection
|
|
1243
|
-
.prepare(`SELECT 1 AS present
|
|
1244
|
-
FROM turn_intents t
|
|
1245
|
-
JOIN effects e ON e.effect_id = t.effect_id
|
|
1246
|
-
JOIN leases l ON l.resource_id = e.resource_id
|
|
1247
|
-
WHERE t.target_session_id = ? AND t.resolved_at IS NULL
|
|
1248
|
-
AND t.eligible_at IS NOT NULL AND t.eligible_at <= ?
|
|
1249
|
-
AND e.status = 'pending'
|
|
1250
|
-
AND (l.owner_id IS NULL OR l.expires_at IS NULL OR l.expires_at <= ?)
|
|
1251
|
-
LIMIT 1`)
|
|
1252
|
-
.get(options.targetSessionId, now, now);
|
|
1253
|
-
return row !== undefined;
|
|
1254
|
-
}
|
|
1255
|
-
makeWorkflowTurnIntentEligible(options) {
|
|
1256
|
-
const eligibleAt = epoch(validTimestamp(options.eligibleAt ?? options.now));
|
|
1257
|
-
const factsHash = this.state.putJson(options.fallbackFacts, eligibleAt);
|
|
1258
|
-
return (this.state.connection
|
|
1259
|
-
.prepare("UPDATE turn_intents SET eligible_at = ?, facts_hash = ? WHERE turn_intent_id = ? AND resolved_at IS NULL")
|
|
1260
|
-
.run(eligibleAt, factsHash, options.intentId).changes === 1);
|
|
1261
|
-
}
|
|
1262
|
-
resolveWorkflowTurnIntent(options) {
|
|
1263
|
-
const now = epoch(validTimestamp(options.now));
|
|
1264
|
-
return this.state.transaction(() => {
|
|
1265
|
-
const row = this.turnIntentRow(options.intentId);
|
|
1266
|
-
if (row === undefined ||
|
|
1267
|
-
row.targetSessionId !== options.targetSessionId ||
|
|
1268
|
-
!this.verifyEffectToken(row.effectId, options.claimToken, options.now)) {
|
|
1269
|
-
return false;
|
|
1270
|
-
}
|
|
1271
|
-
const result = this.state.connection
|
|
1272
|
-
.prepare(`UPDATE turn_intents SET resolved_at = ?, resolution_type = ?, resolution_message_id = ?
|
|
1273
|
-
WHERE turn_intent_id = ? AND resolved_at IS NULL`)
|
|
1274
|
-
.run(now, options.resolution, options.messageId ?? null, options.intentId);
|
|
1275
|
-
if (result.changes === 1)
|
|
1276
|
-
this.completeEffect(row.effectId, "applied", now);
|
|
1277
|
-
return result.changes === 1;
|
|
1278
|
-
});
|
|
1279
|
-
}
|
|
1280
|
-
releaseWorkflowTurnIntentClaim(options) {
|
|
1281
|
-
const row = this.turnIntentRow(options.intentId);
|
|
1282
|
-
return (row !== undefined &&
|
|
1283
|
-
row.targetSessionId === options.targetSessionId &&
|
|
1284
|
-
this.releaseEffectLease(row.effectId, options.claimToken));
|
|
1285
|
-
}
|
|
1286
|
-
listWorkflowTurnIntents(options = {}) {
|
|
1287
|
-
const clauses = ["1 = 1"];
|
|
1288
|
-
const params = [];
|
|
1289
|
-
if (options.runId !== undefined) {
|
|
1290
|
-
clauses.push("t.run_id = ?");
|
|
1291
|
-
params.push(options.runId);
|
|
1292
|
-
}
|
|
1293
|
-
if (options.targetSessionId !== undefined) {
|
|
1294
|
-
clauses.push("t.target_session_id = ?");
|
|
1295
|
-
params.push(options.targetSessionId);
|
|
1296
|
-
}
|
|
1297
|
-
if (options.unresolvedOnly === true)
|
|
1298
|
-
clauses.push("t.resolved_at IS NULL");
|
|
1299
|
-
params.push(options.limit ?? 100);
|
|
1300
|
-
const rows = this.state.connection
|
|
1301
|
-
.prepare(turnIntentSelect(`WHERE ${clauses.join(" AND ")} ORDER BY t.requested_at DESC LIMIT ?`))
|
|
1302
|
-
.all(...params);
|
|
1303
|
-
return rows.filter(isTurnIntentRow).map((row) => this.mapTurnIntent(row));
|
|
1304
|
-
}
|
|
1305
|
-
enqueueWorkflowNotification(options) {
|
|
1306
|
-
const existing = this.notification(options.runId, options.attemptId, options.notificationIndex);
|
|
1307
|
-
if (existing !== undefined)
|
|
1308
|
-
return existing;
|
|
1309
|
-
const run = this.requireWorkflowRunRow(options.runId);
|
|
1310
|
-
const now = epoch(validTimestamp(options.now));
|
|
1311
|
-
const notificationId = `notification-${randomUUID()}`;
|
|
1312
|
-
const resourceId = resourceIdFor("notification", notificationId);
|
|
1313
|
-
const effectId = `effect-${randomUUID()}`;
|
|
1314
|
-
this.state.transaction(() => {
|
|
1315
|
-
const contentHash = this.state.putText(options.content, now);
|
|
1316
|
-
this.insertEffectResource(effectId, resourceIdFor("effect", effectId), run.resourceId, this.resourceRevision(run.resourceId), "notification.deliver", notificationId, contentHash, "run", now);
|
|
1317
|
-
this.state.connection
|
|
1318
|
-
.prepare("INSERT INTO resources(resource_id, resource_type, aggregate_key, revision, created_at, updated_at) VALUES (?, 'notification', ?, 1, ?, ?)")
|
|
1319
|
-
.run(resourceId, notificationId, now, now);
|
|
1320
|
-
this.state.connection
|
|
1321
|
-
.prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
|
|
1322
|
-
.run(resourceId);
|
|
1323
|
-
this.state.connection
|
|
1324
|
-
.prepare(`INSERT INTO notifications(
|
|
1325
|
-
notification_id, effect_id, run_id, attempt_id, notification_index,
|
|
1326
|
-
target_session_id, notification_type, content_hash, created_at
|
|
1327
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1328
|
-
.run(notificationId, effectId, options.runId, options.attemptId, options.notificationIndex, options.targetSessionId, options.kind, contentHash, now);
|
|
1329
|
-
this.insertEvent(resourceId, 1, "notification.queued", "session", options.targetSessionId, {}, now);
|
|
1330
|
-
});
|
|
1331
|
-
return this.requireNotification(options.runId, options.attemptId, options.notificationIndex);
|
|
1332
|
-
}
|
|
1333
|
-
hasClaimableWorkflowNotification(options) {
|
|
1334
|
-
const now = epoch(validTimestamp(options.now));
|
|
1335
|
-
const row = this.state.connection
|
|
1336
|
-
.prepare(`SELECT 1 AS present
|
|
1337
|
-
FROM notifications n
|
|
1338
|
-
JOIN effects e ON e.effect_id = n.effect_id
|
|
1339
|
-
JOIN leases l ON l.resource_id = e.resource_id
|
|
1340
|
-
WHERE n.target_session_id = ? AND e.status = 'pending'
|
|
1341
|
-
AND (l.owner_id IS NULL OR l.expires_at IS NULL OR l.expires_at <= ?)
|
|
1342
|
-
LIMIT 1`)
|
|
1343
|
-
.get(options.targetSessionId, now);
|
|
1344
|
-
return row !== undefined;
|
|
1345
|
-
}
|
|
1346
|
-
listPendingWorkflowNotifications(options) {
|
|
1347
|
-
const now = Date.now();
|
|
1348
|
-
return this.notificationRows(options.targetSessionId, options.limit ?? 20)
|
|
1349
|
-
.map((row) => this.mapNotification(row))
|
|
1350
|
-
.filter((notification) => notification.deliveryClaimExpiresAt === null ||
|
|
1351
|
-
Date.parse(notification.deliveryClaimExpiresAt) <= now);
|
|
1352
|
-
}
|
|
1353
|
-
claimPendingWorkflowNotifications(options) {
|
|
1354
|
-
const rows = this.notificationRows(options.targetSessionId, options.limit ?? 20);
|
|
1355
|
-
const result = [];
|
|
1356
|
-
for (const row of rows) {
|
|
1357
|
-
if (this.claimEffect(row.effectId, options.targetSessionId, options.claimToken, options.leaseMs, options.now)) {
|
|
1358
|
-
result.push(this.mapNotification(row));
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
return result;
|
|
1362
|
-
}
|
|
1363
|
-
isWorkflowNotificationClaimLive(options) {
|
|
1364
|
-
const row = this.notificationRowById(options.notificationId);
|
|
1365
|
-
return (row !== undefined &&
|
|
1366
|
-
row.targetSessionId === options.targetSessionId &&
|
|
1367
|
-
this.verifyEffectToken(row.effectId, options.claimToken, options.now));
|
|
1368
|
-
}
|
|
1369
|
-
markWorkflowNotificationDelivered(options) {
|
|
1370
|
-
const now = epoch(validTimestamp(options.now));
|
|
1371
|
-
return this.state.transaction(() => {
|
|
1372
|
-
const row = this.notificationRowById(options.notificationId);
|
|
1373
|
-
if (row === undefined ||
|
|
1374
|
-
row.targetSessionId !== options.targetSessionId ||
|
|
1375
|
-
!this.verifyEffectToken(row.effectId, options.claimToken, options.now)) {
|
|
1376
|
-
return false;
|
|
1377
|
-
}
|
|
1378
|
-
this.completeEffect(row.effectId, "applied", now);
|
|
1379
|
-
return true;
|
|
1380
|
-
});
|
|
1381
|
-
}
|
|
1382
1265
|
settleRunEffect(runId, effectType) {
|
|
1383
1266
|
const rows = this.state.connection
|
|
1384
1267
|
.prepare(`SELECT e.effect_id AS effectId, e.resource_id AS resourceId
|
|
@@ -1722,6 +1605,17 @@ export class SqliteControllerStore {
|
|
|
1722
1605
|
return this.mapWorkflowRun(this.requireWorkflowRunRow(runId));
|
|
1723
1606
|
}
|
|
1724
1607
|
assertWorkflowRunPreparationCompatible(row, options, definitionDigest) {
|
|
1608
|
+
const expectedLineageKind = options.parentRunId === undefined ? null : (options.lineageKind ?? "continuation");
|
|
1609
|
+
const parent = options.parentRunId === undefined
|
|
1610
|
+
? undefined
|
|
1611
|
+
: this.requireWorkflowRunRow(options.parentRunId);
|
|
1612
|
+
const expectedRootRunId = parent?.rootRunId ?? options.runId;
|
|
1613
|
+
const expectedRestartNumber = expectedLineageKind === "restart"
|
|
1614
|
+
? (options.restartNumber ?? (parent?.restartNumber ?? -1) + 1)
|
|
1615
|
+
: (parent?.restartNumber ?? 0);
|
|
1616
|
+
const expectedParentTerminalFingerprint = options.parentTerminalFingerprint === undefined
|
|
1617
|
+
? null
|
|
1618
|
+
: terminalFingerprintBuffer(options.parentTerminalFingerprint);
|
|
1725
1619
|
const compatible = row.workflowName === options.workflowName &&
|
|
1726
1620
|
row.workflowRef === options.workflowSourceRef &&
|
|
1727
1621
|
row.definitionDigest.equals(definitionDigest) &&
|
|
@@ -1732,7 +1626,14 @@ export class SqliteControllerStore {
|
|
|
1732
1626
|
canonicalJson(options.launchOptions ?? {}) &&
|
|
1733
1627
|
row.originSessionId === options.originSessionId &&
|
|
1734
1628
|
row.executionMode === (options.executionMode ?? "interactive") &&
|
|
1735
|
-
row.parentRunId === (options.parentRunId ?? null)
|
|
1629
|
+
row.parentRunId === (options.parentRunId ?? null) &&
|
|
1630
|
+
row.rootRunId === expectedRootRunId &&
|
|
1631
|
+
row.lineageKind === expectedLineageKind &&
|
|
1632
|
+
row.restartNumber === expectedRestartNumber &&
|
|
1633
|
+
((row.parentTerminalFingerprint === null && expectedParentTerminalFingerprint === null) ||
|
|
1634
|
+
(row.parentTerminalFingerprint !== null &&
|
|
1635
|
+
expectedParentTerminalFingerprint !== null &&
|
|
1636
|
+
row.parentTerminalFingerprint.equals(expectedParentTerminalFingerprint)));
|
|
1736
1637
|
if (!compatible) {
|
|
1737
1638
|
throw new Error(`Workflow run preparation conflicts: ${options.runId}`);
|
|
1738
1639
|
}
|
|
@@ -1763,6 +1664,10 @@ export class SqliteControllerStore {
|
|
|
1763
1664
|
originSessionId: row.executionMode === "headless" ? null : row.originSessionId,
|
|
1764
1665
|
executionMode: row.executionMode,
|
|
1765
1666
|
parentRunId: row.parentRunId,
|
|
1667
|
+
rootRunId: row.rootRunId,
|
|
1668
|
+
lineageKind: row.lineageKind,
|
|
1669
|
+
restartNumber: row.restartNumber,
|
|
1670
|
+
parentTerminalFingerprint: row.parentTerminalFingerprint?.toString("hex") ?? null,
|
|
1766
1671
|
errorCode: row.errorCode,
|
|
1767
1672
|
errorMessage: row.errorHash === null
|
|
1768
1673
|
? null
|
|
@@ -1783,7 +1688,7 @@ export class SqliteControllerStore {
|
|
|
1783
1688
|
if (options.allowPendingInteraction !== true &&
|
|
1784
1689
|
this.state.connection
|
|
1785
1690
|
.prepare(`SELECT 1 FROM interactive_requests
|
|
1786
|
-
WHERE run_id = ? AND status
|
|
1691
|
+
WHERE run_id = ? AND status = 'pending' LIMIT 1`)
|
|
1787
1692
|
.get(runId) !== undefined) {
|
|
1788
1693
|
return undefined;
|
|
1789
1694
|
}
|
|
@@ -1912,6 +1817,7 @@ export class SqliteControllerStore {
|
|
|
1912
1817
|
updated_at = ?, finished_at = ?
|
|
1913
1818
|
WHERE run_id = ?`)
|
|
1914
1819
|
.run(status, errorHash, now, now, runId);
|
|
1820
|
+
this.settleWorkflowRunMessages(runId, now);
|
|
1915
1821
|
if (status === "cancelled") {
|
|
1916
1822
|
this.cancelWorkflowRunDependents(runId, lease.ownerId ?? "workflow-control", errorHash, now);
|
|
1917
1823
|
}
|
|
@@ -1921,6 +1827,10 @@ export class SqliteControllerStore {
|
|
|
1921
1827
|
return true;
|
|
1922
1828
|
});
|
|
1923
1829
|
}
|
|
1830
|
+
settleWorkflowRunMessages(runId, now) {
|
|
1831
|
+
this.workflowMessages.settleOpenTurnsForRun(runId, "lost", now);
|
|
1832
|
+
this.workflowMessages.cancelPendingForRun(runId, now);
|
|
1833
|
+
}
|
|
1924
1834
|
cancelWorkflowRunDependents(runId, actorId, errorHash, now) {
|
|
1925
1835
|
this.state.connection
|
|
1926
1836
|
.prepare(`UPDATE node_attempts
|
|
@@ -1977,165 +1887,10 @@ export class SqliteControllerStore {
|
|
|
1977
1887
|
.run(receiptHash, runId);
|
|
1978
1888
|
this.state.connection
|
|
1979
1889
|
.prepare(`UPDATE interactive_requests
|
|
1980
|
-
SET status = 'cancelled',
|
|
1981
|
-
|
|
1982
|
-
WHERE run_id = ? AND status IN ('pending', 'presenting')`)
|
|
1890
|
+
SET status = 'cancelled', revision = revision + 1, updated_at = ?
|
|
1891
|
+
WHERE run_id = ? AND status = 'pending'`)
|
|
1983
1892
|
.run(now, runId);
|
|
1984
1893
|
}
|
|
1985
|
-
turnIntent(intentId) {
|
|
1986
|
-
const row = this.turnIntentRow(intentId);
|
|
1987
|
-
return row === undefined ? undefined : this.mapTurnIntent(row);
|
|
1988
|
-
}
|
|
1989
|
-
requireTurnIntent(intentId) {
|
|
1990
|
-
const row = this.turnIntent(intentId);
|
|
1991
|
-
if (row === undefined)
|
|
1992
|
-
throw new Error(`Workflow turn intent not found: ${intentId}`);
|
|
1993
|
-
return row;
|
|
1994
|
-
}
|
|
1995
|
-
turnIntentRow(intentId) {
|
|
1996
|
-
const row = this.state.connection
|
|
1997
|
-
.prepare(turnIntentSelect("WHERE t.turn_intent_id = ?"))
|
|
1998
|
-
.get(intentId);
|
|
1999
|
-
return isTurnIntentRow(row) ? row : undefined;
|
|
2000
|
-
}
|
|
2001
|
-
turnIntentBySource(sourceEventId) {
|
|
2002
|
-
const row = this.state.connection
|
|
2003
|
-
.prepare(turnIntentSelect("WHERE t.source_event_id = ?"))
|
|
2004
|
-
.get(sourceEventId);
|
|
2005
|
-
return isTurnIntentRow(row) ? this.mapTurnIntent(row) : undefined;
|
|
2006
|
-
}
|
|
2007
|
-
/* istanbul ignore next -- pure projection covered by integration tests */
|
|
2008
|
-
mapTurnIntent(row) {
|
|
2009
|
-
return {
|
|
2010
|
-
intentId: row.intentId,
|
|
2011
|
-
sourceEventId: row.sourceEventId,
|
|
2012
|
-
runId: row.runId,
|
|
2013
|
-
workflowRef: row.workflowRef,
|
|
2014
|
-
targetSessionId: row.targetSessionId,
|
|
2015
|
-
cause: row.cause,
|
|
2016
|
-
nodeId: row.nodeId,
|
|
2017
|
-
attemptId: row.attemptId,
|
|
2018
|
-
fallbackFacts: this.state.readJson(row.factsHash),
|
|
2019
|
-
requestedAt: new Date(row.requestedAt).toISOString(),
|
|
2020
|
-
eligibleAt: row.eligibleAt === null ? null : new Date(row.eligibleAt).toISOString(),
|
|
2021
|
-
resolvedAt: row.resolvedAt === null ? null : new Date(row.resolvedAt).toISOString(),
|
|
2022
|
-
resolution: row.resolution,
|
|
2023
|
-
resolutionMessageId: row.resolutionMessageId,
|
|
2024
|
-
deliveryClaimExpiresAt: row.claimExpiresAt === null ? null : new Date(row.claimExpiresAt).toISOString(),
|
|
2025
|
-
};
|
|
2026
|
-
}
|
|
2027
|
-
claimEffectForTurn(intentId, token, leaseMs, nowValue) {
|
|
2028
|
-
const row = this.turnIntentRow(intentId);
|
|
2029
|
-
return (row !== undefined &&
|
|
2030
|
-
this.claimEffect(row.effectId, row.targetSessionId, token, leaseMs, nowValue));
|
|
2031
|
-
}
|
|
2032
|
-
claimEffect(effectId, ownerId, token, leaseMs, nowValue) {
|
|
2033
|
-
const now = epoch(validTimestamp(nowValue));
|
|
2034
|
-
return this.state.transaction(() => {
|
|
2035
|
-
const row = this.state.connection
|
|
2036
|
-
.prepare("SELECT resource_id AS resourceId FROM effects WHERE effect_id = ? AND status = 'pending'")
|
|
2037
|
-
.get(effectId);
|
|
2038
|
-
/* istanbul ignore if -- impossible after exact schema and transaction checks */
|
|
2039
|
-
if (!isResourceIdRow(row))
|
|
2040
|
-
return false;
|
|
2041
|
-
const lease = this.requireLease(row.resourceId);
|
|
2042
|
-
if (lease.ownerId !== null && lease.expiresAt !== null && lease.expiresAt > now)
|
|
2043
|
-
return false;
|
|
2044
|
-
return (this.state.connection
|
|
2045
|
-
.prepare(`UPDATE leases SET generation = ?, owner_type = 'session', owner_id = ?, token_hash = ?,
|
|
2046
|
-
acquired_at = ?, heartbeat_at = ?, expires_at = ?
|
|
2047
|
-
WHERE resource_id = ? AND generation = ?`)
|
|
2048
|
-
.run(lease.generation + 1, ownerId, tokenHash(token), now, now, now + leaseMs, row.resourceId, lease.generation).changes === 1);
|
|
2049
|
-
});
|
|
2050
|
-
}
|
|
2051
|
-
verifyEffectToken(effectId, token, nowValue) {
|
|
2052
|
-
const now = epoch(validTimestamp(nowValue));
|
|
2053
|
-
const row = this.state.connection
|
|
2054
|
-
.prepare(`SELECT l.token_hash AS tokenHash, l.expires_at AS expiresAt
|
|
2055
|
-
FROM effects e JOIN leases l ON l.resource_id = e.resource_id
|
|
2056
|
-
WHERE e.effect_id = ?`)
|
|
2057
|
-
.get(effectId);
|
|
2058
|
-
return (isEffectTokenRow(row) &&
|
|
2059
|
-
row.tokenHash !== null &&
|
|
2060
|
-
row.tokenHash.equals(tokenHash(token)) &&
|
|
2061
|
-
row.expiresAt !== null &&
|
|
2062
|
-
row.expiresAt > now);
|
|
2063
|
-
}
|
|
2064
|
-
releaseEffectLease(effectId, token) {
|
|
2065
|
-
return (this.state.connection
|
|
2066
|
-
.prepare(`UPDATE leases SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
|
|
2067
|
-
acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
|
|
2068
|
-
WHERE resource_id = (SELECT resource_id FROM effects WHERE effect_id = ?)
|
|
2069
|
-
AND token_hash = ?`)
|
|
2070
|
-
.run(effectId, tokenHash(token)).changes === 1);
|
|
2071
|
-
}
|
|
2072
|
-
completeEffect(effectId, status, now) {
|
|
2073
|
-
this.state.connection
|
|
2074
|
-
.prepare("UPDATE effects SET status = ?, updated_at = ?, settled_at = ? WHERE effect_id = ?")
|
|
2075
|
-
.run(status, now, now, effectId);
|
|
2076
|
-
this.state.connection
|
|
2077
|
-
.prepare(`UPDATE leases SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
|
|
2078
|
-
acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
|
|
2079
|
-
WHERE resource_id = (SELECT resource_id FROM effects WHERE effect_id = ?)`)
|
|
2080
|
-
.run(effectId);
|
|
2081
|
-
}
|
|
2082
|
-
insertEffectResource(effectId, resourceId, sourceResourceId, sourceRevision, type, key, payloadHash, scope, now) {
|
|
2083
|
-
this.state.connection
|
|
2084
|
-
.prepare("INSERT INTO resources(resource_id, resource_type, aggregate_key, revision, created_at, updated_at) VALUES (?, 'effect', ?, 1, ?, ?)")
|
|
2085
|
-
.run(resourceId, effectId, now, now);
|
|
2086
|
-
this.state.connection
|
|
2087
|
-
.prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
|
|
2088
|
-
.run(resourceId);
|
|
2089
|
-
this.state.connection
|
|
2090
|
-
.prepare(`INSERT INTO effects(
|
|
2091
|
-
effect_id, resource_id, source_resource_id, source_revision, effect_type,
|
|
2092
|
-
idempotency_key, payload_hash, owner_scope, status, attempt_count,
|
|
2093
|
-
created_at, updated_at
|
|
2094
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, ?)`)
|
|
2095
|
-
.run(effectId, resourceId, sourceResourceId, sourceRevision, type, key, payloadHash, scope, now, now);
|
|
2096
|
-
this.insertEvent(resourceId, 1, "effect.created", "system", null, {}, now);
|
|
2097
|
-
}
|
|
2098
|
-
notification(runId, attemptId, index) {
|
|
2099
|
-
const row = this.state.connection
|
|
2100
|
-
.prepare(notificationSelect("WHERE n.run_id = ? AND n.attempt_id = ? AND n.notification_index = ?"))
|
|
2101
|
-
.get(runId, attemptId, index);
|
|
2102
|
-
return isNotificationRow(row) ? this.mapNotification(row) : undefined;
|
|
2103
|
-
}
|
|
2104
|
-
requireNotification(runId, attemptId, index) {
|
|
2105
|
-
const record = this.notification(runId, attemptId, index);
|
|
2106
|
-
/* istanbul ignore if -- impossible after exact schema and transaction checks */
|
|
2107
|
-
if (record === undefined)
|
|
2108
|
-
throw new Error("Workflow notification was not stored");
|
|
2109
|
-
return record;
|
|
2110
|
-
}
|
|
2111
|
-
notificationRows(sessionId, limit) {
|
|
2112
|
-
return this.state.connection
|
|
2113
|
-
.prepare(notificationSelect("WHERE n.target_session_id = ? AND e.status = 'pending' ORDER BY n.created_at LIMIT ?"))
|
|
2114
|
-
.all(sessionId, limit)
|
|
2115
|
-
.filter(isNotificationRow);
|
|
2116
|
-
}
|
|
2117
|
-
notificationRowById(id) {
|
|
2118
|
-
const row = this.state.connection
|
|
2119
|
-
.prepare(notificationSelect("WHERE n.notification_id = ?"))
|
|
2120
|
-
.get(id);
|
|
2121
|
-
return isNotificationRow(row) ? row : undefined;
|
|
2122
|
-
}
|
|
2123
|
-
/* istanbul ignore next -- pure projection covered by integration tests */
|
|
2124
|
-
mapNotification(row) {
|
|
2125
|
-
return {
|
|
2126
|
-
notificationId: row.notificationId,
|
|
2127
|
-
runId: row.runId,
|
|
2128
|
-
nodeId: row.nodeId,
|
|
2129
|
-
attemptId: row.attemptId,
|
|
2130
|
-
notificationIndex: row.notificationIndex,
|
|
2131
|
-
targetSessionId: row.targetSessionId,
|
|
2132
|
-
kind: row.kind,
|
|
2133
|
-
content: this.state.readBlob(row.contentHash)?.content.toString("utf8") ?? "",
|
|
2134
|
-
createdAt: new Date(row.createdAt).toISOString(),
|
|
2135
|
-
deliveryClaimExpiresAt: row.claimExpiresAt === null ? null : new Date(row.claimExpiresAt).toISOString(),
|
|
2136
|
-
deliveredAt: row.deliveredAt === null ? null : new Date(row.deliveredAt).toISOString(),
|
|
2137
|
-
};
|
|
2138
|
-
}
|
|
2139
1894
|
assertWritable() {
|
|
2140
1895
|
if (this.state.mode === "read-only")
|
|
2141
1896
|
throw new Error("Controller store is read-only");
|
|
@@ -2177,6 +1932,9 @@ function workflowRunSelect(clause) {
|
|
|
2177
1932
|
q.consecutive_errors AS consecutiveErrors, q.error_code AS errorCode,
|
|
2178
1933
|
q.error_hash AS errorHash, b.origin_session_id AS originSessionId,
|
|
2179
1934
|
b.execution_mode AS executionMode, r.parent_run_id AS parentRunId,
|
|
1935
|
+
r.root_run_id AS rootRunId, r.lineage_kind AS lineageKind,
|
|
1936
|
+
r.restart_number AS restartNumber,
|
|
1937
|
+
r.parent_terminal_fingerprint AS parentTerminalFingerprint,
|
|
2180
1938
|
q.created_at AS createdAt, q.updated_at AS updatedAt,
|
|
2181
1939
|
q.started_at AS startedAt, q.finished_at AS finishedAt,
|
|
2182
1940
|
l.generation AS leaseGeneration, l.owner_id AS ownerId, l.expires_at AS claimExpiresAt
|
|
@@ -2190,8 +1948,10 @@ function workflowRunViewSelect(clause) {
|
|
|
2190
1948
|
r.workflow_ref AS workflowRef, r.status AS runStatus, r.paused,
|
|
2191
1949
|
r.definition_digest AS definitionDigest, q.status,
|
|
2192
1950
|
b.origin_session_id AS originSessionId, b.execution_mode AS executionMode,
|
|
2193
|
-
r.parent_run_id AS parentRunId,
|
|
2194
|
-
|
|
1951
|
+
r.parent_run_id AS parentRunId, r.root_run_id AS rootRunId,
|
|
1952
|
+
r.lineage_kind AS lineageKind, r.restart_number AS restartNumber,
|
|
1953
|
+
r.parent_terminal_fingerprint AS parentTerminalFingerprint,
|
|
1954
|
+
q.error_code AS errorCode, CAST(error.content AS TEXT) AS errorMessage,
|
|
2195
1955
|
q.created_at AS createdAt, q.updated_at AS updatedAt,
|
|
2196
1956
|
q.started_at AS startedAt, q.finished_at AS finishedAt,
|
|
2197
1957
|
source.source_type AS sourceType, source.source_ref AS sourceRef,
|
|
@@ -2202,29 +1962,6 @@ function workflowRunViewSelect(clause) {
|
|
|
2202
1962
|
LEFT JOIN run_sources source ON source.run_id = r.run_id AND source.mount_path = ''
|
|
2203
1963
|
LEFT JOIN blobs error ON error.blob_hash = q.error_hash ${clause}`;
|
|
2204
1964
|
}
|
|
2205
|
-
function turnIntentSelect(clause) {
|
|
2206
|
-
return `SELECT t.turn_intent_id AS intentId, t.source_event_id AS sourceEventId,
|
|
2207
|
-
t.run_id AS runId, t.workflow_ref AS workflowRef,
|
|
2208
|
-
t.target_session_id AS targetSessionId, t.cause,
|
|
2209
|
-
t.node_id AS nodeId, t.attempt_id AS attemptId, t.facts_hash AS factsHash,
|
|
2210
|
-
t.requested_at AS requestedAt, t.eligible_at AS eligibleAt,
|
|
2211
|
-
t.resolved_at AS resolvedAt, t.resolution_type AS resolution,
|
|
2212
|
-
t.resolution_message_id AS resolutionMessageId, l.expires_at AS claimExpiresAt,
|
|
2213
|
-
t.effect_id AS effectId
|
|
2214
|
-
FROM turn_intents t JOIN effects e ON e.effect_id = t.effect_id
|
|
2215
|
-
JOIN leases l ON l.resource_id = e.resource_id ${clause}`;
|
|
2216
|
-
}
|
|
2217
|
-
function notificationSelect(clause) {
|
|
2218
|
-
return `SELECT n.notification_id AS notificationId, n.effect_id AS effectId,
|
|
2219
|
-
n.run_id AS runId, a.node_id AS nodeId, n.attempt_id AS attemptId,
|
|
2220
|
-
n.notification_index AS notificationIndex, n.target_session_id AS targetSessionId,
|
|
2221
|
-
n.notification_type AS kind, n.content_hash AS contentHash,
|
|
2222
|
-
n.created_at AS createdAt, l.expires_at AS claimExpiresAt,
|
|
2223
|
-
e.settled_at AS deliveredAt
|
|
2224
|
-
FROM notifications n JOIN effects e ON e.effect_id = n.effect_id
|
|
2225
|
-
JOIN leases l ON l.resource_id = e.resource_id
|
|
2226
|
-
JOIN node_attempts a ON a.attempt_id = n.attempt_id ${clause}`;
|
|
2227
|
-
}
|
|
2228
1965
|
function isRecord(value) {
|
|
2229
1966
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2230
1967
|
}
|
|
@@ -2251,6 +1988,10 @@ function workflowRunViewRecord(row) {
|
|
|
2251
1988
|
originSessionId: row.executionMode === "headless" ? null : row.originSessionId,
|
|
2252
1989
|
executionMode: row.executionMode,
|
|
2253
1990
|
parentRunId: row.parentRunId,
|
|
1991
|
+
rootRunId: row.rootRunId,
|
|
1992
|
+
lineageKind: row.lineageKind,
|
|
1993
|
+
restartNumber: row.restartNumber,
|
|
1994
|
+
parentTerminalFingerprint: row.parentTerminalFingerprint?.toString("hex") ?? null,
|
|
2254
1995
|
errorCode: row.errorCode,
|
|
2255
1996
|
errorMessage: row.errorMessage,
|
|
2256
1997
|
createdAt: new Date(row.createdAt).toISOString(),
|
|
@@ -2271,6 +2012,13 @@ function isWorkflowRunViewRow(value) {
|
|
|
2271
2012
|
(value.originSessionId === null || typeof value.originSessionId === "string") &&
|
|
2272
2013
|
(value.executionMode === "interactive" || value.executionMode === "headless") &&
|
|
2273
2014
|
(value.parentRunId === null || typeof value.parentRunId === "string") &&
|
|
2015
|
+
typeof value.rootRunId === "string" &&
|
|
2016
|
+
(value.lineageKind === null ||
|
|
2017
|
+
value.lineageKind === "continuation" ||
|
|
2018
|
+
value.lineageKind === "restart") &&
|
|
2019
|
+
typeof value.restartNumber === "number" &&
|
|
2020
|
+
(value.parentTerminalFingerprint === null ||
|
|
2021
|
+
Buffer.isBuffer(value.parentTerminalFingerprint)) &&
|
|
2274
2022
|
(value.errorCode === null || typeof value.errorCode === "string") &&
|
|
2275
2023
|
(value.errorMessage === null || typeof value.errorMessage === "string") &&
|
|
2276
2024
|
typeof value.createdAt === "number" &&
|
|
@@ -2318,12 +2066,6 @@ function isControllerEventRow(value) {
|
|
|
2318
2066
|
function isRunEventRow(value) {
|
|
2319
2067
|
return isRecord(value);
|
|
2320
2068
|
}
|
|
2321
|
-
function isTurnIntentRow(value) {
|
|
2322
|
-
return isRecord(value);
|
|
2323
|
-
}
|
|
2324
|
-
function isNotificationRow(value) {
|
|
2325
|
-
return isRecord(value);
|
|
2326
|
-
}
|
|
2327
2069
|
function isProjectRow(value) {
|
|
2328
2070
|
return isRecord(value);
|
|
2329
2071
|
}
|
|
@@ -2348,12 +2090,6 @@ function isSequenceRow(value) {
|
|
|
2348
2090
|
function isKeyRow(value) {
|
|
2349
2091
|
return isRecord(value);
|
|
2350
2092
|
}
|
|
2351
|
-
function isResourceIdRow(value) {
|
|
2352
|
-
return isRecord(value);
|
|
2353
|
-
}
|
|
2354
|
-
function isEffectTokenRow(value) {
|
|
2355
|
-
return isRecord(value);
|
|
2356
|
-
}
|
|
2357
2093
|
function isEffectIdentityRow(value) {
|
|
2358
2094
|
return isRecord(value);
|
|
2359
2095
|
}
|
|
@@ -2377,6 +2113,12 @@ function digestBuffer(value) {
|
|
|
2377
2113
|
throw new Error("Expected a SHA-256 digest");
|
|
2378
2114
|
return Buffer.from(hex, "hex");
|
|
2379
2115
|
}
|
|
2116
|
+
function terminalFingerprintBuffer(value) {
|
|
2117
|
+
const hex = value.startsWith("sha256:") ? value.slice(7) : value;
|
|
2118
|
+
if (!/^[a-f0-9]{64}$/i.test(hex))
|
|
2119
|
+
throw new Error("Expected a terminal fingerprint");
|
|
2120
|
+
return Buffer.from(hex, "hex");
|
|
2121
|
+
}
|
|
2380
2122
|
function queuedWorkflowSource(value) {
|
|
2381
2123
|
if (isWorkflowSource(value)) {
|
|
2382
2124
|
return { root: value, mounted: [] };
|