@osolmaz/pi-workflows 0.16.1 → 0.16.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 (327) hide show
  1. package/README.md +41 -41
  2. package/dist/builtins/monitor.workflow.d.ts +1 -1
  3. package/dist/builtins/monitor.workflow.js +8 -8
  4. package/dist/builtins/pi-agent-group.js +3 -3
  5. package/dist/builtins/pi-agent-group.js.map +1 -1
  6. package/dist/channels/adapter-entry.js +14 -14
  7. package/dist/channels/adapter-entry.js.map +1 -1
  8. package/dist/client/client.d.ts +6 -6
  9. package/dist/client/client.js +27 -27
  10. package/dist/client/client.js.map +1 -1
  11. package/dist/client/protocol.d.ts +4 -1
  12. package/dist/client/protocol.js +17 -11
  13. package/dist/client/protocol.js.map +1 -1
  14. package/dist/client/resolver.d.ts +4 -4
  15. package/dist/client/view.d.ts +7 -1
  16. package/dist/client/view.js +1 -0
  17. package/dist/client/view.js.map +1 -1
  18. package/dist/extension/index.d.ts +1 -1
  19. package/dist/extension/index.js +66 -47
  20. package/dist/extension/index.js.map +1 -1
  21. package/dist/extension/recorder.d.ts +1 -1
  22. package/dist/extension/recorder.js +1 -1
  23. package/dist/extension/recorder.js.map +1 -1
  24. package/dist/extension/remote-recorder-store.d.ts +1 -1
  25. package/dist/extension/remote-recorder-store.js +2 -2
  26. package/dist/extension/remote-recorder-store.js.map +1 -1
  27. package/dist/extension/{controller-command.d.ts → resource-manager-command.d.ts} +6 -6
  28. package/dist/extension/{controller-command.js → resource-manager-command.js} +7 -7
  29. package/dist/extension/resource-manager-command.js.map +1 -0
  30. package/dist/extension/session-delivery.d.ts +1 -1
  31. package/dist/extension/session-delivery.js +2 -2
  32. package/dist/extension/session-delivery.js.map +1 -1
  33. package/dist/extension/session-view.d.ts +1 -1
  34. package/dist/extension/session-view.js +1 -1
  35. package/dist/extension/session-view.js.map +1 -1
  36. package/dist/extension/workflow-message-coordinator.d.ts +4 -3
  37. package/dist/extension/workflow-message-coordinator.js +81 -29
  38. package/dist/extension/workflow-message-coordinator.js.map +1 -1
  39. package/dist/resource-managers/conditions.d.ts +6 -0
  40. package/dist/{controllers → resource-managers}/conditions.js +3 -3
  41. package/dist/resource-managers/conditions.js.map +1 -0
  42. package/dist/resource-managers/definition.d.ts +6 -0
  43. package/dist/resource-managers/definition.js +45 -0
  44. package/dist/resource-managers/definition.js.map +1 -0
  45. package/dist/resource-managers/effects.d.ts +15 -0
  46. package/dist/{controllers → resource-managers}/effects.js +2 -2
  47. package/dist/resource-managers/effects.js.map +1 -0
  48. package/dist/resource-managers/errors.d.ts +12 -0
  49. package/dist/resource-managers/errors.js +25 -0
  50. package/dist/resource-managers/errors.js.map +1 -0
  51. package/dist/resource-managers/index.d.ts +12 -0
  52. package/dist/resource-managers/index.js +12 -0
  53. package/dist/resource-managers/index.js.map +1 -0
  54. package/dist/resource-managers/json.js.map +1 -0
  55. package/dist/resource-managers/loader.d.ts +23 -0
  56. package/dist/resource-managers/loader.js +75 -0
  57. package/dist/resource-managers/loader.js.map +1 -0
  58. package/dist/resource-managers/results.d.ts +5 -0
  59. package/dist/resource-managers/results.js.map +1 -0
  60. package/dist/resource-managers/runtime.d.ts +59 -0
  61. package/dist/{controllers/manager.js → resource-managers/runtime.js} +57 -57
  62. package/dist/resource-managers/runtime.js.map +1 -0
  63. package/dist/{controllers → resource-managers}/sqlite.d.ts +51 -43
  64. package/dist/{controllers → resource-managers}/sqlite.js +187 -125
  65. package/dist/resource-managers/sqlite.js.map +1 -0
  66. package/dist/{controllers → resource-managers}/store.d.ts +33 -33
  67. package/dist/{controllers → resource-managers}/store.js.map +1 -1
  68. package/dist/{controllers → resource-managers}/types.d.ts +41 -41
  69. package/dist/{controllers → resource-managers}/types.js.map +1 -1
  70. package/dist/resource-managers/workflows.d.ts +34 -0
  71. package/dist/{controllers → resource-managers}/workflows.js +15 -15
  72. package/dist/resource-managers/workflows.js.map +1 -0
  73. package/dist/server/channel-effects.js.map +1 -0
  74. package/dist/{host → server}/channel-supervisor.d.ts +6 -6
  75. package/dist/{host → server}/channel-supervisor.js +3 -3
  76. package/dist/server/channel-supervisor.js.map +1 -0
  77. package/dist/{host/child-worker-supervisor.d.ts → server/child-runner-supervisor.d.ts} +15 -13
  78. package/dist/{host/child-worker-supervisor.js → server/child-runner-supervisor.js} +6 -5
  79. package/dist/server/child-runner-supervisor.js.map +1 -0
  80. package/dist/{host → server}/processes.d.ts +3 -3
  81. package/dist/{host → server}/processes.js +3 -3
  82. package/dist/server/processes.js.map +1 -0
  83. package/dist/{host → server}/resolver-entry.d.ts +6 -6
  84. package/dist/{host → server}/resolver-entry.js +14 -14
  85. package/dist/server/resolver-entry.js.map +1 -0
  86. package/dist/server/resource-runner-entry.d.ts +2 -0
  87. package/dist/{host/controller-worker-entry.js → server/resource-runner-entry.js} +33 -33
  88. package/dist/server/resource-runner-entry.js.map +1 -0
  89. package/dist/server/resource-runner-protocol.d.ts +36 -0
  90. package/dist/server/resource-runner-protocol.js +49 -0
  91. package/dist/server/resource-runner-protocol.js.map +1 -0
  92. package/dist/server/resource-runner-supervisor.d.ts +21 -0
  93. package/dist/{host/worker-supervisor.js → server/resource-runner-supervisor.js} +17 -17
  94. package/dist/server/resource-runner-supervisor.js.map +1 -0
  95. package/dist/{host → server}/rpc-bridge.d.ts +3 -3
  96. package/dist/{host → server}/rpc-bridge.js +3 -3
  97. package/dist/server/rpc-bridge.js.map +1 -0
  98. package/dist/{host → server}/rpc-executor.d.ts +1 -1
  99. package/dist/{host → server}/rpc-executor.js +3 -3
  100. package/dist/server/rpc-executor.js.map +1 -0
  101. package/dist/{host/host-entry.js → server/server-entry.js} +6 -6
  102. package/dist/server/server-entry.js.map +1 -0
  103. package/dist/{host/runner.d.ts → server/server.d.ts} +47 -39
  104. package/dist/{host/runner.js → server/server.js} +624 -535
  105. package/dist/server/server.js.map +1 -0
  106. package/dist/{host → server}/state.d.ts +25 -25
  107. package/dist/{host → server}/state.js +49 -49
  108. package/dist/server/state.js.map +1 -0
  109. package/dist/{host → server}/view.d.ts +7 -7
  110. package/dist/{host → server}/view.js +19 -15
  111. package/dist/server/view.js.map +1 -0
  112. package/dist/server/workflow-runner-content.d.ts +6 -0
  113. package/dist/server/workflow-runner-content.js +96 -0
  114. package/dist/server/workflow-runner-content.js.map +1 -0
  115. package/dist/server/workflow-runner-entry.d.ts +13 -0
  116. package/dist/{host/worker-entry.js → server/workflow-runner-entry.js} +89 -57
  117. package/dist/server/workflow-runner-entry.js.map +1 -0
  118. package/dist/server/workflow-runner-protocol.d.ts +69 -0
  119. package/dist/server/workflow-runner-protocol.js +176 -0
  120. package/dist/server/workflow-runner-protocol.js.map +1 -0
  121. package/dist/{host/worker-store.d.ts → server/workflow-runner-store.d.ts} +11 -11
  122. package/dist/{host/worker-store.js → server/workflow-runner-store.js} +8 -8
  123. package/dist/server/workflow-runner-store.js.map +1 -0
  124. package/dist/server/workflow-runner-supervisor.d.ts +22 -0
  125. package/dist/server/workflow-runner-supervisor.js +56 -0
  126. package/dist/server/workflow-runner-supervisor.js.map +1 -0
  127. package/dist/state/prune.d.ts +1 -1
  128. package/dist/state/prune.js +17 -10
  129. package/dist/state/prune.js.map +1 -1
  130. package/dist/state/schema.js +2 -2
  131. package/dist/state/workflow-messages.d.ts +3 -1
  132. package/dist/state/workflow-messages.js +32 -1
  133. package/dist/state/workflow-messages.js.map +1 -1
  134. package/dist/viewer/backup.js +1 -1
  135. package/dist/viewer/backup.js.map +1 -1
  136. package/dist/viewer/cli.d.ts +2 -2
  137. package/dist/viewer/cli.js +46 -32
  138. package/dist/viewer/cli.js.map +1 -1
  139. package/dist/viewer/tui.d.ts +1 -1
  140. package/dist/viewer/tui.js +1 -1
  141. package/dist/viewer/tui.js.map +1 -1
  142. package/dist/workflows/command-batch.js +2 -2
  143. package/dist/workflows/composition.js +0 -4
  144. package/dist/workflows/composition.js.map +1 -1
  145. package/dist/workflows/definition.js +0 -8
  146. package/dist/workflows/definition.js.map +1 -1
  147. package/dist/workflows/engine.js +18 -20
  148. package/dist/workflows/engine.js.map +1 -1
  149. package/dist/workflows/schema.js +0 -4
  150. package/dist/workflows/schema.js.map +1 -1
  151. package/dist/workflows/store.d.ts +27 -11
  152. package/dist/workflows/store.js +66 -18
  153. package/dist/workflows/store.js.map +1 -1
  154. package/dist/workflows/types.d.ts +3 -5
  155. package/docs/2026-08-20-durable-workflow-launch-plan.md +12 -12
  156. package/docs/2026-08-25-workflow-follow-ups.md +7 -7
  157. package/docs/2026-08-25-workflow-settings.md +3 -3
  158. package/docs/2026-08-30-out-of-process-workflow-host-plan.md +3 -3
  159. package/docs/2026-09-01-restore-session-delivery-controls-plan.md +5 -5
  160. package/docs/2026-09-01-unified-workflow-client-plan.md +6 -6
  161. package/docs/2026-09-02-installed-live-e2e-plan.md +3 -3
  162. package/docs/2026-09-02-unify-workflow-messages-plan.md +8 -8
  163. package/docs/2026-09-04-workflow-run-state-plan.md +363 -0
  164. package/docs/DEFERRED_TURNS.md +7 -7
  165. package/docs/DESIGN_PHILOSOPHY.md +1 -1
  166. package/docs/HUMAN_DECISIONS.md +11 -11
  167. package/docs/HUMAN_DECISION_PRESENTATIONS.md +4 -4
  168. package/docs/MONITOR.md +1 -1
  169. package/docs/RESOURCE_MANAGERS.md +221 -0
  170. package/docs/SQLITE_STATE.md +40 -34
  171. package/docs/WORKFLOW_COMPOSITION.md +1 -1
  172. package/docs/{WORKFLOW_HOST.md → WORKFLOW_SERVER.md} +131 -116
  173. package/docs/WORKFLOW_STEP_MESSAGES.md +18 -18
  174. package/docs/WORKFLOW_UPDATES.md +16 -16
  175. package/docs/development.md +15 -14
  176. package/docs/live-replay-protocol.md +20 -20
  177. package/docs/plans/2026-08-04-controller-runtime-plan.md +25 -25
  178. package/docs/plans/2026-08-05-always-on-workflows-plan.md +8 -8
  179. package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +2 -2
  180. package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +1 -1
  181. package/docs/plans/2026-08-16-workflow-updates-plan.md +5 -5
  182. package/docs/plans/2026-08-19-human-decision-gates-plan.md +1 -1
  183. package/docs/plans/2026-08-20-bounded-command-batches-plan.md +1 -1
  184. package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +3 -3
  185. package/docs/plans/2026-08-21-sanity-check-plan.md +2 -2
  186. package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +5 -5
  187. package/docs/plans/2026-08-23-sqlite-state-plan.md +25 -25
  188. package/docs/plans/2026-08-25-live-workflow-settings-plan.md +4 -4
  189. package/docs/plans/2026-08-27-workflow-terminal-restart-plan.md +3 -3
  190. package/docs/plans/2026-09-04-workflow-runner-resume-state-plan.md +318 -0
  191. package/docs/plans/piw-viewer-experience-implementation-plan.md +1 -1
  192. package/docs/tui-viewer.md +9 -9
  193. package/docs/workflows.md +78 -63
  194. package/examples/{controllers/pull-request.controller.ts → resource-managers/pull-request.resource-manager.ts} +12 -12
  195. package/herdr-plugin.toml +1 -1
  196. package/package.json +5 -5
  197. package/protocol/client.v1.schema.json +7 -7
  198. package/protocol/fixtures/client-v1.json +1 -1
  199. package/src/builtins/monitor.workflow.ts +9 -9
  200. package/src/builtins/pi-agent-group.ts +3 -3
  201. package/src/channels/adapter-entry.ts +14 -14
  202. package/src/client/client.ts +33 -32
  203. package/src/client/protocol.ts +16 -11
  204. package/src/client/resolver.ts +4 -4
  205. package/src/client/view.ts +9 -1
  206. package/src/extension/index.ts +81 -53
  207. package/src/extension/recorder.ts +1 -1
  208. package/src/extension/remote-recorder-store.ts +2 -2
  209. package/src/extension/resource-manager-command.ts +45 -0
  210. package/src/extension/session-delivery.ts +2 -2
  211. package/src/extension/session-view.ts +1 -1
  212. package/src/extension/workflow-message-coordinator.ts +94 -30
  213. package/src/{controllers → resource-managers}/conditions.ts +23 -23
  214. package/src/resource-managers/definition.ts +71 -0
  215. package/src/{controllers → resource-managers}/effects.ts +9 -9
  216. package/src/resource-managers/errors.ts +27 -0
  217. package/src/resource-managers/index.ts +88 -0
  218. package/src/resource-managers/loader.ts +111 -0
  219. package/src/{controllers → resource-managers}/results.ts +4 -4
  220. package/src/{controllers/manager.ts → resource-managers/runtime.ts} +95 -92
  221. package/src/{controllers → resource-managers}/sqlite.ts +275 -181
  222. package/src/{controllers → resource-managers}/store.ts +46 -38
  223. package/src/{controllers → resource-managers}/types.ts +50 -41
  224. package/src/{controllers → resource-managers}/workflows.ts +43 -41
  225. package/src/{host → server}/channel-supervisor.ts +8 -8
  226. package/src/{host/child-worker-supervisor.ts → server/child-runner-supervisor.ts} +22 -18
  227. package/src/{host → server}/processes.ts +3 -3
  228. package/src/{host → server}/resolver-entry.ts +28 -25
  229. package/src/{host/controller-worker-entry.ts → server/resource-runner-entry.ts} +67 -65
  230. package/src/server/resource-runner-protocol.ts +103 -0
  231. package/src/server/resource-runner-supervisor.ts +76 -0
  232. package/src/{host → server}/rpc-bridge.ts +3 -3
  233. package/src/{host → server}/rpc-executor.ts +4 -4
  234. package/src/{host/host-entry.ts → server/server-entry.ts} +5 -5
  235. package/src/{host/runner.ts → server/server.ts} +786 -672
  236. package/src/{host → server}/state.ts +78 -67
  237. package/src/{host → server}/view.ts +21 -17
  238. package/src/server/workflow-runner-content.ts +119 -0
  239. package/src/{host/worker-entry.ts → server/workflow-runner-entry.ts} +131 -82
  240. package/src/server/workflow-runner-protocol.ts +241 -0
  241. package/src/{host/worker-store.ts → server/workflow-runner-store.ts} +18 -20
  242. package/src/server/workflow-runner-supervisor.ts +79 -0
  243. package/src/state/prune.ts +27 -9
  244. package/src/state/schema.ts +2 -2
  245. package/src/state/workflow-messages.ts +52 -1
  246. package/src/viewer/backup.ts +1 -1
  247. package/src/viewer/cli.ts +50 -36
  248. package/src/viewer/tui.ts +1 -1
  249. package/src/workflows/command-batch.ts +2 -2
  250. package/src/workflows/composition.ts +0 -5
  251. package/src/workflows/definition.ts +0 -8
  252. package/src/workflows/engine.ts +18 -21
  253. package/src/workflows/schema.ts +0 -6
  254. package/src/workflows/store.ts +95 -28
  255. package/src/workflows/types.ts +3 -5
  256. package/dist/controllers/conditions.d.ts +0 -6
  257. package/dist/controllers/conditions.js.map +0 -1
  258. package/dist/controllers/definition.d.ts +0 -6
  259. package/dist/controllers/definition.js +0 -45
  260. package/dist/controllers/definition.js.map +0 -1
  261. package/dist/controllers/effects.d.ts +0 -15
  262. package/dist/controllers/effects.js.map +0 -1
  263. package/dist/controllers/errors.d.ts +0 -12
  264. package/dist/controllers/errors.js +0 -25
  265. package/dist/controllers/errors.js.map +0 -1
  266. package/dist/controllers/index.d.ts +0 -12
  267. package/dist/controllers/index.js +0 -12
  268. package/dist/controllers/index.js.map +0 -1
  269. package/dist/controllers/json.js.map +0 -1
  270. package/dist/controllers/loader.d.ts +0 -23
  271. package/dist/controllers/loader.js +0 -74
  272. package/dist/controllers/loader.js.map +0 -1
  273. package/dist/controllers/manager.d.ts +0 -59
  274. package/dist/controllers/manager.js.map +0 -1
  275. package/dist/controllers/results.d.ts +0 -5
  276. package/dist/controllers/results.js.map +0 -1
  277. package/dist/controllers/sqlite.js.map +0 -1
  278. package/dist/controllers/workflows.d.ts +0 -34
  279. package/dist/controllers/workflows.js.map +0 -1
  280. package/dist/extension/controller-command.js.map +0 -1
  281. package/dist/host/channel-effects.js.map +0 -1
  282. package/dist/host/channel-supervisor.js.map +0 -1
  283. package/dist/host/child-worker-supervisor.js.map +0 -1
  284. package/dist/host/controller-worker-entry.d.ts +0 -2
  285. package/dist/host/controller-worker-entry.js.map +0 -1
  286. package/dist/host/controller-worker-protocol.d.ts +0 -36
  287. package/dist/host/controller-worker-protocol.js +0 -49
  288. package/dist/host/controller-worker-protocol.js.map +0 -1
  289. package/dist/host/controller-worker-supervisor.d.ts +0 -21
  290. package/dist/host/controller-worker-supervisor.js +0 -54
  291. package/dist/host/controller-worker-supervisor.js.map +0 -1
  292. package/dist/host/host-entry.js.map +0 -1
  293. package/dist/host/processes.js.map +0 -1
  294. package/dist/host/resolver-entry.js.map +0 -1
  295. package/dist/host/rpc-bridge.js.map +0 -1
  296. package/dist/host/rpc-executor.js.map +0 -1
  297. package/dist/host/runner.js.map +0 -1
  298. package/dist/host/state.js.map +0 -1
  299. package/dist/host/view.js.map +0 -1
  300. package/dist/host/worker-entry.d.ts +0 -10
  301. package/dist/host/worker-entry.js.map +0 -1
  302. package/dist/host/worker-protocol.d.ts +0 -31
  303. package/dist/host/worker-protocol.js +0 -122
  304. package/dist/host/worker-protocol.js.map +0 -1
  305. package/dist/host/worker-store.js.map +0 -1
  306. package/dist/host/worker-supervisor.d.ts +0 -22
  307. package/dist/host/worker-supervisor.js.map +0 -1
  308. package/docs/CONTROLLERS.md +0 -217
  309. package/src/controllers/definition.ts +0 -65
  310. package/src/controllers/errors.ts +0 -27
  311. package/src/controllers/index.ts +0 -88
  312. package/src/controllers/loader.ts +0 -104
  313. package/src/extension/controller-command.ts +0 -45
  314. package/src/host/controller-worker-protocol.ts +0 -104
  315. package/src/host/controller-worker-supervisor.ts +0 -79
  316. package/src/host/worker-protocol.ts +0 -165
  317. package/src/host/worker-supervisor.ts +0 -74
  318. /package/dist/{controllers → resource-managers}/json.d.ts +0 -0
  319. /package/dist/{controllers → resource-managers}/json.js +0 -0
  320. /package/dist/{controllers → resource-managers}/results.js +0 -0
  321. /package/dist/{controllers → resource-managers}/store.js +0 -0
  322. /package/dist/{controllers → resource-managers}/types.js +0 -0
  323. /package/dist/{host → server}/channel-effects.d.ts +0 -0
  324. /package/dist/{host → server}/channel-effects.js +0 -0
  325. /package/dist/{host/host-entry.d.ts → server/server-entry.d.ts} +0 -0
  326. /package/src/{controllers → resource-managers}/json.ts +0 -0
  327. /package/src/{host → server}/channel-effects.ts +0 -0
@@ -17,7 +17,6 @@ import { errorMessage } from "../workflows/errors.js";
17
17
  import { discoverWorkflows } from "../workflows/loader.js";
18
18
  import { createRunId } from "../workflows/store.js";
19
19
  import type { AgentStepContract, HumanDecisionResponse } from "../workflows/types.js";
20
- import { parseControllerArgs, type ParsedControllerArgs } from "./controller-command.js";
21
20
  import {
22
21
  HerdrWorkflowViewer,
23
22
  PIW_SHORTCUT,
@@ -27,12 +26,19 @@ import {
27
26
  } from "./herdr-viewer.js";
28
27
  import { SessionRecorder } from "./recorder.js";
29
28
  import { RemoteSessionRecordingStore } from "./remote-recorder-store.js";
29
+ import {
30
+ parseResourceManagerArgs,
31
+ type ParsedResourceManagerArgs,
32
+ } from "./resource-manager-command.js";
30
33
  import { SessionWorkflowView } from "./session-view.js";
31
34
  import { recoverAssistantStep, registerWorkflowAgentStepMessageRenderer } from "./step-message.js";
32
35
  import { responseEntryId, WorkflowMessageCoordinator } from "./workflow-message-coordinator.js";
33
36
  import { parseWorkflowToolInput, WorkflowToolParameters } from "./workflow-tool.js";
34
37
 
35
- export { parseControllerArgs, type ParsedControllerArgs } from "./controller-command.js";
38
+ export {
39
+ parseResourceManagerArgs,
40
+ type ParsedResourceManagerArgs,
41
+ } from "./resource-manager-command.js";
36
42
 
37
43
  export {
38
44
  audienceChannels,
@@ -176,14 +182,16 @@ export default function piWorkflows(pi: ExtensionAPI): void {
176
182
  let sessionGeneration = 0;
177
183
  let sessionUnsubscribe: (() => Promise<void>) | null = null;
178
184
  let sessionConnectTask: Promise<void> | null = null;
179
- let hostUnavailableNotified = false;
185
+ let serverUnavailableNotified = false;
180
186
  let pollTimer: ReturnType<typeof setInterval> | null = null;
181
187
  let presentationTail = Promise.resolve();
182
188
  let toolTail = Promise.resolve();
183
189
  const workflowMessages = new WorkflowMessageCoordinator();
184
190
  const sessionView = new SessionWorkflowView();
185
191
  const sessionRecorders = new Map<string, SessionRecorder>();
192
+ let agentRunning = false;
186
193
  let activeRecorder: SessionRecorder | null = null;
194
+ let activeRecorderMessageId: string | null = null;
187
195
 
188
196
  const ensureRecorder = async (
189
197
  message: WorkflowMessage,
@@ -203,6 +211,33 @@ export default function piWorkflows(pi: ExtensionAPI): void {
203
211
  return recorder;
204
212
  };
205
213
 
214
+ const activateRecorder = async (ctx: ExtensionContext): Promise<void> => {
215
+ if (!agentRunning) return;
216
+ const message = workflowMessages.activeTurnMessage();
217
+ if (message === undefined || activeRecorderMessageId === message.workflowMessageId) return;
218
+ const contract = agentContractForWorkflowMessage(message);
219
+ if (contract === undefined && message.kind !== "terminal" && message.kind !== "followUp") {
220
+ return;
221
+ }
222
+ const recorder = await ensureRecorder(message, ctx);
223
+ if (
224
+ !agentRunning ||
225
+ workflowMessages.activeTurnMessage()?.workflowMessageId !== message.workflowMessageId
226
+ ) {
227
+ return;
228
+ }
229
+ if (contract === undefined) {
230
+ recorder.beginWorkflowMessage(
231
+ message.workflowMessageId,
232
+ message.kind as "terminal" | "followUp",
233
+ );
234
+ } else {
235
+ recorder.beginAttempt(contract);
236
+ }
237
+ activeRecorder = recorder;
238
+ activeRecorderMessageId = message.workflowMessageId;
239
+ };
240
+
206
241
  const presentInOrder = async (ctx: ExtensionContext): Promise<void> => {
207
242
  const prior = presentationTail;
208
243
  let release: (() => void) | undefined;
@@ -212,6 +247,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
212
247
  await prior;
213
248
  try {
214
249
  await workflowMessages.synchronize(pi, client, ctx);
250
+ await activateRecorder(ctx);
215
251
  } finally {
216
252
  sessionView.refresh(ctx);
217
253
  release?.();
@@ -430,8 +466,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
430
466
  },
431
467
  });
432
468
 
433
- pi.registerCommand("controller", {
434
- description: "Manage hosted controller resources: list, get, apply, reconcile, or delete",
469
+ pi.registerCommand("resource-manager", {
470
+ description: "Manage hosted managed resources: list, get, apply, reconcile, or delete",
435
471
  getArgumentCompletions: (prefix: string) => {
436
472
  const items = ["list", "get", "apply", "reconcile", "delete"]
437
473
  .filter((value) => value.startsWith(prefix))
@@ -440,7 +476,11 @@ export default function piWorkflows(pi: ExtensionAPI): void {
440
476
  },
441
477
  handler: async (args, ctx) => {
442
478
  try {
443
- const result = await executeControllerCommand(client, ctx, parseControllerArgs(args));
479
+ const result = await executeResourceManagerCommand(
480
+ client,
481
+ ctx,
482
+ parseResourceManagerArgs(args),
483
+ );
444
484
  ctx.ui.notify(result.message, result.level ?? "info");
445
485
  } catch (error) {
446
486
  ctx.ui.notify(errorMessage(error), "error");
@@ -549,7 +589,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
549
589
 
550
590
  pi.on("session_start", (_event, ctx) => {
551
591
  sessionContext = ctx;
552
- hostUnavailableNotified = false;
592
+ serverUnavailableNotified = false;
553
593
  const sessionId = ctx.sessionManager.getSessionId();
554
594
  const generation = ++sessionGeneration;
555
595
  let snapshotGeneration = 0;
@@ -615,7 +655,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
615
655
  return;
616
656
  }
617
657
  sessionUnsubscribe = unsubscribe;
618
- hostUnavailableNotified = false;
658
+ serverUnavailableNotified = false;
619
659
  const capability = await herdrViewer.probe();
620
660
  if (generation !== sessionGeneration || sessionContext !== ctx) return;
621
661
  sessionView.setActionHint(capability.available ? PIW_SHORTCUT_HINT : undefined, ctx);
@@ -624,10 +664,10 @@ export default function piWorkflows(pi: ExtensionAPI): void {
624
664
  if (
625
665
  generation === sessionGeneration &&
626
666
  sessionContext === ctx &&
627
- !hostUnavailableNotified
667
+ !serverUnavailableNotified
628
668
  ) {
629
- hostUnavailableNotified = true;
630
- ctx.ui.notify(`Workflow host is unavailable: ${errorMessage(error)}`, "warning");
669
+ serverUnavailableNotified = true;
670
+ ctx.ui.notify(`Workflow server is unavailable: ${errorMessage(error)}`, "warning");
631
671
  }
632
672
  }
633
673
  })();
@@ -651,27 +691,11 @@ export default function piWorkflows(pi: ExtensionAPI): void {
651
691
  });
652
692
 
653
693
  pi.on("agent_start", async (_event, ctx) => {
694
+ agentRunning = true;
695
+ activeRecorder = null;
696
+ activeRecorderMessageId = null;
654
697
  workflowMessages.startTurn();
655
698
  await presentInOrder(ctx).catch(() => undefined);
656
- const message = workflowMessages.activeTurnMessage();
657
- const contract = message === undefined ? undefined : agentContractForWorkflowMessage(message);
658
- if (
659
- message === undefined ||
660
- (contract === undefined && message.kind !== "terminal" && message.kind !== "followUp")
661
- ) {
662
- activeRecorder = null;
663
- return;
664
- }
665
- const recorder = await ensureRecorder(message, ctx);
666
- if (contract === undefined) {
667
- recorder.beginWorkflowMessage(
668
- message.workflowMessageId,
669
- message.kind as "terminal" | "followUp",
670
- );
671
- } else {
672
- recorder.beginAttempt(contract);
673
- }
674
- activeRecorder = recorder;
675
699
  });
676
700
 
677
701
  pi.on("agent_end", async (event, ctx) => {
@@ -681,6 +705,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
681
705
  ctx.ui.notify(`Workflow response was rejected: ${errorMessage(error)}`, "error");
682
706
  }
683
707
  const finishedMessage = workflowMessages.activeTurnMessage();
708
+ agentRunning = false;
709
+ activeRecorderMessageId = null;
684
710
  workflowMessages.endTurn(
685
711
  workflowTurnStopReason(event.messages, ctx.signal?.aborted === true),
686
712
  responseEntryId(ctx.sessionManager.getBranch()),
@@ -747,7 +773,9 @@ export default function piWorkflows(pi: ExtensionAPI): void {
747
773
  pi.on("session_shutdown", async (_event, ctx) => {
748
774
  sessionGeneration += 1;
749
775
  sessionContext = null;
776
+ agentRunning = false;
750
777
  activeRecorder = null;
778
+ activeRecorderMessageId = null;
751
779
  await Promise.allSettled(
752
780
  [...sessionRecorders.values()].map(async (recorder) => recorder.stop()),
753
781
  );
@@ -760,7 +788,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
760
788
  if (sessionUnsubscribe !== null) await sessionUnsubscribe().catch(() => undefined);
761
789
  sessionUnsubscribe = null;
762
790
  sessionConnectTask = null;
763
- hostUnavailableNotified = false;
791
+ serverUnavailableNotified = false;
764
792
  await client.close();
765
793
  client = new WorkflowClient({ clientId: `pi-extension-${randomUUID()}` });
766
794
  });
@@ -1065,57 +1093,57 @@ async function executeCommand(
1065
1093
  }
1066
1094
  }
1067
1095
 
1068
- async function executeControllerCommand(
1096
+ async function executeResourceManagerCommand(
1069
1097
  client: WorkflowClient,
1070
1098
  ctx: ExtensionContext,
1071
- command: ParsedControllerArgs,
1099
+ command: ParsedResourceManagerArgs,
1072
1100
  ): Promise<CommandResult> {
1073
1101
  const projectPath = path.resolve(ctx.cwd);
1074
1102
  if (command.kind === "list") {
1075
1103
  const response = await requestAccepted(client, {
1076
- operation: "controller.list",
1104
+ operation: "resourceManager.list",
1077
1105
  payload: { projectPath },
1078
1106
  });
1079
1107
  return {
1080
- message: summarizeControllerResources(response.receipt),
1108
+ message: summarizeManagedResources(response.receipt),
1081
1109
  details: { action: "list", resources: response.receipt ?? [] },
1082
1110
  };
1083
1111
  }
1084
1112
 
1085
1113
  const idempotencyKey = randomUUID();
1086
1114
  if (command.kind === "apply") {
1087
- const resolved = await client.resolveControllerInitialization({
1115
+ const resolved = await client.resolveResourceManagerInitialization({
1088
1116
  cwd: projectPath,
1089
- controllerName: command.controller,
1117
+ resourceManagerName: command.resourceManager,
1090
1118
  spec: command.spec,
1091
1119
  });
1092
1120
  const response = await requestAccepted(client, {
1093
- operation: "controller.apply",
1094
- requestId: `controller-apply-${idempotencyKey}`,
1121
+ operation: "resourceManager.apply",
1122
+ requestId: `resource-manager-apply-${idempotencyKey}`,
1095
1123
  idempotencyKey,
1096
1124
  payload: {
1097
1125
  projectPath,
1098
- controller: resolved.controllerName,
1126
+ resourceManager: resolved.resourceManagerName,
1099
1127
  key: command.key,
1100
1128
  spec: command.spec,
1101
1129
  initialStatus: resolved.initialStatus,
1102
- controllerPath: resolved.controllerPath,
1130
+ resourceManagerPath: resolved.resourceManagerPath,
1103
1131
  sourceHash: resolved.sourceHash,
1104
1132
  },
1105
1133
  });
1106
1134
  return {
1107
- message: `Applied controller resource ${command.controller}/${command.key}.`,
1135
+ message: `Applied managed resource ${command.resourceManager}/${command.key}.`,
1108
1136
  details: { action: "apply", resource: response.receipt ?? null },
1109
1137
  };
1110
1138
  }
1111
1139
 
1112
1140
  const response = await requestAccepted(client, {
1113
- operation: `controller.${command.kind}`,
1114
- requestId: `controller-${command.kind}-${idempotencyKey}`,
1141
+ operation: `resourceManager.${command.kind}`,
1142
+ requestId: `resource-manager-${command.kind}-${idempotencyKey}`,
1115
1143
  idempotencyKey,
1116
1144
  payload: {
1117
1145
  projectPath,
1118
- controller: command.controller,
1146
+ resourceManager: command.resourceManager,
1119
1147
  key: command.key,
1120
1148
  },
1121
1149
  });
@@ -1126,7 +1154,7 @@ async function executeControllerCommand(
1126
1154
  };
1127
1155
  }
1128
1156
  return {
1129
- message: `Controller resource ${command.controller}/${command.key} ${command.kind} request accepted.`,
1157
+ message: `Managed resource ${command.resourceManager}/${command.key} ${command.kind} request accepted.`,
1130
1158
  details: { action: command.kind, resource: response.receipt ?? null },
1131
1159
  };
1132
1160
  }
@@ -1315,7 +1343,7 @@ async function requestAccepted(
1315
1343
  ? await client.request(options)
1316
1344
  : await client.requestDurable({ ...options, idempotencyKey: options.idempotencyKey });
1317
1345
  if (response.outcome !== "accepted" && response.outcome !== "adopted") {
1318
- throw new Error(response.error ?? `Workflow host rejected ${options.operation}`);
1346
+ throw new Error(response.error ?? `Workflow server rejected ${options.operation}`);
1319
1347
  }
1320
1348
  return response;
1321
1349
  }
@@ -1400,18 +1428,18 @@ function summarizeRun(value: JsonValue | undefined): string {
1400
1428
  .join(" ");
1401
1429
  }
1402
1430
 
1403
- function summarizeControllerResources(value: JsonValue | undefined): string {
1404
- if (!Array.isArray(value) || value.length === 0) return "No controller resources.";
1431
+ function summarizeManagedResources(value: JsonValue | undefined): string {
1432
+ if (!Array.isArray(value) || value.length === 0) return "No managed resources.";
1405
1433
  const resources = value.map((item) => {
1406
1434
  if (!isRecord(item) || !isRecord(item.metadata)) return "unknown";
1407
- const controller =
1408
- typeof item.metadata.controller === "string" ? item.metadata.controller : "unknown";
1435
+ const resourceManager =
1436
+ typeof item.metadata.resourceManager === "string" ? item.metadata.resourceManager : "unknown";
1409
1437
  const key = typeof item.metadata.key === "string" ? item.metadata.key : "unknown";
1410
1438
  const generation =
1411
1439
  typeof item.metadata.generation === "number" ? item.metadata.generation : "unknown";
1412
- return `${controller}/${key} generation=${generation}`;
1440
+ return `${resourceManager}/${key} generation=${generation}`;
1413
1441
  });
1414
- return `Controller resources: ${resources.join(", ")}.`;
1442
+ return `Managed resources: ${resources.join(", ")}.`;
1415
1443
  }
1416
1444
 
1417
1445
  function jsonValue(value: unknown): JsonValue {
@@ -104,7 +104,7 @@ export type SessionRecordingStore = {
104
104
  ): Promise<{ eventCount: number; entryCount: number; lastEventSeq: number }>;
105
105
  };
106
106
 
107
- /** Records settled Pi entries and normalized temporal events through the workflow host. */
107
+ /** Records settled Pi entries and normalized temporal events through the workflow server. */
108
108
  export class SessionRecorder {
109
109
  private readonly store: SessionRecordingStore;
110
110
  private readonly runId: string;
@@ -13,7 +13,7 @@ type SessionCommandContext = {
13
13
  coordinatorEpoch: string;
14
14
  };
15
15
 
16
- /** Sends bounded session recording batches to the single workflow host writer. */
16
+ /** Sends bounded session recording batches to the single workflow server writer. */
17
17
  export class RemoteSessionRecordingStore implements SessionRecordingStore {
18
18
  constructor(
19
19
  private readonly client: WorkflowClient,
@@ -103,7 +103,7 @@ export class RemoteSessionRecordingStore implements SessionRecordingStore {
103
103
  payload: { ...this.context(), action, ...extra },
104
104
  });
105
105
  if (response.outcome !== "accepted" && response.outcome !== "adopted") {
106
- throw new Error(response.error ?? "Workflow host rejected the session recording batch");
106
+ throw new Error(response.error ?? "Workflow server rejected the session recording batch");
107
107
  }
108
108
  if (!isRecord(response.receipt)) throw new Error("Session recording receipt is invalid");
109
109
  return response.receipt;
@@ -0,0 +1,45 @@
1
+ import type { JsonValue } from "../state/json.js";
2
+
3
+ export type ParsedResourceManagerArgs =
4
+ | { kind: "list" }
5
+ | { kind: "get"; resourceManager: string; key: string }
6
+ | { kind: "apply"; resourceManager: string; key: string; spec: JsonValue }
7
+ | { kind: "reconcile"; resourceManager: string; key: string }
8
+ | { kind: "delete"; resourceManager: string; key: string };
9
+
10
+ const RESOURCE_MANAGER_USAGE =
11
+ "Usage: /resource-manager [list|get <resource-manager> <key>|apply <resource-manager> <key> <json>|reconcile <resource-manager> <key>|delete <resource-manager> <key>]";
12
+
13
+ export function parseResourceManagerArgs(args: string): ParsedResourceManagerArgs {
14
+ const trimmed = args.trim();
15
+ if (trimmed.length === 0 || trimmed === "list") return { kind: "list" };
16
+
17
+ const apply = trimmed.match(/^apply\s+(\S+)\s+(\S+)\s+([\s\S]+)$/u);
18
+ if (apply !== null) {
19
+ let spec: JsonValue;
20
+ try {
21
+ spec = JSON.parse(apply[3] as string) as JsonValue;
22
+ } catch (error) {
23
+ throw new Error(
24
+ `Invalid resource manager spec JSON: ${error instanceof Error ? error.message : String(error)}`,
25
+ );
26
+ }
27
+ return {
28
+ kind: "apply",
29
+ resourceManager: apply[1] as string,
30
+ key: apply[2] as string,
31
+ spec,
32
+ };
33
+ }
34
+
35
+ const operation = trimmed.match(/^(get|reconcile|delete)\s+(\S+)\s+(\S+)$/u);
36
+ if (operation !== null) {
37
+ return {
38
+ kind: operation[1] as "get" | "reconcile" | "delete",
39
+ resourceManager: operation[2] as string,
40
+ key: operation[3] as string,
41
+ };
42
+ }
43
+
44
+ throw new Error(RESOURCE_MANAGER_USAGE);
45
+ }
@@ -20,7 +20,7 @@ type QueuedSessionDelivery = {
20
20
  const SESSION_ENTRY_CONFIRMATION_MS = 10_000;
21
21
 
22
22
  /**
23
- * Delivers at most one host-owned message into a Pi session at a time.
23
+ * Delivers at most one server-owned message into a Pi session at a time.
24
24
  *
25
25
  * Pi's public sendMessage API does not return the saved session entry ID. Keep
26
26
  * the claimed delivery in memory until that entry becomes observable. Polling
@@ -54,7 +54,7 @@ export class SessionDeliveryCoordinator {
54
54
  return;
55
55
  }
56
56
 
57
- // Remember the host claim before the final idle check. If Pi starts a
57
+ // Remember the server claim before the final idle check. If Pi starts a
58
58
  // turn while the claim request is in flight, a later poll can use this
59
59
  // exact still-live claim instead of making a conflicting second claim.
60
60
  this.claimed = delivery;
@@ -10,7 +10,7 @@ import { buildWidgetView } from "./widget.js";
10
10
  const WIDGET_KEY = "pi-workflows";
11
11
  const WIDGET_SCROLL_STEP = 3;
12
12
 
13
- /** Client-backed projection of the host-owned run into the origin Pi session. */
13
+ /** Client-backed projection of the server-owned run into the origin Pi session. */
14
14
  export class SessionWorkflowView {
15
15
  private session: WorkflowSessionView | null = null;
16
16
  private scroll: number | null = null;
@@ -1,9 +1,18 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import type { WorkflowClient } from "../client/client.js";
4
- import type { WorkflowSessionView, WorkflowTurnReport } from "../client/view.js";
4
+ import {
5
+ WORKFLOW_TURN_REPORT_RECEIPT_SCHEMA,
6
+ type WorkflowSessionView,
7
+ type WorkflowTurnReport,
8
+ type WorkflowTurnReportReceipt,
9
+ } from "../client/view.js";
5
10
  import type { JsonValue } from "../state/json.js";
6
- import type { WorkflowMessage, WorkflowTurnStopReason } from "../state/workflow-messages.js";
11
+ import {
12
+ WORKFLOW_TURN_SCHEMA,
13
+ type WorkflowMessage,
14
+ type WorkflowTurnStopReason,
15
+ } from "../state/workflow-messages.js";
7
16
 
8
17
  const WORKFLOW_MESSAGE_ID_FIELD = "workflowMessageId";
9
18
 
@@ -11,17 +20,19 @@ type PendingTurn = {
11
20
  workflowTurnId: string;
12
21
  workflowMessageId: string | null;
13
22
  runId: string | null;
23
+ message: WorkflowMessage | null;
14
24
  startedReported: boolean;
15
25
  end: { stopReason: WorkflowTurnStopReason; responseSessionEntryId: string | null } | null;
16
26
  };
17
27
 
18
- /** Adds every host-owned workflow message to one Pi session through one public API path. */
28
+ /** Adds every server-owned workflow message to one Pi session through one public API path. */
19
29
  export class WorkflowMessageCoordinator {
20
30
  private readonly queued = new Set<string>();
21
31
  private readonly closedTurnMessages = new Set<string>();
22
32
  private synchronizing = false;
23
33
  private lastBranchEpoch: string | null = null;
24
34
  private view: WorkflowSessionView | null = null;
35
+ private awaitingTurnMessage: WorkflowMessage | null = null;
25
36
  private turn: PendingTurn | null = null;
26
37
 
27
38
  updateView(view: WorkflowSessionView): void {
@@ -50,15 +61,17 @@ export class WorkflowMessageCoordinator {
50
61
  }
51
62
 
52
63
  startTurn(): void {
53
- if (this.turn !== null && this.turn.end === null) return;
64
+ const awaited = this.awaitingTurnMessage;
65
+ this.awaitingTurnMessage = null;
54
66
  this.turn = {
55
67
  workflowTurnId: `workflow-turn-${randomUUID()}`,
56
68
  workflowMessageId: null,
57
69
  runId: null,
70
+ message: null,
58
71
  startedReported: false,
59
72
  end: null,
60
73
  };
61
- const candidate = this.turnCandidate();
74
+ const candidate = awaited ?? this.turnCandidate();
62
75
  if (candidate !== undefined) this.bindTurn(candidate);
63
76
  }
64
77
 
@@ -68,10 +81,8 @@ export class WorkflowMessageCoordinator {
68
81
  }
69
82
 
70
83
  activeTurnMessage(): WorkflowMessage | undefined {
71
- if (this.view === null || this.turn?.workflowMessageId === null || this.turn === null) {
72
- return undefined;
73
- }
74
- return messageById(this.view, this.turn.workflowMessageId);
84
+ if (this.turn === null || !this.turn.startedReported) return undefined;
85
+ return this.turn.message ?? undefined;
75
86
  }
76
87
 
77
88
  async synchronize(
@@ -89,7 +100,8 @@ export class WorkflowMessageCoordinator {
89
100
  workflowTurnId: view.openWorkflowTurn.workflowTurnId,
90
101
  workflowMessageId: view.openWorkflowTurn.workflowMessageId,
91
102
  runId: view.openWorkflowTurn.runId,
92
- startedReported: false,
103
+ message: messageById(view, view.openWorkflowTurn.workflowMessageId) ?? null,
104
+ startedReported: true,
93
105
  end: null,
94
106
  };
95
107
  }
@@ -128,6 +140,9 @@ export class WorkflowMessageCoordinator {
128
140
  this.queued.delete(messageId);
129
141
  return;
130
142
  }
143
+ if (message.content.triggerTurn && messageStartsTurn(message)) {
144
+ this.awaitingTurnMessage = message;
145
+ }
131
146
  try {
132
147
  pi.sendMessage(
133
148
  {
@@ -140,6 +155,9 @@ export class WorkflowMessageCoordinator {
140
155
  );
141
156
  } catch (error) {
142
157
  this.queued.delete(messageId);
158
+ if (this.awaitingTurnMessage?.workflowMessageId === messageId) {
159
+ this.awaitingTurnMessage = null;
160
+ }
143
161
  throw error;
144
162
  }
145
163
  await this.reportBranch(client, ctx, view);
@@ -153,6 +171,7 @@ export class WorkflowMessageCoordinator {
153
171
  this.queued.clear();
154
172
  this.closedTurnMessages.clear();
155
173
  this.view = null;
174
+ this.awaitingTurnMessage = null;
156
175
  this.turn = null;
157
176
  this.lastBranchEpoch = null;
158
177
  this.synchronizing = false;
@@ -182,6 +201,7 @@ export class WorkflowMessageCoordinator {
182
201
  if (this.turn === null) return;
183
202
  this.turn.workflowMessageId = message.workflowMessageId;
184
203
  this.turn.runId = message.runId;
204
+ this.turn.message = message;
185
205
  }
186
206
 
187
207
  private async flushTurn(client: WorkflowClient, view: WorkflowSessionView): Promise<void> {
@@ -195,31 +215,47 @@ export class WorkflowMessageCoordinator {
195
215
  ) {
196
216
  return;
197
217
  }
198
- const message = messageById(view, pending.workflowMessageId);
199
- if (message?.status !== "sent") return;
218
+ let message = pending.message;
200
219
  if (!pending.startedReported) {
220
+ message = messageById(view, pending.workflowMessageId) ?? null;
221
+ if (message?.status !== "sent") return;
222
+ try {
223
+ const receipt = await reportTurn(client, {
224
+ state: "started",
225
+ workflowMessageId: pending.workflowMessageId,
226
+ workflowTurnId: pending.workflowTurnId,
227
+ runId: pending.runId,
228
+ targetSessionId: view.sessionId,
229
+ coordinatorEpoch: view.coordinatorEpoch,
230
+ });
231
+ if (receipt.ownership !== "active") {
232
+ this.turn = null;
233
+ return;
234
+ }
235
+ pending.message = message;
236
+ pending.startedReported = true;
237
+ } catch (error) {
238
+ this.turn = null;
239
+ throw error;
240
+ }
241
+ }
242
+ if (pending.end === null) return;
243
+ try {
201
244
  await reportTurn(client, {
202
- state: "started",
245
+ state: "ended",
203
246
  workflowMessageId: pending.workflowMessageId,
204
247
  workflowTurnId: pending.workflowTurnId,
205
248
  runId: pending.runId,
206
249
  targetSessionId: view.sessionId,
207
250
  coordinatorEpoch: view.coordinatorEpoch,
251
+ stopReason: pending.end.stopReason,
252
+ responseSessionEntryId: pending.end.responseSessionEntryId,
208
253
  });
209
- pending.startedReported = true;
254
+ } catch (error) {
255
+ this.turn = null;
256
+ throw error;
210
257
  }
211
- if (pending.end === null) return;
212
- await reportTurn(client, {
213
- state: "ended",
214
- workflowMessageId: pending.workflowMessageId,
215
- workflowTurnId: pending.workflowTurnId,
216
- runId: pending.runId,
217
- targetSessionId: view.sessionId,
218
- coordinatorEpoch: view.coordinatorEpoch,
219
- stopReason: pending.end.stopReason,
220
- responseSessionEntryId: pending.end.responseSessionEntryId,
221
- });
222
- if (message.kind === "terminal" || message.kind === "followUp") {
258
+ if (message?.kind === "terminal" || message?.kind === "followUp") {
223
259
  this.closedTurnMessages.add(pending.workflowMessageId);
224
260
  }
225
261
  for (const current of new Set([view, this.view])) {
@@ -228,7 +264,7 @@ export class WorkflowMessageCoordinator {
228
264
  }
229
265
  if (
230
266
  current?.openWorkflowMessageId === pending.workflowMessageId &&
231
- (message.kind === "terminal" || message.kind === "followUp")
267
+ (message?.kind === "terminal" || message?.kind === "followUp")
232
268
  ) {
233
269
  current.openWorkflowMessageId = null;
234
270
  }
@@ -260,7 +296,7 @@ export class WorkflowMessageCoordinator {
260
296
  },
261
297
  });
262
298
  if (response.outcome !== "accepted" && response.outcome !== "adopted") {
263
- throw new Error(response.error ?? "Workflow host rejected the Pi branch report");
299
+ throw new Error(response.error ?? "Workflow server rejected the Pi branch report");
264
300
  }
265
301
  for (const entry of entries) {
266
302
  const message = messageById(view, entry.workflowMessageId);
@@ -318,15 +354,43 @@ function messageStartsTurn(message: WorkflowMessage): boolean {
318
354
  return message.kind === "step" || message.kind === "terminal" || message.kind === "followUp";
319
355
  }
320
356
 
321
- async function reportTurn(client: WorkflowClient, report: WorkflowTurnReport): Promise<void> {
357
+ async function reportTurn(
358
+ client: WorkflowClient,
359
+ report: WorkflowTurnReport,
360
+ ): Promise<WorkflowTurnReportReceipt> {
322
361
  const response = await client.request({
323
362
  operation: "workflowTurn.report",
324
363
  runId: report.runId,
325
364
  payload: report as unknown as JsonValue,
326
365
  });
327
366
  if (response.outcome !== "accepted" && response.outcome !== "adopted") {
328
- throw new Error(response.error ?? "Workflow host rejected the model-turn report");
367
+ throw new Error(response.error ?? "Workflow server rejected the model-turn report");
368
+ }
369
+ const receipt = response.receipt;
370
+ if (
371
+ !isRecord(receipt) ||
372
+ receipt.schema !== WORKFLOW_TURN_REPORT_RECEIPT_SCHEMA ||
373
+ !["active", "settled", "absent"].includes(String(receipt.ownership))
374
+ ) {
375
+ throw new Error("Workflow server returned an invalid model-turn receipt");
376
+ }
377
+ const ownership = receipt.ownership as WorkflowTurnReportReceipt["ownership"];
378
+ if (ownership === "absent") {
379
+ if (receipt.turn !== null) {
380
+ throw new Error("Workflow server returned an invalid model-turn receipt");
381
+ }
382
+ } else if (
383
+ !isRecord(receipt.turn) ||
384
+ receipt.turn.schema !== WORKFLOW_TURN_SCHEMA ||
385
+ receipt.turn.workflowTurnId !== report.workflowTurnId ||
386
+ receipt.turn.workflowMessageId !== report.workflowMessageId ||
387
+ receipt.turn.runId !== report.runId ||
388
+ receipt.turn.targetSessionId !== report.targetSessionId ||
389
+ receipt.turn.state !== (ownership === "active" ? "started" : "ended")
390
+ ) {
391
+ throw new Error("Workflow server returned an invalid model-turn receipt");
329
392
  }
393
+ return receipt as unknown as WorkflowTurnReportReceipt;
330
394
  }
331
395
 
332
396
  function isRecord(value: unknown): value is Record<string, unknown> {