@overmux/pi 0.0.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.
- package/README.md +118 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +40 -0
- package/dist/cli.js.map +1 -0
- package/dist/config-2jNv4tol.d.ts +34 -0
- package/dist/config-2jNv4tol.d.ts.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +47 -0
- package/dist/config.js.map +1 -0
- package/dist/extension.d.ts +19 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +265 -0
- package/dist/extension.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/jsonl-tail.d.ts +13 -0
- package/dist/jsonl-tail.d.ts.map +1 -0
- package/dist/jsonl-tail.js +83 -0
- package/dist/jsonl-tail.js.map +1 -0
- package/dist/live-events-ClhFOMGW.js +497 -0
- package/dist/live-events-ClhFOMGW.js.map +1 -0
- package/dist/live-events-DAmf6RRx.d.ts +107 -0
- package/dist/live-events-DAmf6RRx.d.ts.map +1 -0
- package/dist/live-events.d.ts +2 -0
- package/dist/live-events.js +2 -0
- package/dist/notification-DzOd9cRc.d.ts +20 -0
- package/dist/notification-DzOd9cRc.d.ts.map +1 -0
- package/dist/notification.d.ts +2 -0
- package/dist/notification.js +54 -0
- package/dist/notification.js.map +1 -0
- package/dist/plugin.d.ts +90 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +213 -0
- package/dist/plugin.js.map +1 -0
- package/dist/projection.d.ts +101 -0
- package/dist/projection.d.ts.map +1 -0
- package/dist/projection.js +550 -0
- package/dist/projection.js.map +1 -0
- package/dist/protocol-CsrnSPOv.d.ts +115 -0
- package/dist/protocol-CsrnSPOv.d.ts.map +1 -0
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.js +365 -0
- package/dist/protocol.js.map +1 -0
- package/dist/react.d.ts +139 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +796 -0
- package/dist/react.js.map +1 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +4 -0
- package/dist/session-status-CZLTo8Km.d.ts +71 -0
- package/dist/session-status-CZLTo8Km.d.ts.map +1 -0
- package/dist/styles.css +619 -0
- package/docs/index.md +10 -0
- package/package.json +115 -0
- package/src/cli.ts +59 -0
- package/src/config.ts +87 -0
- package/src/extension.ts +486 -0
- package/src/index.ts +14 -0
- package/src/jsonl-tail.ts +110 -0
- package/src/live-events.ts +578 -0
- package/src/notification.ts +109 -0
- package/src/plugin.ts +369 -0
- package/src/projection.ts +995 -0
- package/src/protocol.ts +805 -0
- package/src/react.tsx +1293 -0
- package/src/server.ts +15 -0
- package/src/session-status.ts +379 -0
- package/src/styles.css +619 -0
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
import { lstat, mkdir, unlink } from "node:fs/promises";
|
|
2
|
+
import {
|
|
3
|
+
createConnection,
|
|
4
|
+
createServer,
|
|
5
|
+
type Server,
|
|
6
|
+
type Socket,
|
|
7
|
+
} from "node:net";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
export const CHILD_ENV = "PI_CHILD";
|
|
11
|
+
export const PARENT_SESSION_ENV = "PI_PARENT_SESSION_ID";
|
|
12
|
+
export const TASK_SLUG_ENV = "PI_TASK_SLUG";
|
|
13
|
+
|
|
14
|
+
const MAX_MESSAGE_BYTES = 4096;
|
|
15
|
+
const MAX_USER_MESSAGE_BYTES = 3072;
|
|
16
|
+
const ACK = "ACK";
|
|
17
|
+
const MAX_RESPONSE_BYTES = 512;
|
|
18
|
+
const SOCKET_PATH_LIMIT = 100;
|
|
19
|
+
const ACK_TIMEOUT_MS = 400;
|
|
20
|
+
const PROBE_TIMEOUT_MS = 150;
|
|
21
|
+
const INBOUND_TIMEOUT_MS = 1000;
|
|
22
|
+
const MAX_CONNECTIONS = 32;
|
|
23
|
+
const RETRY_DELAYS_MS = [0, 80, 160] as const;
|
|
24
|
+
const ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
25
|
+
export const TASK_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,11}[a-z0-9])?$/;
|
|
26
|
+
export const DEFAULT_OVERMUX_PI_RUNTIME_DIR = `/tmp/overmux-pi-${process.getuid?.() ?? "u"}`;
|
|
27
|
+
|
|
28
|
+
export type DelegateSettledEnvelope = {
|
|
29
|
+
childSessionId: string;
|
|
30
|
+
taskSlug: string;
|
|
31
|
+
leafId: string;
|
|
32
|
+
cwd: string;
|
|
33
|
+
timestamp: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type UserMessageRequest = {
|
|
37
|
+
version: 1;
|
|
38
|
+
requestId: string;
|
|
39
|
+
type: "user_message";
|
|
40
|
+
message: string;
|
|
41
|
+
deliverAs: "steer" | "followUp";
|
|
42
|
+
expandPromptTemplates: boolean;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type UserMessageResponse =
|
|
46
|
+
| {
|
|
47
|
+
version: 1;
|
|
48
|
+
requestId: string;
|
|
49
|
+
ok: true;
|
|
50
|
+
delivery: "immediate" | "steer" | "followUp";
|
|
51
|
+
}
|
|
52
|
+
| {
|
|
53
|
+
version: 1;
|
|
54
|
+
requestId: string;
|
|
55
|
+
ok: false;
|
|
56
|
+
error: "shutting_down" | "unavailable";
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type UserMessageInput = Omit<
|
|
60
|
+
UserMessageRequest,
|
|
61
|
+
"version" | "type" | "expandPromptTemplates"
|
|
62
|
+
> & {
|
|
63
|
+
expandPromptTemplates?: boolean;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type AbortRequest = {
|
|
67
|
+
version: 1;
|
|
68
|
+
requestId: string;
|
|
69
|
+
type: "abort";
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type AbortResponse =
|
|
73
|
+
| { version: 1; requestId: string; ok: true }
|
|
74
|
+
| {
|
|
75
|
+
version: 1;
|
|
76
|
+
requestId: string;
|
|
77
|
+
ok: false;
|
|
78
|
+
error: "shutting_down" | "unavailable";
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type AbortInput = Omit<AbortRequest, "version" | "type">;
|
|
82
|
+
|
|
83
|
+
type PiControlError = "no_key" | "not_found" | "shutting_down" | "unavailable";
|
|
84
|
+
type PiControlResponse =
|
|
85
|
+
| { version: 1; requestId: string; ok: true }
|
|
86
|
+
| { version: 1; requestId: string; ok: false; error: PiControlError };
|
|
87
|
+
|
|
88
|
+
export type SetModelRequest = {
|
|
89
|
+
version: 1;
|
|
90
|
+
requestId: string;
|
|
91
|
+
type: "set_model";
|
|
92
|
+
provider: string;
|
|
93
|
+
id: string;
|
|
94
|
+
};
|
|
95
|
+
export type SetModelInput = Omit<SetModelRequest, "version" | "type">;
|
|
96
|
+
export type SetModelResponse = PiControlResponse;
|
|
97
|
+
|
|
98
|
+
export type SetThinkingLevelRequest = {
|
|
99
|
+
version: 1;
|
|
100
|
+
requestId: string;
|
|
101
|
+
type: "set_thinking_level";
|
|
102
|
+
level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
103
|
+
};
|
|
104
|
+
export type SetThinkingLevelInput = Omit<
|
|
105
|
+
SetThinkingLevelRequest,
|
|
106
|
+
"version" | "type"
|
|
107
|
+
>;
|
|
108
|
+
export type SetThinkingLevelResponse = PiControlResponse;
|
|
109
|
+
|
|
110
|
+
export type Receiver = { close: () => Promise<void> };
|
|
111
|
+
export type ReceiverOptions = {
|
|
112
|
+
onAbort?: (request: AbortRequest) => AbortResponse;
|
|
113
|
+
onSetModel?: (request: SetModelRequest) => Promise<SetModelResponse>;
|
|
114
|
+
onSetThinkingLevel?: (
|
|
115
|
+
request: SetThinkingLevelRequest,
|
|
116
|
+
) => SetThinkingLevelResponse;
|
|
117
|
+
onUserMessage?: (request: UserMessageRequest) => UserMessageResponse;
|
|
118
|
+
};
|
|
119
|
+
export type SenderOptions = {
|
|
120
|
+
runtimeDir?: string;
|
|
121
|
+
timeoutMs?: number;
|
|
122
|
+
retryDelaysMs?: readonly number[];
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export type ProbeOptions = Pick<SenderOptions, "runtimeDir" | "timeoutMs">;
|
|
126
|
+
|
|
127
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
128
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
129
|
+
|
|
130
|
+
const isId = (value: unknown): value is string =>
|
|
131
|
+
typeof value === "string" && ID_PATTERN.test(value);
|
|
132
|
+
|
|
133
|
+
export const socketPathForSession = (
|
|
134
|
+
sessionId: string,
|
|
135
|
+
runtimeDir = DEFAULT_OVERMUX_PI_RUNTIME_DIR,
|
|
136
|
+
): string => {
|
|
137
|
+
if (!isId(sessionId)) {
|
|
138
|
+
throw new Error("Invalid Pi session ID for delegate notification socket");
|
|
139
|
+
}
|
|
140
|
+
const socketPath = join(runtimeDir, `${sessionId}.sock`);
|
|
141
|
+
if (Buffer.byteLength(socketPath) > SOCKET_PATH_LIMIT) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Delegate notification socket path exceeds ${SOCKET_PATH_LIMIT} bytes`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return socketPath;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const parseEnvelope = (
|
|
150
|
+
input: string,
|
|
151
|
+
): DelegateSettledEnvelope | undefined => {
|
|
152
|
+
if (Buffer.byteLength(input) > MAX_MESSAGE_BYTES) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const value: unknown = JSON.parse(input);
|
|
157
|
+
if (!isRecord(value)) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
if (
|
|
161
|
+
Object.keys(value).length !== 5 ||
|
|
162
|
+
!isId(value.childSessionId) ||
|
|
163
|
+
!isId(value.leafId)
|
|
164
|
+
) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
if (
|
|
168
|
+
typeof value.taskSlug !== "string" ||
|
|
169
|
+
!TASK_SLUG_PATTERN.test(value.taskSlug)
|
|
170
|
+
) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
if (
|
|
174
|
+
typeof value.cwd !== "string" ||
|
|
175
|
+
value.cwd.length < 1 ||
|
|
176
|
+
value.cwd.length > 2048
|
|
177
|
+
) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
if (
|
|
181
|
+
!Number.isSafeInteger(value.timestamp) ||
|
|
182
|
+
(value.timestamp as number) < 0
|
|
183
|
+
) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
return value as DelegateSettledEnvelope;
|
|
187
|
+
} catch {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export const parseAbortRequest = (input: string): AbortRequest | undefined => {
|
|
193
|
+
if (Buffer.byteLength(input) > MAX_MESSAGE_BYTES) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const value: unknown = JSON.parse(input);
|
|
198
|
+
if (
|
|
199
|
+
!isRecord(value) ||
|
|
200
|
+
Object.keys(value).length !== 3 ||
|
|
201
|
+
value.version !== 1 ||
|
|
202
|
+
value.type !== "abort" ||
|
|
203
|
+
!isId(value.requestId)
|
|
204
|
+
) {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
return value as AbortRequest;
|
|
208
|
+
} catch {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const parseSetModelRequest = (
|
|
214
|
+
input: string,
|
|
215
|
+
): SetModelRequest | undefined => {
|
|
216
|
+
try {
|
|
217
|
+
const value: unknown = JSON.parse(input);
|
|
218
|
+
if (
|
|
219
|
+
!isRecord(value) ||
|
|
220
|
+
Object.keys(value).length !== 5 ||
|
|
221
|
+
value.version !== 1 ||
|
|
222
|
+
value.type !== "set_model" ||
|
|
223
|
+
!isId(value.requestId) ||
|
|
224
|
+
typeof value.provider !== "string" ||
|
|
225
|
+
!value.provider ||
|
|
226
|
+
typeof value.id !== "string" ||
|
|
227
|
+
!value.id
|
|
228
|
+
) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
return value as SetModelRequest;
|
|
232
|
+
} catch {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export const parseSetThinkingLevelRequest = (
|
|
238
|
+
input: string,
|
|
239
|
+
): SetThinkingLevelRequest | undefined => {
|
|
240
|
+
try {
|
|
241
|
+
const value: unknown = JSON.parse(input);
|
|
242
|
+
const levels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
243
|
+
if (
|
|
244
|
+
!isRecord(value) ||
|
|
245
|
+
Object.keys(value).length !== 4 ||
|
|
246
|
+
value.version !== 1 ||
|
|
247
|
+
value.type !== "set_thinking_level" ||
|
|
248
|
+
!isId(value.requestId) ||
|
|
249
|
+
typeof value.level !== "string" ||
|
|
250
|
+
!levels.includes(value.level)
|
|
251
|
+
) {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
return value as SetThinkingLevelRequest;
|
|
255
|
+
} catch {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
export const parseUserMessageRequest = (
|
|
261
|
+
input: string,
|
|
262
|
+
): UserMessageRequest | undefined => {
|
|
263
|
+
if (Buffer.byteLength(input) > MAX_MESSAGE_BYTES) {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const value: unknown = JSON.parse(input);
|
|
268
|
+
if (!isRecord(value)) {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
const keys = Object.keys(value);
|
|
272
|
+
const allowedKeys = new Set([
|
|
273
|
+
"version",
|
|
274
|
+
"requestId",
|
|
275
|
+
"type",
|
|
276
|
+
"message",
|
|
277
|
+
"deliverAs",
|
|
278
|
+
"expandPromptTemplates",
|
|
279
|
+
]);
|
|
280
|
+
if (
|
|
281
|
+
keys.length < 5 ||
|
|
282
|
+
keys.length > 6 ||
|
|
283
|
+
keys.some((key) => !allowedKeys.has(key))
|
|
284
|
+
) {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
if (
|
|
288
|
+
value.version !== 1 ||
|
|
289
|
+
value.type !== "user_message" ||
|
|
290
|
+
!isId(value.requestId)
|
|
291
|
+
) {
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
if (typeof value.message !== "string" || !value.message.trim()) {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
if (Buffer.byteLength(value.message) > MAX_USER_MESSAGE_BYTES) {
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
if (value.deliverAs !== "steer" && value.deliverAs !== "followUp") {
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
if (
|
|
304
|
+
value.expandPromptTemplates !== undefined &&
|
|
305
|
+
typeof value.expandPromptTemplates !== "boolean"
|
|
306
|
+
) {
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
...value,
|
|
311
|
+
expandPromptTemplates: value.expandPromptTemplates ?? true,
|
|
312
|
+
} as UserMessageRequest;
|
|
313
|
+
} catch {
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const parseUserMessageResponse = (
|
|
319
|
+
input: string,
|
|
320
|
+
requestId: string,
|
|
321
|
+
): UserMessageResponse | undefined => {
|
|
322
|
+
try {
|
|
323
|
+
const value: unknown = JSON.parse(input);
|
|
324
|
+
if (
|
|
325
|
+
!isRecord(value) ||
|
|
326
|
+
value.version !== 1 ||
|
|
327
|
+
value.requestId !== requestId
|
|
328
|
+
) {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
const keys = Object.keys(value);
|
|
332
|
+
const successKeys = ["version", "requestId", "ok", "delivery"];
|
|
333
|
+
if (
|
|
334
|
+
value.ok === true &&
|
|
335
|
+
keys.length === successKeys.length &&
|
|
336
|
+
keys.every((key) => successKeys.includes(key)) &&
|
|
337
|
+
(value.delivery === "immediate" ||
|
|
338
|
+
value.delivery === "steer" ||
|
|
339
|
+
value.delivery === "followUp")
|
|
340
|
+
) {
|
|
341
|
+
return value as UserMessageResponse;
|
|
342
|
+
}
|
|
343
|
+
const errorKeys = ["version", "requestId", "ok", "error"];
|
|
344
|
+
if (
|
|
345
|
+
value.ok === false &&
|
|
346
|
+
keys.length === errorKeys.length &&
|
|
347
|
+
keys.every((key) => errorKeys.includes(key)) &&
|
|
348
|
+
(value.error === "shutting_down" || value.error === "unavailable")
|
|
349
|
+
) {
|
|
350
|
+
return value as UserMessageResponse;
|
|
351
|
+
}
|
|
352
|
+
return undefined;
|
|
353
|
+
} catch {
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
export const eventKey = ({
|
|
359
|
+
childSessionId,
|
|
360
|
+
leafId,
|
|
361
|
+
}: DelegateSettledEnvelope): string => `${childSessionId}\0${leafId}`;
|
|
362
|
+
|
|
363
|
+
const responsePayload = (
|
|
364
|
+
response:
|
|
365
|
+
| UserMessageResponse
|
|
366
|
+
| AbortResponse
|
|
367
|
+
| SetModelResponse
|
|
368
|
+
| SetThinkingLevelResponse,
|
|
369
|
+
): string => JSON.stringify(response);
|
|
370
|
+
|
|
371
|
+
const parseControlResponse = (
|
|
372
|
+
input: string,
|
|
373
|
+
requestId: string,
|
|
374
|
+
): PiControlResponse | undefined => {
|
|
375
|
+
try {
|
|
376
|
+
const value: unknown = JSON.parse(input);
|
|
377
|
+
if (
|
|
378
|
+
!isRecord(value) ||
|
|
379
|
+
value.version !== 1 ||
|
|
380
|
+
value.requestId !== requestId
|
|
381
|
+
) {
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
if (value.ok === true && Object.keys(value).length === 3) {
|
|
385
|
+
return value as PiControlResponse;
|
|
386
|
+
}
|
|
387
|
+
if (
|
|
388
|
+
value.ok === false &&
|
|
389
|
+
Object.keys(value).length === 4 &&
|
|
390
|
+
(value.error === "no_key" ||
|
|
391
|
+
value.error === "not_found" ||
|
|
392
|
+
value.error === "shutting_down" ||
|
|
393
|
+
value.error === "unavailable")
|
|
394
|
+
) {
|
|
395
|
+
return value as PiControlResponse;
|
|
396
|
+
}
|
|
397
|
+
return undefined;
|
|
398
|
+
} catch {
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const parseAbortResponse = (
|
|
404
|
+
input: string,
|
|
405
|
+
requestId: string,
|
|
406
|
+
): AbortResponse | undefined => {
|
|
407
|
+
try {
|
|
408
|
+
const value: unknown = JSON.parse(input);
|
|
409
|
+
if (
|
|
410
|
+
!isRecord(value) ||
|
|
411
|
+
value.version !== 1 ||
|
|
412
|
+
value.requestId !== requestId
|
|
413
|
+
) {
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
416
|
+
if (value.ok === true && Object.keys(value).length === 3) {
|
|
417
|
+
return value as AbortResponse;
|
|
418
|
+
}
|
|
419
|
+
if (
|
|
420
|
+
value.ok === false &&
|
|
421
|
+
Object.keys(value).length === 4 &&
|
|
422
|
+
(value.error === "shutting_down" || value.error === "unavailable")
|
|
423
|
+
) {
|
|
424
|
+
return value as AbortResponse;
|
|
425
|
+
}
|
|
426
|
+
return undefined;
|
|
427
|
+
} catch {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
export const startReceiver = async (
|
|
433
|
+
socketPath: string,
|
|
434
|
+
onEnvelope: (envelope: DelegateSettledEnvelope) => void,
|
|
435
|
+
options: ReceiverOptions | number = {},
|
|
436
|
+
timeoutMs = INBOUND_TIMEOUT_MS,
|
|
437
|
+
): Promise<Receiver> => {
|
|
438
|
+
const receiverOptions = typeof options === "number" ? {} : options;
|
|
439
|
+
const resolvedTimeoutMs = typeof options === "number" ? options : timeoutMs;
|
|
440
|
+
await removeStaleSocket(socketPath);
|
|
441
|
+
const sockets = new Set<Socket>();
|
|
442
|
+
const server = createServer({ allowHalfOpen: true }, (socket) => {
|
|
443
|
+
sockets.add(socket);
|
|
444
|
+
socket.setEncoding("utf8");
|
|
445
|
+
socket.setTimeout(resolvedTimeoutMs, () => socket.destroy());
|
|
446
|
+
let input = "";
|
|
447
|
+
socket.on("data", (chunk: string) => {
|
|
448
|
+
input += chunk;
|
|
449
|
+
if (Buffer.byteLength(input) > MAX_MESSAGE_BYTES) {
|
|
450
|
+
socket.destroy();
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
socket.on("end", () => {
|
|
454
|
+
const envelope = parseEnvelope(input);
|
|
455
|
+
if (envelope) {
|
|
456
|
+
try {
|
|
457
|
+
onEnvelope(envelope);
|
|
458
|
+
socket.end(ACK);
|
|
459
|
+
} catch {
|
|
460
|
+
socket.destroy();
|
|
461
|
+
}
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const setModelRequest = parseSetModelRequest(input);
|
|
465
|
+
if (setModelRequest && receiverOptions.onSetModel) {
|
|
466
|
+
void receiverOptions
|
|
467
|
+
.onSetModel(setModelRequest)
|
|
468
|
+
.then((response) => socket.end(responsePayload(response)))
|
|
469
|
+
.catch(() => socket.destroy());
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const setThinkingLevelRequest = parseSetThinkingLevelRequest(input);
|
|
473
|
+
if (setThinkingLevelRequest && receiverOptions.onSetThinkingLevel) {
|
|
474
|
+
try {
|
|
475
|
+
socket.end(
|
|
476
|
+
responsePayload(
|
|
477
|
+
receiverOptions.onSetThinkingLevel(setThinkingLevelRequest),
|
|
478
|
+
),
|
|
479
|
+
);
|
|
480
|
+
} catch {
|
|
481
|
+
socket.destroy();
|
|
482
|
+
}
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const abortRequest = parseAbortRequest(input);
|
|
486
|
+
if (abortRequest && receiverOptions.onAbort) {
|
|
487
|
+
try {
|
|
488
|
+
socket.end(responsePayload(receiverOptions.onAbort(abortRequest)));
|
|
489
|
+
} catch {
|
|
490
|
+
socket.destroy();
|
|
491
|
+
}
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const userMessageRequest = parseUserMessageRequest(input);
|
|
495
|
+
if (!userMessageRequest || !receiverOptions.onUserMessage) {
|
|
496
|
+
socket.destroy();
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
socket.end(
|
|
501
|
+
responsePayload(receiverOptions.onUserMessage(userMessageRequest)),
|
|
502
|
+
);
|
|
503
|
+
} catch {
|
|
504
|
+
socket.destroy();
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
socket.on("close", () => sockets.delete(socket));
|
|
508
|
+
socket.on("error", () => undefined);
|
|
509
|
+
});
|
|
510
|
+
server.maxConnections = MAX_CONNECTIONS;
|
|
511
|
+
await listen(server, socketPath);
|
|
512
|
+
let closed = false;
|
|
513
|
+
return {
|
|
514
|
+
close: async () => {
|
|
515
|
+
if (closed) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
closed = true;
|
|
519
|
+
sockets.forEach((socket) => socket.destroy());
|
|
520
|
+
await closeServer(server);
|
|
521
|
+
await unlink(socketPath).catch((error: NodeJS.ErrnoException) => {
|
|
522
|
+
if (error.code !== "ENOENT") {
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const probeSocket = (socketPath: string): Promise<"active" | "stale"> =>
|
|
531
|
+
new Promise((resolve) => {
|
|
532
|
+
const socket = createConnection(socketPath);
|
|
533
|
+
const finish = (result: "active" | "stale") => {
|
|
534
|
+
socket.destroy();
|
|
535
|
+
resolve(result);
|
|
536
|
+
};
|
|
537
|
+
socket.setTimeout(100, () => finish("active"));
|
|
538
|
+
socket.on("connect", () => finish("active"));
|
|
539
|
+
socket.on("error", (error: NodeJS.ErrnoException) =>
|
|
540
|
+
finish(
|
|
541
|
+
error.code === "ECONNREFUSED" || error.code === "ENOENT"
|
|
542
|
+
? "stale"
|
|
543
|
+
: "active",
|
|
544
|
+
),
|
|
545
|
+
);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
const removeStaleSocket = async (socketPath: string): Promise<void> => {
|
|
549
|
+
const stats = await lstat(socketPath).catch(
|
|
550
|
+
(error: NodeJS.ErrnoException) => {
|
|
551
|
+
if (error.code === "ENOENT") {
|
|
552
|
+
return undefined;
|
|
553
|
+
}
|
|
554
|
+
throw error;
|
|
555
|
+
},
|
|
556
|
+
);
|
|
557
|
+
if (!stats) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (!stats.isSocket()) {
|
|
561
|
+
throw new Error(
|
|
562
|
+
`Delegate notification path is not a socket: ${socketPath}`,
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
if ((await probeSocket(socketPath)) === "active") {
|
|
566
|
+
throw new Error(
|
|
567
|
+
`Delegate notification socket is already active: ${socketPath}`,
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
await unlink(socketPath);
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const listen = (server: Server, socketPath: string): Promise<void> =>
|
|
574
|
+
new Promise((resolve, reject) => {
|
|
575
|
+
const onError = (error: Error) => reject(error);
|
|
576
|
+
server.once("error", onError);
|
|
577
|
+
server.listen(socketPath, () => {
|
|
578
|
+
server.off("error", onError);
|
|
579
|
+
resolve();
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
const closeServer = (server: Server): Promise<void> =>
|
|
584
|
+
new Promise((resolve, reject) =>
|
|
585
|
+
server.close((error) => (error ? reject(error) : resolve())),
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
const wait = (milliseconds: number): Promise<void> =>
|
|
589
|
+
new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
590
|
+
|
|
591
|
+
const sendAttempt = (
|
|
592
|
+
socketPath: string,
|
|
593
|
+
payload: string,
|
|
594
|
+
timeoutMs: number,
|
|
595
|
+
): Promise<string | undefined> =>
|
|
596
|
+
new Promise((resolve) => {
|
|
597
|
+
const socket = createConnection(socketPath);
|
|
598
|
+
socket.setEncoding("utf8");
|
|
599
|
+
let response = "";
|
|
600
|
+
let finished = false;
|
|
601
|
+
const finish = (result: string | undefined) => {
|
|
602
|
+
if (finished) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
finished = true;
|
|
606
|
+
socket.destroy();
|
|
607
|
+
resolve(result);
|
|
608
|
+
};
|
|
609
|
+
socket.setTimeout(timeoutMs, () => finish(undefined));
|
|
610
|
+
socket.on("connect", () => socket.end(payload));
|
|
611
|
+
socket.on("data", (chunk: string) => {
|
|
612
|
+
response += chunk;
|
|
613
|
+
if (Buffer.byteLength(response) > MAX_RESPONSE_BYTES) {
|
|
614
|
+
finish(undefined);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
socket.on("end", () => finish(response));
|
|
618
|
+
socket.on("error", () => finish(undefined));
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
export const probePiSession = async (
|
|
622
|
+
sessionId: string,
|
|
623
|
+
options: ProbeOptions = {},
|
|
624
|
+
): Promise<boolean> => {
|
|
625
|
+
let socketPath: string;
|
|
626
|
+
try {
|
|
627
|
+
socketPath = socketPathForSession(sessionId, options.runtimeDir);
|
|
628
|
+
} catch {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
return (
|
|
632
|
+
(await sendAttempt(
|
|
633
|
+
socketPath,
|
|
634
|
+
"",
|
|
635
|
+
options.timeoutMs ?? PROBE_TIMEOUT_MS,
|
|
636
|
+
)) !== undefined
|
|
637
|
+
);
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
export const sendEnvelope = async (
|
|
641
|
+
socketPath: string,
|
|
642
|
+
envelope: DelegateSettledEnvelope,
|
|
643
|
+
options: { ackTimeoutMs?: number; retryDelaysMs?: readonly number[] } = {},
|
|
644
|
+
): Promise<boolean> => {
|
|
645
|
+
const payload = JSON.stringify(envelope);
|
|
646
|
+
if (!parseEnvelope(payload)) {
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
for (const delay of options.retryDelaysMs ?? RETRY_DELAYS_MS) {
|
|
650
|
+
if (delay > 0) {
|
|
651
|
+
await wait(delay);
|
|
652
|
+
}
|
|
653
|
+
if (
|
|
654
|
+
(await sendAttempt(
|
|
655
|
+
socketPath,
|
|
656
|
+
payload,
|
|
657
|
+
options.ackTimeoutMs ?? ACK_TIMEOUT_MS,
|
|
658
|
+
)) === ACK
|
|
659
|
+
) {
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return false;
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
export const sendUserMessage = async (
|
|
667
|
+
sessionId: string,
|
|
668
|
+
request: UserMessageInput,
|
|
669
|
+
options: SenderOptions = {},
|
|
670
|
+
): Promise<UserMessageResponse | undefined> => {
|
|
671
|
+
const payload = JSON.stringify({
|
|
672
|
+
version: 1,
|
|
673
|
+
type: "user_message",
|
|
674
|
+
...request,
|
|
675
|
+
});
|
|
676
|
+
const parsed = parseUserMessageRequest(payload);
|
|
677
|
+
if (!parsed) {
|
|
678
|
+
return undefined;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const socketPath = socketPathForSession(sessionId, options.runtimeDir);
|
|
682
|
+
for (const delay of options.retryDelaysMs ?? RETRY_DELAYS_MS) {
|
|
683
|
+
if (delay > 0) {
|
|
684
|
+
await wait(delay);
|
|
685
|
+
}
|
|
686
|
+
const response = await sendAttempt(
|
|
687
|
+
socketPath,
|
|
688
|
+
payload,
|
|
689
|
+
options.timeoutMs ?? ACK_TIMEOUT_MS,
|
|
690
|
+
);
|
|
691
|
+
const parsedResponse = response
|
|
692
|
+
? parseUserMessageResponse(response, parsed.requestId)
|
|
693
|
+
: undefined;
|
|
694
|
+
if (parsedResponse) {
|
|
695
|
+
return parsedResponse;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return undefined;
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
export const sendAbort = async (
|
|
702
|
+
sessionId: string,
|
|
703
|
+
request: AbortInput,
|
|
704
|
+
options: SenderOptions = {},
|
|
705
|
+
): Promise<AbortResponse | undefined> => {
|
|
706
|
+
const payload = JSON.stringify({ version: 1, type: "abort", ...request });
|
|
707
|
+
const parsed = parseAbortRequest(payload);
|
|
708
|
+
if (!parsed) {
|
|
709
|
+
return undefined;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const socketPath = socketPathForSession(sessionId, options.runtimeDir);
|
|
713
|
+
for (const delay of options.retryDelaysMs ?? RETRY_DELAYS_MS) {
|
|
714
|
+
if (delay > 0) {
|
|
715
|
+
await wait(delay);
|
|
716
|
+
}
|
|
717
|
+
const response = await sendAttempt(
|
|
718
|
+
socketPath,
|
|
719
|
+
payload,
|
|
720
|
+
options.timeoutMs ?? ACK_TIMEOUT_MS,
|
|
721
|
+
);
|
|
722
|
+
const parsedResponse = response
|
|
723
|
+
? parseAbortResponse(response, parsed.requestId)
|
|
724
|
+
: undefined;
|
|
725
|
+
if (parsedResponse) {
|
|
726
|
+
return parsedResponse;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return undefined;
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
const sendControl = async <Input extends { requestId: string }>(
|
|
733
|
+
sessionId: string,
|
|
734
|
+
payload: Record<string, unknown>,
|
|
735
|
+
request: Input,
|
|
736
|
+
options: SenderOptions,
|
|
737
|
+
): Promise<PiControlResponse | undefined> => {
|
|
738
|
+
const socketPath = socketPathForSession(sessionId, options.runtimeDir);
|
|
739
|
+
for (const delay of options.retryDelaysMs ?? RETRY_DELAYS_MS) {
|
|
740
|
+
if (delay > 0) {
|
|
741
|
+
await wait(delay);
|
|
742
|
+
}
|
|
743
|
+
const response = await sendAttempt(
|
|
744
|
+
socketPath,
|
|
745
|
+
JSON.stringify(payload),
|
|
746
|
+
options.timeoutMs ?? ACK_TIMEOUT_MS,
|
|
747
|
+
);
|
|
748
|
+
const parsed = response
|
|
749
|
+
? parseControlResponse(response, request.requestId)
|
|
750
|
+
: undefined;
|
|
751
|
+
if (parsed) {
|
|
752
|
+
return parsed;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return undefined;
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
export const sendSetModel = (
|
|
759
|
+
sessionId: string,
|
|
760
|
+
request: SetModelInput,
|
|
761
|
+
options: SenderOptions = {},
|
|
762
|
+
): Promise<SetModelResponse | undefined> =>
|
|
763
|
+
sendControl(
|
|
764
|
+
sessionId,
|
|
765
|
+
{ version: 1, type: "set_model", ...request },
|
|
766
|
+
request,
|
|
767
|
+
options,
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
export const sendSetThinkingLevel = (
|
|
771
|
+
sessionId: string,
|
|
772
|
+
request: SetThinkingLevelInput,
|
|
773
|
+
options: SenderOptions = {},
|
|
774
|
+
): Promise<SetThinkingLevelResponse | undefined> =>
|
|
775
|
+
sendControl(
|
|
776
|
+
sessionId,
|
|
777
|
+
{ version: 1, type: "set_thinking_level", ...request },
|
|
778
|
+
request,
|
|
779
|
+
options,
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
export const ensureRuntimeDir = async (runtimeDir: string): Promise<void> => {
|
|
783
|
+
await mkdir(runtimeDir, { mode: 0o700 }).catch(
|
|
784
|
+
(error: NodeJS.ErrnoException) => {
|
|
785
|
+
if (error.code !== "EEXIST") {
|
|
786
|
+
throw error;
|
|
787
|
+
}
|
|
788
|
+
},
|
|
789
|
+
);
|
|
790
|
+
const stats = await lstat(runtimeDir);
|
|
791
|
+
const expectedUid = process.getuid?.();
|
|
792
|
+
if (
|
|
793
|
+
!stats.isDirectory() ||
|
|
794
|
+
(expectedUid !== undefined && stats.uid !== expectedUid)
|
|
795
|
+
) {
|
|
796
|
+
throw new Error(
|
|
797
|
+
`Delegate notification runtime directory is not private: ${runtimeDir}`,
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
if ((stats.mode & 0o777) !== 0o700) {
|
|
801
|
+
throw new Error(
|
|
802
|
+
`Delegate notification runtime directory must have mode 0700: ${runtimeDir}`,
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
};
|