@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.
- package/README.md +11 -7
- 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 +90 -2
- package/dist/controllers/sqlite.js +380 -5
- 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 +286 -47
- 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/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/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/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 +19 -23
- 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 +62 -19
- 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 +580 -7
- 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 +359 -49
- package/src/extension/step-message.ts +1 -0
- package/src/herdr/setup.ts +4 -4
- 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/schema.ts +2 -1
- package/src/workflows/shell.ts +10 -0
- package/src/workflows/store.ts +3 -1
- package/src/workflows/types.ts +4 -3
|
@@ -34,6 +34,7 @@ const MAX_KEY_LENGTH = 512;
|
|
|
34
34
|
const MAX_ERROR_LENGTH = 8_192;
|
|
35
35
|
const MAX_EVENT_BYTES = 64 * 1024;
|
|
36
36
|
const MAX_RESOURCE_VALUE_BYTES = 1024 * 1024;
|
|
37
|
+
const TURN_INTENT_FACTS_SCHEMA = "pi-workflows.deferred-turn-facts.v1";
|
|
37
38
|
|
|
38
39
|
type ResourceRow = {
|
|
39
40
|
controller: string;
|
|
@@ -149,7 +150,7 @@ type WorkflowNotificationRow = {
|
|
|
149
150
|
attempt_id: string;
|
|
150
151
|
notification_index: number;
|
|
151
152
|
target_session_id: string;
|
|
152
|
-
kind:
|
|
153
|
+
kind: string;
|
|
153
154
|
content: string;
|
|
154
155
|
created_at: string;
|
|
155
156
|
delivery_claim_token: string | null;
|
|
@@ -164,13 +165,72 @@ export type WorkflowNotificationRecord = {
|
|
|
164
165
|
attemptId: string;
|
|
165
166
|
notificationIndex: number;
|
|
166
167
|
targetSessionId: string;
|
|
167
|
-
kind: "progress" | "final"
|
|
168
|
+
kind: "progress" | "final";
|
|
168
169
|
content: string;
|
|
169
170
|
createdAt: string;
|
|
170
171
|
deliveryClaimExpiresAt: string | null;
|
|
171
172
|
deliveredAt: string | null;
|
|
172
173
|
};
|
|
173
174
|
|
|
175
|
+
export type WorkflowTurnIntentCause =
|
|
176
|
+
| "agentCancelled"
|
|
177
|
+
| "timedOut"
|
|
178
|
+
| "failed"
|
|
179
|
+
| "launchFailed"
|
|
180
|
+
| "controllerInterrupted"
|
|
181
|
+
| "claimLost";
|
|
182
|
+
|
|
183
|
+
export type WorkflowTurnIntentResolution = "workflowPrompt" | "presentation" | "fallback";
|
|
184
|
+
|
|
185
|
+
export type WorkflowTurnIntentFacts = JsonObject & {
|
|
186
|
+
schema: typeof TURN_INTENT_FACTS_SCHEMA;
|
|
187
|
+
workflowName: string;
|
|
188
|
+
runId: string;
|
|
189
|
+
observedState: string;
|
|
190
|
+
cause: WorkflowTurnIntentCause;
|
|
191
|
+
nodeId: string | null;
|
|
192
|
+
attemptId: string | null;
|
|
193
|
+
reason: string | null;
|
|
194
|
+
handoff: boolean;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
type WorkflowTurnIntentRow = {
|
|
198
|
+
intent_id: string;
|
|
199
|
+
source_event_id: string;
|
|
200
|
+
run_id: string;
|
|
201
|
+
workflow_ref: string;
|
|
202
|
+
target_session_id: string;
|
|
203
|
+
cause: string;
|
|
204
|
+
node_id: string | null;
|
|
205
|
+
attempt_id: string | null;
|
|
206
|
+
fallback_facts_json: string;
|
|
207
|
+
requested_at: string;
|
|
208
|
+
eligible_at: string | null;
|
|
209
|
+
resolved_at: string | null;
|
|
210
|
+
resolution: string | null;
|
|
211
|
+
resolution_message_id: string | null;
|
|
212
|
+
delivery_claim_token: string | null;
|
|
213
|
+
delivery_claim_expires_at: number | null;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export type WorkflowTurnIntentRecord = {
|
|
217
|
+
intentId: string;
|
|
218
|
+
sourceEventId: string;
|
|
219
|
+
runId: string;
|
|
220
|
+
workflowRef: string;
|
|
221
|
+
targetSessionId: string;
|
|
222
|
+
cause: WorkflowTurnIntentCause;
|
|
223
|
+
nodeId: string | null;
|
|
224
|
+
attemptId: string | null;
|
|
225
|
+
fallbackFacts: WorkflowTurnIntentFacts;
|
|
226
|
+
requestedAt: string;
|
|
227
|
+
eligibleAt: string | null;
|
|
228
|
+
resolvedAt: string | null;
|
|
229
|
+
resolution: WorkflowTurnIntentResolution | null;
|
|
230
|
+
resolutionMessageId: string | null;
|
|
231
|
+
deliveryClaimExpiresAt: string | null;
|
|
232
|
+
};
|
|
233
|
+
|
|
174
234
|
type RunEventRow = {
|
|
175
235
|
seq: number;
|
|
176
236
|
recorded_at: string;
|
|
@@ -826,7 +886,10 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
826
886
|
const existingQueue = this.database
|
|
827
887
|
.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'workflow_run_queue'")
|
|
828
888
|
.get();
|
|
829
|
-
if (existingQueue !== undefined)
|
|
889
|
+
if (existingQueue !== undefined) {
|
|
890
|
+
this.assertAlphaSchemaLayout(false);
|
|
891
|
+
this.assertNoLegacyPendingLaunchNotifications();
|
|
892
|
+
}
|
|
830
893
|
this.transaction(() => {
|
|
831
894
|
this.database
|
|
832
895
|
.prepare("INSERT OR IGNORE INTO schema_info (singleton, schema_id) VALUES (1, ?)")
|
|
@@ -834,10 +897,11 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
834
897
|
this.database.exec(SCHEMA_SQL);
|
|
835
898
|
});
|
|
836
899
|
}
|
|
837
|
-
this.assertAlphaSchemaLayout();
|
|
900
|
+
this.assertAlphaSchemaLayout(true);
|
|
901
|
+
this.assertNoLegacyPendingLaunchNotifications();
|
|
838
902
|
}
|
|
839
903
|
|
|
840
|
-
private assertAlphaSchemaLayout(): void {
|
|
904
|
+
private assertAlphaSchemaLayout(requireTurnIntents: boolean): void {
|
|
841
905
|
const requiredQueueColumns = new Set([
|
|
842
906
|
"run_id",
|
|
843
907
|
"workflow_ref",
|
|
@@ -873,6 +937,64 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
873
937
|
"Controller store uses an incompatible alpha layout. Preserve needed run bundles, then reset the project controller store.",
|
|
874
938
|
);
|
|
875
939
|
}
|
|
940
|
+
if (!requireTurnIntents) return;
|
|
941
|
+
const requiredIntentColumns = new Set([
|
|
942
|
+
"intent_id",
|
|
943
|
+
"source_event_id",
|
|
944
|
+
"run_id",
|
|
945
|
+
"workflow_ref",
|
|
946
|
+
"target_session_id",
|
|
947
|
+
"cause",
|
|
948
|
+
"node_id",
|
|
949
|
+
"attempt_id",
|
|
950
|
+
"fallback_facts_json",
|
|
951
|
+
"requested_at",
|
|
952
|
+
"eligible_at",
|
|
953
|
+
"resolved_at",
|
|
954
|
+
"resolution",
|
|
955
|
+
"resolution_message_id",
|
|
956
|
+
"delivery_claim_token",
|
|
957
|
+
"delivery_claim_expires_at",
|
|
958
|
+
]);
|
|
959
|
+
const intentColumns = new Set(
|
|
960
|
+
(
|
|
961
|
+
this.database.pragma("table_info(workflow_turn_intents)") as {
|
|
962
|
+
name: string;
|
|
963
|
+
}[]
|
|
964
|
+
).map((column) => column.name),
|
|
965
|
+
);
|
|
966
|
+
const missingIntentColumns = [...requiredIntentColumns].filter(
|
|
967
|
+
(column) => !intentColumns.has(column),
|
|
968
|
+
);
|
|
969
|
+
if (missingIntentColumns.length > 0) {
|
|
970
|
+
throw new Error(
|
|
971
|
+
"Controller store uses an incompatible alpha turn-intent layout. Preserve needed run bundles, then reset the project controller store.",
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
private assertNoLegacyPendingLaunchNotifications(): void {
|
|
977
|
+
const notificationTable = this.database
|
|
978
|
+
.prepare(
|
|
979
|
+
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'workflow_notifications'",
|
|
980
|
+
)
|
|
981
|
+
.get();
|
|
982
|
+
if (notificationTable === undefined) {
|
|
983
|
+
throw new Error(
|
|
984
|
+
"Controller store uses an incompatible alpha notification layout. Preserve needed run bundles, then reset the project controller store.",
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
const legacy = this.database
|
|
988
|
+
.prepare(
|
|
989
|
+
`SELECT 1 FROM workflow_notifications
|
|
990
|
+
WHERE kind = 'launch_failure' AND delivered_at IS NULL LIMIT 1`,
|
|
991
|
+
)
|
|
992
|
+
.get();
|
|
993
|
+
if (legacy !== undefined) {
|
|
994
|
+
throw new Error(
|
|
995
|
+
"Controller store contains pending alpha launch-failure notifications. Preserve needed run bundles, then reset the project controller store.",
|
|
996
|
+
);
|
|
997
|
+
}
|
|
876
998
|
}
|
|
877
999
|
|
|
878
1000
|
private transaction<T>(task: () => T): T {
|
|
@@ -1526,13 +1648,294 @@ export class SqliteControllerStore implements ControllerStore {
|
|
|
1526
1648
|
}));
|
|
1527
1649
|
}
|
|
1528
1650
|
|
|
1651
|
+
ensureWorkflowTurnIntent(options: {
|
|
1652
|
+
intentId: string;
|
|
1653
|
+
sourceEventId: string;
|
|
1654
|
+
runId: string;
|
|
1655
|
+
workflowRef: string;
|
|
1656
|
+
targetSessionId: string;
|
|
1657
|
+
cause: WorkflowTurnIntentCause;
|
|
1658
|
+
nodeId?: string | null;
|
|
1659
|
+
attemptId?: string | null;
|
|
1660
|
+
fallbackFacts: WorkflowTurnIntentFacts;
|
|
1661
|
+
eligible?: boolean;
|
|
1662
|
+
now?: string;
|
|
1663
|
+
}): WorkflowTurnIntentRecord {
|
|
1664
|
+
validateKey(options.intentId, "workflow turn intent id");
|
|
1665
|
+
validateKey(options.sourceEventId, "workflow turn source event id");
|
|
1666
|
+
validateRunId(options.runId);
|
|
1667
|
+
validateKey(options.workflowRef, "workflow turn workflow ref");
|
|
1668
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
1669
|
+
validateWorkflowTurnIntentCause(options.cause);
|
|
1670
|
+
const nodeId = options.nodeId ?? null;
|
|
1671
|
+
const attemptId = options.attemptId ?? null;
|
|
1672
|
+
if (nodeId !== null) validateKey(nodeId, "workflow turn node id");
|
|
1673
|
+
if (attemptId !== null) validateKey(attemptId, "workflow turn attempt id");
|
|
1674
|
+
const factsJson = workflowTurnIntentFactsJson(options.fallbackFacts);
|
|
1675
|
+
const now = validTimestamp(options.now);
|
|
1676
|
+
const eligibleAt = options.eligible === true ? now : null;
|
|
1677
|
+
this.database
|
|
1678
|
+
.prepare(
|
|
1679
|
+
`INSERT INTO workflow_turn_intents (
|
|
1680
|
+
intent_id, source_event_id, run_id, workflow_ref, target_session_id,
|
|
1681
|
+
cause, node_id, attempt_id, fallback_facts_json, requested_at, eligible_at,
|
|
1682
|
+
resolved_at, resolution, resolution_message_id,
|
|
1683
|
+
delivery_claim_token, delivery_claim_expires_at
|
|
1684
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, NULL, NULL)
|
|
1685
|
+
ON CONFLICT(intent_id) DO NOTHING`,
|
|
1686
|
+
)
|
|
1687
|
+
.run(
|
|
1688
|
+
options.intentId,
|
|
1689
|
+
options.sourceEventId,
|
|
1690
|
+
options.runId,
|
|
1691
|
+
options.workflowRef,
|
|
1692
|
+
options.targetSessionId,
|
|
1693
|
+
options.cause,
|
|
1694
|
+
nodeId,
|
|
1695
|
+
attemptId,
|
|
1696
|
+
factsJson,
|
|
1697
|
+
now,
|
|
1698
|
+
eligibleAt,
|
|
1699
|
+
);
|
|
1700
|
+
const existing = this.getWorkflowTurnIntent(options.intentId);
|
|
1701
|
+
if (existing === undefined) {
|
|
1702
|
+
throw new Error(`Workflow turn intent was not stored: ${options.intentId}`);
|
|
1703
|
+
}
|
|
1704
|
+
if (
|
|
1705
|
+
existing.sourceEventId !== options.sourceEventId ||
|
|
1706
|
+
existing.runId !== options.runId ||
|
|
1707
|
+
existing.workflowRef !== options.workflowRef ||
|
|
1708
|
+
existing.targetSessionId !== options.targetSessionId ||
|
|
1709
|
+
existing.cause !== options.cause ||
|
|
1710
|
+
existing.nodeId !== nodeId ||
|
|
1711
|
+
existing.attemptId !== attemptId
|
|
1712
|
+
) {
|
|
1713
|
+
throw new Error(`Workflow turn intent identity conflict: ${options.intentId}`);
|
|
1714
|
+
}
|
|
1715
|
+
return existing;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
getWorkflowTurnIntent(intentId: string): WorkflowTurnIntentRecord | undefined {
|
|
1719
|
+
validateKey(intentId, "workflow turn intent id");
|
|
1720
|
+
const row = this.database
|
|
1721
|
+
.prepare("SELECT * FROM workflow_turn_intents WHERE intent_id = ?")
|
|
1722
|
+
.get(intentId) as WorkflowTurnIntentRow | undefined;
|
|
1723
|
+
return row === undefined ? undefined : workflowTurnIntentFromRow(row);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
findPendingWorkflowTurnIntent(options: {
|
|
1727
|
+
runId: string;
|
|
1728
|
+
targetSessionId: string;
|
|
1729
|
+
}): WorkflowTurnIntentRecord | undefined {
|
|
1730
|
+
validateRunId(options.runId);
|
|
1731
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
1732
|
+
const row = this.database
|
|
1733
|
+
.prepare(
|
|
1734
|
+
`SELECT * FROM workflow_turn_intents
|
|
1735
|
+
WHERE run_id = ? AND target_session_id = ? AND resolved_at IS NULL
|
|
1736
|
+
ORDER BY requested_at DESC, intent_id DESC LIMIT 1`,
|
|
1737
|
+
)
|
|
1738
|
+
.get(options.runId, options.targetSessionId) as WorkflowTurnIntentRow | undefined;
|
|
1739
|
+
return row === undefined ? undefined : workflowTurnIntentFromRow(row);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
claimWorkflowTurnIntent(options: {
|
|
1743
|
+
intentId: string;
|
|
1744
|
+
targetSessionId: string;
|
|
1745
|
+
claimToken: string;
|
|
1746
|
+
leaseMs: number;
|
|
1747
|
+
now?: string;
|
|
1748
|
+
}): WorkflowTurnIntentRecord | undefined {
|
|
1749
|
+
validateKey(options.intentId, "workflow turn intent id");
|
|
1750
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
1751
|
+
validateKey(options.claimToken, "workflow turn claim token");
|
|
1752
|
+
validateDuration(options.leaseMs, "leaseMs");
|
|
1753
|
+
const nowMs = epoch(validTimestamp(options.now));
|
|
1754
|
+
const result = this.database
|
|
1755
|
+
.prepare(
|
|
1756
|
+
`UPDATE workflow_turn_intents
|
|
1757
|
+
SET delivery_claim_token = ?, delivery_claim_expires_at = ?
|
|
1758
|
+
WHERE intent_id = ? AND target_session_id = ? AND resolved_at IS NULL
|
|
1759
|
+
AND (delivery_claim_expires_at IS NULL OR delivery_claim_expires_at <= ?)`,
|
|
1760
|
+
)
|
|
1761
|
+
.run(
|
|
1762
|
+
options.claimToken,
|
|
1763
|
+
nowMs + options.leaseMs,
|
|
1764
|
+
options.intentId,
|
|
1765
|
+
options.targetSessionId,
|
|
1766
|
+
nowMs,
|
|
1767
|
+
);
|
|
1768
|
+
if (result.changes !== 1) return undefined;
|
|
1769
|
+
const row = this.database
|
|
1770
|
+
.prepare("SELECT * FROM workflow_turn_intents WHERE intent_id = ?")
|
|
1771
|
+
.get(options.intentId) as WorkflowTurnIntentRow;
|
|
1772
|
+
return workflowTurnIntentFromRow(row);
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
claimEligibleWorkflowTurnIntents(options: {
|
|
1776
|
+
targetSessionId: string;
|
|
1777
|
+
claimToken: string;
|
|
1778
|
+
leaseMs: number;
|
|
1779
|
+
limit?: number;
|
|
1780
|
+
now?: string;
|
|
1781
|
+
}): WorkflowTurnIntentRecord[] {
|
|
1782
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
1783
|
+
validateKey(options.claimToken, "workflow turn claim token");
|
|
1784
|
+
validateDuration(options.leaseMs, "leaseMs");
|
|
1785
|
+
const limit = options.limit ?? 20;
|
|
1786
|
+
if (!Number.isSafeInteger(limit) || limit <= 0 || limit > 100) {
|
|
1787
|
+
throw new Error("Workflow turn intent limit must be an integer from 1 through 100");
|
|
1788
|
+
}
|
|
1789
|
+
const now = validTimestamp(options.now);
|
|
1790
|
+
const nowMs = epoch(now);
|
|
1791
|
+
return this.transaction(() => {
|
|
1792
|
+
const ids = this.database
|
|
1793
|
+
.prepare(
|
|
1794
|
+
`SELECT intent_id FROM workflow_turn_intents
|
|
1795
|
+
WHERE target_session_id = ? AND resolved_at IS NULL AND eligible_at IS NOT NULL
|
|
1796
|
+
AND eligible_at <= ?
|
|
1797
|
+
AND (delivery_claim_expires_at IS NULL OR delivery_claim_expires_at <= ?)
|
|
1798
|
+
ORDER BY eligible_at, intent_id LIMIT ?`,
|
|
1799
|
+
)
|
|
1800
|
+
.all(options.targetSessionId, now, nowMs, limit) as { intent_id: string }[];
|
|
1801
|
+
if (ids.length === 0) return [];
|
|
1802
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
1803
|
+
this.database
|
|
1804
|
+
.prepare(
|
|
1805
|
+
`UPDATE workflow_turn_intents
|
|
1806
|
+
SET delivery_claim_token = ?, delivery_claim_expires_at = ?
|
|
1807
|
+
WHERE intent_id IN (${placeholders}) AND resolved_at IS NULL
|
|
1808
|
+
AND (delivery_claim_expires_at IS NULL OR delivery_claim_expires_at <= ?)`,
|
|
1809
|
+
)
|
|
1810
|
+
.run(
|
|
1811
|
+
options.claimToken,
|
|
1812
|
+
nowMs + options.leaseMs,
|
|
1813
|
+
...ids.map((row) => row.intent_id),
|
|
1814
|
+
nowMs,
|
|
1815
|
+
);
|
|
1816
|
+
const rows = this.database
|
|
1817
|
+
.prepare(
|
|
1818
|
+
`SELECT * FROM workflow_turn_intents WHERE delivery_claim_token = ?
|
|
1819
|
+
ORDER BY eligible_at, intent_id`,
|
|
1820
|
+
)
|
|
1821
|
+
.all(options.claimToken) as WorkflowTurnIntentRow[];
|
|
1822
|
+
return rows.map(workflowTurnIntentFromRow);
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
makeWorkflowTurnIntentEligible(options: {
|
|
1827
|
+
intentId: string;
|
|
1828
|
+
fallbackFacts: WorkflowTurnIntentFacts;
|
|
1829
|
+
now?: string;
|
|
1830
|
+
}): boolean {
|
|
1831
|
+
validateKey(options.intentId, "workflow turn intent id");
|
|
1832
|
+
const result = this.database
|
|
1833
|
+
.prepare(
|
|
1834
|
+
`UPDATE workflow_turn_intents
|
|
1835
|
+
SET fallback_facts_json = ?, eligible_at = COALESCE(eligible_at, ?)
|
|
1836
|
+
WHERE intent_id = ? AND resolved_at IS NULL`,
|
|
1837
|
+
)
|
|
1838
|
+
.run(
|
|
1839
|
+
workflowTurnIntentFactsJson(options.fallbackFacts),
|
|
1840
|
+
validTimestamp(options.now),
|
|
1841
|
+
options.intentId,
|
|
1842
|
+
);
|
|
1843
|
+
return result.changes === 1;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
resolveWorkflowTurnIntent(options: {
|
|
1847
|
+
intentId: string;
|
|
1848
|
+
targetSessionId: string;
|
|
1849
|
+
claimToken: string;
|
|
1850
|
+
resolution: WorkflowTurnIntentResolution;
|
|
1851
|
+
messageId: string;
|
|
1852
|
+
now?: string;
|
|
1853
|
+
}): boolean {
|
|
1854
|
+
validateKey(options.intentId, "workflow turn intent id");
|
|
1855
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
1856
|
+
validateKey(options.claimToken, "workflow turn claim token");
|
|
1857
|
+
validateWorkflowTurnIntentResolution(options.resolution);
|
|
1858
|
+
validateKey(options.messageId, "workflow turn resolution message id");
|
|
1859
|
+
const result = this.database
|
|
1860
|
+
.prepare(
|
|
1861
|
+
`UPDATE workflow_turn_intents
|
|
1862
|
+
SET resolved_at = ?, resolution = ?, resolution_message_id = ?,
|
|
1863
|
+
delivery_claim_token = NULL, delivery_claim_expires_at = NULL
|
|
1864
|
+
WHERE intent_id = ? AND target_session_id = ? AND resolved_at IS NULL
|
|
1865
|
+
AND delivery_claim_token = ?`,
|
|
1866
|
+
)
|
|
1867
|
+
.run(
|
|
1868
|
+
validTimestamp(options.now),
|
|
1869
|
+
options.resolution,
|
|
1870
|
+
options.messageId,
|
|
1871
|
+
options.intentId,
|
|
1872
|
+
options.targetSessionId,
|
|
1873
|
+
options.claimToken,
|
|
1874
|
+
);
|
|
1875
|
+
return result.changes === 1;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
releaseWorkflowTurnIntentClaim(options: {
|
|
1879
|
+
intentId: string;
|
|
1880
|
+
targetSessionId: string;
|
|
1881
|
+
claimToken: string;
|
|
1882
|
+
}): boolean {
|
|
1883
|
+
validateKey(options.intentId, "workflow turn intent id");
|
|
1884
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
1885
|
+
validateKey(options.claimToken, "workflow turn claim token");
|
|
1886
|
+
const result = this.database
|
|
1887
|
+
.prepare(
|
|
1888
|
+
`UPDATE workflow_turn_intents
|
|
1889
|
+
SET delivery_claim_token = NULL, delivery_claim_expires_at = NULL
|
|
1890
|
+
WHERE intent_id = ? AND target_session_id = ? AND resolved_at IS NULL
|
|
1891
|
+
AND delivery_claim_token = ?`,
|
|
1892
|
+
)
|
|
1893
|
+
.run(options.intentId, options.targetSessionId, options.claimToken);
|
|
1894
|
+
return result.changes === 1;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
listWorkflowTurnIntents(
|
|
1898
|
+
options: {
|
|
1899
|
+
targetSessionId?: string;
|
|
1900
|
+
runId?: string;
|
|
1901
|
+
limit?: number;
|
|
1902
|
+
} = {},
|
|
1903
|
+
): WorkflowTurnIntentRecord[] {
|
|
1904
|
+
if (options.targetSessionId !== undefined) {
|
|
1905
|
+
validateKey(options.targetSessionId, "workflow turn target session id");
|
|
1906
|
+
}
|
|
1907
|
+
if (options.runId !== undefined) validateRunId(options.runId);
|
|
1908
|
+
const limit = options.limit ?? 20;
|
|
1909
|
+
if (!Number.isSafeInteger(limit) || limit <= 0 || limit > 100) {
|
|
1910
|
+
throw new Error("Workflow turn intent limit must be an integer from 1 through 100");
|
|
1911
|
+
}
|
|
1912
|
+
const clauses: string[] = [];
|
|
1913
|
+
const values: unknown[] = [];
|
|
1914
|
+
if (options.targetSessionId !== undefined) {
|
|
1915
|
+
clauses.push("target_session_id = ?");
|
|
1916
|
+
values.push(options.targetSessionId);
|
|
1917
|
+
}
|
|
1918
|
+
if (options.runId !== undefined) {
|
|
1919
|
+
clauses.push("run_id = ?");
|
|
1920
|
+
values.push(options.runId);
|
|
1921
|
+
}
|
|
1922
|
+
const where = clauses.length === 0 ? "" : `WHERE ${clauses.join(" AND ")}`;
|
|
1923
|
+
const rows = this.database
|
|
1924
|
+
.prepare(
|
|
1925
|
+
`SELECT * FROM workflow_turn_intents ${where}
|
|
1926
|
+
ORDER BY requested_at DESC, intent_id DESC LIMIT ?`,
|
|
1927
|
+
)
|
|
1928
|
+
.all(...values, limit) as WorkflowTurnIntentRow[];
|
|
1929
|
+
return rows.map(workflowTurnIntentFromRow);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1529
1932
|
enqueueWorkflowNotification(options: {
|
|
1530
1933
|
runId: string;
|
|
1531
1934
|
nodeId: string;
|
|
1532
1935
|
attemptId: string;
|
|
1533
1936
|
notificationIndex: number;
|
|
1534
1937
|
targetSessionId: string;
|
|
1535
|
-
kind: "progress" | "final"
|
|
1938
|
+
kind: "progress" | "final";
|
|
1536
1939
|
content: string;
|
|
1537
1940
|
notificationId?: string;
|
|
1538
1941
|
now?: string;
|
|
@@ -1766,7 +2169,7 @@ const SCHEMA_SQL = `
|
|
|
1766
2169
|
attempt_id TEXT NOT NULL,
|
|
1767
2170
|
notification_index INTEGER NOT NULL CHECK (notification_index > 0),
|
|
1768
2171
|
target_session_id TEXT NOT NULL,
|
|
1769
|
-
kind TEXT NOT NULL CHECK (kind IN ('progress', 'final'
|
|
2172
|
+
kind TEXT NOT NULL CHECK (kind IN ('progress', 'final')),
|
|
1770
2173
|
content TEXT NOT NULL,
|
|
1771
2174
|
created_at TEXT NOT NULL,
|
|
1772
2175
|
delivery_claim_token TEXT,
|
|
@@ -1778,6 +2181,40 @@ const SCHEMA_SQL = `
|
|
|
1778
2181
|
ON workflow_notifications(target_session_id, delivery_claim_expires_at, created_at)
|
|
1779
2182
|
WHERE delivered_at IS NULL;
|
|
1780
2183
|
|
|
2184
|
+
CREATE TABLE IF NOT EXISTS workflow_turn_intents (
|
|
2185
|
+
intent_id TEXT PRIMARY KEY,
|
|
2186
|
+
source_event_id TEXT NOT NULL,
|
|
2187
|
+
run_id TEXT NOT NULL,
|
|
2188
|
+
workflow_ref TEXT NOT NULL,
|
|
2189
|
+
target_session_id TEXT NOT NULL,
|
|
2190
|
+
cause TEXT NOT NULL CHECK (
|
|
2191
|
+
cause IN ('agentCancelled', 'timedOut', 'failed', 'launchFailed',
|
|
2192
|
+
'controllerInterrupted', 'claimLost')
|
|
2193
|
+
),
|
|
2194
|
+
node_id TEXT,
|
|
2195
|
+
attempt_id TEXT,
|
|
2196
|
+
fallback_facts_json TEXT NOT NULL,
|
|
2197
|
+
requested_at TEXT NOT NULL,
|
|
2198
|
+
eligible_at TEXT,
|
|
2199
|
+
resolved_at TEXT,
|
|
2200
|
+
resolution TEXT CHECK (resolution IN ('workflowPrompt', 'presentation', 'fallback')),
|
|
2201
|
+
resolution_message_id TEXT,
|
|
2202
|
+
delivery_claim_token TEXT,
|
|
2203
|
+
delivery_claim_expires_at INTEGER,
|
|
2204
|
+
CHECK (
|
|
2205
|
+
(resolved_at IS NULL AND resolution IS NULL AND resolution_message_id IS NULL) OR
|
|
2206
|
+
(resolved_at IS NOT NULL AND resolution IS NOT NULL AND resolution_message_id IS NOT NULL)
|
|
2207
|
+
)
|
|
2208
|
+
);
|
|
2209
|
+
CREATE UNIQUE INDEX IF NOT EXISTS workflow_turn_intents_one_pending_run
|
|
2210
|
+
ON workflow_turn_intents(run_id, target_session_id) WHERE resolved_at IS NULL;
|
|
2211
|
+
CREATE INDEX IF NOT EXISTS workflow_turn_intents_pending_run
|
|
2212
|
+
ON workflow_turn_intents(run_id, target_session_id, requested_at)
|
|
2213
|
+
WHERE resolved_at IS NULL;
|
|
2214
|
+
CREATE INDEX IF NOT EXISTS workflow_turn_intents_eligible_session
|
|
2215
|
+
ON workflow_turn_intents(target_session_id, eligible_at, intent_id)
|
|
2216
|
+
WHERE resolved_at IS NULL AND eligible_at IS NOT NULL;
|
|
2217
|
+
|
|
1781
2218
|
CREATE TABLE IF NOT EXISTS effects (
|
|
1782
2219
|
effect_key TEXT NOT NULL,
|
|
1783
2220
|
resource_uid TEXT NOT NULL,
|
|
@@ -1868,7 +2305,56 @@ function workflowFromRow(row: WorkflowRow): ChildWorkflowRecord {
|
|
|
1868
2305
|
};
|
|
1869
2306
|
}
|
|
1870
2307
|
|
|
2308
|
+
function workflowTurnIntentFromRow(row: WorkflowTurnIntentRow): WorkflowTurnIntentRecord {
|
|
2309
|
+
const cause = validateWorkflowTurnIntentCause(row.cause);
|
|
2310
|
+
const resolution =
|
|
2311
|
+
row.resolution === null ? null : validateWorkflowTurnIntentResolution(row.resolution);
|
|
2312
|
+
const fallbackFacts = parseStoredJson<unknown>(
|
|
2313
|
+
row.fallback_facts_json,
|
|
2314
|
+
"workflow turn intent facts",
|
|
2315
|
+
);
|
|
2316
|
+
validateWorkflowTurnIntentFacts(fallbackFacts);
|
|
2317
|
+
validateKey(row.intent_id, "workflow turn intent id");
|
|
2318
|
+
validateKey(row.source_event_id, "workflow turn source event id");
|
|
2319
|
+
validateRunId(row.run_id);
|
|
2320
|
+
validateKey(row.workflow_ref, "workflow turn workflow ref");
|
|
2321
|
+
validateKey(row.target_session_id, "workflow turn target session id");
|
|
2322
|
+
validTimestamp(row.requested_at);
|
|
2323
|
+
if (row.eligible_at !== null) validTimestamp(row.eligible_at);
|
|
2324
|
+
if (row.resolved_at !== null) validTimestamp(row.resolved_at);
|
|
2325
|
+
const allResolutionFieldsNull =
|
|
2326
|
+
row.resolved_at === null && row.resolution === null && row.resolution_message_id === null;
|
|
2327
|
+
const allResolutionFieldsSet =
|
|
2328
|
+
row.resolved_at !== null && row.resolution !== null && row.resolution_message_id !== null;
|
|
2329
|
+
if (!allResolutionFieldsNull && !allResolutionFieldsSet) {
|
|
2330
|
+
throw new Error(`Workflow turn intent ${row.intent_id} has inconsistent resolution fields`);
|
|
2331
|
+
}
|
|
2332
|
+
return {
|
|
2333
|
+
intentId: row.intent_id,
|
|
2334
|
+
sourceEventId: row.source_event_id,
|
|
2335
|
+
runId: row.run_id,
|
|
2336
|
+
workflowRef: row.workflow_ref,
|
|
2337
|
+
targetSessionId: row.target_session_id,
|
|
2338
|
+
cause,
|
|
2339
|
+
nodeId: row.node_id,
|
|
2340
|
+
attemptId: row.attempt_id,
|
|
2341
|
+
fallbackFacts,
|
|
2342
|
+
requestedAt: row.requested_at,
|
|
2343
|
+
eligibleAt: row.eligible_at,
|
|
2344
|
+
resolvedAt: row.resolved_at,
|
|
2345
|
+
resolution,
|
|
2346
|
+
resolutionMessageId: row.resolution_message_id,
|
|
2347
|
+
deliveryClaimExpiresAt:
|
|
2348
|
+
row.delivery_claim_expires_at === null ? null : iso(row.delivery_claim_expires_at),
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
|
|
1871
2352
|
function workflowNotificationFromRow(row: WorkflowNotificationRow): WorkflowNotificationRecord {
|
|
2353
|
+
if (row.kind !== "progress" && row.kind !== "final") {
|
|
2354
|
+
throw new Error(
|
|
2355
|
+
"Controller store contains an incompatible pending alpha workflow notification. Preserve needed run bundles, then reset the project controller store.",
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
1872
2358
|
return {
|
|
1873
2359
|
notificationId: row.notification_id,
|
|
1874
2360
|
runId: row.run_id,
|
|
@@ -1885,6 +2371,93 @@ function workflowNotificationFromRow(row: WorkflowNotificationRow): WorkflowNoti
|
|
|
1885
2371
|
};
|
|
1886
2372
|
}
|
|
1887
2373
|
|
|
2374
|
+
function workflowTurnIntentFactsJson(facts: WorkflowTurnIntentFacts): string {
|
|
2375
|
+
validateWorkflowTurnIntentFacts(facts);
|
|
2376
|
+
const json = canonicalJson(facts, "workflow turn intent facts");
|
|
2377
|
+
validateJsonSize(json, "Workflow turn intent facts", MAX_EVENT_BYTES);
|
|
2378
|
+
return json;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
function validateWorkflowTurnIntentFacts(facts: unknown): asserts facts is WorkflowTurnIntentFacts {
|
|
2382
|
+
if (facts === null || typeof facts !== "object" || Array.isArray(facts)) {
|
|
2383
|
+
throw new Error("Workflow turn intent facts must be an object");
|
|
2384
|
+
}
|
|
2385
|
+
const candidate = facts as Record<string, unknown>;
|
|
2386
|
+
if (candidate.schema !== TURN_INTENT_FACTS_SCHEMA) {
|
|
2387
|
+
throw new Error(`Workflow turn intent facts schema must be ${TURN_INTENT_FACTS_SCHEMA}`);
|
|
2388
|
+
}
|
|
2389
|
+
const allowedKeys = new Set([
|
|
2390
|
+
"schema",
|
|
2391
|
+
"workflowName",
|
|
2392
|
+
"runId",
|
|
2393
|
+
"observedState",
|
|
2394
|
+
"cause",
|
|
2395
|
+
"nodeId",
|
|
2396
|
+
"attemptId",
|
|
2397
|
+
"reason",
|
|
2398
|
+
"handoff",
|
|
2399
|
+
]);
|
|
2400
|
+
for (const key of Object.keys(candidate)) {
|
|
2401
|
+
if (!allowedKeys.has(key)) throw new Error(`Unknown workflow turn intent facts field: ${key}`);
|
|
2402
|
+
}
|
|
2403
|
+
if (typeof candidate.workflowName !== "string") {
|
|
2404
|
+
throw new Error("Workflow turn intent workflowName must be a string");
|
|
2405
|
+
}
|
|
2406
|
+
if (typeof candidate.runId !== "string") {
|
|
2407
|
+
throw new Error("Workflow turn intent runId must be a string");
|
|
2408
|
+
}
|
|
2409
|
+
if (typeof candidate.observedState !== "string") {
|
|
2410
|
+
throw new Error("Workflow turn intent observedState must be a string");
|
|
2411
|
+
}
|
|
2412
|
+
if (typeof candidate.cause !== "string") {
|
|
2413
|
+
throw new Error("Workflow turn intent cause must be a string");
|
|
2414
|
+
}
|
|
2415
|
+
validateKey(candidate.workflowName, "workflow turn facts workflow name");
|
|
2416
|
+
validateRunId(candidate.runId);
|
|
2417
|
+
validateKey(candidate.observedState, "workflow turn facts observed state");
|
|
2418
|
+
validateWorkflowTurnIntentCause(candidate.cause);
|
|
2419
|
+
if (candidate.nodeId !== null && typeof candidate.nodeId !== "string") {
|
|
2420
|
+
throw new Error("Workflow turn intent nodeId must be a string or null");
|
|
2421
|
+
}
|
|
2422
|
+
if (candidate.attemptId !== null && typeof candidate.attemptId !== "string") {
|
|
2423
|
+
throw new Error("Workflow turn intent attemptId must be a string or null");
|
|
2424
|
+
}
|
|
2425
|
+
if (candidate.reason !== null && typeof candidate.reason !== "string") {
|
|
2426
|
+
throw new Error("Workflow turn intent reason must be a string or null");
|
|
2427
|
+
}
|
|
2428
|
+
if (candidate.nodeId !== null) validateKey(candidate.nodeId, "workflow turn facts node id");
|
|
2429
|
+
if (candidate.attemptId !== null) {
|
|
2430
|
+
validateKey(candidate.attemptId, "workflow turn facts attempt id");
|
|
2431
|
+
}
|
|
2432
|
+
if (candidate.reason !== null && candidate.reason.length > MAX_ERROR_LENGTH) {
|
|
2433
|
+
throw new Error(`Workflow turn intent reason must be at most ${MAX_ERROR_LENGTH} characters`);
|
|
2434
|
+
}
|
|
2435
|
+
if (typeof candidate.handoff !== "boolean") {
|
|
2436
|
+
throw new Error("Workflow turn intent handoff must be a boolean");
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
function validateWorkflowTurnIntentCause(value: string): WorkflowTurnIntentCause {
|
|
2441
|
+
if (
|
|
2442
|
+
value !== "agentCancelled" &&
|
|
2443
|
+
value !== "timedOut" &&
|
|
2444
|
+
value !== "failed" &&
|
|
2445
|
+
value !== "launchFailed" &&
|
|
2446
|
+
value !== "controllerInterrupted" &&
|
|
2447
|
+
value !== "claimLost"
|
|
2448
|
+
) {
|
|
2449
|
+
throw new Error(`Invalid workflow turn intent cause: ${value}`);
|
|
2450
|
+
}
|
|
2451
|
+
return value;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
function validateWorkflowTurnIntentResolution(value: string): WorkflowTurnIntentResolution {
|
|
2455
|
+
if (value !== "workflowPrompt" && value !== "presentation" && value !== "fallback") {
|
|
2456
|
+
throw new Error(`Invalid workflow turn intent resolution: ${value}`);
|
|
2457
|
+
}
|
|
2458
|
+
return value;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
1888
2461
|
function workflowRunFromRow(row: WorkflowRunQueueRow): WorkflowRunQueueRecord {
|
|
1889
2462
|
return {
|
|
1890
2463
|
runId: row.run_id,
|