@osolmaz/pi-workflows 0.15.2 → 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 (124) 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 +64 -0
  24. package/dist/controllers/sqlite.js +219 -3
  25. package/dist/controllers/sqlite.js.map +1 -1
  26. package/dist/extension/index.d.ts +1 -0
  27. package/dist/extension/index.js +384 -156
  28. package/dist/extension/index.js.map +1 -1
  29. package/dist/extension/session-delivery.d.ts +6 -0
  30. package/dist/extension/session-delivery.js +80 -25
  31. package/dist/extension/session-delivery.js.map +1 -1
  32. package/dist/extension/session-view.d.ts +21 -0
  33. package/dist/extension/session-view.js +127 -0
  34. package/dist/extension/session-view.js.map +1 -0
  35. package/dist/extension/widget.d.ts +2 -1
  36. package/dist/extension/widget.js +15 -7
  37. package/dist/extension/widget.js.map +1 -1
  38. package/dist/host/child-worker-supervisor.js +1 -1
  39. package/dist/host/child-worker-supervisor.js.map +1 -1
  40. package/dist/host/resolver-entry.d.ts +2 -23
  41. package/dist/host/resolver-entry.js +1 -1
  42. package/dist/host/resolver-entry.js.map +1 -1
  43. package/dist/host/runner.d.ts +12 -0
  44. package/dist/host/runner.js +565 -43
  45. package/dist/host/runner.js.map +1 -1
  46. package/dist/host/state.d.ts +8 -3
  47. package/dist/host/state.js +59 -27
  48. package/dist/host/state.js.map +1 -1
  49. package/dist/host/view.d.ts +73 -0
  50. package/dist/host/view.js +871 -0
  51. package/dist/host/view.js.map +1 -0
  52. package/dist/host/worker-protocol.js +1 -1
  53. package/dist/host/worker-protocol.js.map +1 -1
  54. package/dist/state/database.d.ts +1 -0
  55. package/dist/state/database.js +15 -0
  56. package/dist/state/database.js.map +1 -1
  57. package/dist/state/prune.d.ts +3 -1
  58. package/dist/state/prune.js +6 -8
  59. package/dist/state/prune.js.map +1 -1
  60. package/dist/state/schema.js +12 -1
  61. package/dist/state/schema.js.map +1 -1
  62. package/dist/viewer/backup.d.ts +2 -0
  63. package/dist/viewer/backup.js +28 -0
  64. package/dist/viewer/backup.js.map +1 -0
  65. package/dist/viewer/cli.d.ts +4 -0
  66. package/dist/viewer/cli.js +150 -170
  67. package/dist/viewer/cli.js.map +1 -1
  68. package/dist/viewer/tui.d.ts +5 -7
  69. package/dist/viewer/tui.js +188 -108
  70. package/dist/viewer/tui.js.map +1 -1
  71. package/dist/workflows/store.d.ts +62 -1
  72. package/dist/workflows/store.js +350 -44
  73. package/dist/workflows/store.js.map +1 -1
  74. package/docs/2026-09-01-restore-session-delivery-controls-plan.md +139 -0
  75. package/docs/2026-09-01-unified-workflow-client-plan.md +381 -0
  76. package/docs/2026-09-02-installed-live-e2e-plan.md +225 -0
  77. package/docs/SQLITE_STATE.md +13 -11
  78. package/docs/WORKFLOW_HOST.md +81 -64
  79. package/docs/WORKFLOW_STEP_MESSAGES.md +4 -4
  80. package/docs/development.md +2 -1
  81. package/docs/live-replay-protocol.md +70 -132
  82. package/docs/tui-viewer.md +10 -14
  83. package/docs/workflows.md +56 -3
  84. package/herdr-plugin.toml +1 -1
  85. package/package.json +9 -3
  86. package/protocol/client.v1.schema.json +137 -0
  87. package/protocol/fixtures/client-v1.json +23 -0
  88. package/src/client/activity.ts +6 -0
  89. package/src/client/client.ts +935 -0
  90. package/src/client/index.ts +24 -0
  91. package/src/client/materialize.ts +228 -0
  92. package/src/client/protocol.ts +327 -0
  93. package/src/client/resolver.ts +26 -0
  94. package/src/client/view.ts +138 -0
  95. package/src/controllers/sqlite.ts +342 -3
  96. package/src/extension/index.ts +482 -171
  97. package/src/extension/session-delivery.ts +88 -25
  98. package/src/extension/session-view.ts +154 -0
  99. package/src/extension/widget.ts +18 -9
  100. package/src/host/child-worker-supervisor.ts +1 -1
  101. package/src/host/resolver-entry.ts +11 -26
  102. package/src/host/runner.ts +749 -75
  103. package/src/host/state.ts +82 -44
  104. package/src/host/view.ts +1084 -0
  105. package/src/host/worker-protocol.ts +1 -1
  106. package/src/state/database.ts +13 -0
  107. package/src/state/prune.ts +11 -11
  108. package/src/state/schema.ts +12 -1
  109. package/src/viewer/backup.ts +29 -0
  110. package/src/viewer/cli.ts +171 -185
  111. package/src/viewer/tui.ts +196 -124
  112. package/src/workflows/store.ts +500 -45
  113. package/dist/host/client.d.ts +0 -48
  114. package/dist/host/client.js +0 -216
  115. package/dist/host/client.js.map +0 -1
  116. package/dist/host/protocol.d.ts +0 -38
  117. package/dist/host/protocol.js +0 -156
  118. package/dist/host/protocol.js.map +0 -1
  119. package/dist/viewer/watch.d.ts +0 -6
  120. package/dist/viewer/watch.js +0 -46
  121. package/dist/viewer/watch.js.map +0 -1
  122. package/src/host/client.ts +0 -293
  123. package/src/host/protocol.ts +0 -196
  124. package/src/viewer/watch.ts +0 -51
@@ -0,0 +1,24 @@
1
+ export {
2
+ WorkflowClient,
3
+ WorkflowClientVersionError,
4
+ type WorkflowClientOptions,
5
+ } from "./client.js";
6
+ export {
7
+ CLIENT_PROTOCOL_SCHEMA,
8
+ CLIENT_PROTOCOL_VERSION,
9
+ clientSocketPath,
10
+ type ClientEvent,
11
+ type ClientHello,
12
+ type ClientOperation,
13
+ type ClientOutcome,
14
+ type ClientRequest,
15
+ type ClientResponse,
16
+ } from "./protocol.js";
17
+ export type {
18
+ OriginActivityReport,
19
+ WorkflowDisplay,
20
+ WorkflowDisplayStatus,
21
+ WorkflowRunSummary,
22
+ WorkflowRunView,
23
+ WorkflowSessionView,
24
+ } from "./view.js";
@@ -0,0 +1,228 @@
1
+ import type { JsonValue } from "../state/json.js";
2
+ import type { WorkflowClient } from "./client.js";
3
+ import type { WorkflowSessionView } from "./view.js";
4
+
5
+ type ViewerPageKind =
6
+ | "steps"
7
+ | "trace"
8
+ | "session_entries"
9
+ | "session_events"
10
+ | "settings"
11
+ | "follow_ups"
12
+ | "updates";
13
+
14
+ type ViewerPage = {
15
+ start: number;
16
+ total: number;
17
+ items: JsonValue[];
18
+ };
19
+
20
+ /** Assemble one complete run revision and hydrate every referenced value. */
21
+ export async function materializeRunView(
22
+ client: WorkflowClient,
23
+ value: JsonValue,
24
+ ): Promise<JsonValue> {
25
+ if (
26
+ !isRecord(value) ||
27
+ value.schema !== "pi-workflows.run-view.v1" ||
28
+ typeof value.runId !== "string" ||
29
+ !Number.isSafeInteger(value.revision)
30
+ ) {
31
+ return value;
32
+ }
33
+ const view = structuredClone(value);
34
+ const runId = value.runId;
35
+ const revision = value.revision as number;
36
+ const state = isRecord(view.state) ? view.state : {};
37
+ view.state = state;
38
+ const initialSteps = Array.isArray(state.steps) ? state.steps : [];
39
+ const steps = await completePage(client, runId, revision, "steps", {
40
+ start: safeInteger(view.stepStart, 0),
41
+ total: safeInteger(view.stepTotal, initialSteps.length),
42
+ items: initialSteps,
43
+ });
44
+ state.steps = steps;
45
+ view.stepStart = 0;
46
+ view.stepTotal = steps.length;
47
+
48
+ const tracePage = pageValue(view.tracePage);
49
+ const trace = await completePage(client, runId, revision, "trace", tracePage);
50
+ view.tracePage = { ...(isRecord(view.tracePage) ? view.tracePage : {}), start: 0, items: trace };
51
+
52
+ const session = isRecord(view.session) ? view.session : {};
53
+ view.session = session;
54
+ const entryPage = pageValue(session.entryPage);
55
+ const entries = await completePage(client, runId, revision, "session_entries", entryPage);
56
+ session.entryPage = {
57
+ ...(isRecord(session.entryPage) ? session.entryPage : {}),
58
+ start: 0,
59
+ items: entries,
60
+ };
61
+ const eventPage = pageValue(session.eventPage);
62
+ const events = await completePage(client, runId, revision, "session_events", eventPage);
63
+ session.eventPage = {
64
+ ...(isRecord(session.eventPage) ? session.eventPage : {}),
65
+ start: 0,
66
+ items: events,
67
+ };
68
+
69
+ const initialSettings = Array.isArray(view.settingsScopes) ? view.settingsScopes : [];
70
+ const settings = await completePage(client, runId, revision, "settings", {
71
+ start: safeInteger(view.settingsStart, 0),
72
+ total: safeInteger(view.settingsTotal, initialSettings.length),
73
+ items: initialSettings,
74
+ });
75
+ view.settingsScopes = settings;
76
+ view.settingsStart = 0;
77
+ view.settingsTotal = settings.length;
78
+
79
+ const followUpQueue = isRecord(view.followUpQueue) ? view.followUpQueue : {};
80
+ const initialFollowUps = Array.isArray(followUpQueue.items) ? followUpQueue.items : [];
81
+ const followUps = await completePage(client, runId, revision, "follow_ups", {
82
+ start: safeInteger(view.followUpStart, 0),
83
+ total: safeInteger(view.followUpTotal, initialFollowUps.length),
84
+ items: initialFollowUps,
85
+ });
86
+ followUpQueue.items = followUps;
87
+ view.followUpQueue = followUpQueue;
88
+ view.followUpStart = 0;
89
+ view.followUpTotal = followUps.length;
90
+
91
+ const initialUpdates = Array.isArray(view.updates) ? view.updates : [];
92
+ const updates = await completePage(client, runId, revision, "updates", {
93
+ start: safeInteger(view.updateStart, 0),
94
+ total: safeInteger(view.updateTotal, initialUpdates.length),
95
+ items: initialUpdates,
96
+ });
97
+ view.updates = updates;
98
+ state.updates = updates;
99
+ view.updateStart = 0;
100
+ view.updateTotal = updates.length;
101
+
102
+ const hydrated = await client.hydrateContent(runId, view);
103
+ if (!isRecord(hydrated)) return hydrated;
104
+ const workflow = isRecord(hydrated.workflow) ? hydrated.workflow : null;
105
+ if (workflow !== null && isDefinitionSnapshot(workflow.content)) {
106
+ hydrated.workflow = workflow.content;
107
+ }
108
+ materializeGraphHistory(hydrated);
109
+ return hydrated;
110
+ }
111
+
112
+ /** Assemble and hydrate the one session view consumed by the Pi extension. */
113
+ export async function materializeSessionView(
114
+ client: WorkflowClient,
115
+ session: WorkflowSessionView,
116
+ ): Promise<WorkflowSessionView> {
117
+ const run =
118
+ session.run === null
119
+ ? null
120
+ : ((await materializeRunView(client, session.run as unknown as JsonValue)) as unknown as
121
+ | WorkflowSessionView["run"]
122
+ | null);
123
+ const pendingInteractions = await Promise.all(
124
+ session.pendingInteractions.map(async (interaction) => {
125
+ const runId = isRecord(interaction) ? interaction.runId : undefined;
126
+ return typeof runId === "string"
127
+ ? await client.hydrateContent(runId, interaction)
128
+ : interaction;
129
+ }),
130
+ );
131
+ return { ...session, run, pendingInteractions };
132
+ }
133
+
134
+ async function completePage(
135
+ client: WorkflowClient,
136
+ runId: string,
137
+ revision: number,
138
+ kind: ViewerPageKind,
139
+ initial: ViewerPage,
140
+ ): Promise<JsonValue[]> {
141
+ if (
142
+ initial.start < 0 ||
143
+ initial.total < 0 ||
144
+ initial.start + initial.items.length > initial.total
145
+ ) {
146
+ throw new Error(`Workflow ${kind} page is invalid`);
147
+ }
148
+ const items = Array<JsonValue | undefined>(initial.total).fill(undefined);
149
+ for (const [offset, item] of initial.items.entries()) items[initial.start + offset] = item;
150
+ for (;;) {
151
+ const cursor = items.findIndex((item) => item === undefined);
152
+ if (cursor < 0) return items as JsonValue[];
153
+ const response = await client.request({
154
+ operation: "view.page",
155
+ runId,
156
+ expectedRevision: revision,
157
+ payload: { kind, cursor },
158
+ });
159
+ if (response.outcome !== "accepted" || !isRecord(response.receipt)) {
160
+ throw new Error(response.error ?? `Workflow ${kind} page is unavailable`);
161
+ }
162
+ const page = response.receipt;
163
+ if (
164
+ page.schema !== "pi-workflows.run-page.v1" ||
165
+ page.runId !== runId ||
166
+ page.revision !== revision ||
167
+ page.kind !== kind ||
168
+ page.cursor !== cursor ||
169
+ !Number.isSafeInteger(page.start) ||
170
+ page.total !== initial.total ||
171
+ !Array.isArray(page.items)
172
+ ) {
173
+ throw new Error(`Workflow ${kind} page changed while loading`);
174
+ }
175
+ const start = page.start as number;
176
+ if (start < 0 || start + page.items.length > initial.total) {
177
+ throw new Error(`Workflow ${kind} page is outside its history`);
178
+ }
179
+ let added = 0;
180
+ for (const [offset, item] of page.items.entries()) {
181
+ const index = start + offset;
182
+ if (items[index] === undefined) added += 1;
183
+ items[index] = item;
184
+ }
185
+ if (added === 0) throw new Error(`Workflow ${kind} page made no progress`);
186
+ }
187
+ }
188
+
189
+ function materializeGraphHistory(view: Record<string, JsonValue>): void {
190
+ const history = isRecord(view.graphHistory) ? view.graphHistory : undefined;
191
+ if (
192
+ history === undefined ||
193
+ !Array.isArray(history.steps) ||
194
+ !Array.isArray(history.transitions) ||
195
+ !history.transitions.every((transition) => typeof transition === "string")
196
+ ) {
197
+ throw new Error("Workflow graph history is invalid");
198
+ }
199
+ view.graphSteps = history.steps;
200
+ view.graphStepStart = 0;
201
+ view.graphStepTotal = history.steps.length;
202
+ view.takenTransitions = history.transitions;
203
+ view.takenTransitionStart = 0;
204
+ view.takenTransitionTotal = history.transitions.length;
205
+ }
206
+
207
+ function pageValue(value: JsonValue | undefined): ViewerPage {
208
+ const record = isRecord(value) ? value : {};
209
+ const items = Array.isArray(record.items) ? record.items : [];
210
+ const start = safeInteger(record.start, 0);
211
+ return {
212
+ start,
213
+ total: safeInteger(record.total, start + items.length),
214
+ items,
215
+ };
216
+ }
217
+
218
+ function safeInteger(value: JsonValue | undefined, fallback: number): number {
219
+ return Number.isSafeInteger(value) && (value as number) >= 0 ? (value as number) : fallback;
220
+ }
221
+
222
+ function isDefinitionSnapshot(value: JsonValue | undefined): value is Record<string, JsonValue> {
223
+ return isRecord(value) && value.schema === "pi-workflows.definition-snapshot.v1";
224
+ }
225
+
226
+ function isRecord(value: unknown): value is Record<string, JsonValue> {
227
+ return typeof value === "object" && value !== null && !Array.isArray(value);
228
+ }
@@ -0,0 +1,327 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+ import { canonicalJson, parseJson, type JsonValue } from "../state/json.js";
4
+
5
+ export const CLIENT_PROTOCOL_VERSION = 1;
6
+ export const CLIENT_PROTOCOL_SCHEMA = "pi-workflows.client.v1" as const;
7
+ export const MAX_PROTOCOL_MESSAGE_BYTES = 1024 * 1024;
8
+
9
+ export const CLIENT_OPERATIONS = [
10
+ "run.start",
11
+ "run.pause",
12
+ "run.resume",
13
+ "run.cancel",
14
+ "run.status",
15
+ "run.list",
16
+ "checkpoint.answer",
17
+ "decision.answer",
18
+ "interaction.submit",
19
+ "interaction.update",
20
+ "notification.claim",
21
+ "notification.deliver",
22
+ "turn.claim",
23
+ "turn.resolve",
24
+ "controller.list",
25
+ "controller.get",
26
+ "controller.apply",
27
+ "controller.reconcile",
28
+ "controller.delete",
29
+ "host.status",
30
+ "host.stop",
31
+ "view.runs.watch",
32
+ "view.runs.page",
33
+ "view.run.get",
34
+ "view.run.watch",
35
+ "view.run.unwatch",
36
+ "view.page",
37
+ "view.content",
38
+ "view.session.watch",
39
+ "activity.report",
40
+ "state.status",
41
+ "state.verify",
42
+ "state.backup",
43
+ "state.prune",
44
+ ] as const;
45
+
46
+ export const CLIENT_OUTCOMES = [
47
+ "accepted",
48
+ "adopted",
49
+ "rejected",
50
+ "conflict",
51
+ "notFound",
52
+ "claimLost",
53
+ "unavailable",
54
+ ] as const;
55
+
56
+ export const CLIENT_EVENTS = [
57
+ "runs",
58
+ "run_snapshot",
59
+ "run_patch",
60
+ "run_page",
61
+ "session_snapshot",
62
+ "unavailable",
63
+ ] as const;
64
+
65
+ export type ClientOperation = (typeof CLIENT_OPERATIONS)[number];
66
+ export type ClientOutcome = (typeof CLIENT_OUTCOMES)[number];
67
+ export type ClientEventName = (typeof CLIENT_EVENTS)[number];
68
+
69
+ export type ClientHello = {
70
+ schema: typeof CLIENT_PROTOCOL_SCHEMA;
71
+ type: "hello";
72
+ connectionId: string;
73
+ packageVersion: string;
74
+ };
75
+
76
+ export type ClientRequest = {
77
+ schema: typeof CLIENT_PROTOCOL_SCHEMA;
78
+ type: "request";
79
+ requestId: string;
80
+ clientId: string;
81
+ operation: ClientOperation;
82
+ idempotencyKey: string;
83
+ runId?: string;
84
+ expectedRevision?: number;
85
+ payload: JsonValue;
86
+ };
87
+
88
+ export type ClientResponse = {
89
+ schema: typeof CLIENT_PROTOCOL_SCHEMA;
90
+ type: "response";
91
+ requestId: string;
92
+ outcome: ClientOutcome;
93
+ revision?: number;
94
+ receipt?: JsonValue;
95
+ error?: string;
96
+ };
97
+
98
+ export type ClientEvent = {
99
+ schema: typeof CLIENT_PROTOCOL_SCHEMA;
100
+ type: "event";
101
+ subscriptionId: string;
102
+ event: ClientEventName;
103
+ revision?: number;
104
+ runId?: string;
105
+ payload: JsonValue;
106
+ };
107
+
108
+ export type ClientMessage = ClientHello | ClientRequest | ClientResponse | ClientEvent;
109
+
110
+ export class ClientProtocolError extends Error {
111
+ constructor(message: string) {
112
+ super(message);
113
+ this.name = "ClientProtocolError";
114
+ }
115
+ }
116
+
117
+ export function encodeProtocolMessage(message: ClientMessage): Buffer {
118
+ const encoded = Buffer.from(canonicalJson(message), "utf8");
119
+ if (encoded.byteLength > MAX_PROTOCOL_MESSAGE_BYTES) {
120
+ throw new ClientProtocolError("Client protocol message exceeds 1 MiB");
121
+ }
122
+ return encoded;
123
+ }
124
+
125
+ export function encodeProtocolLine(message: ClientMessage): Buffer {
126
+ const encoded = Buffer.concat([encodeProtocolMessage(message), Buffer.from("\n")]);
127
+ if (encoded.byteLength > MAX_PROTOCOL_MESSAGE_BYTES) {
128
+ throw new ClientProtocolError("Client protocol message exceeds 1 MiB");
129
+ }
130
+ return encoded;
131
+ }
132
+
133
+ export function parseClientMessage(line: string | Buffer): ClientMessage {
134
+ const value = parseProtocolValue(line);
135
+ if (value.schema !== CLIENT_PROTOCOL_SCHEMA) {
136
+ throw new ClientProtocolError("Invalid client protocol schema");
137
+ }
138
+ switch (value.type) {
139
+ case "hello":
140
+ return parseHello(value);
141
+ case "request":
142
+ return parseRequestValue(value);
143
+ case "response":
144
+ return parseResponseValue(value);
145
+ case "event":
146
+ return parseEventValue(value);
147
+ default:
148
+ throw new ClientProtocolError("Invalid client protocol message type");
149
+ }
150
+ }
151
+
152
+ export function parseClientRequest(line: string | Buffer): ClientRequest {
153
+ const message = parseClientMessage(line);
154
+ if (message.type !== "request") throw new ClientProtocolError("Expected client request");
155
+ return message;
156
+ }
157
+
158
+ export function parseClientResponse(line: string | Buffer): ClientResponse {
159
+ const message = parseClientMessage(line);
160
+ if (message.type !== "response") throw new ClientProtocolError("Expected client response");
161
+ return message;
162
+ }
163
+
164
+ export function clientRequestFingerprint(request: ClientRequest): Buffer {
165
+ return createHash("sha256")
166
+ .update(canonicalJson({ ...request, requestId: undefined }))
167
+ .digest();
168
+ }
169
+
170
+ export function clientSocketPath(databasePath: string): string {
171
+ const stateDirectory = path.dirname(path.resolve(databasePath));
172
+ if (process.platform === "win32") {
173
+ const suffix = createHash("sha256").update(stateDirectory).digest("hex").slice(0, 24);
174
+ return `\\\\.\\pipe\\pi-workflows-${suffix}`;
175
+ }
176
+ return path.join(stateDirectory, "host", "host.sock");
177
+ }
178
+
179
+ export class NdjsonFrameDecoder {
180
+ private buffered = Buffer.alloc(0);
181
+
182
+ push(chunk: Buffer): Buffer[] {
183
+ this.buffered = Buffer.concat([this.buffered, chunk]);
184
+ if (this.buffered.byteLength > MAX_PROTOCOL_MESSAGE_BYTES && !this.buffered.includes(0x0a)) {
185
+ throw new ClientProtocolError("Client protocol message exceeds 1 MiB");
186
+ }
187
+ const frames: Buffer[] = [];
188
+ for (;;) {
189
+ const newline = this.buffered.indexOf(0x0a);
190
+ if (newline < 0) break;
191
+ const frame = this.buffered.subarray(0, newline);
192
+ this.buffered = this.buffered.subarray(newline + 1);
193
+ if (frame.byteLength === 0) continue;
194
+ if (frame.byteLength + 1 > MAX_PROTOCOL_MESSAGE_BYTES) {
195
+ throw new ClientProtocolError("Client protocol message exceeds 1 MiB");
196
+ }
197
+ frames.push(frame);
198
+ }
199
+ return frames;
200
+ }
201
+ }
202
+
203
+ function parseHello(value: Record<string, unknown>): ClientHello {
204
+ requireExactKeys(value, ["schema", "type", "connectionId", "packageVersion"]);
205
+ requireOpaqueId(value.connectionId, "connectionId");
206
+ requireText(value.packageVersion, "packageVersion");
207
+ return value as ClientHello;
208
+ }
209
+
210
+ function parseRequestValue(value: Record<string, unknown>): ClientRequest {
211
+ requireExactKeys(value, [
212
+ "schema",
213
+ "type",
214
+ "requestId",
215
+ "clientId",
216
+ "operation",
217
+ "idempotencyKey",
218
+ "runId",
219
+ "expectedRevision",
220
+ "payload",
221
+ ]);
222
+ requireOpaqueId(value.requestId, "requestId");
223
+ requireOpaqueId(value.clientId, "clientId");
224
+ requireOpaqueId(value.idempotencyKey, "idempotencyKey");
225
+ if (!CLIENT_OPERATIONS.includes(value.operation as ClientOperation)) {
226
+ throw new ClientProtocolError("Invalid client request operation");
227
+ }
228
+ if (value.runId !== undefined) requireOpaqueId(value.runId, "runId");
229
+ requireOptionalRevision(value.expectedRevision, "request expectedRevision");
230
+ if (!Object.hasOwn(value, "payload")) {
231
+ throw new ClientProtocolError("Client request payload is required");
232
+ }
233
+ canonicalJson(value.payload);
234
+ return value as ClientRequest;
235
+ }
236
+
237
+ function parseResponseValue(value: Record<string, unknown>): ClientResponse {
238
+ requireExactKeys(value, [
239
+ "schema",
240
+ "type",
241
+ "requestId",
242
+ "outcome",
243
+ "revision",
244
+ "receipt",
245
+ "error",
246
+ ]);
247
+ requireOpaqueId(value.requestId, "requestId");
248
+ if (!CLIENT_OUTCOMES.includes(value.outcome as ClientOutcome)) {
249
+ throw new ClientProtocolError("Invalid client response outcome");
250
+ }
251
+ requireOptionalRevision(value.revision, "response revision");
252
+ if (value.error !== undefined) requireText(value.error, "response error");
253
+ if (value.receipt !== undefined) canonicalJson(value.receipt);
254
+ return value as ClientResponse;
255
+ }
256
+
257
+ function parseEventValue(value: Record<string, unknown>): ClientEvent {
258
+ requireExactKeys(value, [
259
+ "schema",
260
+ "type",
261
+ "subscriptionId",
262
+ "event",
263
+ "revision",
264
+ "runId",
265
+ "payload",
266
+ ]);
267
+ requireOpaqueId(value.subscriptionId, "subscriptionId");
268
+ if (!CLIENT_EVENTS.includes(value.event as ClientEventName)) {
269
+ throw new ClientProtocolError("Invalid client event name");
270
+ }
271
+ requireOptionalRevision(value.revision, "event revision");
272
+ if (value.runId !== undefined) requireOpaqueId(value.runId, "runId");
273
+ if (!Object.hasOwn(value, "payload")) {
274
+ throw new ClientProtocolError("Client event payload is required");
275
+ }
276
+ canonicalJson(value.payload);
277
+ return value as ClientEvent;
278
+ }
279
+
280
+ function parseProtocolValue(line: string | Buffer): Record<string, unknown> {
281
+ const buffer = Buffer.isBuffer(line) ? line : Buffer.from(line, "utf8");
282
+ if (buffer.byteLength > MAX_PROTOCOL_MESSAGE_BYTES) {
283
+ throw new ClientProtocolError("Client protocol message exceeds 1 MiB");
284
+ }
285
+ let value: unknown;
286
+ try {
287
+ value = parseJson(buffer.toString("utf8"));
288
+ } catch {
289
+ throw new ClientProtocolError("Client protocol message is not valid canonical JSON");
290
+ }
291
+ if (!isRecord(value)) throw new ClientProtocolError("Client protocol message must be an object");
292
+ if (canonicalJson(value) !== buffer.toString("utf8")) {
293
+ throw new ClientProtocolError("Client protocol message must use canonical JSON");
294
+ }
295
+ return value;
296
+ }
297
+
298
+ function requireExactKeys(value: Record<string, unknown>, allowed: readonly string[]): void {
299
+ const allowedKeys = new Set(allowed);
300
+ for (const key of Object.keys(value)) {
301
+ if (!allowedKeys.has(key)) {
302
+ throw new ClientProtocolError(`Unknown client protocol field: ${key}`);
303
+ }
304
+ }
305
+ }
306
+
307
+ function requireOpaqueId(value: unknown, field: string): asserts value is string {
308
+ if (typeof value !== "string" || value.length === 0 || Buffer.byteLength(value) > 256) {
309
+ throw new ClientProtocolError(`${field} must be nonempty text of at most 256 bytes`);
310
+ }
311
+ }
312
+
313
+ function requireText(value: unknown, field: string): asserts value is string {
314
+ if (typeof value !== "string" || value.length === 0) {
315
+ throw new ClientProtocolError(`${field} must be nonempty text`);
316
+ }
317
+ }
318
+
319
+ function requireOptionalRevision(value: unknown, field: string): void {
320
+ if (value !== undefined && (!Number.isSafeInteger(value) || (value as number) < 0)) {
321
+ throw new ClientProtocolError(`${field} must be a non-negative integer`);
322
+ }
323
+ }
324
+
325
+ function isRecord(value: unknown): value is Record<string, unknown> {
326
+ return typeof value === "object" && value !== null && !Array.isArray(value);
327
+ }
@@ -0,0 +1,26 @@
1
+ import type { JsonValue } from "../state/json.js";
2
+
3
+ export type ResolvedWorkflowLaunch = {
4
+ schema: "pi-workflows.resolved-launch.v1";
5
+ workflowName: string;
6
+ workflowSourceRef: string;
7
+ workflowSource: JsonValue;
8
+ definitionDigest: string;
9
+ definitionSnapshot: JsonValue;
10
+ };
11
+
12
+ export type ResolvedControllerInitialization = {
13
+ schema: "pi-workflows.resolved-controller-initialization.v1";
14
+ controllerName: string;
15
+ controllerPath: string;
16
+ sourceHash: string;
17
+ initialStatus: JsonValue;
18
+ };
19
+
20
+ export type ResolvedSettingsChange = {
21
+ schema: "pi-workflows.resolved-settings-change.v1";
22
+ definitionDigest: string;
23
+ patch: JsonValue;
24
+ settings: JsonValue;
25
+ paths: JsonValue;
26
+ };