@osolmaz/pi-workflows 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -6
- package/dist/builtins/autoimplement-command-batches.d.ts +52 -0
- package/dist/builtins/autoimplement-command-batches.js +258 -0
- package/dist/builtins/autoimplement-command-batches.js.map +1 -0
- package/dist/builtins/autoimplement.workflow.d.ts +29 -3
- package/dist/builtins/autoimplement.workflow.js +876 -231
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +3 -1
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +2 -0
- package/dist/builtins/index.js +1 -0
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/sanity-check-session.d.ts +17 -0
- package/dist/builtins/sanity-check-session.js +168 -0
- package/dist/builtins/sanity-check-session.js.map +1 -0
- package/dist/builtins/sanity-check.workflow.d.ts +83 -0
- package/dist/builtins/sanity-check.workflow.js +398 -0
- package/dist/builtins/sanity-check.workflow.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +141 -5
- package/dist/controllers/sqlite.js +568 -46
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/extension/decision-channels.js +7 -7
- package/dist/extension/deferred-turn-coordinator.d.ts +32 -0
- package/dist/extension/deferred-turn-coordinator.js +143 -0
- package/dist/extension/deferred-turn-coordinator.js.map +1 -0
- package/dist/extension/deferred-turn.d.ts +44 -0
- package/dist/extension/deferred-turn.js +110 -0
- package/dist/extension/deferred-turn.js.map +1 -0
- package/dist/extension/index.js +515 -84
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/step-message.d.ts +1 -0
- package/dist/extension/step-message.js.map +1 -1
- package/dist/herdr/setup.js +4 -4
- package/dist/host/runner.js +3 -0
- package/dist/host/runner.js.map +1 -1
- package/dist/workflows/command-batch.d.ts +38 -0
- package/dist/workflows/command-batch.js +176 -0
- package/dist/workflows/command-batch.js.map +1 -0
- package/dist/workflows/engine.js +8 -4
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -0
- package/dist/workflows/index.js +1 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/migrate-sources.d.ts +1 -1
- package/dist/workflows/migrate-sources.js.map +1 -1
- package/dist/workflows/schema.js +2 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/shell.d.ts +4 -0
- package/dist/workflows/shell.js +6 -0
- package/dist/workflows/shell.js.map +1 -1
- package/dist/workflows/store.js +3 -1
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/tool-input.d.ts +1 -0
- package/dist/workflows/tool-input.js +2 -2
- package/dist/workflows/tool-input.js.map +1 -1
- package/dist/workflows/types.d.ts +4 -3
- package/docs/2026-08-18-herdr-piw-plan.md +1 -1
- package/docs/2026-08-20-durable-workflow-launch-plan.md +445 -0
- package/docs/CONTROLLERS.md +2 -2
- package/docs/DEFERRED_TURNS.md +298 -0
- package/docs/DESIGN_PHILOSOPHY.md +1 -1
- package/docs/HUMAN_DECISIONS.md +7 -7
- package/docs/HUMAN_DECISION_PRESENTATIONS.md +3 -3
- package/docs/MONITOR.md +2 -2
- package/docs/WORKFLOW_COMPOSITION.md +2 -2
- package/docs/WORKFLOW_STEP_MESSAGES.md +5 -5
- package/docs/WORKFLOW_UPDATES.md +28 -5
- package/docs/plans/2026-08-04-controller-runtime-plan.md +1 -1
- package/docs/plans/2026-08-05-always-on-workflows-plan.md +1 -1
- package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +9 -9
- package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +1 -1
- package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +1 -1
- package/docs/plans/2026-08-16-workflow-updates-plan.md +3 -3
- package/docs/plans/2026-08-17-bundled-skills-plan.md +6 -6
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +5 -5
- package/docs/plans/2026-08-19-human-decision-presentations-plan.md +1 -1
- package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +1 -1
- package/docs/plans/2026-08-19-workflow-composition-plan.md +6 -6
- package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +3 -3
- package/docs/plans/2026-08-20-bounded-command-batches-plan.md +206 -0
- package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +9 -9
- package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +242 -0
- package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +324 -0
- package/docs/plans/2026-08-21-sanity-check-plan.md +175 -0
- package/docs/run-bundles.md +3 -1
- package/docs/workflows.md +67 -17
- package/examples/workflows/command-batch.workflow.ts +50 -0
- package/examples/workflows/sanity-check.workflow.ts +1 -0
- package/herdr-plugin.toml +3 -3
- package/package.json +1 -1
- package/schemas/decision-presentation-v1.schema.json +1 -1
- package/schemas/human-decision-accepted-v1.schema.json +1 -1
- package/schemas/human-decision-accepted-v2.schema.json +1 -1
- package/schemas/human-decision-answer-attempt-v1.schema.json +1 -1
- package/schemas/human-decision-cancellation-v1.schema.json +1 -1
- package/schemas/human-decision-continuation-v1.schema.json +1 -1
- package/schemas/human-decision-delivery-v1.schema.json +1 -1
- package/schemas/human-decision-delivery-v2.schema.json +1 -1
- package/schemas/human-decision-receipt-v1.schema.json +1 -1
- package/schemas/human-decision-receipt-v2.schema.json +1 -1
- package/schemas/human-decision-request-v1.schema.json +1 -1
- package/schemas/human-decision-request-v2.schema.json +1 -1
- package/schemas/human-decision-resolution-v1.schema.json +1 -1
- package/schemas/human-decision-resolution-v2.schema.json +1 -1
- package/schemas/human-decision-settlement-v1.schema.json +1 -1
- package/skills/autodoc/SKILL.md +33 -2
- package/skills/autoimplement/SKILL.md +66 -12
- package/skills/autoplan/SKILL.md +26 -2
- package/skills/monitor/SKILL.md +35 -21
- package/skills/pi-workflows/SKILL.md +19 -5
- package/src/builtins/autoimplement-command-batches.ts +364 -0
- package/src/builtins/autoimplement.workflow.ts +1065 -261
- package/src/builtins/catalog.ts +3 -1
- package/src/builtins/index.ts +13 -0
- package/src/builtins/sanity-check-session.ts +205 -0
- package/src/builtins/sanity-check.workflow.ts +624 -0
- package/src/controllers/sqlite.ts +878 -51
- package/src/extension/decision-channels.ts +7 -7
- package/src/extension/deferred-turn-coordinator.ts +171 -0
- package/src/extension/deferred-turn.ts +166 -0
- package/src/extension/index.ts +646 -91
- package/src/extension/step-message.ts +1 -0
- package/src/herdr/setup.ts +4 -4
- package/src/host/runner.ts +3 -0
- package/src/workflows/command-batch.ts +254 -0
- package/src/workflows/engine.ts +8 -5
- package/src/workflows/index.ts +15 -0
- package/src/workflows/migrate-sources.ts +5 -1
- package/src/workflows/schema.ts +2 -1
- package/src/workflows/shell.ts +10 -0
- package/src/workflows/store.ts +3 -1
- package/src/workflows/tool-input.ts +5 -2
- package/src/workflows/types.ts +4 -3
|
@@ -9,6 +9,7 @@ const MAX_KEY_LENGTH = 512;
|
|
|
9
9
|
const MAX_ERROR_LENGTH = 8_192;
|
|
10
10
|
const MAX_EVENT_BYTES = 64 * 1024;
|
|
11
11
|
const MAX_RESOURCE_VALUE_BYTES = 1024 * 1024;
|
|
12
|
+
const TURN_INTENT_FACTS_SCHEMA = "pi-workflows.deferred-turn-facts.v1";
|
|
12
13
|
export class SqliteControllerStore {
|
|
13
14
|
filePath;
|
|
14
15
|
database;
|
|
@@ -467,19 +468,93 @@ export class SqliteControllerStore {
|
|
|
467
468
|
if (row === undefined) {
|
|
468
469
|
throw new Error("Controller store has no schema identifier");
|
|
469
470
|
}
|
|
470
|
-
return;
|
|
471
471
|
}
|
|
472
|
-
|
|
473
|
-
this.database
|
|
474
|
-
.prepare("
|
|
475
|
-
.
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
this.database.exec("ALTER TABLE workflow_run_queue ADD COLUMN origin_session_id TEXT");
|
|
472
|
+
else {
|
|
473
|
+
const existingQueue = this.database
|
|
474
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'workflow_run_queue'")
|
|
475
|
+
.get();
|
|
476
|
+
if (existingQueue !== undefined) {
|
|
477
|
+
this.assertAlphaSchemaLayout(false);
|
|
478
|
+
this.assertNoLegacyPendingLaunchNotifications();
|
|
480
479
|
}
|
|
481
|
-
this.
|
|
482
|
-
|
|
480
|
+
this.transaction(() => {
|
|
481
|
+
this.database
|
|
482
|
+
.prepare("INSERT OR IGNORE INTO schema_info (singleton, schema_id) VALUES (1, ?)")
|
|
483
|
+
.run(CONTROLLER_STORE_SCHEMA);
|
|
484
|
+
this.database.exec(SCHEMA_SQL);
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
this.assertAlphaSchemaLayout(true);
|
|
488
|
+
this.assertNoLegacyPendingLaunchNotifications();
|
|
489
|
+
}
|
|
490
|
+
assertAlphaSchemaLayout(requireTurnIntents) {
|
|
491
|
+
const requiredQueueColumns = new Set([
|
|
492
|
+
"run_id",
|
|
493
|
+
"workflow_ref",
|
|
494
|
+
"workflow_path",
|
|
495
|
+
"workflow_source_json",
|
|
496
|
+
"definition_digest",
|
|
497
|
+
"input_json",
|
|
498
|
+
"launch_options_json",
|
|
499
|
+
"status",
|
|
500
|
+
"runner_id",
|
|
501
|
+
"claim_token",
|
|
502
|
+
"claim_expires_at",
|
|
503
|
+
"affinity_runner_id",
|
|
504
|
+
"origin_session_id",
|
|
505
|
+
"parent_run_id",
|
|
506
|
+
"error_code",
|
|
507
|
+
"error_message",
|
|
508
|
+
"created_at",
|
|
509
|
+
"updated_at",
|
|
510
|
+
"started_at",
|
|
511
|
+
"finished_at",
|
|
512
|
+
]);
|
|
513
|
+
const actual = new Set(this.database.pragma("table_info(workflow_run_queue)").map((column) => column.name));
|
|
514
|
+
const missing = [...requiredQueueColumns].filter((column) => !actual.has(column));
|
|
515
|
+
if (missing.length > 0) {
|
|
516
|
+
throw new Error("Controller store uses an incompatible alpha layout. Preserve needed run bundles, then reset the project controller store.");
|
|
517
|
+
}
|
|
518
|
+
if (!requireTurnIntents)
|
|
519
|
+
return;
|
|
520
|
+
const requiredIntentColumns = new Set([
|
|
521
|
+
"intent_id",
|
|
522
|
+
"source_event_id",
|
|
523
|
+
"run_id",
|
|
524
|
+
"workflow_ref",
|
|
525
|
+
"target_session_id",
|
|
526
|
+
"cause",
|
|
527
|
+
"node_id",
|
|
528
|
+
"attempt_id",
|
|
529
|
+
"fallback_facts_json",
|
|
530
|
+
"requested_at",
|
|
531
|
+
"eligible_at",
|
|
532
|
+
"resolved_at",
|
|
533
|
+
"resolution",
|
|
534
|
+
"resolution_message_id",
|
|
535
|
+
"delivery_claim_token",
|
|
536
|
+
"delivery_claim_expires_at",
|
|
537
|
+
]);
|
|
538
|
+
const intentColumns = new Set(this.database.pragma("table_info(workflow_turn_intents)").map((column) => column.name));
|
|
539
|
+
const missingIntentColumns = [...requiredIntentColumns].filter((column) => !intentColumns.has(column));
|
|
540
|
+
if (missingIntentColumns.length > 0) {
|
|
541
|
+
throw new Error("Controller store uses an incompatible alpha turn-intent layout. Preserve needed run bundles, then reset the project controller store.");
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
assertNoLegacyPendingLaunchNotifications() {
|
|
545
|
+
const notificationTable = this.database
|
|
546
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'workflow_notifications'")
|
|
547
|
+
.get();
|
|
548
|
+
if (notificationTable === undefined) {
|
|
549
|
+
throw new Error("Controller store uses an incompatible alpha notification layout. Preserve needed run bundles, then reset the project controller store.");
|
|
550
|
+
}
|
|
551
|
+
const legacy = this.database
|
|
552
|
+
.prepare(`SELECT 1 FROM workflow_notifications
|
|
553
|
+
WHERE kind = 'launch_failure' AND delivered_at IS NULL LIMIT 1`)
|
|
554
|
+
.get();
|
|
555
|
+
if (legacy !== undefined) {
|
|
556
|
+
throw new Error("Controller store contains pending alpha launch-failure notifications. Preserve needed run bundles, then reset the project controller store.");
|
|
557
|
+
}
|
|
483
558
|
}
|
|
484
559
|
transaction(task) {
|
|
485
560
|
this.database.exec("BEGIN IMMEDIATE");
|
|
@@ -551,10 +626,32 @@ export class SqliteControllerStore {
|
|
|
551
626
|
}
|
|
552
627
|
return workflow;
|
|
553
628
|
}
|
|
554
|
-
/**
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
629
|
+
/** Reserve a user-started run before the initiating agent turn settles. */
|
|
630
|
+
reserveWorkflowRun(options) {
|
|
631
|
+
validateRunId(options.runId);
|
|
632
|
+
validateKey(options.workflowName, "workflow name");
|
|
633
|
+
validateKey(options.workflowSourceRef, "workflow source ref");
|
|
634
|
+
validateKey(options.definitionDigest, "workflow definition digest");
|
|
635
|
+
validateKey(options.runnerId, "runner id");
|
|
636
|
+
validateKey(options.originSessionId, "origin session id");
|
|
637
|
+
const inputJson = canonicalJson(options.input ?? null, "workflow run input");
|
|
638
|
+
const sourceJson = canonicalJson(options.workflowSource, "workflow run source");
|
|
639
|
+
const launchJson = canonicalJson(options.launchOptions ?? {}, "workflow launch options");
|
|
640
|
+
validateJsonSize(inputJson, "Workflow run input", MAX_RESOURCE_VALUE_BYTES);
|
|
641
|
+
validateJsonSize(sourceJson, "Workflow run source", MAX_EVENT_BYTES);
|
|
642
|
+
validateJsonSize(launchJson, "Workflow launch options", MAX_EVENT_BYTES);
|
|
643
|
+
const now = validTimestamp(options.now);
|
|
644
|
+
this.database
|
|
645
|
+
.prepare(`INSERT INTO workflow_run_queue (
|
|
646
|
+
run_id, workflow_ref, workflow_path, workflow_source_json, definition_digest,
|
|
647
|
+
input_json, launch_options_json, status, runner_id, claim_token, claim_expires_at,
|
|
648
|
+
affinity_runner_id, origin_session_id, parent_run_id, error_code, error_message,
|
|
649
|
+
created_at, updated_at, started_at, finished_at
|
|
650
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'queued', NULL, NULL, NULL, ?, ?, ?, NULL, NULL, ?, ?, NULL, NULL)`)
|
|
651
|
+
.run(options.runId, options.workflowName, options.workflowSourceRef, sourceJson, options.definitionDigest, inputJson, launchJson, options.runnerId, options.originSessionId, options.parentRunId ?? null, now, now);
|
|
652
|
+
return this.requireWorkflowRun(options.runId);
|
|
653
|
+
}
|
|
654
|
+
/** Insert a run that will start immediately outside a live model turn. */
|
|
558
655
|
enqueueWorkflowRun(options) {
|
|
559
656
|
validateRunId(options.runId);
|
|
560
657
|
validateKey(options.workflowName, "workflow name");
|
|
@@ -566,16 +663,19 @@ export class SqliteControllerStore {
|
|
|
566
663
|
validateKey(options.originSessionId, "origin session id");
|
|
567
664
|
}
|
|
568
665
|
const inputJson = canonicalJson(options.input ?? null, "workflow run input");
|
|
666
|
+
const sourceJson = canonicalJson(options.workflowSource ?? { ref: options.workflowSourceRef }, "workflow run source");
|
|
667
|
+
const launchJson = canonicalJson(options.launchOptions ?? {}, "workflow launch options");
|
|
569
668
|
validateJsonSize(inputJson, "Workflow run input", MAX_RESOURCE_VALUE_BYTES);
|
|
570
669
|
const now = validTimestamp(options.now);
|
|
571
670
|
const expiresAt = epoch(now) + options.leaseMs;
|
|
572
671
|
this.database
|
|
573
672
|
.prepare(`INSERT INTO workflow_run_queue (
|
|
574
|
-
run_id, workflow_ref, workflow_path,
|
|
575
|
-
runner_id, claim_token, claim_expires_at,
|
|
576
|
-
origin_session_id, parent_run_id,
|
|
577
|
-
|
|
578
|
-
|
|
673
|
+
run_id, workflow_ref, workflow_path, workflow_source_json, definition_digest,
|
|
674
|
+
input_json, launch_options_json, status, runner_id, claim_token, claim_expires_at,
|
|
675
|
+
affinity_runner_id, origin_session_id, parent_run_id, error_code, error_message,
|
|
676
|
+
created_at, updated_at, started_at, finished_at
|
|
677
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'starting', ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, NULL)`)
|
|
678
|
+
.run(options.runId, options.workflowName, options.workflowSourceRef, sourceJson, options.definitionDigest ?? "unavailable", inputJson, launchJson, options.runnerId, options.claimToken, expiresAt, options.affinityRunnerId ?? options.runnerId, options.originSessionId ?? null, options.parentRunId ?? null, now, now, now);
|
|
579
679
|
return this.requireWorkflowRun(options.runId);
|
|
580
680
|
}
|
|
581
681
|
getWorkflowRun(runId) {
|
|
@@ -595,6 +695,43 @@ export class SqliteControllerStore {
|
|
|
595
695
|
.all(options.status);
|
|
596
696
|
return rows.map(workflowRunFromRow);
|
|
597
697
|
}
|
|
698
|
+
findSessionReservation(sessionId) {
|
|
699
|
+
validateKey(sessionId, "session id");
|
|
700
|
+
const row = this.database
|
|
701
|
+
.prepare(`SELECT * FROM workflow_run_queue
|
|
702
|
+
WHERE origin_session_id = ? AND status IN ('queued', 'starting', 'running')
|
|
703
|
+
ORDER BY created_at LIMIT 1`)
|
|
704
|
+
.get(sessionId);
|
|
705
|
+
return row === undefined ? undefined : workflowRunFromRow(row);
|
|
706
|
+
}
|
|
707
|
+
claimWorkflowRun(options) {
|
|
708
|
+
validateRunId(options.runId);
|
|
709
|
+
validateKey(options.runnerId, "runner id");
|
|
710
|
+
validateKey(options.claimToken, "claim token");
|
|
711
|
+
validateDuration(options.leaseMs, "leaseMs");
|
|
712
|
+
const now = validTimestamp(options.now);
|
|
713
|
+
const nowMs = epoch(now);
|
|
714
|
+
const expiresAt = nowMs + options.leaseMs;
|
|
715
|
+
const result = this.database
|
|
716
|
+
.prepare(`UPDATE workflow_run_queue
|
|
717
|
+
SET status = 'starting', runner_id = ?, claim_token = ?, claim_expires_at = ?,
|
|
718
|
+
started_at = COALESCE(started_at, ?), updated_at = ?
|
|
719
|
+
WHERE run_id = ? AND (
|
|
720
|
+
status IN ('queued', 'parked')
|
|
721
|
+
OR (status = 'starting' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?)
|
|
722
|
+
)`)
|
|
723
|
+
.run(options.runnerId, options.claimToken, expiresAt, now, now, options.runId, nowMs);
|
|
724
|
+
return result.changes === 1 ? this.requireWorkflowRun(options.runId) : undefined;
|
|
725
|
+
}
|
|
726
|
+
markWorkflowRunRunning(options) {
|
|
727
|
+
validateRunId(options.runId);
|
|
728
|
+
const now = validTimestamp(options.now);
|
|
729
|
+
const result = this.database
|
|
730
|
+
.prepare(`UPDATE workflow_run_queue SET status = 'running', updated_at = ?
|
|
731
|
+
WHERE run_id = ? AND claim_token = ? AND status = 'starting'`)
|
|
732
|
+
.run(now, options.runId, options.claimToken);
|
|
733
|
+
return result.changes === 1;
|
|
734
|
+
}
|
|
598
735
|
/**
|
|
599
736
|
* Claim the oldest claimable run, preferring runs with affinity to this
|
|
600
737
|
* runner. Parked rows are claimable immediately; claimed rows become
|
|
@@ -616,11 +753,11 @@ export class SqliteControllerStore {
|
|
|
616
753
|
}
|
|
617
754
|
const exclusion = excluded.length === 0 ? "" : `AND run_id NOT IN (${excluded.map(() => "?").join(", ")})`;
|
|
618
755
|
const sessionFilter = options.sessionId === undefined
|
|
619
|
-
? ""
|
|
756
|
+
? "AND status != 'queued' AND (status != 'starting' OR origin_session_id IS NULL)"
|
|
620
757
|
: "AND (origin_session_id IS NULL OR origin_session_id = ?)";
|
|
621
758
|
const claimable = `(
|
|
622
|
-
status
|
|
623
|
-
OR (status
|
|
759
|
+
status IN ('queued', 'parked')
|
|
760
|
+
OR (status IN ('starting', 'running') AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?)
|
|
624
761
|
)`;
|
|
625
762
|
return this.transaction(() => {
|
|
626
763
|
const candidate = this.database
|
|
@@ -636,13 +773,13 @@ export class SqliteControllerStore {
|
|
|
636
773
|
}
|
|
637
774
|
const result = this.database
|
|
638
775
|
.prepare(`UPDATE workflow_run_queue
|
|
639
|
-
SET status = '
|
|
640
|
-
claim_expires_at = ?, updated_at = ?
|
|
776
|
+
SET status = 'starting', runner_id = ?, claim_token = ?,
|
|
777
|
+
claim_expires_at = ?, started_at = COALESCE(started_at, ?), updated_at = ?
|
|
641
778
|
WHERE run_id = ? AND (
|
|
642
|
-
status
|
|
643
|
-
OR (status
|
|
779
|
+
status IN ('queued', 'parked')
|
|
780
|
+
OR (status IN ('starting', 'running') AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?)
|
|
644
781
|
)`)
|
|
645
|
-
.run(options.runnerId, options.claimToken, expiresAt, now, candidate.run_id, nowMs);
|
|
782
|
+
.run(options.runnerId, options.claimToken, expiresAt, now, now, candidate.run_id, nowMs);
|
|
646
783
|
return result.changes === 1 ? this.requireWorkflowRun(candidate.run_id) : undefined;
|
|
647
784
|
});
|
|
648
785
|
}
|
|
@@ -654,7 +791,7 @@ export class SqliteControllerStore {
|
|
|
654
791
|
const expiresAt = nowMs + options.leaseMs;
|
|
655
792
|
const result = this.database
|
|
656
793
|
.prepare(`UPDATE workflow_run_queue SET claim_expires_at = ?
|
|
657
|
-
WHERE run_id = ? AND claim_token = ? AND status
|
|
794
|
+
WHERE run_id = ? AND claim_token = ? AND status IN ('starting', 'running')
|
|
658
795
|
AND claim_expires_at > ?`)
|
|
659
796
|
.run(expiresAt, options.runId, options.claimToken, nowMs);
|
|
660
797
|
return result.changes === 1;
|
|
@@ -665,7 +802,7 @@ export class SqliteControllerStore {
|
|
|
665
802
|
const nowMs = epoch(validTimestamp(options.now));
|
|
666
803
|
const row = this.database
|
|
667
804
|
.prepare(`SELECT 1 AS live FROM workflow_run_queue
|
|
668
|
-
WHERE run_id = ? AND claim_token = ? AND status
|
|
805
|
+
WHERE run_id = ? AND claim_token = ? AND status IN ('starting', 'running')
|
|
669
806
|
AND claim_expires_at > ?`)
|
|
670
807
|
.get(options.runId, options.claimToken, nowMs);
|
|
671
808
|
return row !== undefined;
|
|
@@ -678,10 +815,42 @@ export class SqliteControllerStore {
|
|
|
678
815
|
.prepare(`UPDATE workflow_run_queue
|
|
679
816
|
SET status = 'parked', runner_id = NULL, claim_token = NULL,
|
|
680
817
|
claim_expires_at = NULL, updated_at = ?
|
|
681
|
-
WHERE run_id = ? AND claim_token = ? AND status
|
|
818
|
+
WHERE run_id = ? AND claim_token = ? AND status IN ('starting', 'running')`)
|
|
682
819
|
.run(now, options.runId, options.claimToken);
|
|
683
820
|
return result.changes === 1;
|
|
684
821
|
}
|
|
822
|
+
failWorkflowRun(options) {
|
|
823
|
+
validateRunId(options.runId);
|
|
824
|
+
validateKey(options.errorCode, "workflow launch error code");
|
|
825
|
+
const message = options.errorMessage.trim();
|
|
826
|
+
if (message.length === 0 || Buffer.byteLength(message) > 2_048) {
|
|
827
|
+
throw new Error("Workflow launch error message must be between 1 and 2048 bytes");
|
|
828
|
+
}
|
|
829
|
+
const now = validTimestamp(options.now);
|
|
830
|
+
const claimFilter = options.claimToken === undefined ? "" : "AND claim_token = ?";
|
|
831
|
+
const result = this.database
|
|
832
|
+
.prepare(`UPDATE workflow_run_queue
|
|
833
|
+
SET status = 'failed', runner_id = NULL, claim_token = NULL,
|
|
834
|
+
claim_expires_at = NULL, parent_run_id = NULL, input_json = 'null',
|
|
835
|
+
launch_options_json = '{}', error_code = ?, error_message = ?,
|
|
836
|
+
finished_at = ?, updated_at = ?
|
|
837
|
+
WHERE run_id = ? AND status IN ('queued', 'starting', 'running') ${claimFilter}`)
|
|
838
|
+
.run(options.errorCode, message, now, now, options.runId, ...(options.claimToken === undefined ? [] : [options.claimToken]));
|
|
839
|
+
return result.changes === 1;
|
|
840
|
+
}
|
|
841
|
+
cancelWorkflowRun(options) {
|
|
842
|
+
validateRunId(options.runId);
|
|
843
|
+
const now = validTimestamp(options.now);
|
|
844
|
+
const claimFilter = options.claimToken === undefined ? "" : "AND claim_token = ?";
|
|
845
|
+
const result = this.database
|
|
846
|
+
.prepare(`UPDATE workflow_run_queue
|
|
847
|
+
SET status = 'cancelled', runner_id = NULL, claim_token = NULL,
|
|
848
|
+
claim_expires_at = NULL, input_json = 'null', launch_options_json = '{}',
|
|
849
|
+
finished_at = ?, updated_at = ?
|
|
850
|
+
WHERE run_id = ? AND status IN ('queued', 'starting') ${claimFilter}`)
|
|
851
|
+
.run(now, now, options.runId, ...(options.claimToken === undefined ? [] : [options.claimToken]));
|
|
852
|
+
return result.changes === 1;
|
|
853
|
+
}
|
|
685
854
|
/**
|
|
686
855
|
* Delete a claimed row. Used when a continuation fails before its bundle
|
|
687
856
|
* exists, so the parent's one-continuation slot is not consumed by a run
|
|
@@ -701,9 +870,10 @@ export class SqliteControllerStore {
|
|
|
701
870
|
const result = this.database
|
|
702
871
|
.prepare(`UPDATE workflow_run_queue
|
|
703
872
|
SET status = 'done', runner_id = NULL, claim_token = NULL,
|
|
704
|
-
claim_expires_at = NULL,
|
|
705
|
-
|
|
706
|
-
|
|
873
|
+
claim_expires_at = NULL, input_json = 'null', launch_options_json = '{}',
|
|
874
|
+
finished_at = ?, updated_at = ?
|
|
875
|
+
WHERE run_id = ? AND claim_token = ? AND status IN ('starting', 'running')`)
|
|
876
|
+
.run(now, now, options.runId, options.claimToken);
|
|
707
877
|
return result.changes === 1;
|
|
708
878
|
}
|
|
709
879
|
/** Repair a canonical bundle's queue source and claim it only when needed. */
|
|
@@ -721,26 +891,27 @@ export class SqliteControllerStore {
|
|
|
721
891
|
const row = this.database
|
|
722
892
|
.prepare("SELECT * FROM workflow_run_queue WHERE run_id = ?")
|
|
723
893
|
.get(options.runId);
|
|
724
|
-
if (row === undefined ||
|
|
894
|
+
if (row === undefined || ["done", "failed", "cancelled"].includes(row.status)) {
|
|
725
895
|
return false;
|
|
896
|
+
}
|
|
726
897
|
if (row.workflow_path === options.workflowSourceRef && row.status === "parked") {
|
|
727
898
|
return "unchanged";
|
|
728
899
|
}
|
|
729
900
|
const claimable = row.status === "parked" ||
|
|
730
|
-
(row.status === "
|
|
901
|
+
(row.status === "starting" &&
|
|
731
902
|
row.claim_token === options.claimToken &&
|
|
732
903
|
row.claim_expires_at !== null &&
|
|
733
904
|
row.claim_expires_at > nowMs) ||
|
|
734
|
-
(row.status === "
|
|
905
|
+
(row.status === "starting" &&
|
|
735
906
|
row.claim_expires_at !== null &&
|
|
736
907
|
row.claim_expires_at <= nowMs);
|
|
737
908
|
if (!claimable)
|
|
738
909
|
return false;
|
|
739
910
|
const result = this.database
|
|
740
911
|
.prepare(`UPDATE workflow_run_queue
|
|
741
|
-
SET workflow_ref = ?, workflow_path = ?, status = '
|
|
912
|
+
SET workflow_ref = ?, workflow_path = ?, status = 'starting', runner_id = ?,
|
|
742
913
|
claim_token = ?, claim_expires_at = ?, updated_at = ?
|
|
743
|
-
WHERE run_id = ? AND status
|
|
914
|
+
WHERE run_id = ? AND status NOT IN ('done', 'failed', 'cancelled')`)
|
|
744
915
|
.run(options.workflowName, options.workflowSourceRef, options.runnerId, options.claimToken, expiresAt, now, options.runId);
|
|
745
916
|
return result.changes === 1 ? "claimed" : false;
|
|
746
917
|
});
|
|
@@ -761,25 +932,26 @@ export class SqliteControllerStore {
|
|
|
761
932
|
const row = this.database
|
|
762
933
|
.prepare("SELECT * FROM workflow_run_queue WHERE run_id = ?")
|
|
763
934
|
.get(options.runId);
|
|
764
|
-
if (row === undefined ||
|
|
935
|
+
if (row === undefined || ["done", "failed", "cancelled"].includes(row.status)) {
|
|
765
936
|
return false;
|
|
937
|
+
}
|
|
766
938
|
const sourceMatches = row.workflow_path === options.oldWorkflowPath ||
|
|
767
939
|
row.workflow_path === options.workflowSourceRef;
|
|
768
940
|
const claimable = row.status === "parked" ||
|
|
769
|
-
(row.status === "
|
|
941
|
+
(row.status === "starting" &&
|
|
770
942
|
row.claim_token === options.claimToken &&
|
|
771
943
|
row.claim_expires_at !== null &&
|
|
772
944
|
row.claim_expires_at > nowMs) ||
|
|
773
|
-
(row.status === "
|
|
945
|
+
(row.status === "starting" &&
|
|
774
946
|
row.claim_expires_at !== null &&
|
|
775
947
|
row.claim_expires_at <= nowMs);
|
|
776
948
|
if (!sourceMatches || !claimable)
|
|
777
949
|
return false;
|
|
778
950
|
const result = this.database
|
|
779
951
|
.prepare(`UPDATE workflow_run_queue
|
|
780
|
-
SET workflow_ref = ?, workflow_path = ?, status = '
|
|
952
|
+
SET workflow_ref = ?, workflow_path = ?, status = 'starting', runner_id = ?,
|
|
781
953
|
claim_token = ?, claim_expires_at = ?, updated_at = ?
|
|
782
|
-
WHERE run_id = ? AND status
|
|
954
|
+
WHERE run_id = ? AND status NOT IN ('done', 'failed', 'cancelled')`)
|
|
783
955
|
.run(options.workflowName, options.workflowSourceRef, options.runnerId, options.claimToken, expiresAt, now, options.runId);
|
|
784
956
|
return result.changes === 1;
|
|
785
957
|
});
|
|
@@ -815,6 +987,179 @@ export class SqliteControllerStore {
|
|
|
815
987
|
payload: parseStoredJson(row.payload_json, "run event payload"),
|
|
816
988
|
}));
|
|
817
989
|
}
|
|
990
|
+
ensureWorkflowTurnIntent(options) {
|
|
991
|
+
validateKey(options.intentId, "workflow turn intent id");
|
|
992
|
+
validateKey(options.sourceEventId, "workflow turn source event id");
|
|
993
|
+
validateRunId(options.runId);
|
|
994
|
+
validateKey(options.workflowRef, "workflow turn workflow ref");
|
|
995
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
996
|
+
validateWorkflowTurnIntentCause(options.cause);
|
|
997
|
+
const nodeId = options.nodeId ?? null;
|
|
998
|
+
const attemptId = options.attemptId ?? null;
|
|
999
|
+
if (nodeId !== null)
|
|
1000
|
+
validateKey(nodeId, "workflow turn node id");
|
|
1001
|
+
if (attemptId !== null)
|
|
1002
|
+
validateKey(attemptId, "workflow turn attempt id");
|
|
1003
|
+
const factsJson = workflowTurnIntentFactsJson(options.fallbackFacts);
|
|
1004
|
+
const now = validTimestamp(options.now);
|
|
1005
|
+
const eligibleAt = options.eligible === true ? now : null;
|
|
1006
|
+
this.database
|
|
1007
|
+
.prepare(`INSERT INTO workflow_turn_intents (
|
|
1008
|
+
intent_id, source_event_id, run_id, workflow_ref, target_session_id,
|
|
1009
|
+
cause, node_id, attempt_id, fallback_facts_json, requested_at, eligible_at,
|
|
1010
|
+
resolved_at, resolution, resolution_message_id,
|
|
1011
|
+
delivery_claim_token, delivery_claim_expires_at
|
|
1012
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, NULL, NULL)
|
|
1013
|
+
ON CONFLICT(intent_id) DO NOTHING`)
|
|
1014
|
+
.run(options.intentId, options.sourceEventId, options.runId, options.workflowRef, options.targetSessionId, options.cause, nodeId, attemptId, factsJson, now, eligibleAt);
|
|
1015
|
+
const existing = this.getWorkflowTurnIntent(options.intentId);
|
|
1016
|
+
if (existing === undefined) {
|
|
1017
|
+
throw new Error(`Workflow turn intent was not stored: ${options.intentId}`);
|
|
1018
|
+
}
|
|
1019
|
+
if (existing.sourceEventId !== options.sourceEventId ||
|
|
1020
|
+
existing.runId !== options.runId ||
|
|
1021
|
+
existing.workflowRef !== options.workflowRef ||
|
|
1022
|
+
existing.targetSessionId !== options.targetSessionId ||
|
|
1023
|
+
existing.cause !== options.cause ||
|
|
1024
|
+
existing.nodeId !== nodeId ||
|
|
1025
|
+
existing.attemptId !== attemptId) {
|
|
1026
|
+
throw new Error(`Workflow turn intent identity conflict: ${options.intentId}`);
|
|
1027
|
+
}
|
|
1028
|
+
return existing;
|
|
1029
|
+
}
|
|
1030
|
+
getWorkflowTurnIntent(intentId) {
|
|
1031
|
+
validateKey(intentId, "workflow turn intent id");
|
|
1032
|
+
const row = this.database
|
|
1033
|
+
.prepare("SELECT * FROM workflow_turn_intents WHERE intent_id = ?")
|
|
1034
|
+
.get(intentId);
|
|
1035
|
+
return row === undefined ? undefined : workflowTurnIntentFromRow(row);
|
|
1036
|
+
}
|
|
1037
|
+
findPendingWorkflowTurnIntent(options) {
|
|
1038
|
+
validateRunId(options.runId);
|
|
1039
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
1040
|
+
const row = this.database
|
|
1041
|
+
.prepare(`SELECT * FROM workflow_turn_intents
|
|
1042
|
+
WHERE run_id = ? AND target_session_id = ? AND resolved_at IS NULL
|
|
1043
|
+
ORDER BY requested_at DESC, intent_id DESC LIMIT 1`)
|
|
1044
|
+
.get(options.runId, options.targetSessionId);
|
|
1045
|
+
return row === undefined ? undefined : workflowTurnIntentFromRow(row);
|
|
1046
|
+
}
|
|
1047
|
+
claimWorkflowTurnIntent(options) {
|
|
1048
|
+
validateKey(options.intentId, "workflow turn intent id");
|
|
1049
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
1050
|
+
validateKey(options.claimToken, "workflow turn claim token");
|
|
1051
|
+
validateDuration(options.leaseMs, "leaseMs");
|
|
1052
|
+
const nowMs = epoch(validTimestamp(options.now));
|
|
1053
|
+
const result = this.database
|
|
1054
|
+
.prepare(`UPDATE workflow_turn_intents
|
|
1055
|
+
SET delivery_claim_token = ?, delivery_claim_expires_at = ?
|
|
1056
|
+
WHERE intent_id = ? AND target_session_id = ? AND resolved_at IS NULL
|
|
1057
|
+
AND (delivery_claim_expires_at IS NULL OR delivery_claim_expires_at <= ?)`)
|
|
1058
|
+
.run(options.claimToken, nowMs + options.leaseMs, options.intentId, options.targetSessionId, nowMs);
|
|
1059
|
+
if (result.changes !== 1)
|
|
1060
|
+
return undefined;
|
|
1061
|
+
const row = this.database
|
|
1062
|
+
.prepare("SELECT * FROM workflow_turn_intents WHERE intent_id = ?")
|
|
1063
|
+
.get(options.intentId);
|
|
1064
|
+
return workflowTurnIntentFromRow(row);
|
|
1065
|
+
}
|
|
1066
|
+
claimEligibleWorkflowTurnIntents(options) {
|
|
1067
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
1068
|
+
validateKey(options.claimToken, "workflow turn claim token");
|
|
1069
|
+
validateDuration(options.leaseMs, "leaseMs");
|
|
1070
|
+
const limit = options.limit ?? 20;
|
|
1071
|
+
if (!Number.isSafeInteger(limit) || limit <= 0 || limit > 100) {
|
|
1072
|
+
throw new Error("Workflow turn intent limit must be an integer from 1 through 100");
|
|
1073
|
+
}
|
|
1074
|
+
const now = validTimestamp(options.now);
|
|
1075
|
+
const nowMs = epoch(now);
|
|
1076
|
+
return this.transaction(() => {
|
|
1077
|
+
const ids = this.database
|
|
1078
|
+
.prepare(`SELECT intent_id FROM workflow_turn_intents
|
|
1079
|
+
WHERE target_session_id = ? AND resolved_at IS NULL AND eligible_at IS NOT NULL
|
|
1080
|
+
AND eligible_at <= ?
|
|
1081
|
+
AND (delivery_claim_expires_at IS NULL OR delivery_claim_expires_at <= ?)
|
|
1082
|
+
ORDER BY eligible_at, intent_id LIMIT ?`)
|
|
1083
|
+
.all(options.targetSessionId, now, nowMs, limit);
|
|
1084
|
+
if (ids.length === 0)
|
|
1085
|
+
return [];
|
|
1086
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
1087
|
+
this.database
|
|
1088
|
+
.prepare(`UPDATE workflow_turn_intents
|
|
1089
|
+
SET delivery_claim_token = ?, delivery_claim_expires_at = ?
|
|
1090
|
+
WHERE intent_id IN (${placeholders}) AND resolved_at IS NULL
|
|
1091
|
+
AND (delivery_claim_expires_at IS NULL OR delivery_claim_expires_at <= ?)`)
|
|
1092
|
+
.run(options.claimToken, nowMs + options.leaseMs, ...ids.map((row) => row.intent_id), nowMs);
|
|
1093
|
+
const rows = this.database
|
|
1094
|
+
.prepare(`SELECT * FROM workflow_turn_intents WHERE delivery_claim_token = ?
|
|
1095
|
+
ORDER BY eligible_at, intent_id`)
|
|
1096
|
+
.all(options.claimToken);
|
|
1097
|
+
return rows.map(workflowTurnIntentFromRow);
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
makeWorkflowTurnIntentEligible(options) {
|
|
1101
|
+
validateKey(options.intentId, "workflow turn intent id");
|
|
1102
|
+
const result = this.database
|
|
1103
|
+
.prepare(`UPDATE workflow_turn_intents
|
|
1104
|
+
SET fallback_facts_json = ?, eligible_at = COALESCE(eligible_at, ?)
|
|
1105
|
+
WHERE intent_id = ? AND resolved_at IS NULL`)
|
|
1106
|
+
.run(workflowTurnIntentFactsJson(options.fallbackFacts), validTimestamp(options.now), options.intentId);
|
|
1107
|
+
return result.changes === 1;
|
|
1108
|
+
}
|
|
1109
|
+
resolveWorkflowTurnIntent(options) {
|
|
1110
|
+
validateKey(options.intentId, "workflow turn intent id");
|
|
1111
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
1112
|
+
validateKey(options.claimToken, "workflow turn claim token");
|
|
1113
|
+
validateWorkflowTurnIntentResolution(options.resolution);
|
|
1114
|
+
validateKey(options.messageId, "workflow turn resolution message id");
|
|
1115
|
+
const result = this.database
|
|
1116
|
+
.prepare(`UPDATE workflow_turn_intents
|
|
1117
|
+
SET resolved_at = ?, resolution = ?, resolution_message_id = ?,
|
|
1118
|
+
delivery_claim_token = NULL, delivery_claim_expires_at = NULL
|
|
1119
|
+
WHERE intent_id = ? AND target_session_id = ? AND resolved_at IS NULL
|
|
1120
|
+
AND delivery_claim_token = ?`)
|
|
1121
|
+
.run(validTimestamp(options.now), options.resolution, options.messageId, options.intentId, options.targetSessionId, options.claimToken);
|
|
1122
|
+
return result.changes === 1;
|
|
1123
|
+
}
|
|
1124
|
+
releaseWorkflowTurnIntentClaim(options) {
|
|
1125
|
+
validateKey(options.intentId, "workflow turn intent id");
|
|
1126
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
1127
|
+
validateKey(options.claimToken, "workflow turn claim token");
|
|
1128
|
+
const result = this.database
|
|
1129
|
+
.prepare(`UPDATE workflow_turn_intents
|
|
1130
|
+
SET delivery_claim_token = NULL, delivery_claim_expires_at = NULL
|
|
1131
|
+
WHERE intent_id = ? AND target_session_id = ? AND resolved_at IS NULL
|
|
1132
|
+
AND delivery_claim_token = ?`)
|
|
1133
|
+
.run(options.intentId, options.targetSessionId, options.claimToken);
|
|
1134
|
+
return result.changes === 1;
|
|
1135
|
+
}
|
|
1136
|
+
listWorkflowTurnIntents(options = {}) {
|
|
1137
|
+
if (options.targetSessionId !== undefined) {
|
|
1138
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
1139
|
+
}
|
|
1140
|
+
if (options.runId !== undefined)
|
|
1141
|
+
validateRunId(options.runId);
|
|
1142
|
+
const limit = options.limit ?? 20;
|
|
1143
|
+
if (!Number.isSafeInteger(limit) || limit <= 0 || limit > 100) {
|
|
1144
|
+
throw new Error("Workflow turn intent limit must be an integer from 1 through 100");
|
|
1145
|
+
}
|
|
1146
|
+
const clauses = [];
|
|
1147
|
+
const values = [];
|
|
1148
|
+
if (options.targetSessionId !== undefined) {
|
|
1149
|
+
clauses.push("target_session_id = ?");
|
|
1150
|
+
values.push(options.targetSessionId);
|
|
1151
|
+
}
|
|
1152
|
+
if (options.runId !== undefined) {
|
|
1153
|
+
clauses.push("run_id = ?");
|
|
1154
|
+
values.push(options.runId);
|
|
1155
|
+
}
|
|
1156
|
+
const where = clauses.length === 0 ? "" : `WHERE ${clauses.join(" AND ")}`;
|
|
1157
|
+
const rows = this.database
|
|
1158
|
+
.prepare(`SELECT * FROM workflow_turn_intents ${where}
|
|
1159
|
+
ORDER BY requested_at DESC, intent_id DESC LIMIT ?`)
|
|
1160
|
+
.all(...values, limit);
|
|
1161
|
+
return rows.map(workflowTurnIntentFromRow);
|
|
1162
|
+
}
|
|
818
1163
|
enqueueWorkflowNotification(options) {
|
|
819
1164
|
validateRunId(options.runId);
|
|
820
1165
|
validateKey(options.nodeId, "workflow node id");
|
|
@@ -945,19 +1290,31 @@ const SCHEMA_SQL = `
|
|
|
945
1290
|
run_id TEXT PRIMARY KEY,
|
|
946
1291
|
workflow_ref TEXT NOT NULL,
|
|
947
1292
|
workflow_path TEXT NOT NULL,
|
|
1293
|
+
workflow_source_json TEXT NOT NULL,
|
|
1294
|
+
definition_digest TEXT NOT NULL,
|
|
948
1295
|
input_json TEXT NOT NULL,
|
|
949
|
-
|
|
1296
|
+
launch_options_json TEXT NOT NULL,
|
|
1297
|
+
status TEXT NOT NULL CHECK (
|
|
1298
|
+
status IN ('queued', 'starting', 'running', 'parked', 'done', 'failed', 'cancelled')
|
|
1299
|
+
),
|
|
950
1300
|
runner_id TEXT,
|
|
951
1301
|
claim_token TEXT,
|
|
952
1302
|
claim_expires_at INTEGER,
|
|
953
1303
|
affinity_runner_id TEXT,
|
|
954
1304
|
origin_session_id TEXT,
|
|
955
1305
|
parent_run_id TEXT,
|
|
1306
|
+
error_code TEXT,
|
|
1307
|
+
error_message TEXT,
|
|
956
1308
|
created_at TEXT NOT NULL,
|
|
957
|
-
updated_at TEXT NOT NULL
|
|
1309
|
+
updated_at TEXT NOT NULL,
|
|
1310
|
+
started_at TEXT,
|
|
1311
|
+
finished_at TEXT
|
|
958
1312
|
);
|
|
959
1313
|
CREATE INDEX IF NOT EXISTS workflow_run_queue_claimable
|
|
960
1314
|
ON workflow_run_queue(status, claim_expires_at);
|
|
1315
|
+
CREATE UNIQUE INDEX IF NOT EXISTS workflow_run_queue_session_reservation
|
|
1316
|
+
ON workflow_run_queue(origin_session_id)
|
|
1317
|
+
WHERE origin_session_id IS NOT NULL AND status IN ('queued', 'starting', 'running');
|
|
961
1318
|
-- A checkpointed parent admits exactly one continuation run, across
|
|
962
1319
|
-- sessions and processes.
|
|
963
1320
|
CREATE UNIQUE INDEX IF NOT EXISTS workflow_run_queue_parent
|
|
@@ -993,6 +1350,40 @@ const SCHEMA_SQL = `
|
|
|
993
1350
|
ON workflow_notifications(target_session_id, delivery_claim_expires_at, created_at)
|
|
994
1351
|
WHERE delivered_at IS NULL;
|
|
995
1352
|
|
|
1353
|
+
CREATE TABLE IF NOT EXISTS workflow_turn_intents (
|
|
1354
|
+
intent_id TEXT PRIMARY KEY,
|
|
1355
|
+
source_event_id TEXT NOT NULL,
|
|
1356
|
+
run_id TEXT NOT NULL,
|
|
1357
|
+
workflow_ref TEXT NOT NULL,
|
|
1358
|
+
target_session_id TEXT NOT NULL,
|
|
1359
|
+
cause TEXT NOT NULL CHECK (
|
|
1360
|
+
cause IN ('agentCancelled', 'timedOut', 'failed', 'launchFailed',
|
|
1361
|
+
'controllerInterrupted', 'claimLost')
|
|
1362
|
+
),
|
|
1363
|
+
node_id TEXT,
|
|
1364
|
+
attempt_id TEXT,
|
|
1365
|
+
fallback_facts_json TEXT NOT NULL,
|
|
1366
|
+
requested_at TEXT NOT NULL,
|
|
1367
|
+
eligible_at TEXT,
|
|
1368
|
+
resolved_at TEXT,
|
|
1369
|
+
resolution TEXT CHECK (resolution IN ('workflowPrompt', 'presentation', 'fallback')),
|
|
1370
|
+
resolution_message_id TEXT,
|
|
1371
|
+
delivery_claim_token TEXT,
|
|
1372
|
+
delivery_claim_expires_at INTEGER,
|
|
1373
|
+
CHECK (
|
|
1374
|
+
(resolved_at IS NULL AND resolution IS NULL AND resolution_message_id IS NULL) OR
|
|
1375
|
+
(resolved_at IS NOT NULL AND resolution IS NOT NULL AND resolution_message_id IS NOT NULL)
|
|
1376
|
+
)
|
|
1377
|
+
);
|
|
1378
|
+
CREATE UNIQUE INDEX IF NOT EXISTS workflow_turn_intents_one_pending_run
|
|
1379
|
+
ON workflow_turn_intents(run_id, target_session_id) WHERE resolved_at IS NULL;
|
|
1380
|
+
CREATE INDEX IF NOT EXISTS workflow_turn_intents_pending_run
|
|
1381
|
+
ON workflow_turn_intents(run_id, target_session_id, requested_at)
|
|
1382
|
+
WHERE resolved_at IS NULL;
|
|
1383
|
+
CREATE INDEX IF NOT EXISTS workflow_turn_intents_eligible_session
|
|
1384
|
+
ON workflow_turn_intents(target_session_id, eligible_at, intent_id)
|
|
1385
|
+
WHERE resolved_at IS NULL AND eligible_at IS NOT NULL;
|
|
1386
|
+
|
|
996
1387
|
CREATE TABLE IF NOT EXISTS effects (
|
|
997
1388
|
effect_key TEXT NOT NULL,
|
|
998
1389
|
resource_uid TEXT NOT NULL,
|
|
@@ -1079,7 +1470,48 @@ function workflowFromRow(row) {
|
|
|
1079
1470
|
...(row.error === null ? {} : { error: row.error }),
|
|
1080
1471
|
};
|
|
1081
1472
|
}
|
|
1473
|
+
function workflowTurnIntentFromRow(row) {
|
|
1474
|
+
const cause = validateWorkflowTurnIntentCause(row.cause);
|
|
1475
|
+
const resolution = row.resolution === null ? null : validateWorkflowTurnIntentResolution(row.resolution);
|
|
1476
|
+
const fallbackFacts = parseStoredJson(row.fallback_facts_json, "workflow turn intent facts");
|
|
1477
|
+
validateWorkflowTurnIntentFacts(fallbackFacts);
|
|
1478
|
+
validateKey(row.intent_id, "workflow turn intent id");
|
|
1479
|
+
validateKey(row.source_event_id, "workflow turn source event id");
|
|
1480
|
+
validateRunId(row.run_id);
|
|
1481
|
+
validateKey(row.workflow_ref, "workflow turn workflow ref");
|
|
1482
|
+
validateKey(row.target_session_id, "workflow turn target session id");
|
|
1483
|
+
validTimestamp(row.requested_at);
|
|
1484
|
+
if (row.eligible_at !== null)
|
|
1485
|
+
validTimestamp(row.eligible_at);
|
|
1486
|
+
if (row.resolved_at !== null)
|
|
1487
|
+
validTimestamp(row.resolved_at);
|
|
1488
|
+
const allResolutionFieldsNull = row.resolved_at === null && row.resolution === null && row.resolution_message_id === null;
|
|
1489
|
+
const allResolutionFieldsSet = row.resolved_at !== null && row.resolution !== null && row.resolution_message_id !== null;
|
|
1490
|
+
if (!allResolutionFieldsNull && !allResolutionFieldsSet) {
|
|
1491
|
+
throw new Error(`Workflow turn intent ${row.intent_id} has inconsistent resolution fields`);
|
|
1492
|
+
}
|
|
1493
|
+
return {
|
|
1494
|
+
intentId: row.intent_id,
|
|
1495
|
+
sourceEventId: row.source_event_id,
|
|
1496
|
+
runId: row.run_id,
|
|
1497
|
+
workflowRef: row.workflow_ref,
|
|
1498
|
+
targetSessionId: row.target_session_id,
|
|
1499
|
+
cause,
|
|
1500
|
+
nodeId: row.node_id,
|
|
1501
|
+
attemptId: row.attempt_id,
|
|
1502
|
+
fallbackFacts,
|
|
1503
|
+
requestedAt: row.requested_at,
|
|
1504
|
+
eligibleAt: row.eligible_at,
|
|
1505
|
+
resolvedAt: row.resolved_at,
|
|
1506
|
+
resolution,
|
|
1507
|
+
resolutionMessageId: row.resolution_message_id,
|
|
1508
|
+
deliveryClaimExpiresAt: row.delivery_claim_expires_at === null ? null : iso(row.delivery_claim_expires_at),
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1082
1511
|
function workflowNotificationFromRow(row) {
|
|
1512
|
+
if (row.kind !== "progress" && row.kind !== "final") {
|
|
1513
|
+
throw new Error("Controller store contains an incompatible pending alpha workflow notification. Preserve needed run bundles, then reset the project controller store.");
|
|
1514
|
+
}
|
|
1083
1515
|
return {
|
|
1084
1516
|
notificationId: row.notification_id,
|
|
1085
1517
|
runId: row.run_id,
|
|
@@ -1094,12 +1526,98 @@ function workflowNotificationFromRow(row) {
|
|
|
1094
1526
|
deliveredAt: row.delivered_at,
|
|
1095
1527
|
};
|
|
1096
1528
|
}
|
|
1529
|
+
function workflowTurnIntentFactsJson(facts) {
|
|
1530
|
+
validateWorkflowTurnIntentFacts(facts);
|
|
1531
|
+
const json = canonicalJson(facts, "workflow turn intent facts");
|
|
1532
|
+
validateJsonSize(json, "Workflow turn intent facts", MAX_EVENT_BYTES);
|
|
1533
|
+
return json;
|
|
1534
|
+
}
|
|
1535
|
+
function validateWorkflowTurnIntentFacts(facts) {
|
|
1536
|
+
if (facts === null || typeof facts !== "object" || Array.isArray(facts)) {
|
|
1537
|
+
throw new Error("Workflow turn intent facts must be an object");
|
|
1538
|
+
}
|
|
1539
|
+
const candidate = facts;
|
|
1540
|
+
if (candidate.schema !== TURN_INTENT_FACTS_SCHEMA) {
|
|
1541
|
+
throw new Error(`Workflow turn intent facts schema must be ${TURN_INTENT_FACTS_SCHEMA}`);
|
|
1542
|
+
}
|
|
1543
|
+
const allowedKeys = new Set([
|
|
1544
|
+
"schema",
|
|
1545
|
+
"workflowName",
|
|
1546
|
+
"runId",
|
|
1547
|
+
"observedState",
|
|
1548
|
+
"cause",
|
|
1549
|
+
"nodeId",
|
|
1550
|
+
"attemptId",
|
|
1551
|
+
"reason",
|
|
1552
|
+
"handoff",
|
|
1553
|
+
]);
|
|
1554
|
+
for (const key of Object.keys(candidate)) {
|
|
1555
|
+
if (!allowedKeys.has(key))
|
|
1556
|
+
throw new Error(`Unknown workflow turn intent facts field: ${key}`);
|
|
1557
|
+
}
|
|
1558
|
+
if (typeof candidate.workflowName !== "string") {
|
|
1559
|
+
throw new Error("Workflow turn intent workflowName must be a string");
|
|
1560
|
+
}
|
|
1561
|
+
if (typeof candidate.runId !== "string") {
|
|
1562
|
+
throw new Error("Workflow turn intent runId must be a string");
|
|
1563
|
+
}
|
|
1564
|
+
if (typeof candidate.observedState !== "string") {
|
|
1565
|
+
throw new Error("Workflow turn intent observedState must be a string");
|
|
1566
|
+
}
|
|
1567
|
+
if (typeof candidate.cause !== "string") {
|
|
1568
|
+
throw new Error("Workflow turn intent cause must be a string");
|
|
1569
|
+
}
|
|
1570
|
+
validateKey(candidate.workflowName, "workflow turn facts workflow name");
|
|
1571
|
+
validateRunId(candidate.runId);
|
|
1572
|
+
validateKey(candidate.observedState, "workflow turn facts observed state");
|
|
1573
|
+
validateWorkflowTurnIntentCause(candidate.cause);
|
|
1574
|
+
if (candidate.nodeId !== null && typeof candidate.nodeId !== "string") {
|
|
1575
|
+
throw new Error("Workflow turn intent nodeId must be a string or null");
|
|
1576
|
+
}
|
|
1577
|
+
if (candidate.attemptId !== null && typeof candidate.attemptId !== "string") {
|
|
1578
|
+
throw new Error("Workflow turn intent attemptId must be a string or null");
|
|
1579
|
+
}
|
|
1580
|
+
if (candidate.reason !== null && typeof candidate.reason !== "string") {
|
|
1581
|
+
throw new Error("Workflow turn intent reason must be a string or null");
|
|
1582
|
+
}
|
|
1583
|
+
if (candidate.nodeId !== null)
|
|
1584
|
+
validateKey(candidate.nodeId, "workflow turn facts node id");
|
|
1585
|
+
if (candidate.attemptId !== null) {
|
|
1586
|
+
validateKey(candidate.attemptId, "workflow turn facts attempt id");
|
|
1587
|
+
}
|
|
1588
|
+
if (candidate.reason !== null && candidate.reason.length > MAX_ERROR_LENGTH) {
|
|
1589
|
+
throw new Error(`Workflow turn intent reason must be at most ${MAX_ERROR_LENGTH} characters`);
|
|
1590
|
+
}
|
|
1591
|
+
if (typeof candidate.handoff !== "boolean") {
|
|
1592
|
+
throw new Error("Workflow turn intent handoff must be a boolean");
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
function validateWorkflowTurnIntentCause(value) {
|
|
1596
|
+
if (value !== "agentCancelled" &&
|
|
1597
|
+
value !== "timedOut" &&
|
|
1598
|
+
value !== "failed" &&
|
|
1599
|
+
value !== "launchFailed" &&
|
|
1600
|
+
value !== "controllerInterrupted" &&
|
|
1601
|
+
value !== "claimLost") {
|
|
1602
|
+
throw new Error(`Invalid workflow turn intent cause: ${value}`);
|
|
1603
|
+
}
|
|
1604
|
+
return value;
|
|
1605
|
+
}
|
|
1606
|
+
function validateWorkflowTurnIntentResolution(value) {
|
|
1607
|
+
if (value !== "workflowPrompt" && value !== "presentation" && value !== "fallback") {
|
|
1608
|
+
throw new Error(`Invalid workflow turn intent resolution: ${value}`);
|
|
1609
|
+
}
|
|
1610
|
+
return value;
|
|
1611
|
+
}
|
|
1097
1612
|
function workflowRunFromRow(row) {
|
|
1098
1613
|
return {
|
|
1099
1614
|
runId: row.run_id,
|
|
1100
1615
|
workflowName: row.workflow_ref,
|
|
1101
1616
|
workflowSourceRef: row.workflow_path,
|
|
1617
|
+
workflowSource: parseStoredJson(row.workflow_source_json, "workflow run source"),
|
|
1618
|
+
definitionDigest: row.definition_digest,
|
|
1102
1619
|
input: parseStoredJson(row.input_json, "workflow run input"),
|
|
1620
|
+
launchOptions: parseStoredJson(row.launch_options_json, "workflow launch options"),
|
|
1103
1621
|
status: row.status,
|
|
1104
1622
|
runnerId: row.runner_id,
|
|
1105
1623
|
claimToken: row.claim_token,
|
|
@@ -1107,8 +1625,12 @@ function workflowRunFromRow(row) {
|
|
|
1107
1625
|
affinityRunnerId: row.affinity_runner_id,
|
|
1108
1626
|
originSessionId: row.origin_session_id,
|
|
1109
1627
|
parentRunId: row.parent_run_id,
|
|
1628
|
+
errorCode: row.error_code,
|
|
1629
|
+
errorMessage: row.error_message,
|
|
1110
1630
|
createdAt: row.created_at,
|
|
1111
1631
|
updatedAt: row.updated_at,
|
|
1632
|
+
startedAt: row.started_at,
|
|
1633
|
+
finishedAt: row.finished_at,
|
|
1112
1634
|
};
|
|
1113
1635
|
}
|
|
1114
1636
|
function validateRunId(runId) {
|