@osolmaz/pi-workflows 0.15.3 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -8
- package/dist/channels/adapter-entry.d.ts +3 -0
- package/dist/channels/adapter-entry.js +203 -0
- package/dist/channels/adapter-entry.js.map +1 -0
- package/dist/channels/config.d.ts +43 -0
- package/dist/channels/config.js +234 -0
- package/dist/channels/config.js.map +1 -0
- package/dist/channels/protocol.d.ts +94 -0
- package/dist/channels/protocol.js +219 -0
- package/dist/channels/protocol.js.map +1 -0
- package/dist/channels/telegram.d.ts +46 -0
- package/dist/channels/telegram.js +285 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/client/activity.d.ts +2 -0
- package/dist/client/activity.js +6 -0
- package/dist/client/activity.js.map +1 -0
- package/dist/client/client.d.ts +102 -0
- package/dist/client/client.js +737 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/materialize.d.ts +7 -0
- package/dist/client/materialize.js +177 -0
- package/dist/client/materialize.js.map +1 -0
- package/dist/client/protocol.d.ts +60 -0
- package/dist/client/protocol.js +275 -0
- package/dist/client/protocol.js.map +1 -0
- package/dist/client/resolver.d.ts +23 -0
- package/dist/client/resolver.js +2 -0
- package/dist/client/resolver.js.map +1 -0
- package/dist/client/view.d.ts +144 -0
- package/dist/client/view.js +3 -0
- package/dist/client/view.js.map +1 -0
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +47 -157
- package/dist/controllers/sqlite.js +230 -406
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/index.d.ts +23 -1
- package/dist/extension/index.js +734 -555
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +95 -0
- package/dist/extension/recorder.js +530 -0
- package/dist/extension/recorder.js.map +1 -0
- package/dist/extension/remote-recorder-store.d.ts +25 -0
- package/dist/extension/remote-recorder-store.js +81 -0
- package/dist/extension/remote-recorder-store.js.map +1 -0
- package/dist/extension/session-events.d.ts +134 -0
- package/dist/extension/session-events.js +60 -0
- package/dist/extension/session-events.js.map +1 -0
- package/dist/extension/session-view.d.ts +9 -2
- package/dist/extension/session-view.js +117 -51
- package/dist/extension/session-view.js.map +1 -1
- package/dist/extension/step-message.d.ts +6 -4
- package/dist/extension/step-message.js +12 -6
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.d.ts +3 -2
- package/dist/extension/widget.js +41 -24
- package/dist/extension/widget.js.map +1 -1
- package/dist/extension/workflow-message-coordinator.d.ts +27 -0
- package/dist/extension/workflow-message-coordinator.js +294 -0
- package/dist/extension/workflow-message-coordinator.js.map +1 -0
- package/dist/host/channel-effects.d.ts +72 -0
- package/dist/host/channel-effects.js +271 -0
- package/dist/host/channel-effects.js.map +1 -0
- package/dist/host/channel-supervisor.d.ts +21 -0
- package/dist/host/channel-supervisor.js +54 -0
- package/dist/host/channel-supervisor.js.map +1 -0
- package/dist/host/child-worker-supervisor.js +1 -1
- package/dist/host/child-worker-supervisor.js.map +1 -1
- package/dist/host/resolver-entry.d.ts +2 -23
- package/dist/host/resolver-entry.js +1 -1
- package/dist/host/resolver-entry.js.map +1 -1
- package/dist/host/runner.d.ts +54 -8
- package/dist/host/runner.js +1997 -305
- package/dist/host/runner.js.map +1 -1
- package/dist/host/state.d.ts +25 -19
- package/dist/host/state.js +303 -89
- package/dist/host/state.js.map +1 -1
- package/dist/host/view.d.ts +83 -0
- package/dist/host/view.js +975 -0
- package/dist/host/view.js.map +1 -0
- package/dist/host/worker-protocol.js +1 -1
- package/dist/host/worker-protocol.js.map +1 -1
- package/dist/state/database.d.ts +1 -0
- package/dist/state/database.js +15 -0
- package/dist/state/database.js.map +1 -1
- package/dist/state/prune.d.ts +3 -1
- package/dist/state/prune.js +9 -33
- package/dist/state/prune.js.map +1 -1
- package/dist/state/schema.js +88 -98
- package/dist/state/schema.js.map +1 -1
- package/dist/state/workflow-messages.d.ts +96 -0
- package/dist/state/workflow-messages.js +335 -0
- package/dist/state/workflow-messages.js.map +1 -0
- package/dist/viewer/backup.d.ts +2 -0
- package/dist/viewer/backup.js +28 -0
- package/dist/viewer/backup.js.map +1 -0
- package/dist/viewer/cli.d.ts +4 -0
- package/dist/viewer/cli.js +150 -170
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.js +1 -1
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/tui.d.ts +5 -7
- package/dist/viewer/tui.js +245 -108
- package/dist/viewer/tui.js.map +1 -1
- package/dist/workflows/human-decision.d.ts +2 -0
- package/dist/workflows/human-decision.js +8 -2
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -1
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/settings.d.ts +1 -8
- package/dist/workflows/settings.js.map +1 -1
- package/dist/workflows/store.d.ts +67 -20
- package/dist/workflows/store.js +451 -378
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +21 -0
- package/dist/workflows/tool-input.js +23 -1
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +5 -5
- package/dist/workflows/workflow-message-content.d.ts +38 -0
- package/dist/workflows/workflow-message-content.js +157 -0
- package/dist/workflows/workflow-message-content.js.map +1 -0
- package/docs/2026-08-18-herdr-piw-plan.md +2 -1
- package/docs/2026-08-20-durable-workflow-launch-plan.md +1 -1
- package/docs/2026-08-25-workflow-follow-ups.md +26 -50
- package/docs/2026-08-30-out-of-process-workflow-host-plan.md +3 -3
- package/docs/2026-09-01-restore-session-delivery-controls-plan.md +4 -0
- package/docs/2026-09-01-unified-workflow-client-plan.md +385 -0
- package/docs/2026-09-02-installed-live-e2e-plan.md +239 -0
- package/docs/2026-09-02-unify-workflow-messages-plan.md +562 -0
- package/docs/DEFERRED_TURNS.md +66 -294
- package/docs/HUMAN_DECISIONS.md +29 -30
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +4 -11
- package/docs/SQLITE_STATE.md +31 -25
- package/docs/WORKFLOW_HOST.md +155 -77
- package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
- package/docs/development.md +2 -1
- package/docs/live-replay-protocol.md +74 -132
- package/docs/tui-viewer.md +12 -14
- package/docs/workflows.md +78 -28
- package/herdr-plugin.toml +1 -1
- package/package.json +9 -3
- package/plugins/herdr/viewer.mjs +14 -5
- package/protocol/client.v1.schema.json +137 -0
- package/protocol/fixtures/client-v1.json +23 -0
- package/src/channels/adapter-entry.ts +220 -0
- package/src/channels/config.ts +296 -0
- package/src/channels/protocol.ts +333 -0
- package/src/channels/telegram.ts +335 -0
- package/src/client/activity.ts +6 -0
- package/src/client/client.ts +939 -0
- package/src/client/index.ts +25 -0
- package/src/client/materialize.ts +228 -0
- package/src/client/protocol.ts +333 -0
- package/src/client/resolver.ts +26 -0
- package/src/client/view.ts +163 -0
- package/src/controllers/index.ts +0 -1
- package/src/controllers/sqlite.ts +339 -808
- package/src/extension/index.ts +853 -665
- package/src/extension/recorder.ts +687 -0
- package/src/extension/remote-recorder-store.ts +126 -0
- package/src/extension/session-events.ts +119 -0
- package/src/extension/session-view.ts +157 -51
- package/src/extension/step-message.ts +19 -9
- package/src/extension/widget.ts +53 -25
- package/src/extension/workflow-message-coordinator.ts +334 -0
- package/src/host/channel-effects.ts +465 -0
- package/src/host/channel-supervisor.ts +73 -0
- package/src/host/child-worker-supervisor.ts +1 -1
- package/src/host/resolver-entry.ts +11 -26
- package/src/host/runner.ts +2639 -450
- package/src/host/state.ts +404 -137
- package/src/host/view.ts +1212 -0
- package/src/host/worker-protocol.ts +1 -1
- package/src/state/database.ts +13 -0
- package/src/state/prune.ts +14 -42
- package/src/state/schema.ts +88 -98
- package/src/state/workflow-messages.ts +548 -0
- package/src/viewer/backup.ts +29 -0
- package/src/viewer/cli.ts +171 -185
- package/src/viewer/render.ts +1 -5
- package/src/viewer/tui.ts +261 -124
- package/src/workflows/human-decision.ts +18 -2
- package/src/workflows/index.ts +0 -1
- package/src/workflows/settings.ts +1 -20
- package/src/workflows/store.ts +650 -511
- package/src/workflows/tool-input.ts +36 -1
- package/src/workflows/types.ts +5 -5
- package/src/workflows/workflow-message-content.ts +197 -0
- package/dist/extension/decision-channels.d.ts +0 -134
- package/dist/extension/decision-channels.js +0 -1307
- package/dist/extension/decision-channels.js.map +0 -1
- package/dist/host/client.d.ts +0 -48
- package/dist/host/client.js +0 -216
- package/dist/host/client.js.map +0 -1
- package/dist/host/protocol.d.ts +0 -38
- package/dist/host/protocol.js +0 -156
- package/dist/host/protocol.js.map +0 -1
- package/dist/viewer/watch.d.ts +0 -6
- package/dist/viewer/watch.js +0 -46
- package/dist/viewer/watch.js.map +0 -1
- package/src/extension/decision-channels.ts +0 -1826
- package/src/host/client.ts +0 -293
- package/src/host/protocol.ts +0 -196
- package/src/viewer/watch.ts +0 -51
|
@@ -6,7 +6,6 @@ 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
8
|
import { EffectRequestConflictError, ResourceConflictError, ResourceNotFoundError, WorkflowRequestConflictError, } from "./errors.js";
|
|
9
|
-
const TURN_INTENT_FACTS_SCHEMA = "pi-workflows.deferred-turn-facts.v1";
|
|
10
9
|
export class SqliteControllerStore {
|
|
11
10
|
filePath;
|
|
12
11
|
state;
|
|
@@ -520,32 +519,65 @@ export class SqliteControllerStore {
|
|
|
520
519
|
if (this.getWorkflowRun(options.runId) !== undefined) {
|
|
521
520
|
throw new Error(`Workflow run already reserved: ${options.runId}`);
|
|
522
521
|
}
|
|
522
|
+
const lineageKind = options.parentRunId === undefined ? null : (options.lineageKind ?? "continuation");
|
|
523
|
+
if (options.parentRunId === undefined && options.lineageKind !== undefined) {
|
|
524
|
+
throw new Error("A root workflow run cannot declare lineage");
|
|
525
|
+
}
|
|
526
|
+
let rootRunId = options.runId;
|
|
527
|
+
let restartNumber = 0;
|
|
528
|
+
let parentTerminalFingerprint = null;
|
|
523
529
|
if (options.parentRunId !== undefined) {
|
|
524
530
|
const parent = this.requireWorkflowRunRow(options.parentRunId);
|
|
525
531
|
if (parent.originSessionId !== options.originSessionId) {
|
|
526
|
-
throw new Error("
|
|
532
|
+
throw new Error("Workflow parent belongs to another Pi session");
|
|
527
533
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if (
|
|
531
|
-
|
|
534
|
+
rootRunId = parent.rootRunId;
|
|
535
|
+
if (lineageKind === "restart") {
|
|
536
|
+
if (!["completed", "failed", "timed_out", "cancelled"].includes(parent.runStatus) ||
|
|
537
|
+
!["done", "failed", "cancelled"].includes(parent.status)) {
|
|
538
|
+
throw new Error("Restart parent is not terminal");
|
|
539
|
+
}
|
|
540
|
+
if (options.parentTerminalFingerprint === undefined) {
|
|
541
|
+
throw new Error("Restart requires the parent terminal fingerprint");
|
|
542
|
+
}
|
|
543
|
+
parentTerminalFingerprint = terminalFingerprintBuffer(options.parentTerminalFingerprint);
|
|
544
|
+
restartNumber = options.restartNumber ?? parent.restartNumber + 1;
|
|
545
|
+
if (restartNumber !== parent.restartNumber + 1) {
|
|
546
|
+
throw new Error("Restart number does not follow its parent");
|
|
532
547
|
}
|
|
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
548
|
}
|
|
545
549
|
else {
|
|
546
|
-
|
|
550
|
+
if (options.parentTerminalFingerprint !== undefined ||
|
|
551
|
+
(options.restartNumber !== undefined && options.restartNumber !== parent.restartNumber)) {
|
|
552
|
+
throw new Error("Continuation lineage conflicts with restart metadata");
|
|
553
|
+
}
|
|
554
|
+
restartNumber = parent.restartNumber;
|
|
555
|
+
if (parent.status === "parked") {
|
|
556
|
+
const parentLease = this.requireLease(parent.resourceId);
|
|
557
|
+
if (parentLease.ownerId !== null) {
|
|
558
|
+
throw new Error("Continuation parent still has an active owner");
|
|
559
|
+
}
|
|
560
|
+
this.state.connection
|
|
561
|
+
.prepare(`UPDATE run_queue
|
|
562
|
+
SET status = 'done', updated_at = ?, finished_at = ?
|
|
563
|
+
WHERE run_id = ? AND status = 'parked'`)
|
|
564
|
+
.run(now, now, parent.runId);
|
|
565
|
+
const parentRevision = this.resourceRevision(parent.resourceId);
|
|
566
|
+
this.bumpResource(parent.resourceId, parentRevision, now);
|
|
567
|
+
this.insertEvent(parent.resourceId, parentRevision + 1, "run.queue_done_for_continuation", "session", options.originSessionId, { continuationRunId: options.runId }, now);
|
|
568
|
+
}
|
|
569
|
+
else if (parent.status === "done") {
|
|
570
|
+
throw new Error("Continuation parent already has a reserved continuation");
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
throw new Error(`Continuation parent queue is ${parent.status}`);
|
|
574
|
+
}
|
|
547
575
|
}
|
|
548
576
|
}
|
|
577
|
+
else if (options.restartNumber !== undefined ||
|
|
578
|
+
options.parentTerminalFingerprint !== undefined) {
|
|
579
|
+
throw new Error("A root workflow run cannot declare restart metadata");
|
|
580
|
+
}
|
|
549
581
|
const resourceId = resourceIdFor("run", options.runId);
|
|
550
582
|
const definitionHash = this.state.putJson(options.definitionSnapshot, now);
|
|
551
583
|
const queuedSource = queuedWorkflowSource(options.workflowSource);
|
|
@@ -566,11 +598,12 @@ export class SqliteControllerStore {
|
|
|
566
598
|
.run(resourceId);
|
|
567
599
|
this.state.connection
|
|
568
600
|
.prepare(`INSERT INTO runs(
|
|
569
|
-
run_id, resource_id, project_id, parent_run_id,
|
|
601
|
+
run_id, resource_id, project_id, parent_run_id, root_run_id, lineage_kind,
|
|
602
|
+
restart_number, parent_terminal_fingerprint, definition_digest,
|
|
570
603
|
workflow_ref, launch_options_hash, status, paused,
|
|
571
604
|
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);
|
|
605
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', 0, ?, ?, ?)`)
|
|
606
|
+
.run(options.runId, resourceId, this.requireProjectId(), options.parentRunId ?? null, rootRunId, lineageKind, restartNumber, parentTerminalFingerprint, definitionDigest, options.workflowSourceRef, launchHash, inputHash, now, now);
|
|
574
607
|
initializeViewerRun(this.state, options.runId, now);
|
|
575
608
|
insertQueuedRunSources(this.state, options.runId, queuedSource);
|
|
576
609
|
this.state.connection
|
|
@@ -621,6 +654,11 @@ export class SqliteControllerStore {
|
|
|
621
654
|
runnerId: options.runnerId,
|
|
622
655
|
originSessionId: options.originSessionId,
|
|
623
656
|
...(options.parentRunId === undefined ? {} : { parentRunId: options.parentRunId }),
|
|
657
|
+
...(options.lineageKind === undefined ? {} : { lineageKind: options.lineageKind }),
|
|
658
|
+
...(options.restartNumber === undefined ? {} : { restartNumber: options.restartNumber }),
|
|
659
|
+
...(options.parentTerminalFingerprint === undefined
|
|
660
|
+
? {}
|
|
661
|
+
: { parentTerminalFingerprint: options.parentTerminalFingerprint }),
|
|
624
662
|
...(options.now === undefined ? {} : { now: options.now }),
|
|
625
663
|
});
|
|
626
664
|
}
|
|
@@ -640,6 +678,32 @@ export class SqliteControllerStore {
|
|
|
640
678
|
const row = this.workflowRunRow(runId);
|
|
641
679
|
return row === undefined ? undefined : this.mapWorkflowRun(row);
|
|
642
680
|
}
|
|
681
|
+
getWorkflowRunView(runId) {
|
|
682
|
+
const row = this.workflowRunViewRows("WHERE r.run_id = ?", [runId])[0];
|
|
683
|
+
return row === undefined ? undefined : workflowRunViewRecord(row);
|
|
684
|
+
}
|
|
685
|
+
workflowRunListRevision() {
|
|
686
|
+
const row = this.state.connection
|
|
687
|
+
.prepare(`SELECT count(*) AS count, COALESCE(sum(res.revision), 0) AS revisionSum,
|
|
688
|
+
COALESCE(max(q.updated_at), 0) AS updatedAt
|
|
689
|
+
FROM runs r JOIN run_queue q ON q.run_id = r.run_id
|
|
690
|
+
JOIN resources res ON res.resource_id = r.resource_id`)
|
|
691
|
+
.get();
|
|
692
|
+
if (!isRunListRevisionRow(row))
|
|
693
|
+
throw new Error("Workflow run list revision is invalid");
|
|
694
|
+
return {
|
|
695
|
+
total: row.count,
|
|
696
|
+
revision: `${row.count}:${row.revisionSum}:${row.updatedAt}`,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
listWorkflowRunViews(options) {
|
|
700
|
+
const current = this.workflowRunListRevision();
|
|
701
|
+
const rows = this.workflowRunViewRows("ORDER BY q.created_at DESC LIMIT ? OFFSET ?", [
|
|
702
|
+
options.limit,
|
|
703
|
+
options.offset,
|
|
704
|
+
]);
|
|
705
|
+
return { ...current, runs: rows.map(workflowRunViewRecord) };
|
|
706
|
+
}
|
|
643
707
|
workflowRunProjectPath(runId) {
|
|
644
708
|
const row = this.state.connection
|
|
645
709
|
.prepare(`SELECT p.canonical_path AS canonicalPath
|
|
@@ -662,9 +726,10 @@ export class SqliteControllerStore {
|
|
|
662
726
|
clauses.push(`r.run_id NOT IN (${options.excludeRunIds.map(() => "?").join(", ")})`);
|
|
663
727
|
params.push(...options.excludeRunIds);
|
|
664
728
|
}
|
|
665
|
-
|
|
729
|
+
if (options.limit !== undefined)
|
|
730
|
+
params.push(options.limit);
|
|
666
731
|
const rows = this.state.connection
|
|
667
|
-
.prepare(workflowRunSelect(`${clauses.length === 0 ? "" : `WHERE ${clauses.join(" AND ")}`} ORDER BY q.created_at DESC LIMIT
|
|
732
|
+
.prepare(workflowRunSelect(`${clauses.length === 0 ? "" : `WHERE ${clauses.join(" AND ")}`} ORDER BY q.created_at DESC${options.limit === undefined ? "" : " LIMIT ?"}`))
|
|
668
733
|
.all(...params);
|
|
669
734
|
return rows.filter(isRunRow).map((row) => this.mapWorkflowRun(row));
|
|
670
735
|
}
|
|
@@ -674,6 +739,10 @@ export class SqliteControllerStore {
|
|
|
674
739
|
.get(sessionId);
|
|
675
740
|
return isRunRow(row) ? this.mapWorkflowRun(row) : undefined;
|
|
676
741
|
}
|
|
742
|
+
findSessionReservationView(sessionId) {
|
|
743
|
+
const row = this.workflowRunViewRows("WHERE b.origin_session_id = ? AND q.status NOT IN ('done', 'failed', 'cancelled') ORDER BY q.created_at DESC LIMIT 1", [sessionId])[0];
|
|
744
|
+
return row === undefined ? undefined : workflowRunViewRecord(row);
|
|
745
|
+
}
|
|
677
746
|
claimWorkflowRun(options) {
|
|
678
747
|
const now = epoch(validTimestamp(options.now));
|
|
679
748
|
return this.claimRun(options.runId, options.runnerId, options.claimToken, options.leaseMs, now);
|
|
@@ -705,17 +774,23 @@ export class SqliteControllerStore {
|
|
|
705
774
|
.prepare(`SELECT i.request_id AS requestId, i.attempt_id AS attemptId
|
|
706
775
|
FROM interactive_requests i
|
|
707
776
|
JOIN node_attempts a ON a.attempt_id = i.attempt_id
|
|
708
|
-
|
|
777
|
+
JOIN runs r ON r.run_id = i.run_id
|
|
778
|
+
WHERE i.run_id = ? AND i.target_session_id = ?
|
|
779
|
+
AND i.status = 'pending' AND r.paused = 0
|
|
709
780
|
AND a.deadline_at IS NOT NULL AND a.deadline_at <= ?
|
|
781
|
+
AND EXISTS (
|
|
782
|
+
SELECT 1 FROM workflow_messages m
|
|
783
|
+
JOIN workflow_turns t ON t.workflow_message_id = m.workflow_message_id
|
|
784
|
+
WHERE m.source_id = i.request_id AND m.kind = 'step' AND t.state = 'started'
|
|
785
|
+
)
|
|
710
786
|
ORDER BY a.deadline_at, i.request_id LIMIT 1`)
|
|
711
|
-
.get(options.runId, now);
|
|
787
|
+
.get(options.runId, options.targetSessionId, now);
|
|
712
788
|
if (!isExpiredInteractionRow(interaction))
|
|
713
789
|
return false;
|
|
714
790
|
const request = this.state.connection
|
|
715
791
|
.prepare(`UPDATE interactive_requests
|
|
716
|
-
SET status = 'cancelled',
|
|
717
|
-
|
|
718
|
-
WHERE request_id = ? AND run_id = ? AND status IN ('pending', 'presenting')`)
|
|
792
|
+
SET status = 'cancelled', revision = revision + 1, updated_at = ?
|
|
793
|
+
WHERE request_id = ? AND run_id = ? AND status = 'pending'`)
|
|
719
794
|
.run(now, interaction.requestId, options.runId);
|
|
720
795
|
const run = this.state.connection
|
|
721
796
|
.prepare(`UPDATE runs
|
|
@@ -732,7 +807,7 @@ export class SqliteControllerStore {
|
|
|
732
807
|
if (request.changes !== 1 || run.changes !== 1 || queue.changes !== 1) {
|
|
733
808
|
throw new Error(`Workflow run ${options.runId} changed during interaction timeout`);
|
|
734
809
|
}
|
|
735
|
-
const error = "Workflow node deadline expired
|
|
810
|
+
const error = "Workflow node model-turn deadline expired";
|
|
736
811
|
const receiptHash = this.state.putJson({ status: "rejected", error }, now);
|
|
737
812
|
this.state.connection
|
|
738
813
|
.prepare(`UPDATE interactive_submissions SET outcome = 'rejected', receipt_hash = ?
|
|
@@ -757,7 +832,7 @@ export class SqliteControllerStore {
|
|
|
757
832
|
"(l.owner_id IS NULL OR l.expires_at <= ? OR l.owner_id = ?)",
|
|
758
833
|
"(q.affinity_runner_id IS NULL OR q.affinity_runner_id = ? OR q.status IN ('parked', 'starting', 'running'))",
|
|
759
834
|
"NOT (q.status = 'parked' AND (r.status = 'waiting' OR r.paused = 1))",
|
|
760
|
-
"NOT EXISTS (SELECT 1 FROM interactive_requests i WHERE i.run_id = r.run_id AND i.status
|
|
835
|
+
"NOT EXISTS (SELECT 1 FROM interactive_requests i WHERE i.run_id = r.run_id AND i.status = 'pending')",
|
|
761
836
|
"(q.error_code IS NULL OR q.error_code <> 'workflowSourceChanged')",
|
|
762
837
|
];
|
|
763
838
|
const params = [now, now, options.runnerId, options.runnerId];
|
|
@@ -843,7 +918,7 @@ export class SqliteControllerStore {
|
|
|
843
918
|
return false;
|
|
844
919
|
const pending = this.state.connection
|
|
845
920
|
.prepare(`SELECT 1 FROM interactive_requests
|
|
846
|
-
WHERE run_id = ? AND status
|
|
921
|
+
WHERE run_id = ? AND status = 'pending' LIMIT 1`)
|
|
847
922
|
.get(options.runId);
|
|
848
923
|
if (pending === undefined)
|
|
849
924
|
return false;
|
|
@@ -879,7 +954,7 @@ export class SqliteControllerStore {
|
|
|
879
954
|
return false;
|
|
880
955
|
const pending = this.state.connection
|
|
881
956
|
.prepare(`SELECT 1 FROM interactive_requests
|
|
882
|
-
WHERE run_id = ? AND status
|
|
957
|
+
WHERE run_id = ? AND status = 'pending' LIMIT 1`)
|
|
883
958
|
.get(options.runId);
|
|
884
959
|
if (pending === undefined)
|
|
885
960
|
return false;
|
|
@@ -1139,187 +1214,6 @@ export class SqliteControllerStore {
|
|
|
1139
1214
|
payload: row.payloadHash === null ? {} : this.state.readJson(row.payloadHash),
|
|
1140
1215
|
}));
|
|
1141
1216
|
}
|
|
1142
|
-
ensureWorkflowTurnIntent(options) {
|
|
1143
|
-
const existing = this.turnIntentBySource(options.sourceEventId);
|
|
1144
|
-
if (existing !== undefined)
|
|
1145
|
-
return existing;
|
|
1146
|
-
const now = epoch(validTimestamp(options.requestedAt));
|
|
1147
|
-
const run = this.requireWorkflowRunRow(options.runId);
|
|
1148
|
-
const intentId = options.intentId;
|
|
1149
|
-
const resourceId = resourceIdFor("turn_intent", intentId);
|
|
1150
|
-
const effectId = `effect-${randomUUID()}`;
|
|
1151
|
-
this.state.transaction(() => {
|
|
1152
|
-
const factsHash = this.state.putJson(options.fallbackFacts, now);
|
|
1153
|
-
this.insertEffectResource(effectId, resourceIdFor("effect", effectId), run.resourceId, this.resourceRevision(run.resourceId), "turn.deliver", intentId, factsHash, "run", now);
|
|
1154
|
-
this.state.connection
|
|
1155
|
-
.prepare("INSERT INTO resources(resource_id, resource_type, aggregate_key, revision, created_at, updated_at) VALUES (?, 'turn_intent', ?, 1, ?, ?)")
|
|
1156
|
-
.run(resourceId, intentId, now, now);
|
|
1157
|
-
this.state.connection
|
|
1158
|
-
.prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
|
|
1159
|
-
.run(resourceId);
|
|
1160
|
-
this.state.connection
|
|
1161
|
-
.prepare(`INSERT INTO turn_intents(
|
|
1162
|
-
turn_intent_id, resource_id, effect_id, source_event_id, run_id,
|
|
1163
|
-
workflow_ref, target_session_id, cause, node_id, attempt_id,
|
|
1164
|
-
facts_hash, requested_at, eligible_at, created_at
|
|
1165
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1166
|
-
.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);
|
|
1167
|
-
this.insertEvent(resourceId, 1, "turn.requested", "session", options.targetSessionId, {}, now);
|
|
1168
|
-
});
|
|
1169
|
-
return this.requireTurnIntent(intentId);
|
|
1170
|
-
}
|
|
1171
|
-
getWorkflowTurnIntent(intentId) {
|
|
1172
|
-
return this.turnIntent(intentId);
|
|
1173
|
-
}
|
|
1174
|
-
findPendingWorkflowTurnIntent(options) {
|
|
1175
|
-
const row = this.state.connection
|
|
1176
|
-
.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"))
|
|
1177
|
-
.get(options.runId, options.targetSessionId);
|
|
1178
|
-
return isTurnIntentRow(row) ? this.mapTurnIntent(row) : undefined;
|
|
1179
|
-
}
|
|
1180
|
-
isWorkflowTurnIntentClaimLive(options) {
|
|
1181
|
-
const row = this.turnIntentRow(options.intentId);
|
|
1182
|
-
return (row !== undefined &&
|
|
1183
|
-
row.targetSessionId === options.targetSessionId &&
|
|
1184
|
-
row.resolvedAt === null &&
|
|
1185
|
-
this.verifyEffectToken(row.effectId, options.claimToken, options.now));
|
|
1186
|
-
}
|
|
1187
|
-
claimWorkflowTurnIntent(options) {
|
|
1188
|
-
const intent = this.turnIntent(options.intentId);
|
|
1189
|
-
if (intent?.targetSessionId !== options.targetSessionId)
|
|
1190
|
-
return undefined;
|
|
1191
|
-
return this.claimEffectForTurn(options.intentId, options.claimToken, options.leaseMs, options.now)
|
|
1192
|
-
? this.requireTurnIntent(options.intentId)
|
|
1193
|
-
: undefined;
|
|
1194
|
-
}
|
|
1195
|
-
claimEligibleWorkflowTurnIntents(options) {
|
|
1196
|
-
const now = epoch(validTimestamp(options.now));
|
|
1197
|
-
const rows = this.state.connection
|
|
1198
|
-
.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 ?"))
|
|
1199
|
-
.all(options.targetSessionId, now, options.limit ?? 10);
|
|
1200
|
-
const claimed = [];
|
|
1201
|
-
for (const row of rows) {
|
|
1202
|
-
if (isTurnIntentRow(row) &&
|
|
1203
|
-
this.claimEffectForTurn(row.intentId, options.claimToken, options.leaseMs, options.now)) {
|
|
1204
|
-
claimed.push(this.requireTurnIntent(row.intentId));
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
return claimed;
|
|
1208
|
-
}
|
|
1209
|
-
makeWorkflowTurnIntentEligible(options) {
|
|
1210
|
-
const eligibleAt = epoch(validTimestamp(options.eligibleAt ?? options.now));
|
|
1211
|
-
const factsHash = this.state.putJson(options.fallbackFacts, eligibleAt);
|
|
1212
|
-
return (this.state.connection
|
|
1213
|
-
.prepare("UPDATE turn_intents SET eligible_at = ?, facts_hash = ? WHERE turn_intent_id = ? AND resolved_at IS NULL")
|
|
1214
|
-
.run(eligibleAt, factsHash, options.intentId).changes === 1);
|
|
1215
|
-
}
|
|
1216
|
-
resolveWorkflowTurnIntent(options) {
|
|
1217
|
-
const now = epoch(validTimestamp(options.now));
|
|
1218
|
-
return this.state.transaction(() => {
|
|
1219
|
-
const row = this.turnIntentRow(options.intentId);
|
|
1220
|
-
if (row === undefined ||
|
|
1221
|
-
row.targetSessionId !== options.targetSessionId ||
|
|
1222
|
-
!this.verifyEffectToken(row.effectId, options.claimToken, options.now)) {
|
|
1223
|
-
return false;
|
|
1224
|
-
}
|
|
1225
|
-
const result = this.state.connection
|
|
1226
|
-
.prepare(`UPDATE turn_intents SET resolved_at = ?, resolution_type = ?, resolution_message_id = ?
|
|
1227
|
-
WHERE turn_intent_id = ? AND resolved_at IS NULL`)
|
|
1228
|
-
.run(now, options.resolution, options.messageId ?? null, options.intentId);
|
|
1229
|
-
if (result.changes === 1)
|
|
1230
|
-
this.completeEffect(row.effectId, "applied", now);
|
|
1231
|
-
return result.changes === 1;
|
|
1232
|
-
});
|
|
1233
|
-
}
|
|
1234
|
-
releaseWorkflowTurnIntentClaim(options) {
|
|
1235
|
-
const row = this.turnIntentRow(options.intentId);
|
|
1236
|
-
return (row !== undefined &&
|
|
1237
|
-
row.targetSessionId === options.targetSessionId &&
|
|
1238
|
-
this.releaseEffectLease(row.effectId, options.claimToken));
|
|
1239
|
-
}
|
|
1240
|
-
listWorkflowTurnIntents(options = {}) {
|
|
1241
|
-
const clauses = ["1 = 1"];
|
|
1242
|
-
const params = [];
|
|
1243
|
-
if (options.runId !== undefined) {
|
|
1244
|
-
clauses.push("t.run_id = ?");
|
|
1245
|
-
params.push(options.runId);
|
|
1246
|
-
}
|
|
1247
|
-
if (options.targetSessionId !== undefined) {
|
|
1248
|
-
clauses.push("t.target_session_id = ?");
|
|
1249
|
-
params.push(options.targetSessionId);
|
|
1250
|
-
}
|
|
1251
|
-
if (options.unresolvedOnly === true)
|
|
1252
|
-
clauses.push("t.resolved_at IS NULL");
|
|
1253
|
-
params.push(options.limit ?? 100);
|
|
1254
|
-
const rows = this.state.connection
|
|
1255
|
-
.prepare(turnIntentSelect(`WHERE ${clauses.join(" AND ")} ORDER BY t.requested_at DESC LIMIT ?`))
|
|
1256
|
-
.all(...params);
|
|
1257
|
-
return rows.filter(isTurnIntentRow).map((row) => this.mapTurnIntent(row));
|
|
1258
|
-
}
|
|
1259
|
-
enqueueWorkflowNotification(options) {
|
|
1260
|
-
const existing = this.notification(options.runId, options.attemptId, options.notificationIndex);
|
|
1261
|
-
if (existing !== undefined)
|
|
1262
|
-
return existing;
|
|
1263
|
-
const run = this.requireWorkflowRunRow(options.runId);
|
|
1264
|
-
const now = epoch(validTimestamp(options.now));
|
|
1265
|
-
const notificationId = `notification-${randomUUID()}`;
|
|
1266
|
-
const resourceId = resourceIdFor("notification", notificationId);
|
|
1267
|
-
const effectId = `effect-${randomUUID()}`;
|
|
1268
|
-
this.state.transaction(() => {
|
|
1269
|
-
const contentHash = this.state.putText(options.content, now);
|
|
1270
|
-
this.insertEffectResource(effectId, resourceIdFor("effect", effectId), run.resourceId, this.resourceRevision(run.resourceId), "notification.deliver", notificationId, contentHash, "run", now);
|
|
1271
|
-
this.state.connection
|
|
1272
|
-
.prepare("INSERT INTO resources(resource_id, resource_type, aggregate_key, revision, created_at, updated_at) VALUES (?, 'notification', ?, 1, ?, ?)")
|
|
1273
|
-
.run(resourceId, notificationId, now, now);
|
|
1274
|
-
this.state.connection
|
|
1275
|
-
.prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
|
|
1276
|
-
.run(resourceId);
|
|
1277
|
-
this.state.connection
|
|
1278
|
-
.prepare(`INSERT INTO notifications(
|
|
1279
|
-
notification_id, effect_id, run_id, attempt_id, notification_index,
|
|
1280
|
-
target_session_id, notification_type, content_hash, created_at
|
|
1281
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1282
|
-
.run(notificationId, effectId, options.runId, options.attemptId, options.notificationIndex, options.targetSessionId, options.kind, contentHash, now);
|
|
1283
|
-
this.insertEvent(resourceId, 1, "notification.queued", "session", options.targetSessionId, {}, now);
|
|
1284
|
-
});
|
|
1285
|
-
return this.requireNotification(options.runId, options.attemptId, options.notificationIndex);
|
|
1286
|
-
}
|
|
1287
|
-
listPendingWorkflowNotifications(options) {
|
|
1288
|
-
const now = Date.now();
|
|
1289
|
-
return this.notificationRows(options.targetSessionId, options.limit ?? 20)
|
|
1290
|
-
.map((row) => this.mapNotification(row))
|
|
1291
|
-
.filter((notification) => notification.deliveryClaimExpiresAt === null ||
|
|
1292
|
-
Date.parse(notification.deliveryClaimExpiresAt) <= now);
|
|
1293
|
-
}
|
|
1294
|
-
claimPendingWorkflowNotifications(options) {
|
|
1295
|
-
const rows = this.notificationRows(options.targetSessionId, options.limit ?? 20);
|
|
1296
|
-
const result = [];
|
|
1297
|
-
for (const row of rows) {
|
|
1298
|
-
if (this.claimEffect(row.effectId, options.targetSessionId, options.claimToken, options.leaseMs, options.now)) {
|
|
1299
|
-
result.push(this.mapNotification(row));
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
return result;
|
|
1303
|
-
}
|
|
1304
|
-
isWorkflowNotificationClaimLive(options) {
|
|
1305
|
-
const row = this.notificationRowById(options.notificationId);
|
|
1306
|
-
return (row !== undefined &&
|
|
1307
|
-
row.targetSessionId === options.targetSessionId &&
|
|
1308
|
-
this.verifyEffectToken(row.effectId, options.claimToken, options.now));
|
|
1309
|
-
}
|
|
1310
|
-
markWorkflowNotificationDelivered(options) {
|
|
1311
|
-
const now = epoch(validTimestamp(options.now));
|
|
1312
|
-
return this.state.transaction(() => {
|
|
1313
|
-
const row = this.notificationRowById(options.notificationId);
|
|
1314
|
-
if (row === undefined ||
|
|
1315
|
-
row.targetSessionId !== options.targetSessionId ||
|
|
1316
|
-
!this.verifyEffectToken(row.effectId, options.claimToken, options.now)) {
|
|
1317
|
-
return false;
|
|
1318
|
-
}
|
|
1319
|
-
this.completeEffect(row.effectId, "applied", now);
|
|
1320
|
-
return true;
|
|
1321
|
-
});
|
|
1322
|
-
}
|
|
1323
1217
|
settleRunEffect(runId, effectType) {
|
|
1324
1218
|
const rows = this.state.connection
|
|
1325
1219
|
.prepare(`SELECT e.effect_id AS effectId, e.resource_id AS resourceId
|
|
@@ -1663,6 +1557,17 @@ export class SqliteControllerStore {
|
|
|
1663
1557
|
return this.mapWorkflowRun(this.requireWorkflowRunRow(runId));
|
|
1664
1558
|
}
|
|
1665
1559
|
assertWorkflowRunPreparationCompatible(row, options, definitionDigest) {
|
|
1560
|
+
const expectedLineageKind = options.parentRunId === undefined ? null : (options.lineageKind ?? "continuation");
|
|
1561
|
+
const parent = options.parentRunId === undefined
|
|
1562
|
+
? undefined
|
|
1563
|
+
: this.requireWorkflowRunRow(options.parentRunId);
|
|
1564
|
+
const expectedRootRunId = parent?.rootRunId ?? options.runId;
|
|
1565
|
+
const expectedRestartNumber = expectedLineageKind === "restart"
|
|
1566
|
+
? (options.restartNumber ?? (parent?.restartNumber ?? -1) + 1)
|
|
1567
|
+
: (parent?.restartNumber ?? 0);
|
|
1568
|
+
const expectedParentTerminalFingerprint = options.parentTerminalFingerprint === undefined
|
|
1569
|
+
? null
|
|
1570
|
+
: terminalFingerprintBuffer(options.parentTerminalFingerprint);
|
|
1666
1571
|
const compatible = row.workflowName === options.workflowName &&
|
|
1667
1572
|
row.workflowRef === options.workflowSourceRef &&
|
|
1668
1573
|
row.definitionDigest.equals(definitionDigest) &&
|
|
@@ -1673,11 +1578,24 @@ export class SqliteControllerStore {
|
|
|
1673
1578
|
canonicalJson(options.launchOptions ?? {}) &&
|
|
1674
1579
|
row.originSessionId === options.originSessionId &&
|
|
1675
1580
|
row.executionMode === (options.executionMode ?? "interactive") &&
|
|
1676
|
-
row.parentRunId === (options.parentRunId ?? null)
|
|
1581
|
+
row.parentRunId === (options.parentRunId ?? null) &&
|
|
1582
|
+
row.rootRunId === expectedRootRunId &&
|
|
1583
|
+
row.lineageKind === expectedLineageKind &&
|
|
1584
|
+
row.restartNumber === expectedRestartNumber &&
|
|
1585
|
+
((row.parentTerminalFingerprint === null && expectedParentTerminalFingerprint === null) ||
|
|
1586
|
+
(row.parentTerminalFingerprint !== null &&
|
|
1587
|
+
expectedParentTerminalFingerprint !== null &&
|
|
1588
|
+
row.parentTerminalFingerprint.equals(expectedParentTerminalFingerprint)));
|
|
1677
1589
|
if (!compatible) {
|
|
1678
1590
|
throw new Error(`Workflow run preparation conflicts: ${options.runId}`);
|
|
1679
1591
|
}
|
|
1680
1592
|
}
|
|
1593
|
+
workflowRunViewRows(clause, params) {
|
|
1594
|
+
return this.state.connection
|
|
1595
|
+
.prepare(workflowRunViewSelect(clause))
|
|
1596
|
+
.all(...params)
|
|
1597
|
+
.filter(isWorkflowRunViewRow);
|
|
1598
|
+
}
|
|
1681
1599
|
/* istanbul ignore next -- pure projection covered by integration tests */
|
|
1682
1600
|
mapWorkflowRun(row) {
|
|
1683
1601
|
return {
|
|
@@ -1698,6 +1616,10 @@ export class SqliteControllerStore {
|
|
|
1698
1616
|
originSessionId: row.executionMode === "headless" ? null : row.originSessionId,
|
|
1699
1617
|
executionMode: row.executionMode,
|
|
1700
1618
|
parentRunId: row.parentRunId,
|
|
1619
|
+
rootRunId: row.rootRunId,
|
|
1620
|
+
lineageKind: row.lineageKind,
|
|
1621
|
+
restartNumber: row.restartNumber,
|
|
1622
|
+
parentTerminalFingerprint: row.parentTerminalFingerprint?.toString("hex") ?? null,
|
|
1701
1623
|
errorCode: row.errorCode,
|
|
1702
1624
|
errorMessage: row.errorHash === null
|
|
1703
1625
|
? null
|
|
@@ -1718,7 +1640,7 @@ export class SqliteControllerStore {
|
|
|
1718
1640
|
if (options.allowPendingInteraction !== true &&
|
|
1719
1641
|
this.state.connection
|
|
1720
1642
|
.prepare(`SELECT 1 FROM interactive_requests
|
|
1721
|
-
WHERE run_id = ? AND status
|
|
1643
|
+
WHERE run_id = ? AND status = 'pending' LIMIT 1`)
|
|
1722
1644
|
.get(runId) !== undefined) {
|
|
1723
1645
|
return undefined;
|
|
1724
1646
|
}
|
|
@@ -1912,165 +1834,10 @@ export class SqliteControllerStore {
|
|
|
1912
1834
|
.run(receiptHash, runId);
|
|
1913
1835
|
this.state.connection
|
|
1914
1836
|
.prepare(`UPDATE interactive_requests
|
|
1915
|
-
SET status = 'cancelled',
|
|
1916
|
-
|
|
1917
|
-
WHERE run_id = ? AND status IN ('pending', 'presenting')`)
|
|
1837
|
+
SET status = 'cancelled', revision = revision + 1, updated_at = ?
|
|
1838
|
+
WHERE run_id = ? AND status = 'pending'`)
|
|
1918
1839
|
.run(now, runId);
|
|
1919
1840
|
}
|
|
1920
|
-
turnIntent(intentId) {
|
|
1921
|
-
const row = this.turnIntentRow(intentId);
|
|
1922
|
-
return row === undefined ? undefined : this.mapTurnIntent(row);
|
|
1923
|
-
}
|
|
1924
|
-
requireTurnIntent(intentId) {
|
|
1925
|
-
const row = this.turnIntent(intentId);
|
|
1926
|
-
if (row === undefined)
|
|
1927
|
-
throw new Error(`Workflow turn intent not found: ${intentId}`);
|
|
1928
|
-
return row;
|
|
1929
|
-
}
|
|
1930
|
-
turnIntentRow(intentId) {
|
|
1931
|
-
const row = this.state.connection
|
|
1932
|
-
.prepare(turnIntentSelect("WHERE t.turn_intent_id = ?"))
|
|
1933
|
-
.get(intentId);
|
|
1934
|
-
return isTurnIntentRow(row) ? row : undefined;
|
|
1935
|
-
}
|
|
1936
|
-
turnIntentBySource(sourceEventId) {
|
|
1937
|
-
const row = this.state.connection
|
|
1938
|
-
.prepare(turnIntentSelect("WHERE t.source_event_id = ?"))
|
|
1939
|
-
.get(sourceEventId);
|
|
1940
|
-
return isTurnIntentRow(row) ? this.mapTurnIntent(row) : undefined;
|
|
1941
|
-
}
|
|
1942
|
-
/* istanbul ignore next -- pure projection covered by integration tests */
|
|
1943
|
-
mapTurnIntent(row) {
|
|
1944
|
-
return {
|
|
1945
|
-
intentId: row.intentId,
|
|
1946
|
-
sourceEventId: row.sourceEventId,
|
|
1947
|
-
runId: row.runId,
|
|
1948
|
-
workflowRef: row.workflowRef,
|
|
1949
|
-
targetSessionId: row.targetSessionId,
|
|
1950
|
-
cause: row.cause,
|
|
1951
|
-
nodeId: row.nodeId,
|
|
1952
|
-
attemptId: row.attemptId,
|
|
1953
|
-
fallbackFacts: this.state.readJson(row.factsHash),
|
|
1954
|
-
requestedAt: new Date(row.requestedAt).toISOString(),
|
|
1955
|
-
eligibleAt: row.eligibleAt === null ? null : new Date(row.eligibleAt).toISOString(),
|
|
1956
|
-
resolvedAt: row.resolvedAt === null ? null : new Date(row.resolvedAt).toISOString(),
|
|
1957
|
-
resolution: row.resolution,
|
|
1958
|
-
resolutionMessageId: row.resolutionMessageId,
|
|
1959
|
-
deliveryClaimExpiresAt: row.claimExpiresAt === null ? null : new Date(row.claimExpiresAt).toISOString(),
|
|
1960
|
-
};
|
|
1961
|
-
}
|
|
1962
|
-
claimEffectForTurn(intentId, token, leaseMs, nowValue) {
|
|
1963
|
-
const row = this.turnIntentRow(intentId);
|
|
1964
|
-
return (row !== undefined &&
|
|
1965
|
-
this.claimEffect(row.effectId, row.targetSessionId, token, leaseMs, nowValue));
|
|
1966
|
-
}
|
|
1967
|
-
claimEffect(effectId, ownerId, token, leaseMs, nowValue) {
|
|
1968
|
-
const now = epoch(validTimestamp(nowValue));
|
|
1969
|
-
return this.state.transaction(() => {
|
|
1970
|
-
const row = this.state.connection
|
|
1971
|
-
.prepare("SELECT resource_id AS resourceId FROM effects WHERE effect_id = ? AND status = 'pending'")
|
|
1972
|
-
.get(effectId);
|
|
1973
|
-
/* istanbul ignore if -- impossible after exact schema and transaction checks */
|
|
1974
|
-
if (!isResourceIdRow(row))
|
|
1975
|
-
return false;
|
|
1976
|
-
const lease = this.requireLease(row.resourceId);
|
|
1977
|
-
if (lease.ownerId !== null && lease.expiresAt !== null && lease.expiresAt > now)
|
|
1978
|
-
return false;
|
|
1979
|
-
return (this.state.connection
|
|
1980
|
-
.prepare(`UPDATE leases SET generation = ?, owner_type = 'session', owner_id = ?, token_hash = ?,
|
|
1981
|
-
acquired_at = ?, heartbeat_at = ?, expires_at = ?
|
|
1982
|
-
WHERE resource_id = ? AND generation = ?`)
|
|
1983
|
-
.run(lease.generation + 1, ownerId, tokenHash(token), now, now, now + leaseMs, row.resourceId, lease.generation).changes === 1);
|
|
1984
|
-
});
|
|
1985
|
-
}
|
|
1986
|
-
verifyEffectToken(effectId, token, nowValue) {
|
|
1987
|
-
const now = epoch(validTimestamp(nowValue));
|
|
1988
|
-
const row = this.state.connection
|
|
1989
|
-
.prepare(`SELECT l.token_hash AS tokenHash, l.expires_at AS expiresAt
|
|
1990
|
-
FROM effects e JOIN leases l ON l.resource_id = e.resource_id
|
|
1991
|
-
WHERE e.effect_id = ?`)
|
|
1992
|
-
.get(effectId);
|
|
1993
|
-
return (isEffectTokenRow(row) &&
|
|
1994
|
-
row.tokenHash !== null &&
|
|
1995
|
-
row.tokenHash.equals(tokenHash(token)) &&
|
|
1996
|
-
row.expiresAt !== null &&
|
|
1997
|
-
row.expiresAt > now);
|
|
1998
|
-
}
|
|
1999
|
-
releaseEffectLease(effectId, token) {
|
|
2000
|
-
return (this.state.connection
|
|
2001
|
-
.prepare(`UPDATE leases SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
|
|
2002
|
-
acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
|
|
2003
|
-
WHERE resource_id = (SELECT resource_id FROM effects WHERE effect_id = ?)
|
|
2004
|
-
AND token_hash = ?`)
|
|
2005
|
-
.run(effectId, tokenHash(token)).changes === 1);
|
|
2006
|
-
}
|
|
2007
|
-
completeEffect(effectId, status, now) {
|
|
2008
|
-
this.state.connection
|
|
2009
|
-
.prepare("UPDATE effects SET status = ?, updated_at = ?, settled_at = ? WHERE effect_id = ?")
|
|
2010
|
-
.run(status, now, now, effectId);
|
|
2011
|
-
this.state.connection
|
|
2012
|
-
.prepare(`UPDATE leases SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
|
|
2013
|
-
acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
|
|
2014
|
-
WHERE resource_id = (SELECT resource_id FROM effects WHERE effect_id = ?)`)
|
|
2015
|
-
.run(effectId);
|
|
2016
|
-
}
|
|
2017
|
-
insertEffectResource(effectId, resourceId, sourceResourceId, sourceRevision, type, key, payloadHash, scope, now) {
|
|
2018
|
-
this.state.connection
|
|
2019
|
-
.prepare("INSERT INTO resources(resource_id, resource_type, aggregate_key, revision, created_at, updated_at) VALUES (?, 'effect', ?, 1, ?, ?)")
|
|
2020
|
-
.run(resourceId, effectId, now, now);
|
|
2021
|
-
this.state.connection
|
|
2022
|
-
.prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
|
|
2023
|
-
.run(resourceId);
|
|
2024
|
-
this.state.connection
|
|
2025
|
-
.prepare(`INSERT INTO effects(
|
|
2026
|
-
effect_id, resource_id, source_resource_id, source_revision, effect_type,
|
|
2027
|
-
idempotency_key, payload_hash, owner_scope, status, attempt_count,
|
|
2028
|
-
created_at, updated_at
|
|
2029
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?, ?)`)
|
|
2030
|
-
.run(effectId, resourceId, sourceResourceId, sourceRevision, type, key, payloadHash, scope, now, now);
|
|
2031
|
-
this.insertEvent(resourceId, 1, "effect.created", "system", null, {}, now);
|
|
2032
|
-
}
|
|
2033
|
-
notification(runId, attemptId, index) {
|
|
2034
|
-
const row = this.state.connection
|
|
2035
|
-
.prepare(notificationSelect("WHERE n.run_id = ? AND n.attempt_id = ? AND n.notification_index = ?"))
|
|
2036
|
-
.get(runId, attemptId, index);
|
|
2037
|
-
return isNotificationRow(row) ? this.mapNotification(row) : undefined;
|
|
2038
|
-
}
|
|
2039
|
-
requireNotification(runId, attemptId, index) {
|
|
2040
|
-
const record = this.notification(runId, attemptId, index);
|
|
2041
|
-
/* istanbul ignore if -- impossible after exact schema and transaction checks */
|
|
2042
|
-
if (record === undefined)
|
|
2043
|
-
throw new Error("Workflow notification was not stored");
|
|
2044
|
-
return record;
|
|
2045
|
-
}
|
|
2046
|
-
notificationRows(sessionId, limit) {
|
|
2047
|
-
return this.state.connection
|
|
2048
|
-
.prepare(notificationSelect("WHERE n.target_session_id = ? AND e.status = 'pending' ORDER BY n.created_at LIMIT ?"))
|
|
2049
|
-
.all(sessionId, limit)
|
|
2050
|
-
.filter(isNotificationRow);
|
|
2051
|
-
}
|
|
2052
|
-
notificationRowById(id) {
|
|
2053
|
-
const row = this.state.connection
|
|
2054
|
-
.prepare(notificationSelect("WHERE n.notification_id = ?"))
|
|
2055
|
-
.get(id);
|
|
2056
|
-
return isNotificationRow(row) ? row : undefined;
|
|
2057
|
-
}
|
|
2058
|
-
/* istanbul ignore next -- pure projection covered by integration tests */
|
|
2059
|
-
mapNotification(row) {
|
|
2060
|
-
return {
|
|
2061
|
-
notificationId: row.notificationId,
|
|
2062
|
-
runId: row.runId,
|
|
2063
|
-
nodeId: row.nodeId,
|
|
2064
|
-
attemptId: row.attemptId,
|
|
2065
|
-
notificationIndex: row.notificationIndex,
|
|
2066
|
-
targetSessionId: row.targetSessionId,
|
|
2067
|
-
kind: row.kind,
|
|
2068
|
-
content: this.state.readBlob(row.contentHash)?.content.toString("utf8") ?? "",
|
|
2069
|
-
createdAt: new Date(row.createdAt).toISOString(),
|
|
2070
|
-
deliveryClaimExpiresAt: row.claimExpiresAt === null ? null : new Date(row.claimExpiresAt).toISOString(),
|
|
2071
|
-
deliveredAt: row.deliveredAt === null ? null : new Date(row.deliveredAt).toISOString(),
|
|
2072
|
-
};
|
|
2073
|
-
}
|
|
2074
1841
|
assertWritable() {
|
|
2075
1842
|
if (this.state.mode === "read-only")
|
|
2076
1843
|
throw new Error("Controller store is read-only");
|
|
@@ -2112,6 +1879,9 @@ function workflowRunSelect(clause) {
|
|
|
2112
1879
|
q.consecutive_errors AS consecutiveErrors, q.error_code AS errorCode,
|
|
2113
1880
|
q.error_hash AS errorHash, b.origin_session_id AS originSessionId,
|
|
2114
1881
|
b.execution_mode AS executionMode, r.parent_run_id AS parentRunId,
|
|
1882
|
+
r.root_run_id AS rootRunId, r.lineage_kind AS lineageKind,
|
|
1883
|
+
r.restart_number AS restartNumber,
|
|
1884
|
+
r.parent_terminal_fingerprint AS parentTerminalFingerprint,
|
|
2115
1885
|
q.created_at AS createdAt, q.updated_at AS updatedAt,
|
|
2116
1886
|
q.started_at AS startedAt, q.finished_at AS finishedAt,
|
|
2117
1887
|
l.generation AS leaseGeneration, l.owner_id AS ownerId, l.expires_at AS claimExpiresAt
|
|
@@ -2120,28 +1890,24 @@ function workflowRunSelect(clause) {
|
|
|
2120
1890
|
LEFT JOIN run_bindings b ON b.run_id = r.run_id
|
|
2121
1891
|
JOIN leases l ON l.resource_id = r.resource_id ${clause}`;
|
|
2122
1892
|
}
|
|
2123
|
-
function
|
|
2124
|
-
return `SELECT
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
e.settled_at AS deliveredAt
|
|
2142
|
-
FROM notifications n JOIN effects e ON e.effect_id = n.effect_id
|
|
2143
|
-
JOIN leases l ON l.resource_id = e.resource_id
|
|
2144
|
-
JOIN node_attempts a ON a.attempt_id = n.attempt_id ${clause}`;
|
|
1893
|
+
function workflowRunViewSelect(clause) {
|
|
1894
|
+
return `SELECT r.run_id AS runId, d.workflow_name AS workflowName,
|
|
1895
|
+
r.workflow_ref AS workflowRef, r.status AS runStatus, r.paused,
|
|
1896
|
+
r.definition_digest AS definitionDigest, q.status,
|
|
1897
|
+
b.origin_session_id AS originSessionId, b.execution_mode AS executionMode,
|
|
1898
|
+
r.parent_run_id AS parentRunId, r.root_run_id AS rootRunId,
|
|
1899
|
+
r.lineage_kind AS lineageKind, r.restart_number AS restartNumber,
|
|
1900
|
+
r.parent_terminal_fingerprint AS parentTerminalFingerprint,
|
|
1901
|
+
q.error_code AS errorCode, CAST(error.content AS TEXT) AS errorMessage,
|
|
1902
|
+
q.created_at AS createdAt, q.updated_at AS updatedAt,
|
|
1903
|
+
q.started_at AS startedAt, q.finished_at AS finishedAt,
|
|
1904
|
+
source.source_type AS sourceType, source.source_ref AS sourceRef,
|
|
1905
|
+
source.source_revision AS sourceRevision
|
|
1906
|
+
FROM runs r JOIN workflow_definitions d ON d.definition_digest = r.definition_digest
|
|
1907
|
+
JOIN run_queue q ON q.run_id = r.run_id
|
|
1908
|
+
LEFT JOIN run_bindings b ON b.run_id = r.run_id
|
|
1909
|
+
LEFT JOIN run_sources source ON source.run_id = r.run_id AND source.mount_path = ''
|
|
1910
|
+
LEFT JOIN blobs error ON error.blob_hash = q.error_hash ${clause}`;
|
|
2145
1911
|
}
|
|
2146
1912
|
function isRecord(value) {
|
|
2147
1913
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -2152,6 +1918,70 @@ function isControllerRow(value) {
|
|
|
2152
1918
|
function isLeaseRow(value) {
|
|
2153
1919
|
return isRecord(value);
|
|
2154
1920
|
}
|
|
1921
|
+
function workflowRunViewRecord(row) {
|
|
1922
|
+
const root = row.sourceType === "builtin"
|
|
1923
|
+
? { kind: "builtin", id: row.sourceRef, revision: row.sourceRevision }
|
|
1924
|
+
: { kind: "file", path: row.sourceRef, hash: row.sourceRevision };
|
|
1925
|
+
return {
|
|
1926
|
+
runId: row.runId,
|
|
1927
|
+
workflowName: row.workflowName,
|
|
1928
|
+
workflowSourceRef: row.workflowRef,
|
|
1929
|
+
workflowSource: { root, mounted: [] },
|
|
1930
|
+
initialized: row.runStatus !== "queued",
|
|
1931
|
+
definitionDigest: `sha256:${row.definitionDigest.toString("hex")}`,
|
|
1932
|
+
runStateStatus: row.runStatus,
|
|
1933
|
+
paused: row.paused === 1,
|
|
1934
|
+
status: row.status,
|
|
1935
|
+
originSessionId: row.executionMode === "headless" ? null : row.originSessionId,
|
|
1936
|
+
executionMode: row.executionMode,
|
|
1937
|
+
parentRunId: row.parentRunId,
|
|
1938
|
+
rootRunId: row.rootRunId,
|
|
1939
|
+
lineageKind: row.lineageKind,
|
|
1940
|
+
restartNumber: row.restartNumber,
|
|
1941
|
+
parentTerminalFingerprint: row.parentTerminalFingerprint?.toString("hex") ?? null,
|
|
1942
|
+
errorCode: row.errorCode,
|
|
1943
|
+
errorMessage: row.errorMessage,
|
|
1944
|
+
createdAt: new Date(row.createdAt).toISOString(),
|
|
1945
|
+
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
1946
|
+
startedAt: row.startedAt === null ? null : new Date(row.startedAt).toISOString(),
|
|
1947
|
+
finishedAt: row.finishedAt === null ? null : new Date(row.finishedAt).toISOString(),
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
function isWorkflowRunViewRow(value) {
|
|
1951
|
+
return (isRecord(value) &&
|
|
1952
|
+
typeof value.runId === "string" &&
|
|
1953
|
+
typeof value.workflowName === "string" &&
|
|
1954
|
+
typeof value.workflowRef === "string" &&
|
|
1955
|
+
typeof value.runStatus === "string" &&
|
|
1956
|
+
typeof value.paused === "number" &&
|
|
1957
|
+
Buffer.isBuffer(value.definitionDigest) &&
|
|
1958
|
+
typeof value.status === "string" &&
|
|
1959
|
+
(value.originSessionId === null || typeof value.originSessionId === "string") &&
|
|
1960
|
+
(value.executionMode === "interactive" || value.executionMode === "headless") &&
|
|
1961
|
+
(value.parentRunId === null || typeof value.parentRunId === "string") &&
|
|
1962
|
+
typeof value.rootRunId === "string" &&
|
|
1963
|
+
(value.lineageKind === null ||
|
|
1964
|
+
value.lineageKind === "continuation" ||
|
|
1965
|
+
value.lineageKind === "restart") &&
|
|
1966
|
+
typeof value.restartNumber === "number" &&
|
|
1967
|
+
(value.parentTerminalFingerprint === null ||
|
|
1968
|
+
Buffer.isBuffer(value.parentTerminalFingerprint)) &&
|
|
1969
|
+
(value.errorCode === null || typeof value.errorCode === "string") &&
|
|
1970
|
+
(value.errorMessage === null || typeof value.errorMessage === "string") &&
|
|
1971
|
+
typeof value.createdAt === "number" &&
|
|
1972
|
+
typeof value.updatedAt === "number" &&
|
|
1973
|
+
(value.startedAt === null || typeof value.startedAt === "number") &&
|
|
1974
|
+
(value.finishedAt === null || typeof value.finishedAt === "number") &&
|
|
1975
|
+
(value.sourceType === "builtin" || value.sourceType === "file") &&
|
|
1976
|
+
typeof value.sourceRef === "string" &&
|
|
1977
|
+
typeof value.sourceRevision === "string");
|
|
1978
|
+
}
|
|
1979
|
+
function isRunListRevisionRow(value) {
|
|
1980
|
+
return (isRecord(value) &&
|
|
1981
|
+
typeof value.count === "number" &&
|
|
1982
|
+
typeof value.revisionSum === "number" &&
|
|
1983
|
+
typeof value.updatedAt === "number");
|
|
1984
|
+
}
|
|
2155
1985
|
function isRunRow(value) {
|
|
2156
1986
|
return isRecord(value);
|
|
2157
1987
|
}
|
|
@@ -2183,12 +2013,6 @@ function isControllerEventRow(value) {
|
|
|
2183
2013
|
function isRunEventRow(value) {
|
|
2184
2014
|
return isRecord(value);
|
|
2185
2015
|
}
|
|
2186
|
-
function isTurnIntentRow(value) {
|
|
2187
|
-
return isRecord(value);
|
|
2188
|
-
}
|
|
2189
|
-
function isNotificationRow(value) {
|
|
2190
|
-
return isRecord(value);
|
|
2191
|
-
}
|
|
2192
2016
|
function isProjectRow(value) {
|
|
2193
2017
|
return isRecord(value);
|
|
2194
2018
|
}
|
|
@@ -2213,12 +2037,6 @@ function isSequenceRow(value) {
|
|
|
2213
2037
|
function isKeyRow(value) {
|
|
2214
2038
|
return isRecord(value);
|
|
2215
2039
|
}
|
|
2216
|
-
function isResourceIdRow(value) {
|
|
2217
|
-
return isRecord(value);
|
|
2218
|
-
}
|
|
2219
|
-
function isEffectTokenRow(value) {
|
|
2220
|
-
return isRecord(value);
|
|
2221
|
-
}
|
|
2222
2040
|
function isEffectIdentityRow(value) {
|
|
2223
2041
|
return isRecord(value);
|
|
2224
2042
|
}
|
|
@@ -2242,6 +2060,12 @@ function digestBuffer(value) {
|
|
|
2242
2060
|
throw new Error("Expected a SHA-256 digest");
|
|
2243
2061
|
return Buffer.from(hex, "hex");
|
|
2244
2062
|
}
|
|
2063
|
+
function terminalFingerprintBuffer(value) {
|
|
2064
|
+
const hex = value.startsWith("sha256:") ? value.slice(7) : value;
|
|
2065
|
+
if (!/^[a-f0-9]{64}$/i.test(hex))
|
|
2066
|
+
throw new Error("Expected a terminal fingerprint");
|
|
2067
|
+
return Buffer.from(hex, "hex");
|
|
2068
|
+
}
|
|
2245
2069
|
function queuedWorkflowSource(value) {
|
|
2246
2070
|
if (isWorkflowSource(value)) {
|
|
2247
2071
|
return { root: value, mounted: [] };
|