@osolmaz/pi-workflows 0.15.3 → 0.16.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 (118) hide show
  1. package/README.md +6 -6
  2. package/dist/client/activity.d.ts +2 -0
  3. package/dist/client/activity.js +6 -0
  4. package/dist/client/activity.js.map +1 -0
  5. package/dist/client/client.d.ts +101 -0
  6. package/dist/client/client.js +733 -0
  7. package/dist/client/client.js.map +1 -0
  8. package/dist/client/index.d.ts +3 -0
  9. package/dist/client/index.js +3 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/client/materialize.d.ts +7 -0
  12. package/dist/client/materialize.js +177 -0
  13. package/dist/client/materialize.js.map +1 -0
  14. package/dist/client/protocol.d.ts +60 -0
  15. package/dist/client/protocol.js +269 -0
  16. package/dist/client/protocol.js.map +1 -0
  17. package/dist/client/resolver.d.ts +23 -0
  18. package/dist/client/resolver.js +2 -0
  19. package/dist/client/resolver.js.map +1 -0
  20. package/dist/client/view.d.ts +118 -0
  21. package/dist/client/view.js +3 -0
  22. package/dist/client/view.js.map +1 -0
  23. package/dist/controllers/sqlite.d.ts +43 -0
  24. package/dist/controllers/sqlite.js +137 -2
  25. package/dist/controllers/sqlite.js.map +1 -1
  26. package/dist/extension/index.d.ts +1 -0
  27. package/dist/extension/index.js +278 -183
  28. package/dist/extension/index.js.map +1 -1
  29. package/dist/extension/session-view.d.ts +7 -2
  30. package/dist/extension/session-view.js +60 -52
  31. package/dist/extension/session-view.js.map +1 -1
  32. package/dist/extension/widget.d.ts +2 -1
  33. package/dist/extension/widget.js +14 -7
  34. package/dist/extension/widget.js.map +1 -1
  35. package/dist/host/child-worker-supervisor.js +1 -1
  36. package/dist/host/child-worker-supervisor.js.map +1 -1
  37. package/dist/host/resolver-entry.d.ts +2 -23
  38. package/dist/host/resolver-entry.js +1 -1
  39. package/dist/host/resolver-entry.js.map +1 -1
  40. package/dist/host/runner.d.ts +11 -0
  41. package/dist/host/runner.js +473 -17
  42. package/dist/host/runner.js.map +1 -1
  43. package/dist/host/state.d.ts +6 -3
  44. package/dist/host/state.js +52 -26
  45. package/dist/host/state.js.map +1 -1
  46. package/dist/host/view.d.ts +73 -0
  47. package/dist/host/view.js +871 -0
  48. package/dist/host/view.js.map +1 -0
  49. package/dist/host/worker-protocol.js +1 -1
  50. package/dist/host/worker-protocol.js.map +1 -1
  51. package/dist/state/database.d.ts +1 -0
  52. package/dist/state/database.js +15 -0
  53. package/dist/state/database.js.map +1 -1
  54. package/dist/state/prune.d.ts +3 -1
  55. package/dist/state/prune.js +6 -8
  56. package/dist/state/prune.js.map +1 -1
  57. package/dist/state/schema.js +12 -1
  58. package/dist/state/schema.js.map +1 -1
  59. package/dist/viewer/backup.d.ts +2 -0
  60. package/dist/viewer/backup.js +28 -0
  61. package/dist/viewer/backup.js.map +1 -0
  62. package/dist/viewer/cli.d.ts +4 -0
  63. package/dist/viewer/cli.js +150 -170
  64. package/dist/viewer/cli.js.map +1 -1
  65. package/dist/viewer/tui.d.ts +5 -7
  66. package/dist/viewer/tui.js +188 -108
  67. package/dist/viewer/tui.js.map +1 -1
  68. package/dist/workflows/store.d.ts +62 -1
  69. package/dist/workflows/store.js +350 -44
  70. package/dist/workflows/store.js.map +1 -1
  71. package/docs/2026-09-01-unified-workflow-client-plan.md +381 -0
  72. package/docs/2026-09-02-installed-live-e2e-plan.md +225 -0
  73. package/docs/SQLITE_STATE.md +13 -11
  74. package/docs/WORKFLOW_HOST.md +74 -61
  75. package/docs/development.md +2 -1
  76. package/docs/live-replay-protocol.md +70 -132
  77. package/docs/tui-viewer.md +10 -14
  78. package/docs/workflows.md +44 -1
  79. package/herdr-plugin.toml +1 -1
  80. package/package.json +9 -3
  81. package/protocol/client.v1.schema.json +137 -0
  82. package/protocol/fixtures/client-v1.json +23 -0
  83. package/src/client/activity.ts +6 -0
  84. package/src/client/client.ts +935 -0
  85. package/src/client/index.ts +24 -0
  86. package/src/client/materialize.ts +228 -0
  87. package/src/client/protocol.ts +327 -0
  88. package/src/client/resolver.ts +26 -0
  89. package/src/client/view.ts +138 -0
  90. package/src/controllers/sqlite.ts +213 -2
  91. package/src/extension/index.ts +349 -208
  92. package/src/extension/session-view.ts +73 -50
  93. package/src/extension/widget.ts +16 -8
  94. package/src/host/child-worker-supervisor.ts +1 -1
  95. package/src/host/resolver-entry.ts +11 -26
  96. package/src/host/runner.ts +648 -48
  97. package/src/host/state.ts +71 -43
  98. package/src/host/view.ts +1084 -0
  99. package/src/host/worker-protocol.ts +1 -1
  100. package/src/state/database.ts +13 -0
  101. package/src/state/prune.ts +11 -11
  102. package/src/state/schema.ts +12 -1
  103. package/src/viewer/backup.ts +29 -0
  104. package/src/viewer/cli.ts +171 -185
  105. package/src/viewer/tui.ts +196 -124
  106. package/src/workflows/store.ts +500 -45
  107. package/dist/host/client.d.ts +0 -48
  108. package/dist/host/client.js +0 -216
  109. package/dist/host/client.js.map +0 -1
  110. package/dist/host/protocol.d.ts +0 -38
  111. package/dist/host/protocol.js +0 -156
  112. package/dist/host/protocol.js.map +0 -1
  113. package/dist/viewer/watch.d.ts +0 -6
  114. package/dist/viewer/watch.js +0 -46
  115. package/dist/viewer/watch.js.map +0 -1
  116. package/src/host/client.ts +0 -293
  117. package/src/host/protocol.ts +0 -196
  118. package/src/viewer/watch.ts +0 -51
@@ -1,32 +1,51 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { SqliteControllerStore } from "../controllers/sqlite.js";
3
- import { workflowStatePath } from "../state/database.js";
4
- import { WorkflowRunStore, type LoadedWorkflowRun } from "../workflows/store.js";
2
+ import type { WorkflowSessionView } from "../client/view.js";
3
+ import type { WorkflowDefinitionSnapshot, WorkflowRunState } from "../workflows/types.js";
5
4
  import { buildWidgetView } from "./widget.js";
6
5
 
7
6
  const WIDGET_KEY = "pi-workflows";
8
7
  const WIDGET_SCROLL_STEP = 3;
9
8
 
10
- /** Read-only projection of the host-owned run into the origin Pi session. */
9
+ /** Client-backed projection of the host-owned run into the origin Pi session. */
11
10
  export class SessionWorkflowView {
12
- private loaded: LoadedWorkflowRun | undefined;
11
+ private session: WorkflowSessionView | null = null;
13
12
  private scroll: number | null = null;
14
13
  private shownScroll = 0;
15
14
  private maxScroll = 0;
16
15
  private stepCount = 0;
17
16
  private visible = false;
17
+ private actionHint: string | undefined;
18
18
 
19
- refresh(ctx: ExtensionContext): void {
20
- const loaded = loadSessionRun(ctx.sessionManager.getSessionId());
21
- if (loaded === undefined) {
22
- this.clear(ctx);
19
+ update(session: WorkflowSessionView, ctx: ExtensionContext): void {
20
+ const run = session.run;
21
+ if (
22
+ run === null ||
23
+ !isWorkflowRunState(run.state) ||
24
+ !isWorkflowDefinitionSnapshot(run.workflow)
25
+ ) {
26
+ this.session = session;
27
+ this.clearWidget(ctx);
23
28
  return;
24
29
  }
25
- if (this.loaded?.runId !== loaded.runId || this.stepCount !== loaded.state.steps.length) {
30
+ const previousRun = this.session?.run;
31
+ if (previousRun?.runId !== run.runId || this.stepCount !== run.state.steps.length) {
26
32
  this.scroll = null;
27
- this.stepCount = loaded.state.steps.length;
33
+ this.stepCount = run.state.steps.length;
34
+ }
35
+ this.session = session;
36
+ this.render(ctx);
37
+ }
38
+
39
+ setActionHint(hint: string | undefined, ctx: ExtensionContext): void {
40
+ this.actionHint = hint;
41
+ if (this.session?.run !== null) this.render(ctx);
42
+ }
43
+
44
+ refresh(ctx: ExtensionContext): void {
45
+ if (this.session?.run === null || this.session === null) {
46
+ this.clearWidget(ctx);
47
+ return;
28
48
  }
29
- this.loaded = loaded;
30
49
  this.render(ctx);
31
50
  }
32
51
 
@@ -39,42 +58,51 @@ export class SessionWorkflowView {
39
58
  }
40
59
 
41
60
  clear(ctx: ExtensionContext): void {
42
- this.loaded = undefined;
61
+ this.session = null;
43
62
  this.scroll = null;
44
63
  this.shownScroll = 0;
45
64
  this.maxScroll = 0;
46
65
  this.stepCount = 0;
47
- if (!this.visible) return;
48
- this.visible = false;
49
- safelyUpdateUi(ctx, () => {
50
- ctx.ui.setWidget(WIDGET_KEY, undefined);
51
- ctx.ui.setStatus(WIDGET_KEY, undefined);
52
- });
66
+ this.clearWidget(ctx);
53
67
  }
54
68
 
55
69
  private scrollBy(ctx: ExtensionContext, delta: number): void {
56
- if (this.loaded === undefined) return;
70
+ if (this.session?.run === null || this.session === null) return;
57
71
  const current = this.scroll ?? this.shownScroll;
58
72
  this.scroll = Math.max(0, Math.min(this.maxScroll, current + delta));
59
73
  this.render(ctx);
60
74
  }
61
75
 
76
+ private clearWidget(ctx: ExtensionContext): void {
77
+ if (!this.visible) return;
78
+ this.visible = false;
79
+ safelyUpdateUi(ctx, () => {
80
+ ctx.ui.setWidget(WIDGET_KEY, undefined);
81
+ ctx.ui.setStatus(WIDGET_KEY, undefined);
82
+ });
83
+ }
84
+
62
85
  private render(ctx: ExtensionContext): void {
63
- const loaded = this.loaded;
64
- if (loaded === undefined) return;
86
+ const run = this.session?.run;
87
+ if (run === null || run === undefined) return;
88
+ if (!isWorkflowRunState(run.state) || !isWorkflowDefinitionSnapshot(run.workflow)) return;
89
+ const state = run.state;
90
+ const snapshot = run.workflow;
65
91
  const render = (
66
92
  width = Number.POSITIVE_INFINITY,
67
93
  theme?: Parameters<typeof buildWidgetView>[6],
68
94
  ) => {
69
95
  const view = buildWidgetView(
70
- loaded.state,
71
- loaded.snapshot,
96
+ state,
97
+ snapshot,
72
98
  new Date(),
73
99
  this.scroll,
74
- loaded.state.paused === true,
100
+ run.display.status === "paused",
75
101
  width,
76
102
  theme,
77
103
  undefined,
104
+ this.actionHint,
105
+ run.display.status,
78
106
  );
79
107
  this.shownScroll = view.scroll;
80
108
  this.maxScroll = view.maxScroll;
@@ -90,42 +118,37 @@ export class SessionWorkflowView {
90
118
  } else {
91
119
  ctx.ui.setWidget(WIDGET_KEY, render());
92
120
  }
93
- const label = loaded.state.paused === true ? "paused" : loaded.state.status;
94
- ctx.ui.setStatus(WIDGET_KEY, `${loaded.state.workflowName} [${label}]`);
121
+ ctx.ui.setStatus(WIDGET_KEY, `${state.workflowName} [${run.display.status}]`);
95
122
  this.visible = true;
96
123
  });
97
124
  }
98
125
  }
99
126
 
100
- function loadSessionRun(sessionId: string): LoadedWorkflowRun | undefined {
101
- try {
102
- const queue = new SqliteControllerStore(workflowStatePath(), {
103
- readOnly: true,
104
- global: true,
105
- });
106
- let runId: string | undefined;
107
- try {
108
- runId = queue.findSessionReservation(sessionId)?.runId;
109
- } finally {
110
- queue.close();
111
- }
112
- if (runId === undefined) return undefined;
127
+ function isWorkflowRunState(value: unknown): value is WorkflowRunState {
128
+ return (
129
+ typeof value === "object" &&
130
+ value !== null &&
131
+ !Array.isArray(value) &&
132
+ (value as { schema?: unknown }).schema === "pi-workflows.run-state.v1" &&
133
+ typeof (value as { workflowName?: unknown }).workflowName === "string" &&
134
+ Array.isArray((value as { steps?: unknown }).steps)
135
+ );
136
+ }
113
137
 
114
- const runs = new WorkflowRunStore(workflowStatePath(), { readOnly: true });
115
- try {
116
- return runs.readRun(runId) ?? undefined;
117
- } finally {
118
- runs.close();
119
- }
120
- } catch {
121
- return undefined;
122
- }
138
+ function isWorkflowDefinitionSnapshot(value: unknown): value is WorkflowDefinitionSnapshot {
139
+ return (
140
+ typeof value === "object" &&
141
+ value !== null &&
142
+ !Array.isArray(value) &&
143
+ (value as { schema?: unknown }).schema === "pi-workflows.definition-snapshot.v1" &&
144
+ typeof (value as { nodes?: unknown }).nodes === "object"
145
+ );
123
146
  }
124
147
 
125
148
  function safelyUpdateUi(ctx: ExtensionContext, update: () => void): void {
126
149
  try {
127
150
  if (ctx.hasUI) update();
128
151
  } catch {
129
- // A session replacement can make a captured context stale between polls.
152
+ // A session replacement can make a captured context stale between updates.
130
153
  }
131
154
  }
@@ -1,5 +1,6 @@
1
1
  import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
2
2
  import { truncateToWidth } from "@earendil-works/pi-tui";
3
+ import type { WorkflowDisplayStatus } from "../client/view.js";
3
4
  import { formatDuration } from "../render/format.js";
4
5
  import { nodeTypeGlyph } from "../render/node-type.js";
5
6
  import {
@@ -14,17 +15,19 @@ import { sanitizeText } from "../workflows/text.js";
14
15
  import type {
15
16
  WorkflowDefinitionSnapshot,
16
17
  WorkflowRunState,
17
- WorkflowRunStatus,
18
18
  WorkflowUpdateRecord,
19
19
  } from "../workflows/types.js";
20
20
 
21
- const STATUS_GLYPHS: Record<WorkflowRunStatus, string> = {
21
+ const STATUS_GLYPHS: Record<WorkflowDisplayStatus, string> = {
22
+ queued: "…",
22
23
  running: "◐",
23
- waiting: "",
24
+ waiting: "",
25
+ paused: "⏸",
24
26
  completed: "✓",
25
27
  failed: "✗",
26
28
  timed_out: "✗",
27
29
  cancelled: "✗",
30
+ ambiguous: "!",
28
31
  };
29
32
 
30
33
  /**
@@ -79,26 +82,28 @@ export function buildWidgetView(
79
82
  theme?: WidgetTheme,
80
83
  updateHistory?: WorkflowUpdateRecord[],
81
84
  actionHint?: string,
85
+ displayStatus?: WorkflowDisplayStatus,
82
86
  ): WidgetView {
83
87
  const availableWidth = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : width;
84
88
  if (availableWidth === 0) return { lines: [], scroll: 0, maxScroll: 0 };
85
89
 
86
90
  // `held` covers pauses the state cannot see yet: an escape-interrupted
87
91
  // step or a pause requested while the current node is still finishing.
88
- const paused = held || state.paused === true;
89
- const glyph = paused ? "⏸" : (STATUS_GLYPHS[state.status] ?? "·");
90
- const statusText = paused ? "paused" : state.status;
92
+ const status = displayStatus ?? (held || state.paused === true ? "paused" : state.status);
93
+ const paused = status === "paused";
94
+ const glyph = STATUS_GLYPHS[status];
95
+ const statusText = status;
91
96
  // Titles, status details, and errors can carry model- or shell-controlled
92
97
  // text; never let escape sequences or newlines reach the terminal.
93
98
  const title = state.runTitle ? ` — ${sanitizeText(state.runTitle)}` : "";
94
- const headerTone = statusTone(paused ? "waiting" : state.status);
99
+ const headerTone = statusTone(status);
95
100
  const header = `${paint(theme, headerTone, glyph)} workflow ${sanitizeText(state.workflowName)}${title} ${paint(theme, headerTone, `[${statusText}]`)}`;
96
101
 
97
102
  const footer: string[] = [];
98
103
  if (state.error) {
99
104
  footer.push(paint(theme, "error", ` error: ${truncate(sanitizeText(state.error), 120)}`));
100
105
  }
101
- if (state.status === "waiting" && state.waitingOn) {
106
+ if (status === "waiting" && state.waitingOn) {
102
107
  footer.push(
103
108
  paint(theme, "warning", ` waiting on checkpoint: ${sanitizeText(state.waitingOn)}`),
104
109
  );
@@ -407,7 +412,10 @@ function statusTone(status: string): ThemeColor {
407
412
  case "cancelled":
408
413
  return "error";
409
414
  case "waiting":
415
+ case "paused":
410
416
  return "warning";
417
+ case "ambiguous":
418
+ return "error";
411
419
  case "running":
412
420
  default:
413
421
  return "accent";
@@ -1,8 +1,8 @@
1
1
  import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
2
2
  import { once } from "node:events";
3
+ import { NdjsonFrameDecoder } from "../client/protocol.js";
3
4
  import { errorMessage } from "../workflows/errors.js";
4
5
  import { HostProcessRegistry, type ProcessIdentity } from "./processes.js";
5
- import { NdjsonFrameDecoder } from "./protocol.js";
6
6
  import type { WorkerOutcome } from "./state.js";
7
7
 
8
8
  const DEFAULT_STARTUP_TIMEOUT_MS = 15_000;
@@ -3,6 +3,12 @@ import { createHash } from "node:crypto";
3
3
  import fs from "node:fs/promises";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { builtinWorkflowCatalog } from "../builtins/catalog.js";
6
+ import { MAX_PROTOCOL_MESSAGE_BYTES } from "../client/protocol.js";
7
+ import type {
8
+ ResolvedControllerInitialization,
9
+ ResolvedSettingsChange,
10
+ ResolvedWorkflowLaunch,
11
+ } from "../client/resolver.js";
6
12
  import { discoverControllers, loadControllerFile } from "../controllers/loader.js";
7
13
  import { canonicalJson, parseJson, type JsonValue } from "../state/json.js";
8
14
  import { compositionMetadata } from "../workflows/composition.js";
@@ -12,32 +18,11 @@ import { resolveWorkflowRef } from "../workflows/loader.js";
12
18
  import { applyWorkflowSettingsPatch } from "../workflows/settings.js";
13
19
  import { createDefinitionSnapshot } from "../workflows/store.js";
14
20
  import type { WorkflowDefinition, WorkflowSource } from "../workflows/types.js";
15
- import { MAX_PROTOCOL_MESSAGE_BYTES } from "./protocol.js";
16
-
17
- export type ResolvedWorkflowLaunch = {
18
- schema: "pi-workflows.resolved-launch.v1";
19
- workflowName: string;
20
- workflowSourceRef: string;
21
- workflowSource: JsonValue;
22
- definitionDigest: string;
23
- definitionSnapshot: JsonValue;
24
- };
25
-
26
- export type ResolvedControllerInitialization = {
27
- schema: "pi-workflows.resolved-controller-initialization.v1";
28
- controllerName: string;
29
- controllerPath: string;
30
- sourceHash: string;
31
- initialStatus: JsonValue;
32
- };
33
-
34
- export type ResolvedSettingsChange = {
35
- schema: "pi-workflows.resolved-settings-change.v1";
36
- definitionDigest: string;
37
- patch: JsonValue;
38
- settings: JsonValue;
39
- paths: JsonValue;
40
- };
21
+ export type {
22
+ ResolvedControllerInitialization,
23
+ ResolvedSettingsChange,
24
+ ResolvedWorkflowLaunch,
25
+ } from "../client/resolver.js";
41
26
 
42
27
  type ResolverRequest = {
43
28
  schema: "pi-workflows.resolve-request.v1";