@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.
Files changed (133) hide show
  1. package/README.md +15 -6
  2. package/dist/builtins/autoimplement-command-batches.d.ts +52 -0
  3. package/dist/builtins/autoimplement-command-batches.js +258 -0
  4. package/dist/builtins/autoimplement-command-batches.js.map +1 -0
  5. package/dist/builtins/autoimplement.workflow.d.ts +29 -3
  6. package/dist/builtins/autoimplement.workflow.js +876 -231
  7. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  8. package/dist/builtins/catalog.js +3 -1
  9. package/dist/builtins/catalog.js.map +1 -1
  10. package/dist/builtins/index.d.ts +2 -0
  11. package/dist/builtins/index.js +1 -0
  12. package/dist/builtins/index.js.map +1 -1
  13. package/dist/builtins/sanity-check-session.d.ts +17 -0
  14. package/dist/builtins/sanity-check-session.js +168 -0
  15. package/dist/builtins/sanity-check-session.js.map +1 -0
  16. package/dist/builtins/sanity-check.workflow.d.ts +83 -0
  17. package/dist/builtins/sanity-check.workflow.js +398 -0
  18. package/dist/builtins/sanity-check.workflow.js.map +1 -0
  19. package/dist/controllers/sqlite.d.ts +141 -5
  20. package/dist/controllers/sqlite.js +568 -46
  21. package/dist/controllers/sqlite.js.map +1 -1
  22. package/dist/extension/decision-channels.js +7 -7
  23. package/dist/extension/deferred-turn-coordinator.d.ts +32 -0
  24. package/dist/extension/deferred-turn-coordinator.js +143 -0
  25. package/dist/extension/deferred-turn-coordinator.js.map +1 -0
  26. package/dist/extension/deferred-turn.d.ts +44 -0
  27. package/dist/extension/deferred-turn.js +110 -0
  28. package/dist/extension/deferred-turn.js.map +1 -0
  29. package/dist/extension/index.js +515 -84
  30. package/dist/extension/index.js.map +1 -1
  31. package/dist/extension/step-message.d.ts +1 -0
  32. package/dist/extension/step-message.js.map +1 -1
  33. package/dist/herdr/setup.js +4 -4
  34. package/dist/host/runner.js +3 -0
  35. package/dist/host/runner.js.map +1 -1
  36. package/dist/workflows/command-batch.d.ts +38 -0
  37. package/dist/workflows/command-batch.js +176 -0
  38. package/dist/workflows/command-batch.js.map +1 -0
  39. package/dist/workflows/engine.js +8 -4
  40. package/dist/workflows/engine.js.map +1 -1
  41. package/dist/workflows/index.d.ts +1 -0
  42. package/dist/workflows/index.js +1 -0
  43. package/dist/workflows/index.js.map +1 -1
  44. package/dist/workflows/migrate-sources.d.ts +1 -1
  45. package/dist/workflows/migrate-sources.js.map +1 -1
  46. package/dist/workflows/schema.js +2 -1
  47. package/dist/workflows/schema.js.map +1 -1
  48. package/dist/workflows/shell.d.ts +4 -0
  49. package/dist/workflows/shell.js +6 -0
  50. package/dist/workflows/shell.js.map +1 -1
  51. package/dist/workflows/store.js +3 -1
  52. package/dist/workflows/store.js.map +1 -1
  53. package/dist/workflows/tool-input.d.ts +1 -0
  54. package/dist/workflows/tool-input.js +2 -2
  55. package/dist/workflows/tool-input.js.map +1 -1
  56. package/dist/workflows/types.d.ts +4 -3
  57. package/docs/2026-08-18-herdr-piw-plan.md +1 -1
  58. package/docs/2026-08-20-durable-workflow-launch-plan.md +445 -0
  59. package/docs/CONTROLLERS.md +2 -2
  60. package/docs/DEFERRED_TURNS.md +298 -0
  61. package/docs/DESIGN_PHILOSOPHY.md +1 -1
  62. package/docs/HUMAN_DECISIONS.md +7 -7
  63. package/docs/HUMAN_DECISION_PRESENTATIONS.md +3 -3
  64. package/docs/MONITOR.md +2 -2
  65. package/docs/WORKFLOW_COMPOSITION.md +2 -2
  66. package/docs/WORKFLOW_STEP_MESSAGES.md +5 -5
  67. package/docs/WORKFLOW_UPDATES.md +28 -5
  68. package/docs/plans/2026-08-04-controller-runtime-plan.md +1 -1
  69. package/docs/plans/2026-08-05-always-on-workflows-plan.md +1 -1
  70. package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +9 -9
  71. package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +1 -1
  72. package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +1 -1
  73. package/docs/plans/2026-08-16-workflow-updates-plan.md +3 -3
  74. package/docs/plans/2026-08-17-bundled-skills-plan.md +6 -6
  75. package/docs/plans/2026-08-19-human-decision-gates-plan.md +5 -5
  76. package/docs/plans/2026-08-19-human-decision-presentations-plan.md +1 -1
  77. package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +1 -1
  78. package/docs/plans/2026-08-19-workflow-composition-plan.md +6 -6
  79. package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +3 -3
  80. package/docs/plans/2026-08-20-bounded-command-batches-plan.md +206 -0
  81. package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +9 -9
  82. package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +242 -0
  83. package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +324 -0
  84. package/docs/plans/2026-08-21-sanity-check-plan.md +175 -0
  85. package/docs/run-bundles.md +3 -1
  86. package/docs/workflows.md +67 -17
  87. package/examples/workflows/command-batch.workflow.ts +50 -0
  88. package/examples/workflows/sanity-check.workflow.ts +1 -0
  89. package/herdr-plugin.toml +3 -3
  90. package/package.json +1 -1
  91. package/schemas/decision-presentation-v1.schema.json +1 -1
  92. package/schemas/human-decision-accepted-v1.schema.json +1 -1
  93. package/schemas/human-decision-accepted-v2.schema.json +1 -1
  94. package/schemas/human-decision-answer-attempt-v1.schema.json +1 -1
  95. package/schemas/human-decision-cancellation-v1.schema.json +1 -1
  96. package/schemas/human-decision-continuation-v1.schema.json +1 -1
  97. package/schemas/human-decision-delivery-v1.schema.json +1 -1
  98. package/schemas/human-decision-delivery-v2.schema.json +1 -1
  99. package/schemas/human-decision-receipt-v1.schema.json +1 -1
  100. package/schemas/human-decision-receipt-v2.schema.json +1 -1
  101. package/schemas/human-decision-request-v1.schema.json +1 -1
  102. package/schemas/human-decision-request-v2.schema.json +1 -1
  103. package/schemas/human-decision-resolution-v1.schema.json +1 -1
  104. package/schemas/human-decision-resolution-v2.schema.json +1 -1
  105. package/schemas/human-decision-settlement-v1.schema.json +1 -1
  106. package/skills/autodoc/SKILL.md +33 -2
  107. package/skills/autoimplement/SKILL.md +66 -12
  108. package/skills/autoplan/SKILL.md +26 -2
  109. package/skills/monitor/SKILL.md +35 -21
  110. package/skills/pi-workflows/SKILL.md +19 -5
  111. package/src/builtins/autoimplement-command-batches.ts +364 -0
  112. package/src/builtins/autoimplement.workflow.ts +1065 -261
  113. package/src/builtins/catalog.ts +3 -1
  114. package/src/builtins/index.ts +13 -0
  115. package/src/builtins/sanity-check-session.ts +205 -0
  116. package/src/builtins/sanity-check.workflow.ts +624 -0
  117. package/src/controllers/sqlite.ts +878 -51
  118. package/src/extension/decision-channels.ts +7 -7
  119. package/src/extension/deferred-turn-coordinator.ts +171 -0
  120. package/src/extension/deferred-turn.ts +166 -0
  121. package/src/extension/index.ts +646 -91
  122. package/src/extension/step-message.ts +1 -0
  123. package/src/herdr/setup.ts +4 -4
  124. package/src/host/runner.ts +3 -0
  125. package/src/workflows/command-batch.ts +254 -0
  126. package/src/workflows/engine.ts +8 -5
  127. package/src/workflows/index.ts +15 -0
  128. package/src/workflows/migrate-sources.ts +5 -1
  129. package/src/workflows/schema.ts +2 -1
  130. package/src/workflows/shell.ts +10 -0
  131. package/src/workflows/store.ts +3 -1
  132. package/src/workflows/tool-input.ts +5 -2
  133. 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
- this.transaction(() => {
473
- this.database
474
- .prepare("INSERT OR IGNORE INTO schema_info (singleton, schema_id) VALUES (1, ?)")
475
- .run(CONTROLLER_STORE_SCHEMA);
476
- this.database.exec(SCHEMA_SQL);
477
- const queueColumns = this.database.pragma("table_info(workflow_run_queue)");
478
- if (!queueColumns.some((column) => column.name === "origin_session_id")) {
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.database.exec("DROP TABLE IF EXISTS session_watermarks");
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
- * Insert a user-started run and claim it in one statement, so the
556
- * originating runner owns the run from birth (origin affinity).
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, input_json, status,
575
- runner_id, claim_token, claim_expires_at, affinity_runner_id,
576
- origin_session_id, parent_run_id, created_at, updated_at
577
- ) VALUES (?, ?, ?, ?, 'claimed', ?, ?, ?, ?, ?, ?, ?, ?)`)
578
- .run(options.runId, options.workflowName, options.workflowSourceRef, inputJson, options.runnerId, options.claimToken, expiresAt, options.affinityRunnerId ?? options.runnerId, options.originSessionId ?? null, options.parentRunId ?? null, now, now);
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 = 'parked'
623
- OR (status = 'claimed' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?)
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 = 'claimed', runner_id = ?, claim_token = ?,
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 = 'parked'
643
- OR (status = 'claimed' AND claim_expires_at IS NOT NULL AND claim_expires_at <= ?)
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 = 'claimed'
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 = 'claimed'
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 = 'claimed'`)
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, updated_at = ?
705
- WHERE run_id = ? AND claim_token = ? AND status = 'claimed'`)
706
- .run(now, options.runId, options.claimToken);
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 || row.status === "done")
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 === "claimed" &&
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 === "claimed" &&
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 = 'claimed', runner_id = ?,
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 != 'done'`)
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 || row.status === "done")
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 === "claimed" &&
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 === "claimed" &&
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 = 'claimed', runner_id = ?,
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 != 'done'`)
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
- status TEXT NOT NULL CHECK (status IN ('claimed', 'parked', 'done')),
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) {