@osolmaz/pi-workflows 0.11.2 → 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 (123) hide show
  1. package/README.md +11 -7
  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 +90 -2
  20. package/dist/controllers/sqlite.js +380 -5
  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 +286 -47
  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/workflows/command-batch.d.ts +38 -0
  35. package/dist/workflows/command-batch.js +176 -0
  36. package/dist/workflows/command-batch.js.map +1 -0
  37. package/dist/workflows/engine.js +8 -4
  38. package/dist/workflows/engine.js.map +1 -1
  39. package/dist/workflows/index.d.ts +1 -0
  40. package/dist/workflows/index.js +1 -0
  41. package/dist/workflows/index.js.map +1 -1
  42. package/dist/workflows/schema.js +2 -1
  43. package/dist/workflows/schema.js.map +1 -1
  44. package/dist/workflows/shell.d.ts +4 -0
  45. package/dist/workflows/shell.js +6 -0
  46. package/dist/workflows/shell.js.map +1 -1
  47. package/dist/workflows/store.js +3 -1
  48. package/dist/workflows/store.js.map +1 -1
  49. package/dist/workflows/types.d.ts +4 -3
  50. package/docs/2026-08-18-herdr-piw-plan.md +1 -1
  51. package/docs/2026-08-20-durable-workflow-launch-plan.md +19 -23
  52. package/docs/CONTROLLERS.md +2 -2
  53. package/docs/DEFERRED_TURNS.md +298 -0
  54. package/docs/DESIGN_PHILOSOPHY.md +1 -1
  55. package/docs/HUMAN_DECISIONS.md +7 -7
  56. package/docs/HUMAN_DECISION_PRESENTATIONS.md +3 -3
  57. package/docs/MONITOR.md +2 -2
  58. package/docs/WORKFLOW_COMPOSITION.md +2 -2
  59. package/docs/WORKFLOW_STEP_MESSAGES.md +5 -5
  60. package/docs/WORKFLOW_UPDATES.md +28 -5
  61. package/docs/plans/2026-08-04-controller-runtime-plan.md +1 -1
  62. package/docs/plans/2026-08-05-always-on-workflows-plan.md +1 -1
  63. package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +9 -9
  64. package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +1 -1
  65. package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +1 -1
  66. package/docs/plans/2026-08-16-workflow-updates-plan.md +3 -3
  67. package/docs/plans/2026-08-17-bundled-skills-plan.md +6 -6
  68. package/docs/plans/2026-08-19-human-decision-gates-plan.md +5 -5
  69. package/docs/plans/2026-08-19-human-decision-presentations-plan.md +1 -1
  70. package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +1 -1
  71. package/docs/plans/2026-08-19-workflow-composition-plan.md +6 -6
  72. package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +3 -3
  73. package/docs/plans/2026-08-20-bounded-command-batches-plan.md +206 -0
  74. package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +9 -9
  75. package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +242 -0
  76. package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +324 -0
  77. package/docs/plans/2026-08-21-sanity-check-plan.md +175 -0
  78. package/docs/run-bundles.md +3 -1
  79. package/docs/workflows.md +62 -19
  80. package/examples/workflows/command-batch.workflow.ts +50 -0
  81. package/examples/workflows/sanity-check.workflow.ts +1 -0
  82. package/herdr-plugin.toml +3 -3
  83. package/package.json +1 -1
  84. package/schemas/decision-presentation-v1.schema.json +1 -1
  85. package/schemas/human-decision-accepted-v1.schema.json +1 -1
  86. package/schemas/human-decision-accepted-v2.schema.json +1 -1
  87. package/schemas/human-decision-answer-attempt-v1.schema.json +1 -1
  88. package/schemas/human-decision-cancellation-v1.schema.json +1 -1
  89. package/schemas/human-decision-continuation-v1.schema.json +1 -1
  90. package/schemas/human-decision-delivery-v1.schema.json +1 -1
  91. package/schemas/human-decision-delivery-v2.schema.json +1 -1
  92. package/schemas/human-decision-receipt-v1.schema.json +1 -1
  93. package/schemas/human-decision-receipt-v2.schema.json +1 -1
  94. package/schemas/human-decision-request-v1.schema.json +1 -1
  95. package/schemas/human-decision-request-v2.schema.json +1 -1
  96. package/schemas/human-decision-resolution-v1.schema.json +1 -1
  97. package/schemas/human-decision-resolution-v2.schema.json +1 -1
  98. package/schemas/human-decision-settlement-v1.schema.json +1 -1
  99. package/skills/autodoc/SKILL.md +33 -2
  100. package/skills/autoimplement/SKILL.md +66 -12
  101. package/skills/autoplan/SKILL.md +26 -2
  102. package/skills/monitor/SKILL.md +35 -21
  103. package/skills/pi-workflows/SKILL.md +19 -5
  104. package/src/builtins/autoimplement-command-batches.ts +364 -0
  105. package/src/builtins/autoimplement.workflow.ts +1065 -261
  106. package/src/builtins/catalog.ts +3 -1
  107. package/src/builtins/index.ts +13 -0
  108. package/src/builtins/sanity-check-session.ts +205 -0
  109. package/src/builtins/sanity-check.workflow.ts +624 -0
  110. package/src/controllers/sqlite.ts +580 -7
  111. package/src/extension/decision-channels.ts +7 -7
  112. package/src/extension/deferred-turn-coordinator.ts +171 -0
  113. package/src/extension/deferred-turn.ts +166 -0
  114. package/src/extension/index.ts +359 -49
  115. package/src/extension/step-message.ts +1 -0
  116. package/src/herdr/setup.ts +4 -4
  117. package/src/workflows/command-batch.ts +254 -0
  118. package/src/workflows/engine.ts +8 -5
  119. package/src/workflows/index.ts +15 -0
  120. package/src/workflows/schema.ts +2 -1
  121. package/src/workflows/shell.ts +10 -0
  122. package/src/workflows/store.ts +3 -1
  123. 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;
@@ -472,8 +473,10 @@ export class SqliteControllerStore {
472
473
  const existingQueue = this.database
473
474
  .prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'workflow_run_queue'")
474
475
  .get();
475
- if (existingQueue !== undefined)
476
- this.assertAlphaSchemaLayout();
476
+ if (existingQueue !== undefined) {
477
+ this.assertAlphaSchemaLayout(false);
478
+ this.assertNoLegacyPendingLaunchNotifications();
479
+ }
477
480
  this.transaction(() => {
478
481
  this.database
479
482
  .prepare("INSERT OR IGNORE INTO schema_info (singleton, schema_id) VALUES (1, ?)")
@@ -481,9 +484,10 @@ export class SqliteControllerStore {
481
484
  this.database.exec(SCHEMA_SQL);
482
485
  });
483
486
  }
484
- this.assertAlphaSchemaLayout();
487
+ this.assertAlphaSchemaLayout(true);
488
+ this.assertNoLegacyPendingLaunchNotifications();
485
489
  }
486
- assertAlphaSchemaLayout() {
490
+ assertAlphaSchemaLayout(requireTurnIntents) {
487
491
  const requiredQueueColumns = new Set([
488
492
  "run_id",
489
493
  "workflow_ref",
@@ -511,6 +515,46 @@ export class SqliteControllerStore {
511
515
  if (missing.length > 0) {
512
516
  throw new Error("Controller store uses an incompatible alpha layout. Preserve needed run bundles, then reset the project controller store.");
513
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
+ }
514
558
  }
515
559
  transaction(task) {
516
560
  this.database.exec("BEGIN IMMEDIATE");
@@ -943,6 +987,179 @@ export class SqliteControllerStore {
943
987
  payload: parseStoredJson(row.payload_json, "run event payload"),
944
988
  }));
945
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
+ }
946
1163
  enqueueWorkflowNotification(options) {
947
1164
  validateRunId(options.runId);
948
1165
  validateKey(options.nodeId, "workflow node id");
@@ -1121,7 +1338,7 @@ const SCHEMA_SQL = `
1121
1338
  attempt_id TEXT NOT NULL,
1122
1339
  notification_index INTEGER NOT NULL CHECK (notification_index > 0),
1123
1340
  target_session_id TEXT NOT NULL,
1124
- kind TEXT NOT NULL CHECK (kind IN ('progress', 'final', 'launch_failure')),
1341
+ kind TEXT NOT NULL CHECK (kind IN ('progress', 'final')),
1125
1342
  content TEXT NOT NULL,
1126
1343
  created_at TEXT NOT NULL,
1127
1344
  delivery_claim_token TEXT,
@@ -1133,6 +1350,40 @@ const SCHEMA_SQL = `
1133
1350
  ON workflow_notifications(target_session_id, delivery_claim_expires_at, created_at)
1134
1351
  WHERE delivered_at IS NULL;
1135
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
+
1136
1387
  CREATE TABLE IF NOT EXISTS effects (
1137
1388
  effect_key TEXT NOT NULL,
1138
1389
  resource_uid TEXT NOT NULL,
@@ -1219,7 +1470,48 @@ function workflowFromRow(row) {
1219
1470
  ...(row.error === null ? {} : { error: row.error }),
1220
1471
  };
1221
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
+ }
1222
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
+ }
1223
1515
  return {
1224
1516
  notificationId: row.notification_id,
1225
1517
  runId: row.run_id,
@@ -1234,6 +1526,89 @@ function workflowNotificationFromRow(row) {
1234
1526
  deliveredAt: row.delivered_at,
1235
1527
  };
1236
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
+ }
1237
1612
  function workflowRunFromRow(row) {
1238
1613
  return {
1239
1614
  runId: row.run_id,