@osolmaz/pi-workflows 0.13.2 → 0.13.3

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 (130) hide show
  1. package/README.md +5 -3
  2. package/dist/builtins/autodoc.workflow.d.ts +5 -5
  3. package/dist/builtins/autoimplement.workflow.d.ts +65 -61
  4. package/dist/builtins/autoimplement.workflow.js +47 -8
  5. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  6. package/dist/builtins/autoplan.workflow.d.ts +5 -5
  7. package/dist/builtins/catalog.js +1 -1
  8. package/dist/builtins/change-verification.workflow.d.ts +1 -1
  9. package/dist/builtins/plain-summary.workflow.d.ts +1 -1
  10. package/dist/builtins/plan-approval.workflow.d.ts +1 -1
  11. package/dist/builtins/plan-change.workflow.d.ts +23 -23
  12. package/dist/builtins/sanity-check.workflow.d.ts +3 -3
  13. package/dist/builtins/workspace-preparation.workflow.d.ts +1 -1
  14. package/dist/controllers/index.d.ts +2 -2
  15. package/dist/controllers/index.js.map +1 -1
  16. package/dist/controllers/sqlite.js +85 -23
  17. package/dist/controllers/sqlite.js.map +1 -1
  18. package/dist/controllers/types.d.ts +34 -0
  19. package/dist/controllers/workflow-engine-scheduler.d.ts +5 -1
  20. package/dist/controllers/workflow-engine-scheduler.js +83 -1
  21. package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
  22. package/dist/controllers/workflows.d.ts +8 -1
  23. package/dist/controllers/workflows.js +36 -0
  24. package/dist/controllers/workflows.js.map +1 -1
  25. package/dist/extension/controller-host.d.ts +2 -1
  26. package/dist/extension/controller-host.js +1 -1
  27. package/dist/extension/controller-host.js.map +1 -1
  28. package/dist/extension/follow-up-coordinator.d.ts +27 -0
  29. package/dist/extension/follow-up-coordinator.js +131 -0
  30. package/dist/extension/follow-up-coordinator.js.map +1 -0
  31. package/dist/extension/index.d.ts +9 -0
  32. package/dist/extension/index.js +460 -75
  33. package/dist/extension/index.js.map +1 -1
  34. package/dist/extension/session-events.d.ts +2 -3
  35. package/dist/extension/session-events.js +11 -10
  36. package/dist/extension/session-events.js.map +1 -1
  37. package/dist/extension/widget.js +9 -0
  38. package/dist/extension/widget.js.map +1 -1
  39. package/dist/state/database.d.ts +2 -1
  40. package/dist/state/database.js +11 -13
  41. package/dist/state/database.js.map +1 -1
  42. package/dist/state/json.js +6 -1
  43. package/dist/state/json.js.map +1 -1
  44. package/dist/state/mutation.d.ts +1 -1
  45. package/dist/state/mutation.js.map +1 -1
  46. package/dist/state/prune.d.ts +18 -0
  47. package/dist/state/prune.js +373 -0
  48. package/dist/state/prune.js.map +1 -0
  49. package/dist/state/schema.d.ts +1 -1
  50. package/dist/state/schema.js +116 -18
  51. package/dist/state/schema.js.map +1 -1
  52. package/dist/viewer/cli.d.ts +3 -1
  53. package/dist/viewer/cli.js +55 -4
  54. package/dist/viewer/cli.js.map +1 -1
  55. package/dist/viewer/render.js +19 -1
  56. package/dist/viewer/render.js.map +1 -1
  57. package/dist/workflows/composition.d.ts +2 -0
  58. package/dist/workflows/composition.js +15 -0
  59. package/dist/workflows/composition.js.map +1 -1
  60. package/dist/workflows/definition.d.ts +6 -3
  61. package/dist/workflows/definition.js +3 -0
  62. package/dist/workflows/definition.js.map +1 -1
  63. package/dist/workflows/engine.d.ts +6 -1
  64. package/dist/workflows/engine.js +301 -35
  65. package/dist/workflows/engine.js.map +1 -1
  66. package/dist/workflows/human-decision.d.ts +1 -0
  67. package/dist/workflows/human-decision.js +25 -9
  68. package/dist/workflows/human-decision.js.map +1 -1
  69. package/dist/workflows/index.d.ts +3 -1
  70. package/dist/workflows/index.js +2 -0
  71. package/dist/workflows/index.js.map +1 -1
  72. package/dist/workflows/json-patch.d.ts +47 -0
  73. package/dist/workflows/json-patch.js +298 -0
  74. package/dist/workflows/json-patch.js.map +1 -0
  75. package/dist/workflows/schema.js +29 -1
  76. package/dist/workflows/schema.js.map +1 -1
  77. package/dist/workflows/settings.d.ts +128 -0
  78. package/dist/workflows/settings.js +199 -0
  79. package/dist/workflows/settings.js.map +1 -0
  80. package/dist/workflows/store.d.ts +75 -2
  81. package/dist/workflows/store.js +1349 -89
  82. package/dist/workflows/store.js.map +1 -1
  83. package/dist/workflows/tool-input.d.ts +22 -0
  84. package/dist/workflows/tool-input.js +43 -0
  85. package/dist/workflows/tool-input.js.map +1 -1
  86. package/dist/workflows/types.d.ts +40 -5
  87. package/docs/2026-08-25-workflow-follow-ups.md +132 -0
  88. package/docs/2026-08-25-workflow-settings.md +169 -0
  89. package/docs/DEFERRED_TURNS.md +6 -0
  90. package/docs/DESIGN_PHILOSOPHY.md +2 -0
  91. package/docs/SQLITE_STATE.md +31 -18
  92. package/docs/WORKFLOW_COMPOSITION.md +15 -0
  93. package/docs/WORKFLOW_STEP_MESSAGES.md +4 -2
  94. package/docs/plans/2026-08-25-live-workflow-settings-plan.md +532 -0
  95. package/docs/plans/2026-08-25-workflow-run-storage-plan.md +67 -0
  96. package/docs/tui-viewer.md +7 -6
  97. package/docs/workflows.md +22 -1
  98. package/examples/workflows/live-settings.workflow.ts +93 -0
  99. package/herdr-plugin.toml +1 -1
  100. package/package.json +1 -1
  101. package/src/builtins/autoimplement.workflow.ts +56 -8
  102. package/src/builtins/catalog.ts +1 -1
  103. package/src/controllers/index.ts +6 -0
  104. package/src/controllers/sqlite.ts +129 -35
  105. package/src/controllers/types.ts +40 -0
  106. package/src/controllers/workflow-engine-scheduler.ts +103 -1
  107. package/src/controllers/workflows.ts +68 -0
  108. package/src/extension/controller-host.ts +6 -1
  109. package/src/extension/follow-up-coordinator.ts +151 -0
  110. package/src/extension/index.ts +538 -88
  111. package/src/extension/session-events.ts +15 -13
  112. package/src/extension/widget.ts +8 -0
  113. package/src/state/database.ts +12 -12
  114. package/src/state/json.ts +6 -1
  115. package/src/state/mutation.ts +4 -1
  116. package/src/state/prune.ts +502 -0
  117. package/src/state/schema.ts +116 -18
  118. package/src/viewer/cli.ts +52 -5
  119. package/src/viewer/render.ts +43 -1
  120. package/src/workflows/composition.ts +19 -0
  121. package/src/workflows/definition.ts +19 -5
  122. package/src/workflows/engine.ts +363 -35
  123. package/src/workflows/human-decision.ts +41 -11
  124. package/src/workflows/index.ts +43 -0
  125. package/src/workflows/json-patch.ts +375 -0
  126. package/src/workflows/schema.ts +31 -1
  127. package/src/workflows/settings.ts +430 -0
  128. package/src/workflows/store.ts +1977 -184
  129. package/src/workflows/tool-input.ts +58 -0
  130. package/src/workflows/types.ts +43 -4
@@ -0,0 +1,93 @@
1
+ import {
2
+ agent,
3
+ allowSettingsPath,
4
+ compute,
5
+ defineWorkflow,
6
+ settingsRoute,
7
+ workflowSettings,
8
+ } from "@osolmaz/pi-workflows";
9
+
10
+ type Settings = {
11
+ instructions: string[];
12
+ route: "normal" | "careful";
13
+ variables: Record<string, unknown>;
14
+ };
15
+
16
+ function parseSettings(value: unknown): Settings {
17
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
18
+ throw new Error("settings must be an object");
19
+ }
20
+ const settings = value as Partial<Settings>;
21
+ if (
22
+ !Array.isArray(settings.instructions) ||
23
+ !settings.instructions.every((item) => typeof item === "string") ||
24
+ (settings.route !== "normal" && settings.route !== "careful") ||
25
+ settings.variables === null ||
26
+ typeof settings.variables !== "object" ||
27
+ Array.isArray(settings.variables)
28
+ ) {
29
+ throw new Error("settings are invalid");
30
+ }
31
+ return {
32
+ instructions: [...settings.instructions],
33
+ route: settings.route,
34
+ variables: { ...settings.variables },
35
+ };
36
+ }
37
+
38
+ export default defineWorkflow({
39
+ name: "live-settings",
40
+ settings: workflowSettings<Settings>({
41
+ initial: { instructions: [], route: "normal", variables: {} },
42
+ parse: parseSettings,
43
+ description: "Instructions, variables, and the future route for this example.",
44
+ paths: [
45
+ allowSettingsPath("/instructions", {
46
+ read: ["session", "human"],
47
+ add: ["session", "human"],
48
+ remove: ["session", "human"],
49
+ replace: ["session", "human"],
50
+ }),
51
+ allowSettingsPath("/route", {
52
+ read: ["session", "human"],
53
+ replace: ["session", "human"],
54
+ }),
55
+ allowSettingsPath("/variables", {
56
+ read: ["session", "human"],
57
+ add: ["session", "human"],
58
+ remove: ["session", "human"],
59
+ replace: ["session", "human"],
60
+ }),
61
+ ],
62
+ }),
63
+ startAt: "prepare",
64
+ nodes: {
65
+ prepare: agent({
66
+ prompt: ({ settings }) =>
67
+ [
68
+ "Prepare the requested work.",
69
+ `Current instructions and variables: ${JSON.stringify(settings)}`,
70
+ "The user can change future settings or queue several post-completion prompts while this step runs.",
71
+ ].join("\n"),
72
+ expectedOutput: `{ "prepared": "summary" }`,
73
+ }),
74
+ choose: settingsRoute({
75
+ run: ({ settings }) => ({ route: (settings as Settings).route }),
76
+ }),
77
+ normal: compute({
78
+ run: ({ settings }) => ({ mode: "normal", settings }),
79
+ }),
80
+ careful: compute({
81
+ run: ({ settings }) => ({ mode: "careful", settings }),
82
+ }),
83
+ },
84
+ edges: [
85
+ { from: "prepare", to: "choose" },
86
+ {
87
+ from: "choose",
88
+ switch: { on: "$.route", cases: { normal: "normal", careful: "careful" } },
89
+ },
90
+ ],
91
+ presentationPrompt:
92
+ "Explain which route and settings were used. Any queued follow-up prompts run only after this response settles.",
93
+ });
package/herdr-plugin.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  id = "osolmaz.pi-workflows"
2
2
  name = "pi-workflows"
3
- version = "0.13.2"
3
+ version = "0.13.3"
4
4
  min_herdr_version = "0.7.0"
5
5
  description = "Open the active pi-workflows run in piw from a managed Herdr pane."
6
6
  platforms = ["linux", "macos"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osolmaz/pi-workflows",
3
- "version": "0.13.2",
3
+ "version": "0.13.3",
4
4
  "description": "Workflow and controller runtime with a live terminal viewer for the pi coding agent",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -13,6 +13,7 @@ import {
13
13
  includedResult,
14
14
  } from "../workflows/definition.js";
15
15
  import { digest } from "../workflows/human-decision.js";
16
+ import { allowSettingsPath, workflowSettings } from "../workflows/settings.js";
16
17
  import type { WorkflowActionContext, WorkflowNodeContext } from "../workflows/types.js";
17
18
  import autodocWorkflow, { type AutodocInput } from "./autodoc.workflow.js";
18
19
  import {
@@ -40,6 +41,11 @@ import workspacePreparationWorkflow, {
40
41
  type WorkspacePreparationInput,
41
42
  } from "./workspace-preparation.workflow.js";
42
43
 
44
+ export type AutoimplementSettings = {
45
+ merge: boolean;
46
+ addedInstructions: string[];
47
+ };
48
+
43
49
  export type AutoimplementInput = {
44
50
  task: string;
45
51
  plan?: unknown;
@@ -577,6 +583,27 @@ function parseInput(value: unknown): AutoimplementInput {
577
583
  };
578
584
  }
579
585
 
586
+ function parseAutoimplementSettings(value: unknown): AutoimplementSettings {
587
+ const settings = requireRecord(value, "autoimplement settings");
588
+ if (typeof settings.merge !== "boolean") {
589
+ throw new Error("autoimplement settings merge must be a boolean");
590
+ }
591
+ if (
592
+ !Array.isArray(settings.addedInstructions) ||
593
+ settings.addedInstructions.some((item) => typeof item !== "string")
594
+ ) {
595
+ throw new Error("autoimplement settings addedInstructions must be an array of strings");
596
+ }
597
+ return {
598
+ merge: settings.merge,
599
+ addedInstructions: [...settings.addedInstructions] as string[],
600
+ };
601
+ }
602
+
603
+ function autoimplementSettings(context: WorkflowNodeContext): AutoimplementSettings {
604
+ return parseAutoimplementSettings(context.settings);
605
+ }
606
+
580
607
  function parseExistingPlan(value: unknown): ExistingPlanDiscovery {
581
608
  const result = requireRecord(value, "existing plan discovery");
582
609
  if (result.route !== "found" && result.route !== "blocked") {
@@ -790,7 +817,7 @@ function createBlockerClaim(context: WorkflowNodeContext): BlockerClaim {
790
817
  unrelatedFailures: Array.isArray(result.unrelatedFailures) ? result.unrelatedFailures : [],
791
818
  recoveryAttempts: Array.isArray(result.repairAttempts) ? result.repairAttempts : [],
792
819
  alternativesChecked: [],
793
- authorityFact: `scope=${(context.input as AutoimplementInput).scope ?? "unspecified"}; merge=${(context.input as AutoimplementInput).merge === true}`,
820
+ authorityFact: `scope=${(context.input as AutoimplementInput).scope ?? "unspecified"}; merge=${autoimplementSettings(context).merge}`,
794
821
  };
795
822
  }
796
823
 
@@ -919,8 +946,8 @@ function parseDeliveryResult(
919
946
  if (result.status !== "completed" && result.status !== "blocked") {
920
947
  throw new Error("delivery status must be completed or blocked");
921
948
  }
922
- const request = context.input as AutoimplementInput;
923
- if (request.merge !== true && result.merged === true) {
949
+ const settings = autoimplementSettings(context);
950
+ if (settings.merge !== true && result.merged === true) {
924
951
  throw new Error("delivery cannot merge without explicit merge: true");
925
952
  }
926
953
  if (result.status === "blocked") return result;
@@ -960,7 +987,7 @@ function parseDeliveryResult(
960
987
  }
961
988
  actual.set(repository.repository, repository);
962
989
  }
963
- const mergeExpected = request.merge === true;
990
+ const mergeExpected = settings.merge === true;
964
991
  for (const expected of published) {
965
992
  const repository = actual.get(path.resolve(expected.repository));
966
993
  if (repository === undefined || repository.pr !== expected.pr) {
@@ -1298,6 +1325,28 @@ export const autoimplementWorkflow = defineWorkflow({
1298
1325
  contractId: "pi-workflows.autoimplement.v1",
1299
1326
  name: "autoimplement",
1300
1327
  input: parseInput,
1328
+ settings: workflowSettings<AutoimplementSettings, AutoimplementInput>({
1329
+ initial: (input) => ({ merge: input.merge === true, addedInstructions: [] }),
1330
+ parse: parseAutoimplementSettings,
1331
+ description: "Future merge behavior and instructions added during this run.",
1332
+ paths: [
1333
+ allowSettingsPath("/merge", {
1334
+ read: ["session", "human"],
1335
+ replace: ["session", "human"],
1336
+ }),
1337
+ allowSettingsPath("/addedInstructions", {
1338
+ read: ["session", "human"],
1339
+ add: ["session", "human"],
1340
+ remove: ["session", "human"],
1341
+ replace: ["session", "human"],
1342
+ }),
1343
+ ],
1344
+ validateChange: ({ before, after, actor }) => {
1345
+ if (actor.type === "session" && before.merge === false && after.merge === true) {
1346
+ throw new Error("A model workflow step cannot grant merge authority");
1347
+ }
1348
+ },
1349
+ }),
1301
1350
  title: ({ input }) => `autoimplement: ${input.task.slice(0, 60)}`,
1302
1351
  presentationPrompt:
1303
1352
  "Summarize what was implemented, the review rounds by severity, the CI result, the PR or merge result, and any remaining limitation. Include exact validation commands.",
@@ -1616,7 +1665,7 @@ export const autoimplementWorkflow = defineWorkflow({
1616
1665
  `Current result and claimed blocker: ${JSON.stringify(latestBlockerClaim(context))}`,
1617
1666
  `Authorized scope: ${request.scope ?? request.repository ?? "the current repository and task"}`,
1618
1667
  `Constraints and authority: ${JSON.stringify(request.constraints ?? [])}`,
1619
- `Merge authorized: ${request.merge === true}`,
1668
+ `Merge currently allowed: ${autoimplementSettings(context).merge === true}`,
1620
1669
  `Previous blocker challenges: ${JSON.stringify(blockerChallenges(context))}`,
1621
1670
  `Recent workflow attempts: ${JSON.stringify(recentWorkflowAttempts(context))}`,
1622
1671
  ].join("\n");
@@ -1925,10 +1974,9 @@ export const autoimplementWorkflow = defineWorkflow({
1925
1974
  timeoutMs: 30 * 60_000,
1926
1975
  statusDetail: "finalizing PRs",
1927
1976
  prompt: (context) => {
1928
- const request = context.input as AutoimplementInput;
1929
1977
  return [
1930
- request.merge === false
1931
- ? "Leave every verified PR ready without merging because input disabled merge."
1978
+ autoimplementSettings(context).merge === false
1979
+ ? "Leave every verified PR ready without merging because current workflow settings disable merge."
1932
1980
  : "Handle verified PRs one at a time and merge each unless repository policy or explicit user instructions prohibit it.",
1933
1981
  "Before each mutation, inspect the current PR head, merge state, and existing final report. Do not merge an already merged expected head or post a duplicate report.",
1934
1982
  "Use each repository's required merge method.",
@@ -11,7 +11,7 @@ export const builtinWorkflowCatalog = new BuiltinWorkflowCatalog([
11
11
  { id: "plain-summary", revision: "3", definition: plainSummaryWorkflow },
12
12
  { id: "autoplan", revision: "5", definition: autoplanWorkflow },
13
13
  { id: "autodoc", revision: "2", definition: autodocWorkflow },
14
- { id: "autoimplement", revision: "10", definition: autoimplementWorkflow },
14
+ { id: "autoimplement", revision: "11", definition: autoimplementWorkflow },
15
15
  { id: "plan-approval", revision: "4", definition: planApprovalWorkflow },
16
16
  { id: "sanity-check", revision: "6", definition: sanityCheckWorkflow },
17
17
  {
@@ -49,6 +49,7 @@ export {
49
49
  } from "./workflow-engine-scheduler.js";
50
50
  export {
51
51
  ControllerWorkflowCoordinator,
52
+ type ControllerWorkflowControlRequest,
52
53
  type ControllerWorkflowScheduler,
53
54
  type WorkflowSchedulerRequest,
54
55
  type WorkflowSchedulerResult,
@@ -64,6 +65,11 @@ export type {
64
65
  ControllerConditionStatus,
65
66
  ControllerDefinition,
66
67
  ControllerEffects,
68
+ ControllerFollowUpResult,
69
+ ControllerQueueFollowUpRequest,
70
+ ControllerRemoveFollowUpRequest,
71
+ ControllerSettingsChangeRequest,
72
+ ControllerSettingsChangeResult,
67
73
  ControllerEvent,
68
74
  ControllerQueueClaim,
69
75
  ControllerResource,
@@ -155,9 +155,8 @@ type RunRow = {
155
155
  workflowName: string;
156
156
  workflowRef: string;
157
157
  runStatus: string;
158
- workflowSourceHash: Buffer;
159
- workflowSourcesHash: Buffer | null;
160
158
  definitionDigest: Buffer;
159
+ definitionHash: Buffer;
161
160
  inputHash: Buffer;
162
161
  launchOptionsHash: Buffer;
163
162
  status: WorkflowRunLaunchStatus;
@@ -176,6 +175,12 @@ type RunRow = {
176
175
  ownerId: string | null;
177
176
  claimExpiresAt: number | null;
178
177
  };
178
+ type RunSourceIdentityRow = {
179
+ mountPath: string;
180
+ sourceType: "builtin" | "file";
181
+ sourceRef: string;
182
+ sourceRevision: string;
183
+ };
179
184
 
180
185
  type EffectRow = {
181
186
  effectId: string;
@@ -1049,9 +1054,6 @@ export class SqliteControllerStore implements ControllerStore {
1049
1054
  const resourceId = resourceIdFor("run", options.runId);
1050
1055
  const definitionHash = this.state.putJson(options.definitionSnapshot, now);
1051
1056
  const queuedSource = queuedWorkflowSource(options.workflowSource);
1052
- const sourceHash = this.state.putJson(queuedSource.root, now);
1053
- const mountedSourcesHash =
1054
- queuedSource.mounted.length === 0 ? null : this.state.putJson(queuedSource.mounted, now);
1055
1057
  const inputHash = this.state.putJson(options.input ?? null, now);
1056
1058
  const launchHash = this.state.putJson(options.launchOptions ?? {}, now);
1057
1059
  this.state.connection
@@ -1071,15 +1073,13 @@ export class SqliteControllerStore implements ControllerStore {
1071
1073
  this.state.connection
1072
1074
  .prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0)")
1073
1075
  .run(resourceId);
1074
- const source = sourceParts(options.workflowSourceRef, options.definitionDigest);
1075
1076
  this.state.connection
1076
1077
  .prepare(
1077
1078
  `INSERT INTO runs(
1078
1079
  run_id, resource_id, project_id, parent_run_id, definition_digest,
1079
- workflow_ref, workflow_source_hash, launch_options_hash,
1080
- source_type, source_ref, source_revision, status, paused,
1081
- input_hash, workflow_sources_hash, created_at, updated_at
1082
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', 0, ?, ?, ?, ?)`,
1080
+ workflow_ref, launch_options_hash, status, paused,
1081
+ input_hash, created_at, updated_at
1082
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'queued', 0, ?, ?, ?)`,
1083
1083
  )
1084
1084
  .run(
1085
1085
  options.runId,
@@ -1088,16 +1088,12 @@ export class SqliteControllerStore implements ControllerStore {
1088
1088
  options.parentRunId ?? null,
1089
1089
  definitionDigest,
1090
1090
  options.workflowSourceRef,
1091
- sourceHash,
1092
1091
  launchHash,
1093
- source.type,
1094
- source.ref,
1095
- source.revision,
1096
1092
  inputHash,
1097
- mountedSourcesHash,
1098
1093
  now,
1099
1094
  now,
1100
1095
  );
1096
+ insertQueuedRunSources(this.state, options.runId, queuedSource);
1101
1097
  this.state.connection
1102
1098
  .prepare(
1103
1099
  `INSERT INTO run_bindings(run_id, origin_session_id, execution_mode, created_at)
@@ -2238,11 +2234,7 @@ export class SqliteControllerStore implements ControllerStore {
2238
2234
  runId: row.runId,
2239
2235
  workflowName: row.workflowName,
2240
2236
  workflowSourceRef: row.workflowRef,
2241
- workflowSource: {
2242
- root: this.state.readJson(row.workflowSourceHash),
2243
- mounted:
2244
- row.workflowSourcesHash === null ? [] : this.state.readJson(row.workflowSourcesHash),
2245
- },
2237
+ workflowSource: readQueuedRunSources(this.state, row),
2246
2238
  initialized: row.runStatus !== "queued",
2247
2239
  definitionDigest: `sha256:${row.definitionDigest.toString("hex")}`,
2248
2240
  input: this.state.readJson(row.inputHash),
@@ -2469,8 +2461,6 @@ export class SqliteControllerStore implements ControllerStore {
2469
2461
  .prepare(
2470
2462
  `UPDATE runs
2471
2463
  SET status = ?, paused = 0, status_detail = NULL, error_hash = ?,
2472
- current_node = NULL, current_attempt_id = NULL,
2473
- current_node_started_at = NULL, waiting_on = NULL,
2474
2464
  updated_at = ?, finished_at = ?
2475
2465
  WHERE run_id = ?`,
2476
2466
  )
@@ -2776,9 +2766,8 @@ function workflowSelect(clause: string): string {
2776
2766
  function workflowRunSelect(clause: string): string {
2777
2767
  return `SELECT r.run_id AS runId, r.resource_id AS resourceId,
2778
2768
  d.workflow_name AS workflowName, r.workflow_ref AS workflowRef, r.status AS runStatus,
2779
- r.workflow_source_hash AS workflowSourceHash,
2780
- r.workflow_sources_hash AS workflowSourcesHash,
2781
- r.definition_digest AS definitionDigest, r.input_hash AS inputHash,
2769
+ r.definition_digest AS definitionDigest, d.definition_hash AS definitionHash,
2770
+ r.input_hash AS inputHash,
2782
2771
  r.launch_options_hash AS launchOptionsHash,
2783
2772
  q.status, q.available_at AS availableAt, q.affinity_runner_id AS affinityRunnerId,
2784
2773
  q.consecutive_errors AS consecutiveErrors, q.error_code AS errorCode,
@@ -2886,6 +2875,9 @@ function isLeaseRow(value: unknown): value is LeaseRow {
2886
2875
  function isRunRow(value: unknown): value is RunRow {
2887
2876
  return isRecord(value);
2888
2877
  }
2878
+ function isRunSourceIdentityRow(value: unknown): value is RunSourceIdentityRow {
2879
+ return isRecord(value);
2880
+ }
2889
2881
  function isEffectRow(value: unknown): value is EffectRow {
2890
2882
  return isRecord(value);
2891
2883
  }
@@ -2958,24 +2950,126 @@ function digestBuffer(value: string): Buffer {
2958
2950
  if (!/^[a-f0-9]{64}$/i.test(hex)) throw new Error("Expected a SHA-256 digest");
2959
2951
  return Buffer.from(hex, "hex");
2960
2952
  }
2961
- function queuedWorkflowSource(value: unknown): { root: unknown; mounted: unknown[] } {
2962
- if (isRecord(value) && (value.kind === "builtin" || value.kind === "file")) {
2953
+ type QueuedSource = {
2954
+ root:
2955
+ | { kind: "builtin"; id: string; revision: string }
2956
+ | { kind: "file"; path: string; hash: string };
2957
+ mounted: Array<{
2958
+ mountPath: string[];
2959
+ workflowName: string;
2960
+ source:
2961
+ | { kind: "builtin"; id: string; revision: string }
2962
+ | { kind: "file"; path: string; hash: string };
2963
+ }>;
2964
+ };
2965
+
2966
+ function queuedWorkflowSource(value: unknown): QueuedSource {
2967
+ if (isWorkflowSource(value)) {
2963
2968
  return { root: value, mounted: [] };
2964
2969
  }
2965
- if (isRecord(value) && isRecord(value.root) && Array.isArray(value.mounted)) {
2970
+ if (
2971
+ isRecord(value) &&
2972
+ isWorkflowSource(value.root) &&
2973
+ Array.isArray(value.mounted) &&
2974
+ value.mounted.every(isMountedWorkflowSource)
2975
+ ) {
2966
2976
  return { root: value.root, mounted: value.mounted };
2967
2977
  }
2968
2978
  throw new Error("Stored workflow source identity is invalid");
2969
2979
  }
2970
2980
 
2971
- function sourceParts(
2972
- ref: string,
2973
- revision: string,
2974
- ): { type: "builtin" | "file"; ref: string; revision: string } {
2975
- return ref.startsWith("builtin:")
2976
- ? { type: "builtin", ref: ref.slice(8), revision }
2977
- : { type: "file", ref, revision };
2981
+ function isWorkflowSource(value: unknown): value is QueuedSource["root"] {
2982
+ return (
2983
+ isRecord(value) &&
2984
+ ((value.kind === "builtin" &&
2985
+ typeof value.id === "string" &&
2986
+ typeof value.revision === "string") ||
2987
+ (value.kind === "file" && typeof value.path === "string" && typeof value.hash === "string"))
2988
+ );
2989
+ }
2990
+
2991
+ function isMountedWorkflowSource(value: unknown): value is QueuedSource["mounted"][number] {
2992
+ return (
2993
+ isRecord(value) &&
2994
+ Array.isArray(value.mountPath) &&
2995
+ value.mountPath.every((part) => typeof part === "string") &&
2996
+ typeof value.workflowName === "string" &&
2997
+ isWorkflowSource(value.source)
2998
+ );
2999
+ }
3000
+
3001
+ function insertQueuedRunSources(state: StateDatabase, runId: string, value: QueuedSource): void {
3002
+ const insert = state.connection.prepare(
3003
+ `INSERT INTO run_sources(run_id, mount_path, source_type, source_ref, source_revision)
3004
+ VALUES (?, ?, ?, ?, ?)`,
3005
+ );
3006
+ const root = queuedSourceParts(value.root);
3007
+ insert.run(runId, "", root.type, root.ref, root.revision);
3008
+ for (const mounted of value.mounted) {
3009
+ const source = queuedSourceParts(mounted.source);
3010
+ insert.run(runId, mounted.mountPath.join("/"), source.type, source.ref, source.revision);
3011
+ }
3012
+ }
3013
+
3014
+ function queuedSourceParts(source: QueuedSource["root"]): {
3015
+ type: "builtin" | "file";
3016
+ ref: string;
3017
+ revision: string;
3018
+ } {
3019
+ return source.kind === "builtin"
3020
+ ? { type: "builtin", ref: source.id, revision: source.revision }
3021
+ : { type: "file", ref: source.path, revision: source.hash };
3022
+ }
3023
+
3024
+ function readQueuedRunSources(state: StateDatabase, run: RunRow): QueuedSource {
3025
+ const rows = state.connection
3026
+ .prepare(
3027
+ `SELECT mount_path AS mountPath, source_type AS sourceType,
3028
+ source_ref AS sourceRef, source_revision AS sourceRevision
3029
+ FROM run_sources WHERE run_id = ? ORDER BY mount_path`,
3030
+ )
3031
+ .all(run.runId)
3032
+ .filter(isRunSourceIdentityRow);
3033
+ const rootRow = rows.find((row) => row.mountPath === "");
3034
+ if (rootRow === undefined) throw new Error(`Workflow run source is missing: ${run.runId}`);
3035
+ const snapshot = state.readJson(run.definitionHash);
3036
+ const mounts =
3037
+ isRecord(snapshot) &&
3038
+ isRecord(snapshot.composition) &&
3039
+ Array.isArray(snapshot.composition.mounts)
3040
+ ? snapshot.composition.mounts
3041
+ : [];
3042
+ const names = new Map(
3043
+ mounts.flatMap((mount) => {
3044
+ if (
3045
+ !isRecord(mount) ||
3046
+ !Array.isArray(mount.mountPath) ||
3047
+ !mount.mountPath.every((part) => typeof part === "string") ||
3048
+ typeof mount.workflowName !== "string"
3049
+ ) {
3050
+ return [];
3051
+ }
3052
+ return [[mount.mountPath.join("/"), mount.workflowName] as const];
3053
+ }),
3054
+ );
3055
+ return {
3056
+ root: rowToSource(rootRow),
3057
+ mounted: rows
3058
+ .filter((row) => row.mountPath !== "")
3059
+ .map((row) => ({
3060
+ mountPath: row.mountPath.split("/"),
3061
+ workflowName: names.get(row.mountPath) ?? row.mountPath,
3062
+ source: rowToSource(row),
3063
+ })),
3064
+ };
2978
3065
  }
3066
+
3067
+ function rowToSource(row: RunSourceIdentityRow): QueuedSource["root"] {
3068
+ return row.sourceType === "builtin"
3069
+ ? { kind: "builtin", id: row.sourceRef, revision: row.sourceRevision }
3070
+ : { kind: "file", path: row.sourceRef, hash: row.sourceRevision };
3071
+ }
3072
+
2979
3073
  function effectStatus(value: EffectRecord["state"]): string {
2980
3074
  return value === "indeterminate" ? "ambiguous" : value;
2981
3075
  }
@@ -1,3 +1,5 @@
1
+ import type { JsonPatch } from "../workflows/json-patch.js";
2
+
1
3
  export type MaybePromise<T> = T | Promise<T>;
2
4
  export type JsonPrimitive = string | number | boolean | null;
3
5
  export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
@@ -131,8 +133,46 @@ export type ChildWorkflowRecord = ChildWorkflowReference & {
131
133
  error?: string;
132
134
  };
133
135
 
136
+ export type ControllerSettingsChangeRequest = {
137
+ requestKey: string;
138
+ runId: string;
139
+ scopeId?: string;
140
+ expectedChangeNumber?: number;
141
+ patch: JsonPatch;
142
+ };
143
+
144
+ export type ControllerSettingsChangeResult = {
145
+ runId: string;
146
+ scopeId: string;
147
+ changeNumber: number;
148
+ adopted: boolean;
149
+ };
150
+
151
+ export type ControllerQueueFollowUpRequest = {
152
+ requestKey: string;
153
+ runId: string;
154
+ prompt: string;
155
+ };
156
+
157
+ export type ControllerFollowUpResult = {
158
+ runId: string;
159
+ followUpId: string;
160
+ order: number;
161
+ state: string;
162
+ adopted: boolean;
163
+ };
164
+
165
+ export type ControllerRemoveFollowUpRequest = {
166
+ requestKey: string;
167
+ runId: string;
168
+ followUpId: string;
169
+ };
170
+
134
171
  export interface ControllerWorkflows {
135
172
  ensure(request: ChildWorkflowRequest): Promise<ChildWorkflowRecord>;
173
+ changeSettings(request: ControllerSettingsChangeRequest): Promise<ControllerSettingsChangeResult>;
174
+ queueFollowUp(request: ControllerQueueFollowUpRequest): Promise<ControllerFollowUpResult>;
175
+ removeFollowUp(request: ControllerRemoveFollowUpRequest): Promise<ControllerFollowUpResult>;
136
176
  }
137
177
 
138
178
  export type ReconcileContext<TStatus> = {