@osolmaz/pi-workflows 0.13.2 → 0.13.4

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 (134) 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 +77 -73
  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 +8 -8
  7. package/dist/builtins/autoplan.workflow.js +44 -35
  8. package/dist/builtins/autoplan.workflow.js.map +1 -1
  9. package/dist/builtins/catalog.js +2 -2
  10. package/dist/builtins/change-verification.workflow.d.ts +1 -1
  11. package/dist/builtins/plain-summary.workflow.d.ts +1 -1
  12. package/dist/builtins/plan-approval.workflow.d.ts +1 -1
  13. package/dist/builtins/plan-change.workflow.d.ts +29 -29
  14. package/dist/builtins/sanity-check.workflow.d.ts +3 -3
  15. package/dist/builtins/workspace-preparation.workflow.d.ts +1 -1
  16. package/dist/controllers/index.d.ts +2 -2
  17. package/dist/controllers/index.js.map +1 -1
  18. package/dist/controllers/sqlite.js +85 -23
  19. package/dist/controllers/sqlite.js.map +1 -1
  20. package/dist/controllers/types.d.ts +34 -0
  21. package/dist/controllers/workflow-engine-scheduler.d.ts +5 -1
  22. package/dist/controllers/workflow-engine-scheduler.js +83 -1
  23. package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
  24. package/dist/controllers/workflows.d.ts +8 -1
  25. package/dist/controllers/workflows.js +36 -0
  26. package/dist/controllers/workflows.js.map +1 -1
  27. package/dist/extension/controller-host.d.ts +2 -1
  28. package/dist/extension/controller-host.js +1 -1
  29. package/dist/extension/controller-host.js.map +1 -1
  30. package/dist/extension/follow-up-coordinator.d.ts +27 -0
  31. package/dist/extension/follow-up-coordinator.js +131 -0
  32. package/dist/extension/follow-up-coordinator.js.map +1 -0
  33. package/dist/extension/index.d.ts +9 -0
  34. package/dist/extension/index.js +460 -75
  35. package/dist/extension/index.js.map +1 -1
  36. package/dist/extension/session-events.d.ts +2 -3
  37. package/dist/extension/session-events.js +11 -10
  38. package/dist/extension/session-events.js.map +1 -1
  39. package/dist/extension/widget.js +9 -0
  40. package/dist/extension/widget.js.map +1 -1
  41. package/dist/state/database.d.ts +2 -1
  42. package/dist/state/database.js +11 -13
  43. package/dist/state/database.js.map +1 -1
  44. package/dist/state/json.js +6 -1
  45. package/dist/state/json.js.map +1 -1
  46. package/dist/state/mutation.d.ts +1 -1
  47. package/dist/state/mutation.js.map +1 -1
  48. package/dist/state/prune.d.ts +18 -0
  49. package/dist/state/prune.js +373 -0
  50. package/dist/state/prune.js.map +1 -0
  51. package/dist/state/schema.d.ts +1 -1
  52. package/dist/state/schema.js +116 -18
  53. package/dist/state/schema.js.map +1 -1
  54. package/dist/viewer/cli.d.ts +3 -1
  55. package/dist/viewer/cli.js +55 -4
  56. package/dist/viewer/cli.js.map +1 -1
  57. package/dist/viewer/render.js +19 -1
  58. package/dist/viewer/render.js.map +1 -1
  59. package/dist/workflows/composition.d.ts +2 -0
  60. package/dist/workflows/composition.js +15 -0
  61. package/dist/workflows/composition.js.map +1 -1
  62. package/dist/workflows/definition.d.ts +6 -3
  63. package/dist/workflows/definition.js +3 -0
  64. package/dist/workflows/definition.js.map +1 -1
  65. package/dist/workflows/engine.d.ts +6 -1
  66. package/dist/workflows/engine.js +301 -35
  67. package/dist/workflows/engine.js.map +1 -1
  68. package/dist/workflows/human-decision.d.ts +1 -0
  69. package/dist/workflows/human-decision.js +25 -9
  70. package/dist/workflows/human-decision.js.map +1 -1
  71. package/dist/workflows/index.d.ts +3 -1
  72. package/dist/workflows/index.js +2 -0
  73. package/dist/workflows/index.js.map +1 -1
  74. package/dist/workflows/json-patch.d.ts +47 -0
  75. package/dist/workflows/json-patch.js +298 -0
  76. package/dist/workflows/json-patch.js.map +1 -0
  77. package/dist/workflows/schema.js +29 -1
  78. package/dist/workflows/schema.js.map +1 -1
  79. package/dist/workflows/settings.d.ts +128 -0
  80. package/dist/workflows/settings.js +199 -0
  81. package/dist/workflows/settings.js.map +1 -0
  82. package/dist/workflows/store.d.ts +75 -2
  83. package/dist/workflows/store.js +1349 -89
  84. package/dist/workflows/store.js.map +1 -1
  85. package/dist/workflows/tool-input.d.ts +22 -0
  86. package/dist/workflows/tool-input.js +43 -0
  87. package/dist/workflows/tool-input.js.map +1 -1
  88. package/dist/workflows/types.d.ts +40 -5
  89. package/docs/2026-08-25-workflow-follow-ups.md +132 -0
  90. package/docs/2026-08-25-workflow-settings.md +169 -0
  91. package/docs/DEFERRED_TURNS.md +6 -0
  92. package/docs/DESIGN_PHILOSOPHY.md +2 -0
  93. package/docs/SQLITE_STATE.md +31 -18
  94. package/docs/WORKFLOW_COMPOSITION.md +16 -1
  95. package/docs/WORKFLOW_STEP_MESSAGES.md +4 -2
  96. package/docs/plans/2026-08-25-live-workflow-settings-plan.md +532 -0
  97. package/docs/plans/2026-08-25-workflow-run-storage-plan.md +67 -0
  98. package/docs/tui-viewer.md +7 -6
  99. package/docs/workflows.md +22 -1
  100. package/examples/workflows/live-settings.workflow.ts +93 -0
  101. package/herdr-plugin.toml +1 -1
  102. package/package.json +1 -1
  103. package/skills/autoplan/SKILL.md +6 -6
  104. package/src/builtins/autoimplement.workflow.ts +56 -8
  105. package/src/builtins/autoplan.workflow.ts +45 -37
  106. package/src/builtins/catalog.ts +2 -2
  107. package/src/controllers/index.ts +6 -0
  108. package/src/controllers/sqlite.ts +129 -35
  109. package/src/controllers/types.ts +40 -0
  110. package/src/controllers/workflow-engine-scheduler.ts +103 -1
  111. package/src/controllers/workflows.ts +68 -0
  112. package/src/extension/controller-host.ts +6 -1
  113. package/src/extension/follow-up-coordinator.ts +151 -0
  114. package/src/extension/index.ts +538 -88
  115. package/src/extension/session-events.ts +15 -13
  116. package/src/extension/widget.ts +8 -0
  117. package/src/state/database.ts +12 -12
  118. package/src/state/json.ts +6 -1
  119. package/src/state/mutation.ts +4 -1
  120. package/src/state/prune.ts +502 -0
  121. package/src/state/schema.ts +116 -18
  122. package/src/viewer/cli.ts +52 -5
  123. package/src/viewer/render.ts +43 -1
  124. package/src/workflows/composition.ts +19 -0
  125. package/src/workflows/definition.ts +19 -5
  126. package/src/workflows/engine.ts +363 -35
  127. package/src/workflows/human-decision.ts +41 -11
  128. package/src/workflows/index.ts +43 -0
  129. package/src/workflows/json-patch.ts +375 -0
  130. package/src/workflows/schema.ts +31 -1
  131. package/src/workflows/settings.ts +430 -0
  132. package/src/workflows/store.ts +1977 -184
  133. package/src/workflows/tool-input.ts +58 -0
  134. package/src/workflows/types.ts +43 -4
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
3
3
  export const STATE_APPLICATION_ID = 0x50495746;
4
4
  export const STATE_SCHEMA_NAME = "pi-workflows-state";
5
5
  export const STATE_SCHEMA_VERSION = 1;
6
- export const STATE_APP_VERSION = "0.12.1";
6
+ export const STATE_APP_VERSION = "0.13.3";
7
7
 
8
8
  export const STATE_SCHEMA_SQL = String.raw`
9
9
  CREATE TABLE schema_meta (
@@ -34,7 +34,8 @@ CREATE TABLE blobs (
34
34
  CREATE TABLE resources (
35
35
  resource_id TEXT PRIMARY KEY,
36
36
  resource_type TEXT NOT NULL CHECK (resource_type IN (
37
- 'run', 'session', 'decision', 'controller', 'effect', 'channel', 'notification', 'turn_intent'
37
+ 'run', 'session', 'decision', 'controller', 'effect', 'channel', 'notification', 'turn_intent',
38
+ 'settings', 'follow_up_queue', 'follow_up'
38
39
  )),
39
40
  aggregate_key TEXT NOT NULL,
40
41
  revision INTEGER NOT NULL DEFAULT 0 CHECK (revision >= 0),
@@ -94,11 +95,7 @@ CREATE TABLE runs (
94
95
  parent_run_id TEXT REFERENCES runs(run_id),
95
96
  definition_digest BLOB NOT NULL REFERENCES workflow_definitions(definition_digest),
96
97
  workflow_ref TEXT NOT NULL,
97
- workflow_source_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
98
98
  launch_options_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
99
- source_type TEXT NOT NULL CHECK (source_type IN ('builtin', 'file')),
100
- source_ref TEXT NOT NULL,
101
- source_revision TEXT NOT NULL,
102
99
  title TEXT,
103
100
  status TEXT NOT NULL CHECK (status IN (
104
101
  'queued', 'running', 'waiting', 'completed', 'failed', 'timed_out', 'cancelled'
@@ -106,15 +103,8 @@ CREATE TABLE runs (
106
103
  paused INTEGER NOT NULL DEFAULT 0 CHECK (paused IN (0, 1)),
107
104
  status_detail TEXT,
108
105
  input_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
109
- workflow_sources_hash BLOB REFERENCES blobs(blob_hash),
110
- human_decision_hash BLOB REFERENCES blobs(blob_hash),
111
106
  final_output_hash BLOB REFERENCES blobs(blob_hash),
112
107
  error_hash BLOB REFERENCES blobs(blob_hash),
113
- carried_step_count INTEGER NOT NULL DEFAULT 0 CHECK (carried_step_count >= 0),
114
- current_node TEXT,
115
- current_attempt_id TEXT,
116
- current_node_started_at INTEGER,
117
- waiting_on TEXT,
118
108
  created_at INTEGER NOT NULL,
119
109
  updated_at INTEGER NOT NULL,
120
110
  finished_at INTEGER,
@@ -125,6 +115,15 @@ CREATE INDEX runs_project_idx ON runs(project_id, created_at DESC);
125
115
  CREATE INDEX runs_status_idx ON runs(status, updated_at DESC);
126
116
  CREATE INDEX runs_parent_idx ON runs(parent_run_id);
127
117
 
118
+ CREATE TABLE run_sources (
119
+ run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
120
+ mount_path TEXT NOT NULL,
121
+ source_type TEXT NOT NULL CHECK (source_type IN ('builtin', 'file')),
122
+ source_ref TEXT NOT NULL,
123
+ source_revision TEXT NOT NULL,
124
+ PRIMARY KEY (run_id, mount_path)
125
+ ) STRICT;
126
+
128
127
  CREATE TABLE run_bindings (
129
128
  run_id TEXT PRIMARY KEY REFERENCES runs(run_id) ON DELETE CASCADE,
130
129
  origin_session_id TEXT NOT NULL,
@@ -134,6 +133,92 @@ CREATE TABLE run_bindings (
134
133
 
135
134
  CREATE INDEX run_bindings_session_idx ON run_bindings(origin_session_id, created_at DESC);
136
135
 
136
+ CREATE TABLE workflow_settings (
137
+ scope_id TEXT PRIMARY KEY,
138
+ resource_id TEXT NOT NULL UNIQUE REFERENCES resources(resource_id) ON DELETE CASCADE,
139
+ origin_run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
140
+ active_run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
141
+ mount_path TEXT NOT NULL,
142
+ invocation INTEGER NOT NULL CHECK (invocation > 0),
143
+ initial_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
144
+ current_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
145
+ created_at INTEGER NOT NULL,
146
+ updated_at INTEGER NOT NULL,
147
+ UNIQUE (origin_run_id, mount_path, invocation)
148
+ ) STRICT;
149
+
150
+ CREATE INDEX workflow_settings_active_run_idx ON workflow_settings(active_run_id, mount_path, invocation);
151
+
152
+ CREATE TABLE workflow_setting_changes (
153
+ change_id TEXT PRIMARY KEY,
154
+ scope_id TEXT NOT NULL REFERENCES workflow_settings(scope_id) ON DELETE CASCADE,
155
+ request_id TEXT NOT NULL,
156
+ change_number INTEGER NOT NULL CHECK (change_number > 0),
157
+ actor_type TEXT NOT NULL CHECK (actor_type IN (
158
+ 'session', 'host', 'controller', 'channel', 'human', 'policy', 'control', 'system'
159
+ )),
160
+ actor_id TEXT,
161
+ source_type TEXT NOT NULL,
162
+ patch_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
163
+ before_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
164
+ after_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
165
+ accepted_at INTEGER NOT NULL,
166
+ UNIQUE (scope_id, request_id),
167
+ UNIQUE (scope_id, change_number)
168
+ ) STRICT;
169
+
170
+ CREATE TABLE workflow_follow_up_queues (
171
+ run_id TEXT PRIMARY KEY REFERENCES runs(run_id) ON DELETE CASCADE,
172
+ resource_id TEXT NOT NULL UNIQUE REFERENCES resources(resource_id) ON DELETE CASCADE,
173
+ origin_session_id TEXT,
174
+ presentation_state TEXT NOT NULL DEFAULT 'none' CHECK (presentation_state IN (
175
+ 'none', 'not-needed', 'pending', 'settled', 'unavailable'
176
+ )),
177
+ presentation_entry_id TEXT,
178
+ presentation_assistant_entry_id TEXT,
179
+ presentation_reason_hash BLOB REFERENCES blobs(blob_hash),
180
+ presentation_updated_at INTEGER,
181
+ created_at INTEGER NOT NULL,
182
+ updated_at INTEGER NOT NULL,
183
+ CHECK (
184
+ (presentation_state IN ('none', 'not-needed', 'pending') AND presentation_assistant_entry_id IS NULL AND presentation_reason_hash IS NULL)
185
+ OR
186
+ (presentation_state = 'unavailable' AND presentation_assistant_entry_id IS NULL AND presentation_reason_hash IS NOT NULL)
187
+ OR
188
+ (presentation_state = 'settled' AND presentation_entry_id IS NOT NULL AND presentation_assistant_entry_id IS NOT NULL AND presentation_reason_hash IS NULL)
189
+ )
190
+ ) STRICT;
191
+
192
+ CREATE TABLE workflow_follow_ups (
193
+ follow_up_id TEXT PRIMARY KEY,
194
+ resource_id TEXT NOT NULL UNIQUE REFERENCES resources(resource_id) ON DELETE CASCADE,
195
+ queue_resource_id TEXT NOT NULL REFERENCES resources(resource_id),
196
+ run_id TEXT NOT NULL REFERENCES workflow_follow_up_queues(run_id) ON DELETE CASCADE,
197
+ request_id TEXT NOT NULL,
198
+ order_number INTEGER NOT NULL CHECK (order_number > 0),
199
+ target_session_id TEXT NOT NULL,
200
+ actor_type TEXT NOT NULL CHECK (actor_type IN (
201
+ 'session', 'host', 'controller', 'channel', 'human', 'policy', 'control', 'system'
202
+ )),
203
+ actor_id TEXT,
204
+ source_type TEXT NOT NULL,
205
+ prompt_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
206
+ status TEXT NOT NULL CHECK (status IN (
207
+ 'queued', 'pending_presentation', 'ready', 'sent', 'removed', 'cancelled'
208
+ )),
209
+ session_entry_id TEXT,
210
+ reason_hash BLOB REFERENCES blobs(blob_hash),
211
+ created_at INTEGER NOT NULL,
212
+ updated_at INTEGER NOT NULL,
213
+ sent_at INTEGER,
214
+ UNIQUE (run_id, request_id),
215
+ UNIQUE (run_id, order_number),
216
+ CHECK ((status = 'sent') = (session_entry_id IS NOT NULL AND sent_at IS NOT NULL)),
217
+ CHECK ((status IN ('removed', 'cancelled')) = (reason_hash IS NOT NULL))
218
+ ) STRICT;
219
+
220
+ CREATE INDEX workflow_follow_ups_delivery_idx ON workflow_follow_ups(target_session_id, status, order_number);
221
+
137
222
  CREATE TABLE run_queue (
138
223
  run_id TEXT PRIMARY KEY REFERENCES runs(run_id) ON DELETE CASCADE,
139
224
  status TEXT NOT NULL CHECK (status IN (
@@ -162,14 +247,16 @@ CREATE TABLE node_attempts (
162
247
  attempt_number INTEGER NOT NULL CHECK (attempt_number > 0),
163
248
  node_type TEXT NOT NULL,
164
249
  status TEXT NOT NULL CHECK (status IN (
165
- 'pending', 'running', 'waiting', 'completed', 'failed', 'timed_out', 'cancelled'
250
+ 'pending', 'running', 'waiting', 'interrupted',
251
+ 'completed', 'failed', 'timed_out', 'cancelled'
166
252
  )),
167
- input_hash BLOB REFERENCES blobs(blob_hash),
168
- contract_hash BLOB REFERENCES blobs(blob_hash),
169
253
  prompt_hash BLOB REFERENCES blobs(blob_hash),
170
254
  output_hash BLOB REFERENCES blobs(blob_hash),
171
- step_metadata_hash BLOB REFERENCES blobs(blob_hash),
255
+ receipt_hash BLOB REFERENCES blobs(blob_hash),
172
256
  error_hash BLOB REFERENCES blobs(blob_hash),
257
+ settings_scope_id TEXT REFERENCES workflow_settings(scope_id),
258
+ settings_change_number INTEGER CHECK (settings_change_number IS NULL OR settings_change_number >= 0),
259
+ settings_hash BLOB REFERENCES blobs(blob_hash),
173
260
  started_at INTEGER,
174
261
  deadline_at INTEGER,
175
262
  finished_at INTEGER,
@@ -235,6 +322,18 @@ CREATE TABLE session_entries (
235
322
  UNIQUE (segment_id, entry_id)
236
323
  ) STRICT;
237
324
 
325
+ CREATE TABLE attempt_entries (
326
+ attempt_id TEXT NOT NULL REFERENCES node_attempts(attempt_id) ON DELETE CASCADE,
327
+ role TEXT NOT NULL CHECK (role IN ('prompt', 'response', 'first', 'last')),
328
+ segment_id TEXT NOT NULL,
329
+ entry_id TEXT NOT NULL,
330
+ PRIMARY KEY (attempt_id, role),
331
+ FOREIGN KEY (segment_id, entry_id)
332
+ REFERENCES session_entries(segment_id, entry_id) ON DELETE CASCADE
333
+ ) STRICT;
334
+
335
+ CREATE INDEX attempt_entries_entry_idx ON attempt_entries(segment_id, entry_id);
336
+
238
337
  CREATE TABLE session_events (
239
338
  segment_id TEXT NOT NULL REFERENCES session_segments(segment_id) ON DELETE CASCADE,
240
339
  event_seq INTEGER NOT NULL CHECK (event_seq > 0),
@@ -306,7 +405,6 @@ CREATE TABLE continuations (
306
405
  decision_id TEXT PRIMARY KEY REFERENCES human_decisions(decision_id) ON DELETE CASCADE,
307
406
  parent_run_id TEXT NOT NULL UNIQUE REFERENCES runs(run_id),
308
407
  continuation_run_id TEXT NOT NULL UNIQUE REFERENCES runs(run_id),
309
- response_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
310
408
  created_at INTEGER NOT NULL
311
409
  ) STRICT;
312
410
 
package/src/viewer/cli.ts CHANGED
@@ -6,6 +6,7 @@ import { SqliteControllerStore } from "../controllers/sqlite.js";
6
6
  import { syncHerdrPlugin } from "../herdr/setup.js";
7
7
  import { sanitizeText } from "../render/ansi.js";
8
8
  import { StateDatabase } from "../state/database.js";
9
+ import { pruneState } from "../state/prune.js";
9
10
  import { WorkflowRunStore, workflowStateDatabasePath } from "../workflows/store.js";
10
11
  import {
11
12
  formatDuration,
@@ -25,6 +26,8 @@ Usage:
25
26
  pi-workflows controller <controller> <key>
26
27
  pi-workflows state status|verify
27
28
  pi-workflows state backup <destination>
29
+ pi-workflows state prune --before <timestamp> --dry-run
30
+ pi-workflows state prune --before <timestamp> --backup <absolute-path> --apply
28
31
  pi-workflows host [--project <dir>] [-- <extra pi args>]
29
32
  pi-workflows herdr sync [--json]
30
33
  pi-workflows herdr setup [--json]
@@ -38,8 +41,10 @@ export type CliArgs = {
38
41
  controllerName?: string;
39
42
  resourceKey?: string;
40
43
  herdrAction?: string;
41
- stateAction?: "status" | "verify" | "backup";
44
+ stateAction?: "status" | "verify" | "backup" | "prune";
42
45
  backupDestination?: string;
46
+ pruneBefore?: string;
47
+ pruneApply?: boolean;
43
48
  once: boolean;
44
49
  json: boolean;
45
50
  project?: string | undefined;
@@ -52,12 +57,20 @@ export function parseCliArgs(argv: string[]): CliArgs {
52
57
  let once = false;
53
58
  let json = false;
54
59
  let project: string | undefined;
60
+ let pruneBefore: string | undefined;
61
+ let backupDestination: string | undefined;
62
+ let pruneApply = false;
63
+ let pruneDryRun = false;
55
64
  const positionals: string[] = [];
56
65
  const piArgs: string[] = [];
57
66
 
58
67
  while (args.length > 0) {
59
68
  const arg = args.shift() as string;
60
69
  if (arg === "--project") project = requiredValue(args, "--project");
70
+ else if (arg === "--before") pruneBefore = requiredValue(args, "--before");
71
+ else if (arg === "--backup") backupDestination = requiredValue(args, "--backup");
72
+ else if (arg === "--apply") pruneApply = true;
73
+ else if (arg === "--dry-run") pruneDryRun = true;
61
74
  else if (arg === "--once") once = true;
62
75
  else if (arg === "--json") json = true;
63
76
  else if (arg === "--help" || arg === "-h") return { command: "help", once, json };
@@ -80,8 +93,30 @@ export function parseCliArgs(argv: string[]): CliArgs {
80
93
  }
81
94
  if (command === "state") {
82
95
  const action = positionals[0];
83
- if (action !== "status" && action !== "verify" && action !== "backup") {
84
- throw new Error("state requires status, verify, or backup");
96
+ if (action !== "status" && action !== "verify" && action !== "backup" && action !== "prune") {
97
+ throw new Error("state requires status, verify, backup, or prune");
98
+ }
99
+ if (action === "prune") {
100
+ if (positionals.length !== 1) throw new Error("state prune accepts no positional arguments");
101
+ if (pruneBefore === undefined) throw new Error("state prune requires --before <timestamp>");
102
+ if (pruneApply === pruneDryRun) {
103
+ throw new Error("state prune requires exactly one of --dry-run or --apply");
104
+ }
105
+ if (pruneApply && backupDestination === undefined) {
106
+ throw new Error("state prune --apply requires --backup <absolute-path>");
107
+ }
108
+ if (pruneDryRun && backupDestination !== undefined) {
109
+ throw new Error("state prune --dry-run does not accept --backup");
110
+ }
111
+ return {
112
+ command,
113
+ stateAction: action,
114
+ pruneBefore,
115
+ pruneApply,
116
+ ...(backupDestination === undefined ? {} : { backupDestination }),
117
+ once,
118
+ json,
119
+ };
85
120
  }
86
121
  if (action === "backup") {
87
122
  if (positionals.length !== 2) throw new Error("state backup requires <destination>");
@@ -251,6 +286,15 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
251
286
  }
252
287
 
253
288
  async function runStateCommand(databasePath: string, args: CliArgs): Promise<void> {
289
+ if (args.stateAction === "prune") {
290
+ const report = await pruneState(databasePath, {
291
+ before: args.pruneBefore as string,
292
+ apply: args.pruneApply === true,
293
+ ...(args.backupDestination === undefined ? {} : { backupPath: args.backupDestination }),
294
+ });
295
+ process.stdout.write(`${JSON.stringify(report)}\n`);
296
+ return;
297
+ }
254
298
  if (args.stateAction === "backup") {
255
299
  const state = new StateDatabase({ filePath: databasePath });
256
300
  try {
@@ -274,10 +318,13 @@ async function runStateCommand(databasePath: string, args: CliArgs): Promise<voi
274
318
  (SELECT count(*) FROM runs) AS runs,
275
319
  (SELECT count(*) FROM controller_resources) AS controllers,
276
320
  (SELECT count(*) FROM human_decisions) AS decisions,
321
+ (SELECT count(*) FROM workflow_settings) AS settingsScopes,
322
+ (SELECT count(*) FROM workflow_follow_ups WHERE status IN ('queued', 'pending_presentation', 'ready')) AS pendingFollowUps,
277
323
  (SELECT count(*) FROM effects WHERE status IN ('pending', 'applying', 'ambiguous')) AS unsettledEffects,
278
- (SELECT count(*) FROM leases WHERE owner_id IS NOT NULL) AS activeLeases`,
324
+ (SELECT count(*) FROM leases
325
+ WHERE owner_id IS NOT NULL AND expires_at > ?) AS activeLeases`,
279
326
  )
280
- .get();
327
+ .get(Date.now());
281
328
  process.stdout.write(
282
329
  `Database: ${databasePath}\nSize: ${fs.statSync(databasePath).size} bytes\n${JSON.stringify(counts)}\n`,
283
330
  );
@@ -102,8 +102,10 @@ function stepLine(
102
102
  step.error !== undefined
103
103
  ? ansi.red(previewValue(step.error, 60))
104
104
  : ansi.dim(previewValue(step.output, 60));
105
+ const settings =
106
+ step.settingsChangeNumber === undefined ? "" : `, settings ${step.settingsChangeNumber}`;
105
107
  return fitWidth(
106
- ` ${marker}${glyph} ${step.nodeId} ${ansi.dim(`(${step.nodeType}, ${formatDuration(durationMs)})`)} ${preview}`,
108
+ ` ${marker}${glyph} ${step.nodeId} ${ansi.dim(`(${step.nodeType}, ${formatDuration(durationMs)}${settings})`)} ${preview}`,
107
109
  width,
108
110
  );
109
111
  }
@@ -181,6 +183,16 @@ function inspectorLines(step: WorkflowStepRecord, width: number): string[] {
181
183
  for (const raw of (rendered ?? "null").split("\n")) {
182
184
  lines.push(fitWidth(` ${sanitizeText(raw)}`, width));
183
185
  }
186
+ if (step.settingsScopeId !== undefined) {
187
+ lines.push(
188
+ fitWidth(
189
+ ansi.dim(
190
+ ` settings ${step.settingsChangeNumber ?? 0} · ${sanitizeText(step.settingsScopeId)} · ${step.settingsHash?.slice(0, 12) ?? "unknown hash"}`,
191
+ ),
192
+ width,
193
+ ),
194
+ );
195
+ }
184
196
  if (step.action) {
185
197
  const receipt = [
186
198
  step.action.actionType,
@@ -273,6 +285,36 @@ export function renderRunDetailLines(
273
285
  }
274
286
  }
275
287
 
288
+ if ((bundle.settingsScopes?.length ?? 0) > 0) {
289
+ lines.push("");
290
+ lines.push(ansi.bold("workflow settings"));
291
+ for (const scope of bundle.settingsScopes ?? []) {
292
+ lines.push(
293
+ fitWidth(
294
+ ` ${sanitizeText(scope.mountPath || "root")} #${scope.invocation} · change ${scope.changeNumber} · ${scope.settingsHash.slice(0, 12)}`,
295
+ size.width,
296
+ ),
297
+ );
298
+ }
299
+ }
300
+
301
+ if ((bundle.followUpQueue?.followUps.length ?? 0) > 0) {
302
+ lines.push("");
303
+ lines.push(
304
+ ansi.bold(
305
+ `follow-ups · presentation ${bundle.followUpQueue?.presentationState ?? "unknown"}`,
306
+ ),
307
+ );
308
+ for (const followUp of bundle.followUpQueue?.followUps ?? []) {
309
+ lines.push(
310
+ fitWidth(
311
+ ` ${followUp.order}. ${followUp.state} · ${followUp.followUpId} · ${sanitizeText(followUp.source)}`,
312
+ size.width,
313
+ ),
314
+ );
315
+ }
316
+ }
317
+
276
318
  if (steps.length > 0) {
277
319
  lines.push("");
278
320
  lines.push(ansi.bold("steps"));
@@ -31,6 +31,7 @@ export type WorkflowScopeMetadata = {
31
31
  path: string;
32
32
  workflowName: string;
33
33
  maxSteps?: number;
34
+ settings?: WorkflowDefinition["settings"];
34
35
  authoredNodes: Record<string, string>;
35
36
  childMounts: Record<string, string>;
36
37
  };
@@ -41,6 +42,7 @@ export type WorkflowEntryMetadata = {
41
42
  parentPath: string;
42
43
  workflowName: string;
43
44
  input: WorkflowIncludeDefinition["input"];
45
+ settings?: WorkflowIncludeDefinition["settings"];
44
46
  normalizeInput?: WorkflowDefinition["input"];
45
47
  };
46
48
 
@@ -125,6 +127,7 @@ export function compileWorkflowDefinition<TWorkflow extends WorkflowDefinition<a
125
127
  path: scopePath,
126
128
  workflowName: current.name,
127
129
  ...(current.maxSteps !== undefined ? { maxSteps: current.maxSteps } : {}),
130
+ ...(current.settings !== undefined ? { settings: current.settings } : {}),
128
131
  authoredNodes,
129
132
  childMounts,
130
133
  };
@@ -155,6 +158,11 @@ export function compileWorkflowDefinition<TWorkflow extends WorkflowDefinition<a
155
158
  `Workflow include ${qualify(scopePath, name)} is unresolved: ${include.workflow}`,
156
159
  );
157
160
  }
161
+ if (include.settings !== undefined && child.settings === undefined) {
162
+ throw new Error(
163
+ `Workflow include ${qualify(scopePath, name)} maps settings but child ${child.name} declares none`,
164
+ );
165
+ }
158
166
  const mountPath = qualify(scopePath, name);
159
167
  const childExits = child.exits ?? {};
160
168
  if (Object.keys(childExits).length === 0) {
@@ -167,6 +175,7 @@ export function compileWorkflowDefinition<TWorkflow extends WorkflowDefinition<a
167
175
  parentPath: scopePath,
168
176
  workflowName: child.name,
169
177
  input: include.input,
178
+ ...(include.settings !== undefined ? { settings: include.settings } : {}),
170
179
  normalizeInput: child.input,
171
180
  };
172
181
  nodes[entryNode] = {
@@ -176,10 +185,14 @@ export function compileWorkflowDefinition<TWorkflow extends WorkflowDefinition<a
176
185
  const parentContext = projectWorkflowContext(context, scopePath, scopes, entries, exits);
177
186
  const mapped = include.input ? await include.input(parentContext) : parentContext.input;
178
187
  const normalized = child.input ? await child.input(mapped) : mapped;
188
+ const initialSettings = include.settings
189
+ ? await include.settings(parentContext)
190
+ : undefined;
179
191
  return {
180
192
  schema: "pi-workflows.include-entry.v1",
181
193
  invocation: countCompletedEntries(context.state, entryNode) + 1,
182
194
  input: normalized === undefined ? null : normalized,
195
+ ...(initialSettings !== undefined ? { settings: initialSettings } : {}),
183
196
  };
184
197
  },
185
198
  };
@@ -478,6 +491,7 @@ function wrapNode(
478
491
  ...common,
479
492
  nodeType: "compute",
480
493
  run: (context) => (node as ComputeNodeDefinition).run(project(context)),
494
+ ...(node.settingsRoute === true ? { settingsRoute: true as const } : {}),
481
495
  };
482
496
  case "notify":
483
497
  return {
@@ -637,6 +651,11 @@ export function projectWorkflowContext(
637
651
  outputs,
638
652
  results,
639
653
  state,
654
+ ...(context.settings !== undefined ? { settings: context.settings } : {}),
655
+ ...(context.settingsScopeId !== undefined ? { settingsScopeId: context.settingsScopeId } : {}),
656
+ ...(context.settingsChangeNumber !== undefined
657
+ ? { settingsChangeNumber: context.settingsChangeNumber }
658
+ : {}),
640
659
  signal: context.signal,
641
660
  };
642
661
  }
@@ -7,6 +7,7 @@ import {
7
7
  assertValidShellActionNode,
8
8
  assertValidWorkflowDefinitionShape,
9
9
  } from "./schema.js";
10
+ import type { WorkflowSettingsDefinition } from "./settings.js";
10
11
  import type {
11
12
  AgentNodeDefinition,
12
13
  AssistantAgentNodeDefinition,
@@ -26,6 +27,7 @@ import type {
26
27
  WorkflowInputOf,
27
28
  WorkflowNodeContext,
28
29
  WorkflowNodeDefinition,
30
+ WorkflowSettingsOf,
29
31
  WorkflowTypedEdge,
30
32
  WorkflowValueParser,
31
33
  } from "./types.js";
@@ -37,11 +39,13 @@ type WorkflowDefinitionInput<
37
39
  TNodes extends Record<string, WorkflowNodeDefinition>,
38
40
  TIncludes extends WorkflowIncludeMap,
39
41
  TExits extends WorkflowExitMap,
42
+ TSettings,
40
43
  > = Omit<
41
- WorkflowDefinition<TInput, TExits, TIncludes>,
42
- "input" | "nodes" | "includes" | "exits" | "edges"
44
+ WorkflowDefinition<TInput, TExits, TIncludes, TSettings>,
45
+ "input" | "settings" | "nodes" | "includes" | "exits" | "edges"
43
46
  > & {
44
47
  input?: WorkflowValueParser<TInput>;
48
+ settings?: WorkflowSettingsDefinition<TSettings, TInput>;
45
49
  nodes: TNodes;
46
50
  includes?: TIncludes;
47
51
  exits?: TExits;
@@ -56,15 +60,16 @@ export function defineWorkflow<
56
60
  >,
57
61
  const TIncludes extends WorkflowIncludeMap = Record<never, never>,
58
62
  const TExits extends WorkflowExitMap = Record<never, never>,
63
+ TSettings = unknown,
59
64
  >(
60
- definition: WorkflowDefinitionInput<TInput, TNodes, TIncludes, TExits>,
61
- ): WorkflowDefinition<TInput, TExits, TIncludes> & {
65
+ definition: WorkflowDefinitionInput<TInput, TNodes, TIncludes, TExits, TSettings>,
66
+ ): WorkflowDefinition<TInput, TExits, TIncludes, TSettings> & {
62
67
  nodes: TNodes;
63
68
  includes?: TIncludes;
64
69
  exits?: TExits;
65
70
  } {
66
71
  assertValidWorkflowDefinitionShape(definition as WorkflowDefinition);
67
- const typed = definition as WorkflowDefinition<TInput, TExits, TIncludes> & {
72
+ const typed = definition as WorkflowDefinition<TInput, TExits, TIncludes, TSettings> & {
68
73
  nodes: TNodes;
69
74
  includes?: TIncludes;
70
75
  exits?: TExits;
@@ -87,6 +92,9 @@ export function includeWorkflow<TWorkflow extends WorkflowDefinition<any, any, a
87
92
  input?: (
88
93
  context: WorkflowNodeContext,
89
94
  ) => Promise<WorkflowInputOf<TWorkflow>> | WorkflowInputOf<TWorkflow>;
95
+ settings?: (
96
+ context: WorkflowNodeContext,
97
+ ) => Promise<WorkflowSettingsOf<TWorkflow>> | WorkflowSettingsOf<TWorkflow>;
90
98
  },
91
99
  ): WorkflowIncludeDefinition<TWorkflow>;
92
100
  export function includeWorkflow<TWorkflow extends WorkflowDefinition<any, any, any>>(
@@ -98,6 +106,9 @@ export function includeWorkflow<TWorkflow extends WorkflowDefinition<any, any, a
98
106
  input?: (
99
107
  context: WorkflowNodeContext,
100
108
  ) => Promise<WorkflowInputOf<TWorkflow>> | WorkflowInputOf<TWorkflow>;
109
+ settings?: (
110
+ context: WorkflowNodeContext,
111
+ ) => Promise<WorkflowSettingsOf<TWorkflow>> | WorkflowSettingsOf<TWorkflow>;
101
112
  } = {},
102
113
  ): WorkflowIncludeDefinition<TWorkflow> {
103
114
  const definition = isWorkflowDefinition(workflowOrDefinition)
@@ -109,6 +120,9 @@ export function includeWorkflow<TWorkflow extends WorkflowDefinition<any, any, a
109
120
  if (definition.input !== undefined && typeof definition.input !== "function") {
110
121
  throw new Error("Included workflow input must be a function");
111
122
  }
123
+ if (definition.settings !== undefined && typeof definition.settings !== "function") {
124
+ throw new Error("Included workflow settings must be a function");
125
+ }
112
126
  if (definition.contract !== undefined && !isWorkflowDefinition(definition.contract)) {
113
127
  throw new Error("Included workflow contract must be defined with defineWorkflow");
114
128
  }