@zetaloop/chappie 0.5.0 → 1.0.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.
- package/.codex-plugin/plugin.json +13 -0
- package/README.md +26 -29
- package/dist/chunk-5CKVVDZ2.js +1042 -0
- package/dist/chunk-DM523QRR.js +148 -0
- package/dist/chunk-DN7OHZK6.js +110 -0
- package/dist/chunk-DQWC5GFN.js +1745 -0
- package/dist/chunk-EIPDEA3I.js +286 -0
- package/dist/chunk-GWYP7B66.js +8479 -0
- package/dist/chunk-GX4JWLHH.js +98 -0
- package/dist/chunk-K2VE2MDU.js +14916 -0
- package/dist/chunk-OWAAWZNT.js +56 -0
- package/dist/chunk-UOFCCHBS.js +94 -0
- package/dist/cli.js +47 -0
- package/dist/codex-RI2XL4HV.js +5144 -0
- package/dist/codex-plugin-NSKWB3WS.js +179 -0
- package/dist/codex.json +23 -0
- package/dist/index.js +4752 -0
- package/dist/instructions.md +13 -0
- package/dist/omp.js +240 -0
- package/dist/opencode.js +904 -0
- package/dist/promise-OHH5J44X.js +72 -0
- package/dist/stdio-KJVCYPXJ.js +1279 -0
- package/docs/setup.md +82 -0
- package/docs/tools.md +106 -96
- package/package.json +100 -56
- package/src/activity.ts +0 -21
- package/src/broker.ts +0 -780
- package/src/config.ts +0 -20
- package/src/delivery.ts +0 -54
- package/src/history.ts +0 -136
- package/src/index.ts +0 -42
- package/src/instructions.md +0 -17
- package/src/ipc.ts +0 -492
- package/src/provider.ts +0 -194
- package/src/questions.ts +0 -87
- package/src/resources.ts +0 -209
- package/src/server.ts +0 -566
- package/src/session.ts +0 -848
- package/src/state.ts +0 -163
- package/src/stdio.ts +0 -58
- package/src/tools.ts +0 -91
- package/src/transfer.ts +0 -313
- /package/{src → dist}/question.html +0 -0
package/src/broker.ts
DELETED
|
@@ -1,780 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import type { ToolCall, ToolResultMessage } from "@earendil-works/pi-ai";
|
|
3
|
-
import { type Activity, chatLabel, source } from "./activity.ts";
|
|
4
|
-
import { readConfig } from "./config.ts";
|
|
5
|
-
import type { DeliveryRecord } from "./delivery.ts";
|
|
6
|
-
import { type HistoryRange, historyInstructions } from "./history.ts";
|
|
7
|
-
import {
|
|
8
|
-
type BrokerMessage,
|
|
9
|
-
IpcServer,
|
|
10
|
-
type JsonLinePeer,
|
|
11
|
-
type SessionDescription,
|
|
12
|
-
type SessionInput,
|
|
13
|
-
type SessionInspection,
|
|
14
|
-
type SessionMessage,
|
|
15
|
-
type SessionResult,
|
|
16
|
-
} from "./ipc.ts";
|
|
17
|
-
import {
|
|
18
|
-
type Question,
|
|
19
|
-
type QuestionAnswer,
|
|
20
|
-
type QuestionInput,
|
|
21
|
-
type QuestionRecord,
|
|
22
|
-
questionView,
|
|
23
|
-
} from "./questions.ts";
|
|
24
|
-
import { type ResourceData, resourceSessionId } from "./resources.ts";
|
|
25
|
-
import { State } from "./state.ts";
|
|
26
|
-
import type { ToolInput } from "./tools.ts";
|
|
27
|
-
|
|
28
|
-
const observerInstructions =
|
|
29
|
-
"This ChatGPT conversation recently initialized or resumed work in this Pi session. A parallel execution is already continuing the task. Participate as an observer for this task: read history with observer: true, follow new entries with after and wait: true, and think independently. Leave execution and Pi communication to the ongoing work. Once its completion is recorded, explain the actual results in ChatGPT and finish your response. Continue observing this task rather than reinitializing to take over.";
|
|
30
|
-
|
|
31
|
-
interface RegisteredSession {
|
|
32
|
-
description: SessionDescription;
|
|
33
|
-
peer: JsonLinePeer<SessionMessage, BrokerMessage>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface PendingRequest {
|
|
37
|
-
peer: JsonLinePeer<SessionMessage, BrokerMessage>;
|
|
38
|
-
resolve(result: SessionResult): void;
|
|
39
|
-
reject(error: Error): void;
|
|
40
|
-
signal: AbortSignal;
|
|
41
|
-
onAbort(): void;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface ChangeWaiter {
|
|
45
|
-
resolve(): void;
|
|
46
|
-
reject(error: Error): void;
|
|
47
|
-
signal: AbortSignal;
|
|
48
|
-
onAbort(): void;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface Initialization {
|
|
52
|
-
sessionId: string;
|
|
53
|
-
instructions: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface InitializedSession extends Omit<SessionInspection, "tools"> {
|
|
57
|
-
selection: "existing" | "explicit" | "automatic";
|
|
58
|
-
initialization?: Initialization;
|
|
59
|
-
globalAgents?: { path: string };
|
|
60
|
-
inputs: SessionInput[];
|
|
61
|
-
tools: { name: string; description: string }[];
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface InspectedSession extends SessionInspection {
|
|
65
|
-
initialization?: Initialization;
|
|
66
|
-
inputs: SessionInput[];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export interface ChatResult {
|
|
70
|
-
initialization?: Initialization;
|
|
71
|
-
sessionId: string;
|
|
72
|
-
cwd: string;
|
|
73
|
-
inputs: SessionInput[];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface CallResult extends ChatResult {
|
|
77
|
-
toolResults: ToolResultMessage[];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export class Broker {
|
|
81
|
-
readonly #agentDir: string;
|
|
82
|
-
readonly #ipc: IpcServer;
|
|
83
|
-
readonly #state: State;
|
|
84
|
-
readonly #sessions = new Map<string, RegisteredSession>();
|
|
85
|
-
readonly #pending = new Map<number, PendingRequest>();
|
|
86
|
-
readonly #waiters = new Set<ChangeWaiter>();
|
|
87
|
-
readonly #cooldowns = new Map<string, number>();
|
|
88
|
-
readonly #relays = new Map<
|
|
89
|
-
JsonLinePeer<SessionMessage, BrokerMessage>,
|
|
90
|
-
Map<number, AbortController>
|
|
91
|
-
>();
|
|
92
|
-
#ask = true;
|
|
93
|
-
#nextRequestId = 1;
|
|
94
|
-
|
|
95
|
-
constructor(agentDir: string) {
|
|
96
|
-
this.#agentDir = agentDir;
|
|
97
|
-
this.#state = new State(agentDir);
|
|
98
|
-
this.#ipc = new IpcServer(
|
|
99
|
-
agentDir,
|
|
100
|
-
(peer, message) => this.#receive(peer, message),
|
|
101
|
-
(peer) => this.#removePeer(peer),
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async start(): Promise<void> {
|
|
106
|
-
const config = await readConfig(this.#agentDir);
|
|
107
|
-
this.#ask = config.ask ?? true;
|
|
108
|
-
await this.#state.load();
|
|
109
|
-
await this.#ipc.start(config.listen ?? false);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async close(): Promise<void> {
|
|
113
|
-
const error = new Error("Chappie broker ended");
|
|
114
|
-
this.#cooldowns.clear();
|
|
115
|
-
for (const relays of this.#relays.values()) {
|
|
116
|
-
for (const controller of relays.values()) controller.abort(error);
|
|
117
|
-
}
|
|
118
|
-
this.#relays.clear();
|
|
119
|
-
for (const [id, pending] of this.#pending) {
|
|
120
|
-
this.#finishRequest(id, pending);
|
|
121
|
-
pending.reject(error);
|
|
122
|
-
}
|
|
123
|
-
for (const waiter of this.#waiters) {
|
|
124
|
-
waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
125
|
-
waiter.reject(error);
|
|
126
|
-
}
|
|
127
|
-
this.#waiters.clear();
|
|
128
|
-
this.#sessions.clear();
|
|
129
|
-
await this.#ipc.close();
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
listSessions(sessionId?: string): (SessionDescription & {
|
|
133
|
-
bindingCount: number;
|
|
134
|
-
})[] {
|
|
135
|
-
const counts = this.#state.bindingCounts();
|
|
136
|
-
return [...this.#sessions.values()]
|
|
137
|
-
.filter(({ description }) => !sessionId || description.id === sessionId)
|
|
138
|
-
.map(({ description }) => ({
|
|
139
|
-
...description,
|
|
140
|
-
bindingCount: counts.get(description.id) ?? 0,
|
|
141
|
-
}));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
get askEnabled(): boolean {
|
|
145
|
-
return this.#ask;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
binding(chatId: string): string | undefined {
|
|
149
|
-
return this.#state.binding(chatId);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async initialize(
|
|
153
|
-
chatId: string,
|
|
154
|
-
sessionId: string | undefined,
|
|
155
|
-
requestId: unknown,
|
|
156
|
-
signal: AbortSignal,
|
|
157
|
-
): Promise<InitializedSession> {
|
|
158
|
-
const {
|
|
159
|
-
sessionId: target,
|
|
160
|
-
selection,
|
|
161
|
-
initialization,
|
|
162
|
-
} = await this.#selectSession(chatId, sessionId, requestId, signal, true);
|
|
163
|
-
const { inspection, inputs, globalAgents } = await this.#inspect(
|
|
164
|
-
target,
|
|
165
|
-
signal,
|
|
166
|
-
);
|
|
167
|
-
await this.#ackInputs(target, inputs, signal);
|
|
168
|
-
return {
|
|
169
|
-
selection,
|
|
170
|
-
...(initialization ? { initialization } : {}),
|
|
171
|
-
...inspection,
|
|
172
|
-
tools: inspection.tools.map(({ name, description }) => ({
|
|
173
|
-
name,
|
|
174
|
-
description: description.split("\n", 1)[0] ?? description,
|
|
175
|
-
})),
|
|
176
|
-
inputs,
|
|
177
|
-
...(globalAgents ? { globalAgents } : {}),
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async chat(
|
|
182
|
-
chatId: string,
|
|
183
|
-
sessionId: string | undefined,
|
|
184
|
-
text: string,
|
|
185
|
-
requestId: unknown,
|
|
186
|
-
signal: AbortSignal,
|
|
187
|
-
): Promise<ChatResult> {
|
|
188
|
-
const { sessionId: target, initialization } = await this.#selectSession(
|
|
189
|
-
chatId,
|
|
190
|
-
sessionId,
|
|
191
|
-
requestId,
|
|
192
|
-
signal,
|
|
193
|
-
);
|
|
194
|
-
const result = await this.#request(
|
|
195
|
-
target,
|
|
196
|
-
(id) => ({
|
|
197
|
-
type: "chat",
|
|
198
|
-
id,
|
|
199
|
-
...source(chatId, requestId),
|
|
200
|
-
sessionId: target,
|
|
201
|
-
text,
|
|
202
|
-
}),
|
|
203
|
-
signal,
|
|
204
|
-
);
|
|
205
|
-
if ("message" in result) {
|
|
206
|
-
const inputs = result.inputs;
|
|
207
|
-
await this.#ackInputs(target, inputs, signal);
|
|
208
|
-
return {
|
|
209
|
-
sessionId: target,
|
|
210
|
-
cwd: result.cwd,
|
|
211
|
-
inputs,
|
|
212
|
-
...(initialization ? { initialization } : {}),
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
throw new Error("Pi session returned no assistant message");
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async tools(
|
|
219
|
-
chatId: string,
|
|
220
|
-
sessionId: string | undefined,
|
|
221
|
-
names: string[] | undefined,
|
|
222
|
-
requestId: unknown,
|
|
223
|
-
signal: AbortSignal,
|
|
224
|
-
): Promise<InspectedSession> {
|
|
225
|
-
const { sessionId: target, initialization } = await this.#selectSession(
|
|
226
|
-
chatId,
|
|
227
|
-
sessionId,
|
|
228
|
-
requestId,
|
|
229
|
-
signal,
|
|
230
|
-
);
|
|
231
|
-
const { inspection, inputs } = await this.#inspect(target, signal);
|
|
232
|
-
await this.#ackInputs(target, inputs, signal);
|
|
233
|
-
const selected = names ? new Set(names) : undefined;
|
|
234
|
-
return {
|
|
235
|
-
...inspection,
|
|
236
|
-
...(initialization ? { initialization } : {}),
|
|
237
|
-
tools: selected
|
|
238
|
-
? inspection.tools.filter(({ name }) => selected.has(name))
|
|
239
|
-
: inspection.tools,
|
|
240
|
-
inputs,
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
async call(
|
|
245
|
-
chatId: string,
|
|
246
|
-
sessionId: string | undefined,
|
|
247
|
-
calls: ToolInput[],
|
|
248
|
-
requestId: unknown,
|
|
249
|
-
signal: AbortSignal,
|
|
250
|
-
): Promise<CallResult> {
|
|
251
|
-
const { sessionId: target, initialization } = await this.#selectSession(
|
|
252
|
-
chatId,
|
|
253
|
-
sessionId,
|
|
254
|
-
requestId,
|
|
255
|
-
signal,
|
|
256
|
-
);
|
|
257
|
-
const toolCalls: ToolCall[] = calls.map((call) => ({
|
|
258
|
-
type: "toolCall",
|
|
259
|
-
id: `chappie-${randomUUID()}`,
|
|
260
|
-
name: call.name,
|
|
261
|
-
arguments: call.arguments,
|
|
262
|
-
}));
|
|
263
|
-
const result = await this.#request(
|
|
264
|
-
target,
|
|
265
|
-
(id) => ({
|
|
266
|
-
type: "call",
|
|
267
|
-
id,
|
|
268
|
-
...source(chatId, requestId),
|
|
269
|
-
sessionId: target,
|
|
270
|
-
calls: toolCalls,
|
|
271
|
-
}),
|
|
272
|
-
signal,
|
|
273
|
-
);
|
|
274
|
-
if ("toolResults" in result) {
|
|
275
|
-
await this.#ackInputs(target, result.inputs, signal);
|
|
276
|
-
return {
|
|
277
|
-
sessionId: target,
|
|
278
|
-
...(initialization ? { initialization } : {}),
|
|
279
|
-
cwd: result.cwd,
|
|
280
|
-
toolResults: result.toolResults,
|
|
281
|
-
inputs: result.inputs,
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
throw new Error("Pi session returned no tool results");
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async history(
|
|
288
|
-
chatId: string,
|
|
289
|
-
sessionId: string | undefined,
|
|
290
|
-
range: HistoryRange,
|
|
291
|
-
requestId: unknown,
|
|
292
|
-
signal: AbortSignal,
|
|
293
|
-
) {
|
|
294
|
-
const target = sessionId ?? this.#state.binding(chatId);
|
|
295
|
-
if (!target) throw new Error("Specify a Pi sessionId to read history");
|
|
296
|
-
await this.#waitForSession(target, signal);
|
|
297
|
-
const result = await this.#request(
|
|
298
|
-
target,
|
|
299
|
-
(id) => ({
|
|
300
|
-
type: "history",
|
|
301
|
-
id,
|
|
302
|
-
sessionId: target,
|
|
303
|
-
range,
|
|
304
|
-
...source(chatId, requestId),
|
|
305
|
-
}),
|
|
306
|
-
signal,
|
|
307
|
-
);
|
|
308
|
-
if ("history" in result) return { sessionId: target, ...result };
|
|
309
|
-
throw new Error("Pi session returned no history");
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
async inputs(
|
|
313
|
-
chatId: string,
|
|
314
|
-
sessionId: string | undefined,
|
|
315
|
-
signal: AbortSignal,
|
|
316
|
-
): Promise<SessionInput[]> {
|
|
317
|
-
const target = sessionId ?? this.#state.binding(chatId);
|
|
318
|
-
if (!target || !this.#sessions.has(target)) return [];
|
|
319
|
-
const { inputs } = await this.#inspect(target, signal);
|
|
320
|
-
await this.#ackInputs(target, inputs, signal);
|
|
321
|
-
return inputs;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
async ask(
|
|
325
|
-
chatId: string,
|
|
326
|
-
sessionId: string | undefined,
|
|
327
|
-
input: QuestionInput,
|
|
328
|
-
requestId: unknown,
|
|
329
|
-
signal: AbortSignal,
|
|
330
|
-
): Promise<Question & { initialization?: Initialization }> {
|
|
331
|
-
const { sessionId: target, initialization } = await this.#selectSession(
|
|
332
|
-
chatId,
|
|
333
|
-
sessionId,
|
|
334
|
-
requestId,
|
|
335
|
-
signal,
|
|
336
|
-
);
|
|
337
|
-
signal.throwIfAborted();
|
|
338
|
-
const session = this.#sessions.get(target);
|
|
339
|
-
if (!session) throw new Error(`Pi session ${target} is offline`);
|
|
340
|
-
const question: QuestionRecord = {
|
|
341
|
-
...input,
|
|
342
|
-
id: randomUUID(),
|
|
343
|
-
chatId,
|
|
344
|
-
sessionId: target,
|
|
345
|
-
cwd: session.description.cwd,
|
|
346
|
-
delivered: false,
|
|
347
|
-
};
|
|
348
|
-
await this.#state.addQuestion(question);
|
|
349
|
-
const activity = source(chatId, requestId);
|
|
350
|
-
void this.#notify(
|
|
351
|
-
target,
|
|
352
|
-
`${chatLabel(activity)} asked: ${question.question}`,
|
|
353
|
-
{
|
|
354
|
-
event: "asked",
|
|
355
|
-
...activity,
|
|
356
|
-
},
|
|
357
|
-
).catch(() => {});
|
|
358
|
-
return {
|
|
359
|
-
...questionView(question),
|
|
360
|
-
...(initialization ? { initialization } : {}),
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
async assertQuestion(
|
|
365
|
-
chatId: string,
|
|
366
|
-
id: string,
|
|
367
|
-
signal: AbortSignal,
|
|
368
|
-
): Promise<Question> {
|
|
369
|
-
const timeout = AbortSignal.timeout(10_000);
|
|
370
|
-
const combined = AbortSignal.any([signal, timeout]);
|
|
371
|
-
try {
|
|
372
|
-
for (;;) {
|
|
373
|
-
signal.throwIfAborted();
|
|
374
|
-
const question = this.#state.question(chatId, id);
|
|
375
|
-
if (question.loaded) return questionView(question);
|
|
376
|
-
await this.#waitForChange(combined);
|
|
377
|
-
}
|
|
378
|
-
} catch (error) {
|
|
379
|
-
signal.throwIfAborted();
|
|
380
|
-
if (!timeout.aborted) throw error;
|
|
381
|
-
const question = this.#state.question(chatId, id);
|
|
382
|
-
if (question.loaded) return questionView(question);
|
|
383
|
-
if (!question.answer) {
|
|
384
|
-
await this.#state.answer(chatId, id, {
|
|
385
|
-
selections: [],
|
|
386
|
-
text: "",
|
|
387
|
-
skipped: true,
|
|
388
|
-
});
|
|
389
|
-
void this.#notify(
|
|
390
|
-
question.sessionId,
|
|
391
|
-
`Question skipped after display timeout: ${question.question}`,
|
|
392
|
-
{ event: "skipped", chatId },
|
|
393
|
-
).catch(() => {});
|
|
394
|
-
}
|
|
395
|
-
throw new Error(
|
|
396
|
-
"Question widget did not load within 10 seconds. The question was automatically skipped. Use an installed Pi interactive tool through call if an answer is needed.",
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
async answer(
|
|
402
|
-
chatId: string,
|
|
403
|
-
id: string,
|
|
404
|
-
answer?: QuestionAnswer,
|
|
405
|
-
loaded = false,
|
|
406
|
-
): Promise<Question> {
|
|
407
|
-
let question = this.#state.question(chatId, id);
|
|
408
|
-
if ((loaded || answer) && !question.loaded) {
|
|
409
|
-
this.#cooldown(chatId, question.sessionId);
|
|
410
|
-
question = { ...question, loaded: true };
|
|
411
|
-
await this.#state.addQuestion(question);
|
|
412
|
-
this.#notifyChange();
|
|
413
|
-
}
|
|
414
|
-
if (answer) {
|
|
415
|
-
const previous = question.answer;
|
|
416
|
-
question = await this.#state.answer(chatId, id, answer);
|
|
417
|
-
if (question.answer !== previous) {
|
|
418
|
-
const response = [
|
|
419
|
-
...(question.answer?.selections ?? []).map(
|
|
420
|
-
(index) => question.options[index]?.title,
|
|
421
|
-
),
|
|
422
|
-
question.answer?.text,
|
|
423
|
-
]
|
|
424
|
-
.filter(Boolean)
|
|
425
|
-
.join(", ");
|
|
426
|
-
const message = question.answer?.skipped
|
|
427
|
-
? `Skipped in ChatGPT ${chatId.slice(-4)}: ${question.question}`
|
|
428
|
-
: previous
|
|
429
|
-
? `Answer updated in ChatGPT ${chatId.slice(-4)}: ${question.question} — ${response}`
|
|
430
|
-
: `Answered in ChatGPT ${chatId.slice(-4)}: ${question.question} — ${response}`;
|
|
431
|
-
void this.#notify(question.sessionId, message, {
|
|
432
|
-
event: question.answer?.skipped ? "skipped" : "answered",
|
|
433
|
-
chatId,
|
|
434
|
-
}).catch(() => {});
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
return questionView(question);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
answers(chatId: string): QuestionRecord[] {
|
|
441
|
-
return this.#state.answers(chatId);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
async readResource(uri: string, signal: AbortSignal): Promise<ResourceData> {
|
|
445
|
-
const requested = new URL(uri);
|
|
446
|
-
const chatId = requested.searchParams.get("chatId");
|
|
447
|
-
requested.search = "";
|
|
448
|
-
const sessionId = resourceSessionId(requested.href);
|
|
449
|
-
if (chatId) this.#cooldown(chatId, sessionId);
|
|
450
|
-
await this.#waitForSession(sessionId, signal);
|
|
451
|
-
const result = await this.#request(
|
|
452
|
-
sessionId,
|
|
453
|
-
(id) => ({ type: "readResource", id, sessionId, uri: requested.href }),
|
|
454
|
-
signal,
|
|
455
|
-
);
|
|
456
|
-
if ("resource" in result) {
|
|
457
|
-
if (chatId) this.#cooldown(chatId, sessionId);
|
|
458
|
-
return { ...result.resource, uri };
|
|
459
|
-
}
|
|
460
|
-
throw new Error("Pi session returned no resource");
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
deliveries(chatId: string): DeliveryRecord[] {
|
|
464
|
-
return this.#state.deliveries(chatId);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
acknowledge(
|
|
468
|
-
deliveries: DeliveryRecord[],
|
|
469
|
-
answers: QuestionRecord[],
|
|
470
|
-
signal: AbortSignal,
|
|
471
|
-
): Promise<void> {
|
|
472
|
-
return this.#state.acknowledge(deliveries, answers, signal);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
async #receive(
|
|
476
|
-
peer: JsonLinePeer<SessionMessage, BrokerMessage>,
|
|
477
|
-
message: SessionMessage,
|
|
478
|
-
): Promise<void> {
|
|
479
|
-
switch (message.type) {
|
|
480
|
-
case "request": {
|
|
481
|
-
let relays = this.#relays.get(peer);
|
|
482
|
-
if (!relays) {
|
|
483
|
-
relays = new Map();
|
|
484
|
-
this.#relays.set(peer, relays);
|
|
485
|
-
}
|
|
486
|
-
const controller = new AbortController();
|
|
487
|
-
relays.set(message.id, controller);
|
|
488
|
-
void this.#request(
|
|
489
|
-
message.request.sessionId,
|
|
490
|
-
(id) => ({ ...message.request, id }),
|
|
491
|
-
controller.signal,
|
|
492
|
-
)
|
|
493
|
-
.then(
|
|
494
|
-
(result) =>
|
|
495
|
-
peer.send({ ...result, type: "response", id: message.id }),
|
|
496
|
-
(error: unknown) =>
|
|
497
|
-
peer.send({
|
|
498
|
-
type: "response",
|
|
499
|
-
id: message.id,
|
|
500
|
-
error: error instanceof Error ? error.message : String(error),
|
|
501
|
-
}),
|
|
502
|
-
)
|
|
503
|
-
.finally(() => relays.delete(message.id))
|
|
504
|
-
.catch(() => {});
|
|
505
|
-
break;
|
|
506
|
-
}
|
|
507
|
-
case "cancelRequest":
|
|
508
|
-
this.#relays
|
|
509
|
-
.get(peer)
|
|
510
|
-
?.get(message.id)
|
|
511
|
-
?.abort(new Error("Transfer cancelled"));
|
|
512
|
-
break;
|
|
513
|
-
case "sync": {
|
|
514
|
-
const registered =
|
|
515
|
-
this.#sessions.get(message.session.id)?.peer === peer;
|
|
516
|
-
this.#sessions.set(message.session.id, {
|
|
517
|
-
description: message.session,
|
|
518
|
-
peer,
|
|
519
|
-
});
|
|
520
|
-
await peer.send({
|
|
521
|
-
type: "synced",
|
|
522
|
-
id: message.id,
|
|
523
|
-
sessionId: message.session.id,
|
|
524
|
-
});
|
|
525
|
-
if (!registered)
|
|
526
|
-
await this.#notify(message.session.id, "Chappie connected", {
|
|
527
|
-
event: "connected",
|
|
528
|
-
});
|
|
529
|
-
this.#notifyChange();
|
|
530
|
-
break;
|
|
531
|
-
}
|
|
532
|
-
case "unregister": {
|
|
533
|
-
const session = this.#sessions.get(message.sessionId);
|
|
534
|
-
if (session?.peer === peer) this.#removeSession(message.sessionId);
|
|
535
|
-
break;
|
|
536
|
-
}
|
|
537
|
-
case "delivery":
|
|
538
|
-
await this.#state.addDelivery(message.delivery);
|
|
539
|
-
await peer.send({ type: "stored", id: message.delivery.id });
|
|
540
|
-
break;
|
|
541
|
-
case "result": {
|
|
542
|
-
const pending = this.#pending.get(message.id);
|
|
543
|
-
if (!pending || pending.peer !== peer) break;
|
|
544
|
-
this.#finishRequest(message.id, pending);
|
|
545
|
-
if ("error" in message) pending.reject(new Error(message.error));
|
|
546
|
-
else {
|
|
547
|
-
const { type: _type, id: _id, ...result } = message;
|
|
548
|
-
pending.resolve(result);
|
|
549
|
-
}
|
|
550
|
-
break;
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
async #selectSession(
|
|
556
|
-
chatId: string,
|
|
557
|
-
requestedId: string | undefined,
|
|
558
|
-
requestId: unknown,
|
|
559
|
-
signal: AbortSignal,
|
|
560
|
-
bindRequested = false,
|
|
561
|
-
): Promise<{
|
|
562
|
-
sessionId: string;
|
|
563
|
-
selection: InitializedSession["selection"];
|
|
564
|
-
initialization?: Initialization;
|
|
565
|
-
}> {
|
|
566
|
-
for (;;) {
|
|
567
|
-
signal.throwIfAborted();
|
|
568
|
-
const boundId = this.#state.binding(chatId);
|
|
569
|
-
let target = requestedId ?? boundId;
|
|
570
|
-
if (!target) {
|
|
571
|
-
const occupied = this.#state.bindingCounts();
|
|
572
|
-
target = [...this.#sessions.keys()].find((id) => !occupied.has(id));
|
|
573
|
-
}
|
|
574
|
-
if (!target) {
|
|
575
|
-
await this.#waitForChange(signal);
|
|
576
|
-
continue;
|
|
577
|
-
}
|
|
578
|
-
await this.#waitForSession(target, signal);
|
|
579
|
-
signal.throwIfAborted();
|
|
580
|
-
const initialization =
|
|
581
|
-
bindRequested || !this.#state.binding(chatId)
|
|
582
|
-
? await this.#join(chatId, target, requestId, signal, bindRequested)
|
|
583
|
-
: undefined;
|
|
584
|
-
return {
|
|
585
|
-
sessionId: target,
|
|
586
|
-
selection: requestedId
|
|
587
|
-
? "explicit"
|
|
588
|
-
: boundId
|
|
589
|
-
? "existing"
|
|
590
|
-
: "automatic",
|
|
591
|
-
...(initialization ? { initialization } : {}),
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
async #join(
|
|
597
|
-
chatId: string,
|
|
598
|
-
sessionId: string,
|
|
599
|
-
requestId: unknown,
|
|
600
|
-
signal: AbortSignal,
|
|
601
|
-
explicit = false,
|
|
602
|
-
): Promise<Initialization> {
|
|
603
|
-
signal.throwIfAborted();
|
|
604
|
-
const previous = this.#state.binding(chatId);
|
|
605
|
-
const key = JSON.stringify([chatId, sessionId]);
|
|
606
|
-
const observer = (this.#cooldowns.get(key) ?? 0) > Date.now();
|
|
607
|
-
if (!observer) this.#cooldown(chatId, sessionId);
|
|
608
|
-
await this.#state.bind(chatId, sessionId);
|
|
609
|
-
signal.throwIfAborted();
|
|
610
|
-
const activity = source(chatId, requestId);
|
|
611
|
-
if (previous !== sessionId) {
|
|
612
|
-
this.#notifyChange();
|
|
613
|
-
if (previous)
|
|
614
|
-
await this.#notify(previous, `${chatLabel(activity)} left`, {
|
|
615
|
-
...activity,
|
|
616
|
-
event: "left",
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
signal.throwIfAborted();
|
|
620
|
-
await this.#notify(sessionId, `${chatLabel(activity)} joined`, {
|
|
621
|
-
...activity,
|
|
622
|
-
event: "joined",
|
|
623
|
-
initialization: explicit ? "explicit" : "implicit",
|
|
624
|
-
});
|
|
625
|
-
return {
|
|
626
|
-
sessionId,
|
|
627
|
-
instructions: observer ? observerInstructions : historyInstructions,
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
#cooldown(chatId: string, sessionId: string): void {
|
|
632
|
-
const now = Date.now();
|
|
633
|
-
for (const [key, expires] of this.#cooldowns) {
|
|
634
|
-
if (expires <= now) this.#cooldowns.delete(key);
|
|
635
|
-
}
|
|
636
|
-
this.#cooldowns.set(JSON.stringify([chatId, sessionId]), now + 10_000);
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
async #notify(
|
|
640
|
-
sessionId: string,
|
|
641
|
-
message: string,
|
|
642
|
-
activity: Activity = {},
|
|
643
|
-
): Promise<void> {
|
|
644
|
-
await this.#sessions
|
|
645
|
-
.get(sessionId)
|
|
646
|
-
?.peer.send({ type: "notice", sessionId, message, activity });
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
async #waitForSession(sessionId: string, signal: AbortSignal): Promise<void> {
|
|
650
|
-
while (!this.#sessions.has(sessionId)) await this.#waitForChange(signal);
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
async #inspect(
|
|
654
|
-
sessionId: string,
|
|
655
|
-
signal: AbortSignal,
|
|
656
|
-
): Promise<Extract<SessionResult, { inspection: SessionInspection }>> {
|
|
657
|
-
const result = await this.#request(
|
|
658
|
-
sessionId,
|
|
659
|
-
(id) => ({ type: "inspect", id, sessionId }),
|
|
660
|
-
signal,
|
|
661
|
-
);
|
|
662
|
-
if ("inspection" in result) return result;
|
|
663
|
-
throw new Error("Pi session returned no inspection");
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
async #ackInputs(
|
|
667
|
-
sessionId: string,
|
|
668
|
-
inputs: SessionInput[],
|
|
669
|
-
signal: AbortSignal,
|
|
670
|
-
): Promise<void> {
|
|
671
|
-
signal.throwIfAborted();
|
|
672
|
-
if (inputs.length === 0) return;
|
|
673
|
-
const session = this.#sessions.get(sessionId);
|
|
674
|
-
if (!session) throw new Error(`Pi session ${sessionId} is offline`);
|
|
675
|
-
await session.peer.send({
|
|
676
|
-
type: "ackInputs",
|
|
677
|
-
sessionId,
|
|
678
|
-
ids: inputs.map(({ id }) => id),
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
async #request(
|
|
683
|
-
sessionId: string,
|
|
684
|
-
message: (id: number) => BrokerMessage,
|
|
685
|
-
signal: AbortSignal,
|
|
686
|
-
): Promise<SessionResult> {
|
|
687
|
-
const session = this.#sessions.get(sessionId);
|
|
688
|
-
if (!session) throw new Error(`Pi session ${sessionId} is offline`);
|
|
689
|
-
if (signal.aborted) throw abortError(signal);
|
|
690
|
-
const id = this.#nextRequestId++;
|
|
691
|
-
const completion = Promise.withResolvers<SessionResult>();
|
|
692
|
-
const onAbort = (): void => {
|
|
693
|
-
const pending = this.#pending.get(id);
|
|
694
|
-
if (!pending) return;
|
|
695
|
-
void pending.peer
|
|
696
|
-
.send({
|
|
697
|
-
type: "cancel",
|
|
698
|
-
id,
|
|
699
|
-
sessionId,
|
|
700
|
-
reason: abortError(signal).message,
|
|
701
|
-
})
|
|
702
|
-
.catch(() => {});
|
|
703
|
-
this.#finishRequest(id, pending);
|
|
704
|
-
pending.reject(abortError(signal));
|
|
705
|
-
};
|
|
706
|
-
const pending: PendingRequest = {
|
|
707
|
-
peer: session.peer,
|
|
708
|
-
resolve: completion.resolve,
|
|
709
|
-
reject: completion.reject,
|
|
710
|
-
signal,
|
|
711
|
-
onAbort,
|
|
712
|
-
};
|
|
713
|
-
this.#pending.set(id, pending);
|
|
714
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
715
|
-
void session.peer.send(message(id)).catch((error: unknown) => {
|
|
716
|
-
this.#finishRequest(id, pending);
|
|
717
|
-
pending.reject(error instanceof Error ? error : new Error(String(error)));
|
|
718
|
-
});
|
|
719
|
-
return completion.promise;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
#waitForChange(signal: AbortSignal): Promise<void> {
|
|
723
|
-
if (signal.aborted) return Promise.reject(abortError(signal));
|
|
724
|
-
const completion = Promise.withResolvers<void>();
|
|
725
|
-
const onAbort = (): void => {
|
|
726
|
-
this.#waiters.delete(waiter);
|
|
727
|
-
waiter.reject(abortError(signal));
|
|
728
|
-
};
|
|
729
|
-
const waiter: ChangeWaiter = {
|
|
730
|
-
resolve: completion.resolve,
|
|
731
|
-
reject: completion.reject,
|
|
732
|
-
signal,
|
|
733
|
-
onAbort,
|
|
734
|
-
};
|
|
735
|
-
this.#waiters.add(waiter);
|
|
736
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
737
|
-
return completion.promise;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
#notifyChange(): void {
|
|
741
|
-
for (const waiter of this.#waiters) {
|
|
742
|
-
this.#waiters.delete(waiter);
|
|
743
|
-
waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
744
|
-
waiter.resolve();
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
#finishRequest(id: number, pending: PendingRequest): void {
|
|
749
|
-
this.#pending.delete(id);
|
|
750
|
-
pending.signal.removeEventListener("abort", pending.onAbort);
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
#removePeer(peer: JsonLinePeer<SessionMessage, BrokerMessage>): void {
|
|
754
|
-
for (const controller of this.#relays.get(peer)?.values() ?? []) {
|
|
755
|
-
controller.abort(new Error("Pi session disconnected"));
|
|
756
|
-
}
|
|
757
|
-
this.#relays.delete(peer);
|
|
758
|
-
for (const [sessionId, session] of this.#sessions) {
|
|
759
|
-
if (session.peer === peer) this.#removeSession(sessionId);
|
|
760
|
-
}
|
|
761
|
-
for (const [id, pending] of this.#pending) {
|
|
762
|
-
if (pending.peer !== peer) continue;
|
|
763
|
-
this.#finishRequest(id, pending);
|
|
764
|
-
pending.reject(new Error("Pi session disconnected"));
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
#removeSession(sessionId: string): void {
|
|
769
|
-
this.#sessions.delete(sessionId);
|
|
770
|
-
this.#notifyChange();
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
function abortError(signal: AbortSignal): Error {
|
|
775
|
-
return signal.reason instanceof Error
|
|
776
|
-
? signal.reason
|
|
777
|
-
: new Error(
|
|
778
|
-
typeof signal.reason === "string" ? signal.reason : "Request cancelled",
|
|
779
|
-
);
|
|
780
|
-
}
|