@osolmaz/pi-workflows 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/README.md +56 -31
  2. package/dist/builtins/autodoc.workflow.d.ts +4 -4
  3. package/dist/builtins/autoimplement.workflow.d.ts +797 -142
  4. package/dist/builtins/autoimplement.workflow.js +17 -105
  5. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  6. package/dist/builtins/autoplan.workflow.d.ts +126 -13
  7. package/dist/builtins/autoplan.workflow.js +200 -29
  8. package/dist/builtins/autoplan.workflow.js.map +1 -1
  9. package/dist/builtins/catalog.js +7 -5
  10. package/dist/builtins/catalog.js.map +1 -1
  11. package/dist/builtins/index.d.ts +4 -2
  12. package/dist/builtins/index.js +3 -1
  13. package/dist/builtins/index.js.map +1 -1
  14. package/dist/builtins/monitor.workflow.d.ts +56 -23
  15. package/dist/builtins/monitor.workflow.js +508 -329
  16. package/dist/builtins/monitor.workflow.js.map +1 -1
  17. package/dist/builtins/pi-agent-group.d.ts +72 -0
  18. package/dist/builtins/pi-agent-group.js +1087 -0
  19. package/dist/builtins/pi-agent-group.js.map +1 -0
  20. package/dist/builtins/plain-summary.workflow.d.ts +32 -0
  21. package/dist/builtins/plain-summary.workflow.js +134 -0
  22. package/dist/builtins/plain-summary.workflow.js.map +1 -0
  23. package/dist/builtins/plan-approval.workflow.d.ts +39 -5
  24. package/dist/builtins/plan-approval.workflow.js +92 -14
  25. package/dist/builtins/plan-approval.workflow.js.map +1 -1
  26. package/dist/builtins/plan-change.workflow.d.ts +449 -0
  27. package/dist/builtins/plan-change.workflow.js +256 -0
  28. package/dist/builtins/plan-change.workflow.js.map +1 -0
  29. package/dist/builtins/plan-presentation.js +2 -2
  30. package/dist/builtins/plan-presentation.js.map +1 -1
  31. package/dist/builtins/sanity-check.workflow.d.ts +50 -6
  32. package/dist/builtins/sanity-check.workflow.js +150 -28
  33. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  34. package/dist/controllers/effects.d.ts +3 -2
  35. package/dist/controllers/effects.js +8 -1
  36. package/dist/controllers/effects.js.map +1 -1
  37. package/dist/controllers/index.d.ts +1 -1
  38. package/dist/controllers/index.js +1 -1
  39. package/dist/controllers/index.js.map +1 -1
  40. package/dist/controllers/manager.d.ts +1 -0
  41. package/dist/controllers/manager.js +41 -17
  42. package/dist/controllers/manager.js.map +1 -1
  43. package/dist/controllers/sqlite.d.ts +107 -85
  44. package/dist/controllers/sqlite.js +1611 -1486
  45. package/dist/controllers/sqlite.js.map +1 -1
  46. package/dist/controllers/store.d.ts +9 -8
  47. package/dist/controllers/store.js +1 -31
  48. package/dist/controllers/store.js.map +1 -1
  49. package/dist/controllers/types.d.ts +3 -0
  50. package/dist/controllers/workflow-engine-scheduler.d.ts +1 -1
  51. package/dist/controllers/workflow-engine-scheduler.js +3 -5
  52. package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
  53. package/dist/controllers/workflows.d.ts +2 -2
  54. package/dist/controllers/workflows.js +11 -10
  55. package/dist/controllers/workflows.js.map +1 -1
  56. package/dist/extension/controller-host.js +2 -2
  57. package/dist/extension/controller-host.js.map +1 -1
  58. package/dist/extension/decision-channels.d.ts +4 -2
  59. package/dist/extension/decision-channels.js +333 -146
  60. package/dist/extension/decision-channels.js.map +1 -1
  61. package/dist/extension/executor.d.ts +9 -2
  62. package/dist/extension/executor.js +89 -2
  63. package/dist/extension/executor.js.map +1 -1
  64. package/dist/extension/herdr-viewer.d.ts +0 -1
  65. package/dist/extension/herdr-viewer.js +0 -4
  66. package/dist/extension/herdr-viewer.js.map +1 -1
  67. package/dist/extension/index.js +209 -127
  68. package/dist/extension/index.js.map +1 -1
  69. package/dist/extension/recorder.d.ts +3 -10
  70. package/dist/extension/recorder.js +13 -22
  71. package/dist/extension/recorder.js.map +1 -1
  72. package/dist/extension/session-events.d.ts +2 -2
  73. package/dist/extension/step-message.d.ts +7 -2
  74. package/dist/extension/step-message.js +94 -2
  75. package/dist/extension/step-message.js.map +1 -1
  76. package/dist/extension/widget.js +29 -3
  77. package/dist/extension/widget.js.map +1 -1
  78. package/dist/host/rpc-executor.d.ts +3 -0
  79. package/dist/host/rpc-executor.js +2 -0
  80. package/dist/host/rpc-executor.js.map +1 -1
  81. package/dist/host/runner.d.ts +3 -6
  82. package/dist/host/runner.js +83 -32
  83. package/dist/host/runner.js.map +1 -1
  84. package/dist/render/graph-render.js +15 -10
  85. package/dist/render/graph-render.js.map +1 -1
  86. package/dist/state/database.d.ts +35 -0
  87. package/dist/state/database.js +287 -0
  88. package/dist/state/database.js.map +1 -0
  89. package/dist/state/index.d.ts +4 -0
  90. package/dist/state/index.js +5 -0
  91. package/dist/state/index.js.map +1 -0
  92. package/dist/state/json.d.ts +6 -0
  93. package/dist/state/json.js +38 -0
  94. package/dist/state/json.js.map +1 -0
  95. package/dist/state/mutation.d.ts +57 -0
  96. package/dist/state/mutation.js +255 -0
  97. package/dist/state/mutation.js.map +1 -0
  98. package/dist/state/schema.d.ts +6 -0
  99. package/dist/state/schema.js +473 -0
  100. package/dist/state/schema.js.map +1 -0
  101. package/dist/viewer/cli.d.ts +2 -2
  102. package/dist/viewer/cli.js +142 -122
  103. package/dist/viewer/cli.js.map +1 -1
  104. package/dist/viewer/render.d.ts +4 -4
  105. package/dist/viewer/render.js +9 -23
  106. package/dist/viewer/render.js.map +1 -1
  107. package/dist/viewer/session-reducer.d.ts +1 -1
  108. package/dist/viewer/session-reducer.js +1 -1
  109. package/dist/viewer/tui.d.ts +3 -3
  110. package/dist/viewer/tui.js +15 -13
  111. package/dist/viewer/tui.js.map +1 -1
  112. package/dist/viewer/watch.d.ts +2 -5
  113. package/dist/viewer/watch.js +13 -13
  114. package/dist/viewer/watch.js.map +1 -1
  115. package/dist/workflows/catalog.js +7 -2
  116. package/dist/workflows/catalog.js.map +1 -1
  117. package/dist/workflows/composition.js +20 -2
  118. package/dist/workflows/composition.js.map +1 -1
  119. package/dist/workflows/decision-presentation.d.ts +1 -1
  120. package/dist/workflows/decision-presentation.js +51 -38
  121. package/dist/workflows/decision-presentation.js.map +1 -1
  122. package/dist/workflows/definition.d.ts +6 -2
  123. package/dist/workflows/definition.js +17 -0
  124. package/dist/workflows/definition.js.map +1 -1
  125. package/dist/workflows/engine.d.ts +7 -7
  126. package/dist/workflows/engine.js +196 -115
  127. package/dist/workflows/engine.js.map +1 -1
  128. package/dist/workflows/errors.d.ts +16 -3
  129. package/dist/workflows/errors.js +18 -3
  130. package/dist/workflows/errors.js.map +1 -1
  131. package/dist/workflows/human-decision.d.ts +45 -8
  132. package/dist/workflows/human-decision.js +641 -274
  133. package/dist/workflows/human-decision.js.map +1 -1
  134. package/dist/workflows/index.d.ts +5 -6
  135. package/dist/workflows/index.js +4 -5
  136. package/dist/workflows/index.js.map +1 -1
  137. package/dist/workflows/progress.d.ts +1 -0
  138. package/dist/workflows/progress.js +15 -3
  139. package/dist/workflows/progress.js.map +1 -1
  140. package/dist/workflows/schema.js +27 -1
  141. package/dist/workflows/schema.js.map +1 -1
  142. package/dist/workflows/store.d.ts +83 -103
  143. package/dist/workflows/store.js +907 -951
  144. package/dist/workflows/store.js.map +1 -1
  145. package/dist/workflows/types.d.ts +88 -126
  146. package/docs/CONTROLLERS.md +10 -8
  147. package/docs/DEFERRED_TURNS.md +2 -2
  148. package/docs/DESIGN_PHILOSOPHY.md +4 -2
  149. package/docs/HUMAN_DECISIONS.md +36 -47
  150. package/docs/HUMAN_DECISION_PRESENTATIONS.md +14 -24
  151. package/docs/MONITOR.md +179 -237
  152. package/docs/SQLITE_STATE.md +219 -0
  153. package/docs/WORKFLOW_COMPOSITION.md +27 -26
  154. package/docs/WORKFLOW_STEP_MESSAGES.md +20 -18
  155. package/docs/WORKFLOW_UPDATES.md +10 -9
  156. package/docs/development.md +9 -9
  157. package/docs/live-replay-protocol.md +28 -37
  158. package/docs/plans/2026-08-16-workflow-updates-plan.md +1 -1
  159. package/docs/plans/2026-08-19-human-decision-gates-plan.md +1 -1
  160. package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +1 -1
  161. package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +322 -0
  162. package/docs/plans/2026-08-21-sanity-check-plan.md +217 -95
  163. package/docs/plans/2026-08-22-goal-finishing-monitor-plan.md +200 -0
  164. package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +701 -0
  165. package/docs/plans/2026-08-23-sqlite-state-plan.md +970 -0
  166. package/docs/plans/replayable-run-bundles-implementation-plan.md +2 -2
  167. package/docs/plans/session-event-replay-implementation-plan.md +1 -1
  168. package/docs/plans/tui-viewer-implementation-plan.md +1 -1
  169. package/docs/session-event-journal.md +63 -434
  170. package/docs/tui-viewer.md +13 -15
  171. package/docs/workflows.md +127 -53
  172. package/examples/workflows/approved-plan.workflow.ts +19 -46
  173. package/examples/workflows/plain-summary.workflow.ts +1 -0
  174. package/herdr-plugin.toml +1 -1
  175. package/package.json +7 -7
  176. package/plugins/herdr/viewer.mjs +1 -11
  177. package/schemas/human-decision-accepted-v1.schema.json +15 -3
  178. package/schemas/human-decision-continuation-v1.schema.json +10 -1
  179. package/schemas/human-decision-delivery-v1.schema.json +8 -0
  180. package/schemas/human-decision-receipt-v1.schema.json +8 -0
  181. package/schemas/human-decision-request-v1.schema.json +24 -4
  182. package/skills/autoimplement/SKILL.md +27 -0
  183. package/skills/autoplan/SKILL.md +5 -4
  184. package/skills/monitor/SKILL.md +91 -135
  185. package/skills/pi-workflows/SKILL.md +9 -8
  186. package/skills/sanity-check/SKILL.md +44 -0
  187. package/src/builtins/autoimplement.workflow.ts +19 -118
  188. package/src/builtins/autoplan.workflow.ts +290 -40
  189. package/src/builtins/catalog.ts +7 -5
  190. package/src/builtins/index.ts +18 -1
  191. package/src/builtins/monitor.workflow.ts +673 -382
  192. package/src/builtins/pi-agent-group.ts +1407 -0
  193. package/src/builtins/plain-summary.workflow.ts +185 -0
  194. package/src/builtins/plan-approval.workflow.ts +157 -24
  195. package/src/builtins/plan-change.workflow.ts +321 -0
  196. package/src/builtins/plan-presentation.ts +2 -2
  197. package/src/builtins/sanity-check.workflow.ts +248 -48
  198. package/src/controllers/effects.ts +7 -0
  199. package/src/controllers/index.ts +0 -6
  200. package/src/controllers/manager.ts +72 -32
  201. package/src/controllers/sqlite.ts +2420 -1948
  202. package/src/controllers/store.ts +17 -38
  203. package/src/controllers/types.ts +3 -0
  204. package/src/controllers/workflow-engine-scheduler.ts +4 -8
  205. package/src/controllers/workflows.ts +45 -21
  206. package/src/extension/controller-host.ts +1 -2
  207. package/src/extension/decision-channels.ts +456 -203
  208. package/src/extension/executor.ts +105 -2
  209. package/src/extension/herdr-viewer.ts +0 -5
  210. package/src/extension/index.ts +251 -151
  211. package/src/extension/recorder.ts +13 -22
  212. package/src/extension/session-events.ts +2 -2
  213. package/src/extension/step-message.ts +106 -4
  214. package/src/extension/widget.ts +32 -5
  215. package/src/host/rpc-executor.ts +4 -0
  216. package/src/host/runner.ts +93 -41
  217. package/src/render/graph-render.ts +12 -6
  218. package/src/state/database.ts +375 -0
  219. package/src/state/index.ts +29 -0
  220. package/src/state/json.ts +42 -0
  221. package/src/state/mutation.ts +435 -0
  222. package/src/state/schema.ts +475 -0
  223. package/src/viewer/cli.ts +151 -144
  224. package/src/viewer/render.ts +19 -30
  225. package/src/viewer/session-reducer.ts +1 -1
  226. package/src/viewer/tui.ts +18 -17
  227. package/src/viewer/watch.ts +13 -17
  228. package/src/workflows/catalog.ts +7 -2
  229. package/src/workflows/composition.ts +21 -3
  230. package/src/workflows/decision-presentation.ts +56 -43
  231. package/src/workflows/definition.ts +36 -3
  232. package/src/workflows/engine.ts +228 -130
  233. package/src/workflows/errors.ts +27 -3
  234. package/src/workflows/human-decision.ts +921 -349
  235. package/src/workflows/index.ts +20 -30
  236. package/src/workflows/progress.ts +18 -3
  237. package/src/workflows/schema.ts +36 -1
  238. package/src/workflows/store.ts +1300 -1163
  239. package/src/workflows/types.ts +100 -137
  240. package/dist/builtins/sanity-check-session.d.ts +0 -17
  241. package/dist/builtins/sanity-check-session.js +0 -168
  242. package/dist/builtins/sanity-check-session.js.map +0 -1
  243. package/dist/workflows/artifacts.d.ts +0 -40
  244. package/dist/workflows/artifacts.js +0 -155
  245. package/dist/workflows/artifacts.js.map +0 -1
  246. package/dist/workflows/migrate-sources.d.ts +0 -42
  247. package/dist/workflows/migrate-sources.js +0 -133
  248. package/dist/workflows/migrate-sources.js.map +0 -1
  249. package/docs/run-bundles.md +0 -481
  250. package/schemas/human-decision-accepted-v2.schema.json +0 -50
  251. package/schemas/human-decision-delivery-v2.schema.json +0 -36
  252. package/schemas/human-decision-receipt-v2.schema.json +0 -39
  253. package/schemas/human-decision-request-v2.schema.json +0 -69
  254. package/schemas/human-decision-resolution-v2.schema.json +0 -27
  255. package/src/builtins/sanity-check-session.ts +0 -205
  256. package/src/workflows/artifacts.ts +0 -188
  257. package/src/workflows/migrate-sources.ts +0 -178
@@ -76,19 +76,12 @@ function failureMessage(error: unknown): string {
76
76
  }
77
77
 
78
78
  /**
79
- * Records both settled Pi entries and normalized temporal events into one run
80
- * bundle. The hot event path only stamps and queues records; filesystem work
81
- * runs on a separate ordered append chain.
79
+ * Records settled Pi entries and normalized temporal events in SQLite.
82
80
  */
83
81
  export class SessionRecorder {
84
82
  private readonly store: WorkflowRunStore;
85
- private readonly runDir: string;
86
83
  private readonly runId: string;
87
- /**
88
- * Capture segment for this recorder. Undefined writes the legacy flat
89
- * stream; a second recorder attaching to the same bundle (resume or
90
- * handoff) writes a segment instead so the first capture stays intact.
91
- */
84
+ /** Capture segment for this recorder after an ownership handoff. */
92
85
  private segmentId: string | undefined;
93
86
  private cursor: string | null = null;
94
87
  private readonly recorded: string[] = [];
@@ -122,9 +115,8 @@ export class SessionRecorder {
122
115
  private flushPromise: Promise<void> | null = null;
123
116
  private captureFailure: WorkflowSessionCaptureFailure | null = null;
124
117
 
125
- constructor(store: WorkflowRunStore, runDir: string, runId: string) {
118
+ constructor(store: WorkflowRunStore, runId: string) {
126
119
  this.store = store;
127
- this.runDir = runDir;
128
120
  this.runId = runId;
129
121
  }
130
122
 
@@ -135,13 +127,12 @@ export class SessionRecorder {
135
127
  this.bound = true;
136
128
  this.cursor = ctx.sessionManager.getLeafId();
137
129
  const sessionFile = ctx.sessionManager.getSessionFile();
138
- // A bundle that already has a binding belongs to an earlier capture
139
- // attempt; this recorder writes its own segment under session/segments.
140
- if (await this.store.hasSessionBinding(this.runDir)) {
130
+ // An existing binding belongs to an earlier capture segment.
131
+ if (await this.store.hasSessionBinding(this.runId)) {
141
132
  this.segmentId = randomUUID();
142
133
  }
143
134
  await this.store.writeSessionBinding(
144
- this.runDir,
135
+ this.runId,
145
136
  {
146
137
  schema: SESSION_BINDING_SCHEMA,
147
138
  runId: this.runId,
@@ -153,7 +144,7 @@ export class SessionRecorder {
153
144
  this.segmentId,
154
145
  );
155
146
  await this.store.writeSessionCapture(
156
- this.runDir,
147
+ this.runId,
157
148
  {
158
149
  schema: SESSION_CAPTURE_SCHEMA,
159
150
  eventSchema: SESSION_EVENT_SCHEMA,
@@ -344,7 +335,7 @@ export class SessionRecorder {
344
335
  }
345
336
  const appended: RecordedEntry[] = [];
346
337
  for (const entry of branch.slice(startIndex)) {
347
- await this.store.appendSessionEntry(this.runDir, entry, this.segmentId);
338
+ await this.store.appendSessionEntry(this.runId, entry, this.segmentId);
348
339
  const recorded = { id: entry.id, entry, claimed: false };
349
340
  appended.push(recorded);
350
341
  this.unclaimedEntries.push(recorded);
@@ -426,9 +417,9 @@ export class SessionRecorder {
426
417
  try {
427
418
  await this.flushAllEvents();
428
419
  await this.entryChain;
429
- const counts = await this.store.sessionCounts(this.runDir, this.segmentId);
420
+ const counts = await this.store.sessionCounts(this.runId, this.segmentId);
430
421
  await this.store.writeSessionCapture(
431
- this.runDir,
422
+ this.runId,
432
423
  {
433
424
  schema: SESSION_CAPTURE_SCHEMA,
434
425
  eventSchema: SESSION_EVENT_SCHEMA,
@@ -443,9 +434,9 @@ export class SessionRecorder {
443
434
  // the workflow's terminal persistence hook.
444
435
  this.failCapture("capture_finalize_failed", failureMessage(error));
445
436
  try {
446
- const counts = await this.store.sessionCounts(this.runDir, this.segmentId);
437
+ const counts = await this.store.sessionCounts(this.runId, this.segmentId);
447
438
  await this.store.writeSessionCapture(
448
- this.runDir,
439
+ this.runId,
449
440
  {
450
441
  schema: SESSION_CAPTURE_SCHEMA,
451
442
  eventSchema: SESSION_EVENT_SCHEMA,
@@ -602,7 +593,7 @@ export class SessionRecorder {
602
593
  }
603
594
  this.flushPromise = this.store
604
595
  .appendSessionEventBatch(
605
- this.runDir,
596
+ this.runId,
606
597
  batch.map((queued) => queued.record),
607
598
  this.segmentId,
608
599
  )
@@ -9,7 +9,7 @@ export type PublicAssistantMessageEvent =
9
9
  | { type: "toolcall_start"; contentIndex: number; partial: unknown }
10
10
  | { type: "toolcall_delta"; contentIndex: number; delta: string; partial: unknown }
11
11
  | { type: "toolcall_end"; contentIndex: number; toolCall: unknown; partial: unknown }
12
- | { type: "done"; reason: "stop" | "length" | "toolUse"; message: unknown }
12
+ | { type: "done"; reason: "stop" | "length" | "toolUse" | "deferred"; message: unknown }
13
13
  | { type: "error"; reason: "aborted" | "error"; error: unknown };
14
14
 
15
15
  export type TurnStartEventLike = { turnIndex: number };
@@ -44,7 +44,7 @@ export type NormalizedAssistantEvent =
44
44
  | { type: "toolcall_start"; contentIndex: number }
45
45
  | { type: "toolcall_delta"; contentIndex: number; delta: string }
46
46
  | { type: "toolcall_end"; contentIndex: number; toolCall: unknown }
47
- | { type: "done"; reason: "stop" | "length" | "toolUse" }
47
+ | { type: "done"; reason: "stop" | "length" | "toolUse" | "deferred" }
48
48
  | { type: "error"; reason: "aborted" | "error" };
49
49
 
50
50
  /** Remove cumulative snapshots while preserving every semantic stream event. */
@@ -1,8 +1,13 @@
1
+ import { createHash } from "node:crypto";
1
2
  import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
2
3
  import { Box, Text, TruncatedText } from "@earendil-works/pi-tui";
3
4
  import { sanitizeText } from "../workflows/text.js";
4
- import type { AgentStepContract, AgentStepPresentation } from "../workflows/types.js";
5
- import type { PromptDeliveryKind } from "./executor.js";
5
+ import type {
6
+ AgentStepContract,
7
+ AgentStepPresentation,
8
+ AgentStepSubmission,
9
+ } from "../workflows/types.js";
10
+ import { visibleAssistantText, type PromptDeliveryKind } from "./executor.js";
6
11
 
7
12
  export const WORKFLOW_AGENT_STEP_MESSAGE_TYPE = "pi-workflows-agent-step";
8
13
  export const WORKFLOW_AGENT_STEP_MESSAGE_SCHEMA = "pi-workflows.agent-step-message.v1";
@@ -69,6 +74,10 @@ export function buildWorkflowAgentStepView(
69
74
  };
70
75
  }
71
76
 
77
+ const assistant = contract?.completion === "assistant";
78
+ const expectedOutput = assistant
79
+ ? `normal assistant response${contract?.maxOutputChars === undefined ? "" : ` (maximum ${contract.maxOutputChars} characters)`}`
80
+ : (contract?.expectedOutput ?? "a JSON object with your result");
72
81
  const metadata = [
73
82
  `Workflow: ${workflowName}`,
74
83
  ...(runTitle !== undefined ? [`Run title: ${runTitle}`] : []),
@@ -76,7 +85,8 @@ export function buildWorkflowAgentStepView(
76
85
  `Node id: ${nodeId}`,
77
86
  `Attempt id: ${cleanSingleLine(contract?.attemptId ?? "unknown")}`,
78
87
  `Delivery: ${kind}`,
79
- `Expected output: ${cleanSingleLine(contract?.expectedOutput ?? "a JSON object with your result")}`,
88
+ `Completion: ${assistant ? "assistant response" : "workflow submission"}`,
89
+ `Expected output: ${cleanSingleLine(expectedOutput)}`,
80
90
  ];
81
91
  const prompt = contentText(message.content);
82
92
  return {
@@ -86,6 +96,93 @@ export function buildWorkflowAgentStepView(
86
96
  };
87
97
  }
88
98
 
99
+ /**
100
+ * Adopt an already visible response for the same assistant-output attempt.
101
+ * This is used only when an interrupted run resumes in its origin session.
102
+ */
103
+ export function recoverAssistantStep(
104
+ entries: readonly unknown[],
105
+ contract: AgentStepContract,
106
+ ): AgentStepSubmission | undefined {
107
+ if (contract.completion !== "assistant") return undefined;
108
+ for (let promptIndex = entries.length - 1; promptIndex >= 0; promptIndex -= 1) {
109
+ const prompt = sessionEntry(entries[promptIndex]);
110
+ if (
111
+ prompt?.type !== "custom_message" ||
112
+ prompt.customType !== WORKFLOW_AGENT_STEP_MESSAGE_TYPE
113
+ ) {
114
+ continue;
115
+ }
116
+ const details = parseDetails(prompt.details);
117
+ if (details === undefined || !sameAttempt(details.contract, contract)) continue;
118
+
119
+ let response: Record<string, unknown> | undefined;
120
+ for (let responseIndex = promptIndex + 1; responseIndex < entries.length; responseIndex += 1) {
121
+ const candidate = sessionEntry(entries[responseIndex]);
122
+ if (candidate === undefined) continue;
123
+ if (
124
+ candidate.type === "custom_message" &&
125
+ candidate.customType === WORKFLOW_AGENT_STEP_MESSAGE_TYPE
126
+ ) {
127
+ break;
128
+ }
129
+ if (candidate.type !== "message") continue;
130
+ const message = sessionEntry(candidate.message);
131
+ if (message?.role === "user") break;
132
+ if (isAssistantWithVisibleText(message)) response = candidate;
133
+ }
134
+ if (response === undefined) return undefined;
135
+ const output = visibleAssistantText(response.message, contract.maxOutputChars);
136
+ const entryId = typeof response.id === "string" ? response.id : undefined;
137
+ const promptId = typeof prompt.id === "string" ? prompt.id : undefined;
138
+ return {
139
+ output,
140
+ assistantMessage: {
141
+ sha256: createHash("sha256").update(output).digest("hex"),
142
+ ...(entryId !== undefined ? { entryId } : {}),
143
+ ...(contract.maxOutputChars !== undefined ? { maxChars: contract.maxOutputChars } : {}),
144
+ recovered: true,
145
+ },
146
+ ...(promptId !== undefined && entryId !== undefined
147
+ ? { conversation: { firstEntryId: promptId, lastEntryId: entryId } }
148
+ : {}),
149
+ };
150
+ }
151
+ return undefined;
152
+ }
153
+
154
+ function sameAttempt(left: AgentStepContract, right: AgentStepContract): boolean {
155
+ return (
156
+ left.runId === right.runId &&
157
+ left.workflowName === right.workflowName &&
158
+ left.nodeId === right.nodeId &&
159
+ left.attemptId === right.attemptId &&
160
+ left.completion === "assistant" &&
161
+ left.maxOutputChars === right.maxOutputChars
162
+ );
163
+ }
164
+
165
+ function sessionEntry(value: unknown): Record<string, unknown> | undefined {
166
+ return value !== null && typeof value === "object" && !Array.isArray(value)
167
+ ? (value as Record<string, unknown>)
168
+ : undefined;
169
+ }
170
+
171
+ function isAssistantWithVisibleText(message: unknown): boolean {
172
+ if (message === null || typeof message !== "object") return false;
173
+ const candidate = message as { role?: unknown; content?: unknown; stopReason?: unknown };
174
+ if (candidate.role !== "assistant" || !Array.isArray(candidate.content)) return false;
175
+ if (candidate.stopReason !== "stop" && candidate.stopReason !== "length") return false;
176
+ return candidate.content.some(
177
+ (part) =>
178
+ part !== null &&
179
+ typeof part === "object" &&
180
+ (part as { type?: unknown }).type === "text" &&
181
+ typeof (part as { text?: unknown }).text === "string" &&
182
+ (part as { text: string }).text.trim().length > 0,
183
+ );
184
+ }
185
+
89
186
  function parseDetails(value: unknown): WorkflowAgentStepMessageDetails | undefined {
90
187
  if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
91
188
  const candidate = value as Partial<WorkflowAgentStepMessageDetails>;
@@ -100,7 +197,12 @@ function parseDetails(value: unknown): WorkflowAgentStepMessageDetails | undefin
100
197
  typeof contract.runId !== "string" ||
101
198
  typeof contract.workflowName !== "string" ||
102
199
  typeof contract.nodeId !== "string" ||
103
- typeof contract.attemptId !== "string"
200
+ typeof contract.attemptId !== "string" ||
201
+ (contract.completion !== "submit" && contract.completion !== "assistant") ||
202
+ (contract.maxOutputChars !== undefined &&
203
+ (typeof contract.maxOutputChars !== "number" ||
204
+ !Number.isInteger(contract.maxOutputChars) ||
205
+ contract.maxOutputChars <= 0))
104
206
  ) {
105
207
  return undefined;
106
208
  }
@@ -162,9 +162,11 @@ function progressLines(
162
162
  : progressTracksFromRecords(updateHistory, now).map((track) => track.estimate);
163
163
  const projected = mergeProgressEstimates(latestProgressEstimates(state, now), measured ?? []);
164
164
  const estimates = mergeProgressEstimates(projected, monitorEstimates(state) ?? []);
165
- const lines = prioritizeProgressEstimates(estimates).map((estimate) =>
166
- formatProgressLine(estimate, now),
167
- );
165
+ const lines = prioritizeWidgetProgress(prioritizeProgressEstimates(estimates)).map((estimate) => {
166
+ const indentation =
167
+ estimate.key.startsWith("agents/") && estimate.key.split("/").length > 2 ? " " : "";
168
+ return `${indentation}${formatProgressLine(estimate, now)}`;
169
+ });
168
170
  const schedule = (state.updates ?? []).find(
169
171
  (record) => record.type === "monitor.schedule" && record.key === "next-check",
170
172
  );
@@ -180,6 +182,24 @@ function progressLines(
180
182
  return lines;
181
183
  }
182
184
 
185
+ function prioritizeWidgetProgress(estimates: ProgressEstimate[]): ProgressEstimate[] {
186
+ const weight = (estimate: ProgressEstimate) => {
187
+ if (!estimate.key.startsWith("agents/")) return 0;
188
+ const child = estimate.key.split("/").length > 2;
189
+ if (!child) return -4;
190
+ if (estimate.data.status === "failed" || estimate.data.status === "blocked") return -3;
191
+ if (
192
+ estimate.data.status === "running" ||
193
+ estimate.data.status === "pending" ||
194
+ estimate.data.status === "waiting"
195
+ ) {
196
+ return -2;
197
+ }
198
+ return 2;
199
+ };
200
+ return [...estimates].sort((left, right) => weight(left) - weight(right));
201
+ }
202
+
183
203
  function monitorEstimates(state: WorkflowRunState): ProgressEstimate[] | undefined {
184
204
  const output = state.outputs.estimate;
185
205
  if (output === null || typeof output !== "object" || Array.isArray(output)) return undefined;
@@ -278,6 +298,14 @@ function nodeRuntimeSegments(
278
298
  now: Date,
279
299
  ): string[] {
280
300
  const segments: string[] = [];
301
+ const node = snapshot.nodes[nodeId];
302
+ if (
303
+ node?.nodeType === "agent" &&
304
+ typeof node.expectedOutput === "object" &&
305
+ node.expectedOutput.kind === "assistant-message"
306
+ ) {
307
+ segments.push("assistant response");
308
+ }
281
309
  const completedAttempts = state.steps.filter((step) => step.nodeId === nodeId).length;
282
310
  const attempts = completedAttempts + (state.currentNode === nodeId ? 1 : 0);
283
311
  if (attempts > 1) {
@@ -309,8 +337,7 @@ function nodeRuntimeSegments(
309
337
  })
310
338
  : undefined;
311
339
  const requestAudience =
312
- (request?.schema === "pi-workflows.human-decision-request.v1" ||
313
- request?.schema === "pi-workflows.human-decision-request.v2") &&
340
+ request?.schema === "pi-workflows.human-decision-request.v1" &&
314
341
  typeof request.audience === "string"
315
342
  ? request.audience
316
343
  : human.audience;
@@ -43,6 +43,8 @@ export type RpcStepExecutorOptions = {
43
43
  env?: Record<string, string>;
44
44
  /** Extra pi arguments, for example a model override. */
45
45
  piArgs?: string[];
46
+ /** Park assistant-message nodes for a visible origin session. */
47
+ assistantMessageMode?: "park";
46
48
  };
47
49
 
48
50
  /**
@@ -53,6 +55,7 @@ export type RpcStepExecutorOptions = {
53
55
  * the model sees the same tool contract as an in-session run.
54
56
  */
55
57
  export class RpcStepExecutor implements AgentStepExecutor {
58
+ readonly assistantMessageMode: "park" | "unsupported";
56
59
  private readonly options: RpcStepExecutorOptions;
57
60
  private child: ChildProcess | null = null;
58
61
  private childExited: { code: number | null; signal: string | null } | null = null;
@@ -63,6 +66,7 @@ export class RpcStepExecutor implements AgentStepExecutor {
63
66
 
64
67
  constructor(options: RpcStepExecutorOptions) {
65
68
  this.options = options;
69
+ this.assistantMessageMode = options.assistantMessageMode ?? "unsupported";
66
70
  }
67
71
 
68
72
  async runAgentStep(request: AgentStepRequest, signal: AbortSignal): Promise<AgentStepSubmission> {
@@ -1,16 +1,16 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { builtinWorkflowCatalog } from "../builtins/catalog.js";
5
5
  import {
6
6
  ControllerManager,
7
7
  loadDiscoveredControllers,
8
- projectControllerStorePath,
9
8
  SqliteControllerStore,
10
9
  WorkflowEngineScheduler,
11
10
  type WorkflowRunQueueRecord,
12
11
  } from "../controllers/index.js";
13
12
  import type { JsonObject } from "../controllers/types.js";
13
+ import { workflowStatePath } from "../state/database.js";
14
14
  import { WorkflowEngine } from "../workflows/engine.js";
15
15
  import {
16
16
  ClaimLostError,
@@ -19,9 +19,8 @@ import {
19
19
  WorkflowSourceChangedError,
20
20
  } from "../workflows/errors.js";
21
21
  import { resolveWorkflowRef, resolveWorkflowSource } from "../workflows/loader.js";
22
- import { migrateLegacyWorkflowSources } from "../workflows/migrate-sources.js";
23
- import { WorkflowRunStore, readRunBundle } from "../workflows/store.js";
24
- import type { WorkflowDefinition } from "../workflows/types.js";
22
+ import { WorkflowRunStore } from "../workflows/store.js";
23
+ import type { ResolvedHumanDecision, WorkflowDefinition } from "../workflows/types.js";
25
24
  import { HostProcessRegistry } from "./processes.js";
26
25
  import { RpcStepExecutor } from "./rpc-executor.js";
27
26
 
@@ -32,10 +31,8 @@ const RUN_CLAIM_RENEW_MS = 10_000;
32
31
  export type WorkflowHostOptions = {
33
32
  cwd: string;
34
33
  runnerId?: string;
35
- /** Explicit store path; defaults to the project-scoped controller store. */
36
- storeFile?: string;
37
- /** Explicit run-bundle root; defaults to the shared runs directory. */
38
- runsDir?: string;
34
+ /** Explicit database path; defaults to the canonical workflow state database. */
35
+ databasePath?: string;
39
36
  registry?: HostProcessRegistry;
40
37
  piArgs?: string[];
41
38
  /** Extra environment for headless children (for example a test provider). */
@@ -49,7 +46,7 @@ export type WorkflowHostOptions = {
49
46
  * The always-on runner: claims parked workflow runs and reconciles durable
50
47
  * controllers without a Pi session. Conversation nodes execute in headless
51
48
  * `pi --mode rpc` children. Everything the host does is recoverable: claims
52
- * expire, bundles fence stale writers, and child processes are reaped by
49
+ * expire, SQLite leases fence stale writers, and child processes are reaped by
53
50
  * the next host.
54
51
  */
55
52
  export class WorkflowHost {
@@ -61,7 +58,6 @@ export class WorkflowHost {
61
58
  private manager: ControllerManager | null = null;
62
59
  private pollTimer: ReturnType<typeof setInterval> | null = null;
63
60
  private readonly activeRuns = new Map<string, Promise<void>>();
64
- private readonly migrationBlockedRuns = new Set<string>();
65
61
  private readonly schedulerExecutors = new Map<WorkflowEngine, RpcStepExecutor>();
66
62
  /** Runs whose resume refused (edited source); skipped until a host restart. */
67
63
  private readonly skippedRuns = new Set<string>();
@@ -72,14 +68,15 @@ export class WorkflowHost {
72
68
  constructor(options: WorkflowHostOptions) {
73
69
  this.options = options;
74
70
  this.runnerId = options.runnerId ?? `host-${randomUUID().slice(0, 8)}`;
75
- this.store = new SqliteControllerStore(
76
- options.storeFile ?? projectControllerStorePath(options.cwd),
71
+ const databasePath = options.databasePath ?? workflowStatePath();
72
+ this.store = new SqliteControllerStore(databasePath, { projectPath: options.cwd });
73
+ this.stateDir = path.join(
74
+ path.dirname(this.store.filePath),
75
+ "hosts",
76
+ hostProjectScope(options.cwd),
77
77
  );
78
- this.stateDir = path.dirname(this.store.filePath);
79
78
  this.registry = options.registry ?? new HostProcessRegistry(this.stateDir);
80
- this.childRunStore = new WorkflowRunStore(
81
- options.runsDir ?? process.env.PI_WORKFLOWS_RUNS_DIR ?? undefined,
82
- );
79
+ this.childRunStore = new WorkflowRunStore(databasePath);
83
80
  }
84
81
 
85
82
  private log(message: string): void {
@@ -97,16 +94,6 @@ export class WorkflowHost {
97
94
  this.log(`reaped ${reaped.length} orphaned headless session(s): ${reaped.join(", ")}`);
98
95
  }
99
96
 
100
- const migration = await migrateLegacyWorkflowSources({
101
- catalog: builtinWorkflowCatalog,
102
- store: this.childRunStore,
103
- queue: this.store,
104
- });
105
- for (const blocked of migration.blocked) {
106
- this.migrationBlockedRuns.add(blocked.runId);
107
- this.log(`run ${blocked.runId} parked: ${blocked.reason}`);
108
- }
109
-
110
97
  const definitions = await loadDiscoveredControllers({ cwd: this.options.cwd });
111
98
  if (definitions.length > 0) {
112
99
  const scheduler = new WorkflowEngineScheduler({
@@ -192,7 +179,7 @@ export class WorkflowHost {
192
179
  runnerId: this.runnerId,
193
180
  claimToken: randomUUID(),
194
181
  leaseMs: RUN_CLAIM_LEASE_MS,
195
- excludeRunIds: [...this.skippedRuns, ...this.migrationBlockedRuns],
182
+ excludeRunIds: [...this.skippedRuns],
196
183
  });
197
184
  } catch (error) {
198
185
  // Store contention or corruption must not kill the host's loop.
@@ -215,7 +202,15 @@ export class WorkflowHost {
215
202
  let workflow: WorkflowDefinition;
216
203
  let workflowSource: import("../workflows/types.js").WorkflowSource;
217
204
  try {
218
- const bundle = await readRunBundle(this.childRunStore.runDirFor(runId));
205
+ const bundle = this.childRunStore.readRun(runId);
206
+ if (bundle?.state.status === "waiting") {
207
+ if (this.store.parkWorkflowRun({ runId, claimToken })) {
208
+ this.store.settleRunEffect(runId, "run.park_queue");
209
+ this.skippedRuns.add(runId);
210
+ this.log(`left ${record.workflowName} run ${runId} waiting for its decision owner`);
211
+ }
212
+ return;
213
+ }
219
214
  if (bundle?.state.workflowSource === undefined) {
220
215
  throw new Error(`Workflow run ${runId} has no canonical workflow source`);
221
216
  }
@@ -260,12 +255,13 @@ export class WorkflowHost {
260
255
  throw new ClaimLostError(runId);
261
256
  }
262
257
  };
263
- const fencedStore = new WorkflowRunStore(this.childRunStore.outputRoot, {
264
- fenceProvider: () => fence,
258
+ const fencedStore = new WorkflowRunStore(this.childRunStore.databasePath, {
259
+ authorityProvider: () => store.workflowRunAuthority(runId, claimToken),
265
260
  });
266
261
  const executor = new RpcStepExecutor({
267
262
  cwd: this.options.cwd,
268
263
  registry: this.registry,
264
+ ...(record.originSessionId !== null ? { assistantMessageMode: "park" as const } : {}),
269
265
  ...(this.options.piArgs !== undefined ? { piArgs: this.options.piArgs } : {}),
270
266
  ...(this.options.env !== undefined ? { env: this.options.env } : {}),
271
267
  });
@@ -308,16 +304,39 @@ export class WorkflowHost {
308
304
 
309
305
  this.recordEvent(runId, record.workflowName, "resumed", { runnerId: this.runnerId });
310
306
  try {
311
- const result = await engine.resumeRun(workflow, runId, { workflowSource });
307
+ const humanDecision =
308
+ record.parentRunId === null ? undefined : hostHumanDecision(record.launchOptions);
309
+ const result = record.initialized
310
+ ? await engine.resumeRun(workflow, runId, { workflowSource })
311
+ : record.parentRunId === null
312
+ ? await engine.run(workflow, record.input, { workflowSource, runId })
313
+ : await engine.continueRun(workflow, record.parentRunId, record.input, {
314
+ workflowSource,
315
+ runId,
316
+ ...(humanDecision === undefined ? {} : { humanDecision }),
317
+ });
312
318
  clearInterval(renewTimer);
313
- if (result.state.status === "running") {
314
- // Parked again mid-drain: leave it claimable for the next runner.
319
+ if (result.state.status === "running" || result.state.status === "waiting") {
320
+ // A drained run, visible-assistant handoff, or new checkpoint stays
321
+ // available to its next owner. Skip waits in this host process so it
322
+ // cannot reclaim the same run in a tight loop.
315
323
  this.store.parkWorkflowRun({ runId, claimToken });
316
- this.recordEvent(runId, record.workflowName, "parked", {});
317
- this.log(`parked ${record.workflowName} run ${runId}`);
324
+ this.store.settleRunEffect(runId, "run.park_queue");
325
+ const waitingForOrigin =
326
+ result.state.status === "running" &&
327
+ result.state.statusDetail === "waiting for origin Pi session";
328
+ if (result.state.status === "waiting" || waitingForOrigin) this.skippedRuns.add(runId);
329
+ this.recordEvent(runId, record.workflowName, "parked", {
330
+ status: result.state.status,
331
+ ...(waitingForOrigin ? { reason: "waiting for origin Pi session" } : {}),
332
+ });
333
+ this.log(
334
+ `parked ${record.workflowName} run ${runId}${waitingForOrigin ? ": waiting for origin Pi session" : ""}`,
335
+ );
318
336
  return;
319
337
  }
320
338
  this.store.completeWorkflowRun({ runId, claimToken });
339
+ this.store.settleRunEffect(runId, "run.settle_queue");
321
340
  this.recordEvent(runId, record.workflowName, result.state.status, {
322
341
  ...(result.state.error !== undefined ? { error: result.state.error } : {}),
323
342
  ...(result.state.waitingOn !== undefined ? { waitingOn: result.state.waitingOn } : {}),
@@ -352,6 +371,7 @@ export class WorkflowHost {
352
371
  if (index !== -1) {
353
372
  this.parkedEngines.splice(index, 1);
354
373
  }
374
+ fencedStore.close();
355
375
  await executor.close().catch(() => undefined);
356
376
  }
357
377
  }
@@ -365,17 +385,22 @@ export class WorkflowHost {
365
385
  let heldClaim = false;
366
386
  try {
367
387
  // The interruption write obeys the same fencing rule as every other
368
- // bundle write: without a live claim, the current owner decides.
388
+ // state write: without a live claim, the current owner decides.
369
389
  heldClaim = this.store.verifyWorkflowRunClaim({ runId: record.runId, claimToken });
370
390
  if (heldClaim) {
371
- const bundle = await this.childRunStore.markRunInterrupted(record.runId, message);
391
+ const claimedStore = new WorkflowRunStore(this.store.filePath, {
392
+ state: this.store.state,
393
+ authorityProvider: () => this.store.workflowRunAuthority(record.runId, claimToken),
394
+ });
395
+ const bundle = await claimedStore.markRunInterrupted(record.runId, message);
372
396
  actualStatus = bundle?.state.status;
373
397
  }
374
398
  } catch {
375
- // The bundle may be unreadable; the queue row still needs closure.
399
+ // The run state may be unreadable; the queue row still needs closure.
376
400
  }
377
401
  try {
378
402
  this.store.completeWorkflowRun({ runId: record.runId, claimToken });
403
+ this.store.settleRunEffect(record.runId, "run.settle_queue");
379
404
  } catch {
380
405
  // Best-effort.
381
406
  }
@@ -384,8 +409,8 @@ export class WorkflowHost {
384
409
  this.log(`run ${record.runId} continues under another runner`);
385
410
  return;
386
411
  }
387
- // Report the bundle's real terminal state when the interruption was a
388
- // no-op (the bundle was already waiting or completed), so the feed
412
+ // Report the run's real terminal state when the interruption was a
413
+ // no-op (the run was already waiting or completed), so the feed
389
414
  // stays truthful for sessions syncing from it.
390
415
  if (actualStatus !== undefined && actualStatus !== "failed") {
391
416
  this.recordEvent(record.runId, record.workflowName, actualStatus, {});
@@ -404,6 +429,33 @@ export class WorkflowHost {
404
429
  }
405
430
  }
406
431
 
432
+ function hostHumanDecision(value: unknown): ResolvedHumanDecision | undefined {
433
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
434
+ throw new Error("Stored workflow launch options are invalid");
435
+ }
436
+ const decision = Reflect.get(value, "humanDecision");
437
+ if (decision === undefined) return undefined;
438
+ if (
439
+ typeof decision !== "object" ||
440
+ decision === null ||
441
+ Array.isArray(decision) ||
442
+ Reflect.get(decision, "schema") !== "pi-workflows.human-decision-accepted.v1"
443
+ ) {
444
+ throw new Error("Stored human decision launch option is invalid");
445
+ }
446
+ return decision as ResolvedHumanDecision;
447
+ }
448
+
449
+ function hostProjectScope(cwd: string): string {
450
+ let canonical: string;
451
+ try {
452
+ canonical = fs.realpathSync.native(cwd);
453
+ } catch {
454
+ canonical = path.resolve(cwd);
455
+ }
456
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 24);
457
+ }
458
+
407
459
  function hostLockPath(stateDir: string): string {
408
460
  return path.join(stateDir, "host.lock");
409
461
  }
@@ -313,8 +313,7 @@ function renderCellText(
313
313
  ? (state.finalOutput as { schema?: unknown }).schema
314
314
  : undefined;
315
315
  const waitingRequest =
316
- waitingSchema === "pi-workflows.human-decision-request.v1" ||
317
- waitingSchema === "pi-workflows.human-decision-request.v2"
316
+ waitingSchema === "pi-workflows.human-decision-request.v1"
318
317
  ? (state.finalOutput as {
319
318
  audience?: unknown;
320
319
  presentationDigest?: unknown;
@@ -341,15 +340,22 @@ function renderCellText(
341
340
  : selected !== undefined
342
341
  ? `human: ${sanitizeText(selected.label)}`
343
342
  : undefined;
344
- const detail =
345
- humanDetail ??
346
- (atLatestStep && state.currentNode === nodeId && state.statusDetail
343
+ const configuredDetail =
344
+ atLatestStep && state.currentNode === nodeId && state.statusDetail
347
345
  ? sanitizeText(state.statusDetail)
348
346
  : node?.statusDetail
349
347
  ? sanitizeText(node.statusDetail)
350
348
  : node?.summary
351
349
  ? sanitizeText(node.summary)
352
- : "");
350
+ : "";
351
+ const assistantDetail =
352
+ node?.nodeType === "agent" &&
353
+ typeof node.expectedOutput === "object" &&
354
+ node.expectedOutput.kind === "assistant-message"
355
+ ? "assistant response"
356
+ : "";
357
+ const nodeDetail = [assistantDetail, configuredDetail].filter(Boolean).join(" · ");
358
+ const detail = humanDetail ?? nodeDetail;
353
359
  const branchLines = Array.from({ length: metrics.branchRows }, (_, index) =>
354
360
  labels[index] ? `◇ ${labels[index]}` : "",
355
361
  );