@osolmaz/pi-workflows 0.15.3 → 0.16.1

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 (207) hide show
  1. package/README.md +24 -8
  2. package/dist/channels/adapter-entry.d.ts +3 -0
  3. package/dist/channels/adapter-entry.js +203 -0
  4. package/dist/channels/adapter-entry.js.map +1 -0
  5. package/dist/channels/config.d.ts +43 -0
  6. package/dist/channels/config.js +234 -0
  7. package/dist/channels/config.js.map +1 -0
  8. package/dist/channels/protocol.d.ts +94 -0
  9. package/dist/channels/protocol.js +219 -0
  10. package/dist/channels/protocol.js.map +1 -0
  11. package/dist/channels/telegram.d.ts +46 -0
  12. package/dist/channels/telegram.js +285 -0
  13. package/dist/channels/telegram.js.map +1 -0
  14. package/dist/client/activity.d.ts +2 -0
  15. package/dist/client/activity.js +6 -0
  16. package/dist/client/activity.js.map +1 -0
  17. package/dist/client/client.d.ts +102 -0
  18. package/dist/client/client.js +737 -0
  19. package/dist/client/client.js.map +1 -0
  20. package/dist/client/index.d.ts +3 -0
  21. package/dist/client/index.js +3 -0
  22. package/dist/client/index.js.map +1 -0
  23. package/dist/client/materialize.d.ts +7 -0
  24. package/dist/client/materialize.js +177 -0
  25. package/dist/client/materialize.js.map +1 -0
  26. package/dist/client/protocol.d.ts +60 -0
  27. package/dist/client/protocol.js +275 -0
  28. package/dist/client/protocol.js.map +1 -0
  29. package/dist/client/resolver.d.ts +23 -0
  30. package/dist/client/resolver.js +2 -0
  31. package/dist/client/resolver.js.map +1 -0
  32. package/dist/client/view.d.ts +144 -0
  33. package/dist/client/view.js +3 -0
  34. package/dist/client/view.js.map +1 -0
  35. package/dist/controllers/index.d.ts +1 -1
  36. package/dist/controllers/index.js.map +1 -1
  37. package/dist/controllers/sqlite.d.ts +47 -157
  38. package/dist/controllers/sqlite.js +230 -406
  39. package/dist/controllers/sqlite.js.map +1 -1
  40. package/dist/extension/index.d.ts +23 -1
  41. package/dist/extension/index.js +734 -555
  42. package/dist/extension/index.js.map +1 -1
  43. package/dist/extension/recorder.d.ts +95 -0
  44. package/dist/extension/recorder.js +530 -0
  45. package/dist/extension/recorder.js.map +1 -0
  46. package/dist/extension/remote-recorder-store.d.ts +25 -0
  47. package/dist/extension/remote-recorder-store.js +81 -0
  48. package/dist/extension/remote-recorder-store.js.map +1 -0
  49. package/dist/extension/session-events.d.ts +134 -0
  50. package/dist/extension/session-events.js +60 -0
  51. package/dist/extension/session-events.js.map +1 -0
  52. package/dist/extension/session-view.d.ts +9 -2
  53. package/dist/extension/session-view.js +117 -51
  54. package/dist/extension/session-view.js.map +1 -1
  55. package/dist/extension/step-message.d.ts +6 -4
  56. package/dist/extension/step-message.js +12 -6
  57. package/dist/extension/step-message.js.map +1 -1
  58. package/dist/extension/widget.d.ts +3 -2
  59. package/dist/extension/widget.js +41 -24
  60. package/dist/extension/widget.js.map +1 -1
  61. package/dist/extension/workflow-message-coordinator.d.ts +27 -0
  62. package/dist/extension/workflow-message-coordinator.js +294 -0
  63. package/dist/extension/workflow-message-coordinator.js.map +1 -0
  64. package/dist/host/channel-effects.d.ts +72 -0
  65. package/dist/host/channel-effects.js +271 -0
  66. package/dist/host/channel-effects.js.map +1 -0
  67. package/dist/host/channel-supervisor.d.ts +21 -0
  68. package/dist/host/channel-supervisor.js +54 -0
  69. package/dist/host/channel-supervisor.js.map +1 -0
  70. package/dist/host/child-worker-supervisor.js +1 -1
  71. package/dist/host/child-worker-supervisor.js.map +1 -1
  72. package/dist/host/resolver-entry.d.ts +2 -23
  73. package/dist/host/resolver-entry.js +1 -1
  74. package/dist/host/resolver-entry.js.map +1 -1
  75. package/dist/host/runner.d.ts +54 -8
  76. package/dist/host/runner.js +1997 -305
  77. package/dist/host/runner.js.map +1 -1
  78. package/dist/host/state.d.ts +25 -19
  79. package/dist/host/state.js +303 -89
  80. package/dist/host/state.js.map +1 -1
  81. package/dist/host/view.d.ts +83 -0
  82. package/dist/host/view.js +975 -0
  83. package/dist/host/view.js.map +1 -0
  84. package/dist/host/worker-protocol.js +1 -1
  85. package/dist/host/worker-protocol.js.map +1 -1
  86. package/dist/state/database.d.ts +1 -0
  87. package/dist/state/database.js +15 -0
  88. package/dist/state/database.js.map +1 -1
  89. package/dist/state/prune.d.ts +3 -1
  90. package/dist/state/prune.js +9 -33
  91. package/dist/state/prune.js.map +1 -1
  92. package/dist/state/schema.js +88 -98
  93. package/dist/state/schema.js.map +1 -1
  94. package/dist/state/workflow-messages.d.ts +96 -0
  95. package/dist/state/workflow-messages.js +335 -0
  96. package/dist/state/workflow-messages.js.map +1 -0
  97. package/dist/viewer/backup.d.ts +2 -0
  98. package/dist/viewer/backup.js +28 -0
  99. package/dist/viewer/backup.js.map +1 -0
  100. package/dist/viewer/cli.d.ts +4 -0
  101. package/dist/viewer/cli.js +150 -170
  102. package/dist/viewer/cli.js.map +1 -1
  103. package/dist/viewer/render.js +1 -1
  104. package/dist/viewer/render.js.map +1 -1
  105. package/dist/viewer/tui.d.ts +5 -7
  106. package/dist/viewer/tui.js +245 -108
  107. package/dist/viewer/tui.js.map +1 -1
  108. package/dist/workflows/human-decision.d.ts +2 -0
  109. package/dist/workflows/human-decision.js +8 -2
  110. package/dist/workflows/human-decision.js.map +1 -1
  111. package/dist/workflows/index.d.ts +1 -1
  112. package/dist/workflows/index.js.map +1 -1
  113. package/dist/workflows/settings.d.ts +1 -8
  114. package/dist/workflows/settings.js.map +1 -1
  115. package/dist/workflows/store.d.ts +67 -20
  116. package/dist/workflows/store.js +451 -378
  117. package/dist/workflows/store.js.map +1 -1
  118. package/dist/workflows/tool-input.d.ts +21 -0
  119. package/dist/workflows/tool-input.js +23 -1
  120. package/dist/workflows/tool-input.js.map +1 -1
  121. package/dist/workflows/types.d.ts +5 -5
  122. package/dist/workflows/workflow-message-content.d.ts +38 -0
  123. package/dist/workflows/workflow-message-content.js +157 -0
  124. package/dist/workflows/workflow-message-content.js.map +1 -0
  125. package/docs/2026-08-18-herdr-piw-plan.md +2 -1
  126. package/docs/2026-08-20-durable-workflow-launch-plan.md +1 -1
  127. package/docs/2026-08-25-workflow-follow-ups.md +26 -50
  128. package/docs/2026-08-30-out-of-process-workflow-host-plan.md +3 -3
  129. package/docs/2026-09-01-restore-session-delivery-controls-plan.md +4 -0
  130. package/docs/2026-09-01-unified-workflow-client-plan.md +385 -0
  131. package/docs/2026-09-02-installed-live-e2e-plan.md +239 -0
  132. package/docs/2026-09-02-unify-workflow-messages-plan.md +562 -0
  133. package/docs/DEFERRED_TURNS.md +66 -294
  134. package/docs/HUMAN_DECISIONS.md +29 -30
  135. package/docs/HUMAN_DECISION_PRESENTATIONS.md +4 -11
  136. package/docs/SQLITE_STATE.md +31 -25
  137. package/docs/WORKFLOW_HOST.md +155 -77
  138. package/docs/WORKFLOW_STEP_MESSAGES.md +166 -75
  139. package/docs/development.md +2 -1
  140. package/docs/live-replay-protocol.md +74 -132
  141. package/docs/tui-viewer.md +12 -14
  142. package/docs/workflows.md +78 -28
  143. package/herdr-plugin.toml +1 -1
  144. package/package.json +9 -3
  145. package/plugins/herdr/viewer.mjs +14 -5
  146. package/protocol/client.v1.schema.json +137 -0
  147. package/protocol/fixtures/client-v1.json +23 -0
  148. package/src/channels/adapter-entry.ts +220 -0
  149. package/src/channels/config.ts +296 -0
  150. package/src/channels/protocol.ts +333 -0
  151. package/src/channels/telegram.ts +335 -0
  152. package/src/client/activity.ts +6 -0
  153. package/src/client/client.ts +939 -0
  154. package/src/client/index.ts +25 -0
  155. package/src/client/materialize.ts +228 -0
  156. package/src/client/protocol.ts +333 -0
  157. package/src/client/resolver.ts +26 -0
  158. package/src/client/view.ts +163 -0
  159. package/src/controllers/index.ts +0 -1
  160. package/src/controllers/sqlite.ts +339 -808
  161. package/src/extension/index.ts +853 -665
  162. package/src/extension/recorder.ts +687 -0
  163. package/src/extension/remote-recorder-store.ts +126 -0
  164. package/src/extension/session-events.ts +119 -0
  165. package/src/extension/session-view.ts +157 -51
  166. package/src/extension/step-message.ts +19 -9
  167. package/src/extension/widget.ts +53 -25
  168. package/src/extension/workflow-message-coordinator.ts +334 -0
  169. package/src/host/channel-effects.ts +465 -0
  170. package/src/host/channel-supervisor.ts +73 -0
  171. package/src/host/child-worker-supervisor.ts +1 -1
  172. package/src/host/resolver-entry.ts +11 -26
  173. package/src/host/runner.ts +2639 -450
  174. package/src/host/state.ts +404 -137
  175. package/src/host/view.ts +1212 -0
  176. package/src/host/worker-protocol.ts +1 -1
  177. package/src/state/database.ts +13 -0
  178. package/src/state/prune.ts +14 -42
  179. package/src/state/schema.ts +88 -98
  180. package/src/state/workflow-messages.ts +548 -0
  181. package/src/viewer/backup.ts +29 -0
  182. package/src/viewer/cli.ts +171 -185
  183. package/src/viewer/render.ts +1 -5
  184. package/src/viewer/tui.ts +261 -124
  185. package/src/workflows/human-decision.ts +18 -2
  186. package/src/workflows/index.ts +0 -1
  187. package/src/workflows/settings.ts +1 -20
  188. package/src/workflows/store.ts +650 -511
  189. package/src/workflows/tool-input.ts +36 -1
  190. package/src/workflows/types.ts +5 -5
  191. package/src/workflows/workflow-message-content.ts +197 -0
  192. package/dist/extension/decision-channels.d.ts +0 -134
  193. package/dist/extension/decision-channels.js +0 -1307
  194. package/dist/extension/decision-channels.js.map +0 -1
  195. package/dist/host/client.d.ts +0 -48
  196. package/dist/host/client.js +0 -216
  197. package/dist/host/client.js.map +0 -1
  198. package/dist/host/protocol.d.ts +0 -38
  199. package/dist/host/protocol.js +0 -156
  200. package/dist/host/protocol.js.map +0 -1
  201. package/dist/viewer/watch.d.ts +0 -6
  202. package/dist/viewer/watch.js +0 -46
  203. package/dist/viewer/watch.js.map +0 -1
  204. package/src/extension/decision-channels.ts +0 -1826
  205. package/src/host/client.ts +0 -293
  206. package/src/host/protocol.ts +0 -196
  207. package/src/viewer/watch.ts +0 -51
package/src/viewer/tui.ts CHANGED
@@ -1,160 +1,297 @@
1
- import { WorkflowRunStore, type LoadedWorkflowRun } from "../workflows/store.js";
2
- import {
3
- maxDetailScroll,
4
- renderRunDetailLines,
5
- renderRunListLines,
6
- type ViewportSize,
7
- } from "./render.js";
8
- import { watchStateDatabase } from "./watch.js";
1
+ import type { WorkflowClient } from "../client/client.js";
2
+ import { materializeRunView } from "../client/materialize.js";
3
+ import { sanitizeText } from "../render/ansi.js";
4
+ import { formatDuration } from "../render/format.js";
5
+ import type { JsonValue } from "../state/json.js";
9
6
 
10
7
  const ALT_SCREEN_ON = "\u001b[?1049h\u001b[?25l";
11
8
  const ALT_SCREEN_OFF = "\u001b[?25h\u001b[?1049l";
12
9
  const CLEAR = "\u001b[2J\u001b[H";
13
10
 
14
- type ViewerMode = { view: "list" } | { view: "detail"; runId: string };
15
-
16
11
  export type ViewerOptions = {
17
- databasePath: string;
12
+ client: WorkflowClient;
18
13
  runId?: string | undefined;
19
- /** Redraw interval for elapsed timers while a run is active. */
20
- tickMs?: number;
21
14
  };
22
15
 
23
- function viewportSize(): ViewportSize {
24
- return {
25
- width: process.stdout.columns ?? 80,
26
- height: process.stdout.rows ?? 24,
27
- };
28
- }
29
-
30
- /**
31
- * Interactive live viewer. Watches the SQLite database and re-renders after
32
- * committed run changes. Returns when the user quits.
33
- */
16
+ /** Interactive client view. All durable reads stay in the workflow host. */
34
17
  export async function runViewer(options: ViewerOptions): Promise<void> {
35
- const store = new WorkflowRunStore(options.databasePath, { readOnly: true });
36
- let mode: ViewerMode = { view: "list" };
37
- let bundles: LoadedWorkflowRun[] = [];
38
- let selectedIndex = 0;
39
- let detailScroll = 0;
40
- /** Replay position; null follows the latest step live. */
41
- let selectedStep: number | null = null;
42
- let detailStepCount = 0;
43
-
44
- if (options.runId) {
45
- bundles = store.listRuns();
46
- const match = bundles.find((bundle) => bundle.state.runId === options.runId);
47
- if (!match) {
48
- throw new Error(`Run not found: ${options.runId}`);
49
- }
50
- mode = { view: "detail", runId: match.runId };
51
- }
18
+ let value: JsonValue = options.runId === undefined ? [] : null;
19
+ let selected = 0;
20
+ let scroll = 0;
21
+ let selectedStepIndex: number | null = null;
22
+ let mode: "runs" | "run" = options.runId === undefined ? "runs" : "run";
23
+ let unsubscribe: (() => Promise<void>) | undefined;
24
+ let runMaterializationGeneration = 0;
52
25
 
53
- const draw = async () => {
54
- bundles = store.listRuns();
55
- selectedIndex = Math.min(selectedIndex, Math.max(0, bundles.length - 1));
56
- const size = viewportSize();
57
- const lines =
58
- mode.view === "list"
59
- ? renderRunListLines(bundles, selectedIndex, size)
60
- : await renderDetail(mode.runId, size);
61
- process.stdout.write(CLEAR + lines.join("\n"));
26
+ const draw = () => {
27
+ const width = process.stdout.columns ?? 100;
28
+ const height = Math.max(1, (process.stdout.rows ?? 24) - 1);
29
+ const lines = renderClientView(
30
+ value,
31
+ width,
32
+ height,
33
+ mode === "runs" ? selected : undefined,
34
+ scroll,
35
+ new Date(),
36
+ selectedStepIndex,
37
+ );
38
+ process.stdout.write(`${CLEAR}${lines.join("\n")}`);
62
39
  };
63
40
 
64
- const renderDetail = async (runId: string, size: ViewportSize): Promise<string[]> => {
65
- const bundle = store.readRun(runId, { includeTrace: true });
66
- if (!bundle) {
67
- return ["SQLite run state disappeared. Press q to go back."];
68
- }
69
- detailStepCount = bundle.state.steps.length;
70
- if (selectedStep !== null && selectedStep >= detailStepCount - 1) {
71
- // Scrubbed to (or past) the end: snap back to following live updates.
72
- selectedStep = null;
73
- }
74
- detailScroll = Math.min(detailScroll, maxDetailScroll(bundle, size, selectedStep));
75
- return renderRunDetailLines(bundle, size, new Date(), detailScroll, selectedStep);
41
+ const watchRuns = async () => {
42
+ runMaterializationGeneration += 1;
43
+ await unsubscribe?.();
44
+ mode = "runs";
45
+ scroll = 0;
46
+ selectedStepIndex = null;
47
+ unsubscribe = await options.client.watchRuns((event) => {
48
+ value = event.payload;
49
+ selected = Math.min(selected, Math.max(0, arrayLength(value) - 1));
50
+ draw();
51
+ });
76
52
  };
77
53
 
78
- process.stdout.write(ALT_SCREEN_ON);
79
- const stopWatching = watchStateDatabase(options.databasePath, () => {
80
- void draw();
81
- });
82
- const ticker = setInterval(() => {
83
- void draw();
84
- }, options.tickMs ?? 1_000);
54
+ const watchRun = async (runId: string) => {
55
+ runMaterializationGeneration += 1;
56
+ await unsubscribe?.();
57
+ mode = "run";
58
+ scroll = 0;
59
+ selectedStepIndex = null;
60
+ unsubscribe = await options.client.watchRun(runId, (event) => {
61
+ const generation = ++runMaterializationGeneration;
62
+ value = event.payload;
63
+ draw();
64
+ void materializeRunView(options.client, event.payload)
65
+ .then((materialized) => {
66
+ if (generation !== runMaterializationGeneration || mode !== "run") return;
67
+ value = materialized;
68
+ draw();
69
+ })
70
+ .catch(() => {
71
+ // A newer revision event retries the complete view from its own stable snapshot.
72
+ });
73
+ });
74
+ };
85
75
 
76
+ process.stdout.write(ALT_SCREEN_ON);
86
77
  const rawModeSupported = process.stdin.isTTY === true;
87
- if (rawModeSupported) {
88
- process.stdin.setRawMode(true);
89
- }
90
- process.stdin.resume();
91
-
92
78
  try {
79
+ if (options.runId === undefined) await watchRuns();
80
+ else await watchRun(options.runId);
81
+ draw();
82
+ if (rawModeSupported) process.stdin.setRawMode(true);
83
+ process.stdin.resume();
84
+ const redrawTimer = setInterval(draw, 1_000);
85
+ redrawTimer.unref?.();
93
86
  await new Promise<void>((resolve) => {
94
87
  const onKey = (data: Buffer) => {
95
88
  const key = data.toString("utf8");
96
- if (key === "q" || key === "\u0003" || key === "\u001b") {
97
- if (mode.view === "detail" && key === "q") {
98
- mode = { view: "list" };
99
- void draw();
100
- return;
101
- }
89
+ if (key === "\u0003" || key === "\u001b") {
102
90
  resolve();
103
91
  return;
104
92
  }
105
- handleNavigationKey(key);
106
- };
107
-
108
- const handleNavigationKey = (key: string) => {
109
- if (mode.view !== "list") {
110
- if (key === "r") {
111
- void draw();
112
- } else if (key === "\u001b[A" || key === "k") {
113
- detailScroll = Math.max(0, detailScroll - 1);
114
- void draw();
115
- } else if (key === "\u001b[B" || key === "j") {
116
- // Clamped against the content height in renderDetail.
117
- detailScroll += 1;
118
- void draw();
119
- } else if (key === "\u001b[D" || key === "h") {
120
- const current = selectedStep ?? detailStepCount - 1;
121
- selectedStep = Math.max(0, current - 1);
122
- void draw();
123
- } else if (key === "\u001b[C" || key === "l") {
124
- // renderDetail snaps back to live once this reaches the end.
125
- selectedStep = selectedStep === null ? null : selectedStep + 1;
126
- void draw();
127
- }
93
+ if (key === "q") {
94
+ if (mode === "run" && options.runId === undefined) void watchRuns();
95
+ else resolve();
128
96
  return;
129
97
  }
130
- if (key === "\u001b[A" || key === "k") {
131
- selectedIndex = Math.max(0, selectedIndex - 1);
132
- void draw();
133
- } else if (key === "\u001b[B" || key === "j") {
134
- selectedIndex = Math.min(Math.max(0, bundles.length - 1), selectedIndex + 1);
135
- void draw();
136
- } else if (key === "\r" || key === "\n") {
137
- const selected = bundles[selectedIndex];
138
- if (selected) {
139
- mode = { view: "detail", runId: selected.runId };
140
- detailScroll = 0;
141
- selectedStep = null;
142
- void draw();
98
+ if (mode === "run") {
99
+ const page = Math.max(1, (process.stdout.rows ?? 24) - 2);
100
+ if (key === "\u001b[A" || key === "k") {
101
+ scroll = Math.max(0, scroll - 1);
102
+ draw();
103
+ } else if (key === "\u001b[B" || key === "j") {
104
+ scroll += 1;
105
+ draw();
106
+ } else if (key === "\u001b[5~") {
107
+ scroll = Math.max(0, scroll - page);
108
+ draw();
109
+ } else if (key === "\u001b[6~") {
110
+ scroll += page;
111
+ draw();
112
+ } else if (key === "[") {
113
+ const total = runStepCount(value);
114
+ if (total > 0) {
115
+ selectedStepIndex = Math.max(0, (selectedStepIndex ?? total - 1) - 1);
116
+ scroll = 0;
117
+ draw();
118
+ }
119
+ } else if (key === "]") {
120
+ const total = runStepCount(value);
121
+ if (total > 0) {
122
+ const next = (selectedStepIndex ?? total - 1) + 1;
123
+ selectedStepIndex = next >= total - 1 ? null : next;
124
+ scroll = 0;
125
+ draw();
126
+ }
127
+ } else if (key === "\u001b[F" || key === "G" || key === "L") {
128
+ selectedStepIndex = null;
129
+ scroll = 0;
130
+ draw();
131
+ } else if (key === "\u001b[H" || key === "g") {
132
+ selectedStepIndex = runStepCount(value) > 0 ? 0 : null;
133
+ scroll = 0;
134
+ draw();
135
+ }
136
+ } else if (mode === "runs") {
137
+ if (key === "\u001b[A" || key === "k") {
138
+ selected = Math.max(0, selected - 1);
139
+ draw();
140
+ } else if (key === "\u001b[B" || key === "j") {
141
+ selected = Math.min(Math.max(0, arrayLength(value) - 1), selected + 1);
142
+ draw();
143
+ } else if (key === "\r" || key === "\n") {
144
+ const runId = selectedRunId(value, selected);
145
+ if (runId !== undefined) void watchRun(runId);
143
146
  }
144
147
  }
145
148
  };
146
-
147
149
  process.stdin.on("data", onKey);
148
- void draw();
149
150
  });
151
+ clearInterval(redrawTimer);
150
152
  } finally {
151
- clearInterval(ticker);
152
- stopWatching();
153
- store.close();
154
- if (rawModeSupported) {
155
- process.stdin.setRawMode(false);
156
- }
153
+ runMaterializationGeneration += 1;
154
+ await unsubscribe?.();
155
+ if (rawModeSupported) process.stdin.setRawMode(false);
157
156
  process.stdin.pause();
158
157
  process.stdout.write(ALT_SCREEN_OFF);
159
158
  }
160
159
  }
160
+
161
+ export function renderClientView(
162
+ value: JsonValue,
163
+ width: number,
164
+ height: number,
165
+ selected: number | undefined,
166
+ scroll: number,
167
+ now: Date = new Date(),
168
+ selectedStepIndex: number | null = null,
169
+ ): string[] {
170
+ if (Array.isArray(value)) {
171
+ if (value.length === 0) return ["No workflow runs found."];
172
+ const selectedIndex = selected ?? 0;
173
+ const start = Math.min(
174
+ Math.max(0, selectedIndex - height + 1),
175
+ Math.max(0, value.length - height),
176
+ );
177
+ return value.slice(start, start + height).map((item, offset) => {
178
+ const index = start + offset;
179
+ const prefix = index === selected ? "> " : " ";
180
+ return truncate(`${prefix}${summary(item)}`, width);
181
+ });
182
+ }
183
+ const lines = renderRun(value, now, selectedStepIndex);
184
+ const start = Math.min(scroll, Math.max(0, lines.length - height));
185
+ return lines.slice(start, start + height).map((line) => truncate(line, width));
186
+ }
187
+
188
+ function renderRun(value: JsonValue, now: Date, selectedStepIndex: number | null): string[] {
189
+ if (!isRecord(value) || value.schema !== "pi-workflows.run-view.v1") {
190
+ return JSON.stringify(value, null, 2).split("\n");
191
+ }
192
+ const manifest = isRecord(value.manifest) ? value.manifest : {};
193
+ const state = isRecord(value.state) ? value.state : {};
194
+ const display = isRecord(value.display) ? value.display : {};
195
+ const workflowName =
196
+ typeof manifest.workflowName === "string"
197
+ ? sanitizeText(manifest.workflowName)
198
+ : typeof state.workflowName === "string"
199
+ ? sanitizeText(state.workflowName)
200
+ : "unknown";
201
+ const runId = typeof value.runId === "string" ? sanitizeText(value.runId) : "unknown";
202
+ const status = typeof display.status === "string" ? display.status : "unknown";
203
+ const steps = Array.isArray(state.steps) ? state.steps : [];
204
+ const selected =
205
+ steps.length === 0
206
+ ? -1
207
+ : Math.min(Math.max(0, selectedStepIndex ?? steps.length - 1), steps.length - 1);
208
+ const startedAt = typeof manifest.startedAt === "string" ? Date.parse(manifest.startedAt) : NaN;
209
+ const finishedAt =
210
+ typeof manifest.finishedAt === "string" ? Date.parse(manifest.finishedAt) : NaN;
211
+ const elapsed = Number.isFinite(startedAt)
212
+ ? ` · elapsed ${formatDuration(Math.max(0, (Number.isFinite(finishedAt) ? finishedAt : now.getTime()) - startedAt))}`
213
+ : "";
214
+ const position =
215
+ selectedStepIndex === null || selected < 0 ? "" : ` · step ${selected + 1}/${steps.length}`;
216
+ const lines = [
217
+ `workflow ${workflowName}`,
218
+ `${statusGlyph(status)} ${sanitizeText(status)} · run ${runId}${elapsed}${position}`,
219
+ ];
220
+ if (typeof display.reason === "string") lines.push(`reason: ${sanitizeText(display.reason)}`);
221
+
222
+ if (steps.length > 0) {
223
+ lines.push("", "steps");
224
+ for (const [index, step] of steps.entries()) {
225
+ if (!isRecord(step)) continue;
226
+ const nodeId = typeof step.nodeId === "string" ? sanitizeText(step.nodeId) : "unknown";
227
+ const outcome = typeof step.outcome === "string" ? step.outcome : "unknown";
228
+ lines.push(
229
+ `${index === selected ? ">" : " "} ${stepGlyph(outcome)} ${nodeId} · ${sanitizeText(outcome)}`,
230
+ );
231
+ }
232
+ const inspected = steps[selected];
233
+ if (isRecord(inspected)) {
234
+ const nodeId =
235
+ typeof inspected.nodeId === "string" ? sanitizeText(inspected.nodeId) : "unknown";
236
+ const output = Object.hasOwn(inspected, "error") ? inspected.error : inspected.output;
237
+ lines.push("", `step output — ${nodeId}`);
238
+ lines.push(
239
+ ...JSON.stringify(output ?? null, null, 2)
240
+ .split("\n")
241
+ .map((line) => ` ${sanitizeText(line)}`),
242
+ );
243
+ }
244
+ }
245
+ if (status === "completed" && Object.hasOwn(state, "finalOutput")) {
246
+ lines.push("", `output ${JSON.stringify(state.finalOutput)}`);
247
+ } else if (typeof state.error === "string") {
248
+ lines.push("", `error: ${sanitizeText(state.error)}`);
249
+ }
250
+ return lines;
251
+ }
252
+
253
+ function summary(value: JsonValue): string {
254
+ if (!isRecord(value)) return JSON.stringify(value);
255
+ const status = isRecord(value.display) ? value.display.status : undefined;
256
+ const name = typeof value.workflowName === "string" ? value.workflowName : "workflow";
257
+ const runId = typeof value.runId === "string" ? value.runId : "unknown";
258
+ return `${typeof status === "string" ? sanitizeText(status) : "unknown"} ${sanitizeText(name)} ${sanitizeText(runId)}`;
259
+ }
260
+
261
+ function statusGlyph(status: string): string {
262
+ if (status === "completed") return "✓";
263
+ if (status === "failed" || status === "timed_out" || status === "cancelled") return "✗";
264
+ if (status === "paused") return "‖";
265
+ if (status === "waiting") return "○";
266
+ if (status === "queued") return "…";
267
+ if (status === "ambiguous") return "!";
268
+ return "●";
269
+ }
270
+
271
+ function stepGlyph(outcome: string): string {
272
+ return outcome === "ok" ? "✓" : outcome === "failed" || outcome === "timed_out" ? "✗" : "○";
273
+ }
274
+
275
+ function selectedRunId(value: JsonValue, index: number): string | undefined {
276
+ if (!Array.isArray(value)) return undefined;
277
+ const item = value[index];
278
+ return isRecord(item) && typeof item.runId === "string" ? item.runId : undefined;
279
+ }
280
+
281
+ function runStepCount(value: JsonValue): number {
282
+ if (!isRecord(value) || !isRecord(value.state) || !Array.isArray(value.state.steps)) return 0;
283
+ return value.state.steps.length;
284
+ }
285
+
286
+ function arrayLength(value: JsonValue): number {
287
+ return Array.isArray(value) ? value.length : 0;
288
+ }
289
+
290
+ function truncate(value: string, width: number): string {
291
+ if (value.length <= width) return value;
292
+ return width <= 1 ? value.slice(0, width) : `${value.slice(0, width - 1)}…`;
293
+ }
294
+
295
+ function isRecord(value: unknown): value is Record<string, JsonValue> {
296
+ return typeof value === "object" && value !== null && !Array.isArray(value);
297
+ }
@@ -468,6 +468,14 @@ export class HumanDecisionStore {
468
468
  channel: string,
469
469
  value: HumanDecisionDeliveryRecord,
470
470
  ): Promise<"created" | "adopted"> {
471
+ return this.recordDeliverySync(request, channel, value);
472
+ }
473
+
474
+ recordDeliverySync(
475
+ request: HumanDecisionRequest | HumanDecisionChannelRequest,
476
+ channel: string,
477
+ value: HumanDecisionDeliveryRecord,
478
+ ): "created" | "adopted" {
471
479
  return this.recordChannelMessage(request.decisionId, channel, "delivery", value);
472
480
  }
473
481
 
@@ -570,6 +578,14 @@ export class HumanDecisionStore {
570
578
  channel: string,
571
579
  value: HumanDecisionSettlementRecord,
572
580
  ): Promise<"created" | "adopted"> {
581
+ return this.recordSettlementSync(decisionId, channel, value);
582
+ }
583
+
584
+ recordSettlementSync(
585
+ decisionId: string,
586
+ channel: string,
587
+ value: HumanDecisionSettlementRecord,
588
+ ): "created" | "adopted" {
573
589
  return this.recordChannelMessage(decisionId, channel, "settlement", value);
574
590
  }
575
591
 
@@ -1066,7 +1082,7 @@ export class HumanDecisionStore {
1066
1082
  purpose: "delivery" | "settlement",
1067
1083
  value: T,
1068
1084
  ): "created" | "adopted" {
1069
- const safeChannel = requireSimpleId(channel, "Human decision channel");
1085
+ const safeChannel = requireChannelId(channel, "Human decision channel");
1070
1086
  assertId(value.attemptId, `${purpose} attempt id`);
1071
1087
  return this.state.transaction(() => {
1072
1088
  if (this.readRequestRow(decisionId) === undefined) {
@@ -1120,7 +1136,7 @@ export class HumanDecisionStore {
1120
1136
  channel: string,
1121
1137
  purpose: "delivery" | "settlement",
1122
1138
  ): T[] {
1123
- const channelId = channelIdFor(requireSimpleId(channel, "Human decision channel"));
1139
+ const channelId = channelIdFor(requireChannelId(channel, "Human decision channel"));
1124
1140
  const rows = this.state.connection
1125
1141
  .prepare(
1126
1142
  `SELECT content_hash AS contentHash
@@ -92,7 +92,6 @@ export {
92
92
  type WorkflowFollowUpQueueRecord,
93
93
  type WorkflowFollowUpRecord,
94
94
  type WorkflowFollowUpState,
95
- type WorkflowPresentationState,
96
95
  type WorkflowQueueFollowUpRequest,
97
96
  type WorkflowRemoveFollowUpRequest,
98
97
  type WorkflowSettingsChangeContext,
@@ -101,20 +101,7 @@ export type WorkflowSettingsChangeResult = {
101
101
  adopted: boolean;
102
102
  };
103
103
 
104
- export type WorkflowFollowUpState =
105
- | "queued"
106
- | "pending_presentation"
107
- | "ready"
108
- | "sent"
109
- | "removed"
110
- | "cancelled";
111
-
112
- export type WorkflowPresentationState =
113
- | "none"
114
- | "not-needed"
115
- | "pending"
116
- | "settled"
117
- | "unavailable";
104
+ export type WorkflowFollowUpState = "queued" | "removed" | "cancelled";
118
105
 
119
106
  export type WorkflowFollowUpRecord = {
120
107
  followUpId: string;
@@ -126,20 +113,14 @@ export type WorkflowFollowUpRecord = {
126
113
  source: string;
127
114
  prompt: string;
128
115
  state: WorkflowFollowUpState;
129
- sessionEntryId?: string;
130
116
  reason?: string;
131
117
  createdAt: string;
132
118
  updatedAt: string;
133
- sentAt?: string;
134
119
  };
135
120
 
136
121
  export type WorkflowFollowUpQueueRecord = {
137
122
  runId: string;
138
123
  originSessionId?: string;
139
- presentationState: WorkflowPresentationState;
140
- presentationEntryId?: string;
141
- presentationAssistantEntryId?: string;
142
- presentationReason?: string;
143
124
  followUps: WorkflowFollowUpRecord[];
144
125
  };
145
126