@zetaloop/chappie 0.4.0 → 0.5.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/README.md +3 -9
- package/docs/tools.md +36 -36
- package/package.json +1 -1
- package/src/broker.ts +106 -328
- package/src/config.ts +0 -1
- package/src/history.ts +15 -2
- package/src/index.ts +4 -1
- package/src/instructions.md +5 -5
- package/src/ipc.ts +19 -4
- package/src/questions.ts +6 -14
- package/src/resources.ts +26 -6
- package/src/server.ts +40 -89
- package/src/session.ts +272 -27
- package/src/state.ts +1 -17
- package/src/transfer.ts +200 -49
package/src/session.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { access } from "node:fs/promises";
|
|
3
3
|
import { hostname } from "node:os";
|
|
4
|
-
import {
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
5
|
import type {
|
|
6
6
|
AssistantMessage,
|
|
7
7
|
ToolResultMessage,
|
|
@@ -22,10 +22,18 @@ import {
|
|
|
22
22
|
type SessionDescription,
|
|
23
23
|
type SessionInput,
|
|
24
24
|
type SessionInspection,
|
|
25
|
+
type SessionRequest,
|
|
26
|
+
type SessionResult,
|
|
25
27
|
type SessionStatus,
|
|
26
28
|
} from "./ipc.ts";
|
|
27
29
|
import type { ProviderOutput } from "./provider.ts";
|
|
28
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
type ResourceDescriptor,
|
|
32
|
+
readSessionResource,
|
|
33
|
+
rememberImages,
|
|
34
|
+
resourceSessionId,
|
|
35
|
+
} from "./resources.ts";
|
|
36
|
+
import { copyFiles, transfer, transferResult } from "./transfer.ts";
|
|
29
37
|
|
|
30
38
|
type RemoteRequest = Extract<BrokerMessage, { type: "chat" | "call" }>;
|
|
31
39
|
|
|
@@ -44,6 +52,16 @@ interface StoreRequest {
|
|
|
44
52
|
reject(error: Error): void;
|
|
45
53
|
}
|
|
46
54
|
|
|
55
|
+
interface PendingRequest {
|
|
56
|
+
resolve(result: SessionResult): void;
|
|
57
|
+
reject(error: Error): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface HistoryRequest {
|
|
61
|
+
request: Extract<BrokerMessage, { type: "history" }>;
|
|
62
|
+
timeout: NodeJS.Timeout;
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
interface ActiveRequest {
|
|
48
66
|
request: RemoteRequest;
|
|
49
67
|
session: SessionDescription;
|
|
@@ -62,12 +80,15 @@ export class LocalSession {
|
|
|
62
80
|
readonly #queue: RemoteRequest[] = [];
|
|
63
81
|
readonly #pendingInputs = new Map<string, SessionInput>();
|
|
64
82
|
readonly #deliveries = new Map<string, DeliveryRecord>();
|
|
83
|
+
readonly #histories = new Map<number, HistoryRequest>();
|
|
84
|
+
readonly #requests = new Map<number, PendingRequest>();
|
|
85
|
+
readonly #copies = new Map<number, AbortController>();
|
|
65
86
|
#context: ExtensionContext | undefined;
|
|
66
87
|
#connection: IpcClient | undefined;
|
|
67
88
|
#output: ProviderOutput | undefined;
|
|
68
89
|
#active: ActiveRequest | undefined;
|
|
69
90
|
#status: SessionStatus = "idle";
|
|
70
|
-
#
|
|
91
|
+
#nextRequestId = 1;
|
|
71
92
|
#starting = false;
|
|
72
93
|
#sessionId: string | undefined;
|
|
73
94
|
#inputCursor: string | null = null;
|
|
@@ -104,6 +125,20 @@ export class LocalSession {
|
|
|
104
125
|
if (context.model?.provider === "chappie") {
|
|
105
126
|
this.#resetInputs(context, event.newLeafId);
|
|
106
127
|
}
|
|
128
|
+
this.#historyChanged();
|
|
129
|
+
});
|
|
130
|
+
// Pi persists messages after message_end handlers finish.
|
|
131
|
+
this.#pi.on("message_start", (_event, context) => {
|
|
132
|
+
this.#context = context;
|
|
133
|
+
this.#historyChanged();
|
|
134
|
+
});
|
|
135
|
+
this.#pi.on("tool_call", (_event, context) => {
|
|
136
|
+
this.#context = context;
|
|
137
|
+
this.#historyChanged();
|
|
138
|
+
});
|
|
139
|
+
this.#pi.on("session_compact", (_event, context) => {
|
|
140
|
+
this.#context = context;
|
|
141
|
+
this.#historyChanged();
|
|
107
142
|
});
|
|
108
143
|
this.#pi.on("context", (event, context) => ({
|
|
109
144
|
messages:
|
|
@@ -122,6 +157,7 @@ export class LocalSession {
|
|
|
122
157
|
this.#context = context;
|
|
123
158
|
this.#starting = false;
|
|
124
159
|
this.#collectInputs();
|
|
160
|
+
this.#historyChanged();
|
|
125
161
|
await this.#completeActive();
|
|
126
162
|
this.#dispatch();
|
|
127
163
|
});
|
|
@@ -139,6 +175,7 @@ export class LocalSession {
|
|
|
139
175
|
type,
|
|
140
176
|
...activity,
|
|
141
177
|
});
|
|
178
|
+
if (activity.event !== "history") this.#historyChanged();
|
|
142
179
|
}
|
|
143
180
|
|
|
144
181
|
async start(output: ProviderOutput): Promise<void> {
|
|
@@ -157,6 +194,7 @@ export class LocalSession {
|
|
|
157
194
|
await connection.connect();
|
|
158
195
|
if (output.closed) return;
|
|
159
196
|
this.#collectInputs();
|
|
197
|
+
this.#historyChanged();
|
|
160
198
|
await this.#completeActive();
|
|
161
199
|
this.#status = "ready";
|
|
162
200
|
await this.#sync();
|
|
@@ -171,6 +209,52 @@ export class LocalSession {
|
|
|
171
209
|
}
|
|
172
210
|
}
|
|
173
211
|
|
|
212
|
+
async transfer(
|
|
213
|
+
...parameters: Parameters<typeof transfer.execute>
|
|
214
|
+
): ReturnType<typeof transfer.execute> {
|
|
215
|
+
const [id, args, signal, update, context] = parameters;
|
|
216
|
+
if (!args.to) return transfer.execute(...parameters);
|
|
217
|
+
if (args.files) throw new Error("files and to are mutually exclusive");
|
|
218
|
+
if (args.paths.length !== args.to.paths.length)
|
|
219
|
+
throw new Error("Source and destination counts must match");
|
|
220
|
+
const inspected = await this.#request(
|
|
221
|
+
{ type: "inspect", sessionId: args.to.sessionId },
|
|
222
|
+
signal,
|
|
223
|
+
);
|
|
224
|
+
if (!("inspection" in inspected))
|
|
225
|
+
throw new Error("Pi session returned no environment");
|
|
226
|
+
const exported = await transfer.execute(
|
|
227
|
+
id,
|
|
228
|
+
{ paths: args.paths },
|
|
229
|
+
signal,
|
|
230
|
+
undefined,
|
|
231
|
+
context,
|
|
232
|
+
);
|
|
233
|
+
update?.({
|
|
234
|
+
content: [],
|
|
235
|
+
details: {
|
|
236
|
+
...exported.details,
|
|
237
|
+
to: {
|
|
238
|
+
sessionId: args.to.sessionId,
|
|
239
|
+
device: inspected.inspection.session.device,
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
const result = await this.#request(
|
|
244
|
+
{
|
|
245
|
+
type: "copy",
|
|
246
|
+
sessionId: args.to.sessionId,
|
|
247
|
+
paths: args.to.paths,
|
|
248
|
+
resources: exported.details.resources,
|
|
249
|
+
...(args.overwrite !== undefined ? { overwrite: args.overwrite } : {}),
|
|
250
|
+
},
|
|
251
|
+
signal,
|
|
252
|
+
);
|
|
253
|
+
if (!("transfer" in result))
|
|
254
|
+
throw new Error("Pi session returned no transfer result");
|
|
255
|
+
return transferResult({ ...result.transfer, device: hostname() });
|
|
256
|
+
}
|
|
257
|
+
|
|
174
258
|
close(): void {
|
|
175
259
|
const sessionId = this.#context?.sessionManager.getSessionId();
|
|
176
260
|
if (sessionId && this.#connection?.connected) {
|
|
@@ -187,8 +271,10 @@ export class LocalSession {
|
|
|
187
271
|
this.#connection = undefined;
|
|
188
272
|
this.#context = undefined;
|
|
189
273
|
this.#resetInputs();
|
|
274
|
+
this.#cancelRequests(new Error("Chappie session ended"));
|
|
190
275
|
this.#rejectSyncs(new Error("Chappie session ended"));
|
|
191
276
|
this.#rejectStores(new Error("Chappie session ended"));
|
|
277
|
+
for (const id of this.#histories.keys()) this.#finishHistory(id);
|
|
192
278
|
}
|
|
193
279
|
|
|
194
280
|
#update(
|
|
@@ -211,6 +297,8 @@ export class LocalSession {
|
|
|
211
297
|
},
|
|
212
298
|
onMessage: (message) => this.#receive(message),
|
|
213
299
|
onClose: (error) => {
|
|
300
|
+
this.#cancelRequests(error);
|
|
301
|
+
for (const id of this.#histories.keys()) this.#finishHistory(id);
|
|
214
302
|
this.#rejectSyncs(error);
|
|
215
303
|
this.#rejectStores(error);
|
|
216
304
|
if (this.#output && !this.#output.closed) this.#output.fail(error);
|
|
@@ -248,7 +336,7 @@ export class LocalSession {
|
|
|
248
336
|
this.#context.model?.provider !== "chappie"
|
|
249
337
|
)
|
|
250
338
|
return;
|
|
251
|
-
const id = this.#
|
|
339
|
+
const id = this.#nextRequestId++;
|
|
252
340
|
const completion = Promise.withResolvers<void>();
|
|
253
341
|
this.#syncs.set(id, completion);
|
|
254
342
|
try {
|
|
@@ -261,6 +349,16 @@ export class LocalSession {
|
|
|
261
349
|
|
|
262
350
|
async #receive(message: BrokerMessage): Promise<void> {
|
|
263
351
|
switch (message.type) {
|
|
352
|
+
case "response": {
|
|
353
|
+
const pending = this.#requests.get(message.id);
|
|
354
|
+
if (!pending) break;
|
|
355
|
+
if ("error" in message) pending.reject(new Error(message.error));
|
|
356
|
+
else {
|
|
357
|
+
const { type: _type, id: _id, ...result } = message;
|
|
358
|
+
pending.resolve(result);
|
|
359
|
+
}
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
264
362
|
case "synced":
|
|
265
363
|
this.#syncs.get(message.id)?.resolve();
|
|
266
364
|
break;
|
|
@@ -276,7 +374,7 @@ export class LocalSession {
|
|
|
276
374
|
break;
|
|
277
375
|
case "inspect":
|
|
278
376
|
await this.#reply(message.id, message.sessionId, async () => {
|
|
279
|
-
const globalAgents = await this.#
|
|
377
|
+
const globalAgents = await this.#globalAgents();
|
|
280
378
|
return {
|
|
281
379
|
type: "result",
|
|
282
380
|
id: message.id,
|
|
@@ -287,24 +385,7 @@ export class LocalSession {
|
|
|
287
385
|
});
|
|
288
386
|
break;
|
|
289
387
|
case "history":
|
|
290
|
-
await this.#
|
|
291
|
-
const context = this.#context;
|
|
292
|
-
if (!context) throw new Error("Chappie session is not available");
|
|
293
|
-
const history = historyResult(
|
|
294
|
-
context.sessionManager.getBranch(),
|
|
295
|
-
message.sessionId,
|
|
296
|
-
message.range,
|
|
297
|
-
);
|
|
298
|
-
this.#notify(
|
|
299
|
-
`${chatLabel(message)} read history: ${history.count} entries`,
|
|
300
|
-
"info",
|
|
301
|
-
{
|
|
302
|
-
event: "history",
|
|
303
|
-
...source(message.chatId, message.requestId),
|
|
304
|
-
},
|
|
305
|
-
);
|
|
306
|
-
return { type: "result", id: message.id, cwd: context.cwd, history };
|
|
307
|
-
});
|
|
388
|
+
await this.#readHistory(message);
|
|
308
389
|
break;
|
|
309
390
|
case "ackInputs":
|
|
310
391
|
if (
|
|
@@ -317,10 +398,52 @@ export class LocalSession {
|
|
|
317
398
|
await this.#reply(message.id, message.sessionId, async () => ({
|
|
318
399
|
type: "result",
|
|
319
400
|
id: message.id,
|
|
320
|
-
resource: await readSessionResource(
|
|
401
|
+
resource: await readSessionResource(
|
|
402
|
+
message.sessionId,
|
|
403
|
+
message.uri,
|
|
404
|
+
message.offset,
|
|
405
|
+
),
|
|
321
406
|
}));
|
|
322
407
|
break;
|
|
408
|
+
case "copy": {
|
|
409
|
+
const controller = new AbortController();
|
|
410
|
+
this.#copies.set(message.id, controller);
|
|
411
|
+
void this.#reply(message.id, message.sessionId, async () => {
|
|
412
|
+
const context = this.#context;
|
|
413
|
+
if (!context) throw new Error("Chappie session is not available");
|
|
414
|
+
const files = await copyFiles(
|
|
415
|
+
message.paths,
|
|
416
|
+
message.resources,
|
|
417
|
+
context.cwd,
|
|
418
|
+
message.overwrite === true,
|
|
419
|
+
(resource) => this.#readChunks(resource, controller.signal),
|
|
420
|
+
controller.signal,
|
|
421
|
+
);
|
|
422
|
+
return {
|
|
423
|
+
type: "result",
|
|
424
|
+
id: message.id,
|
|
425
|
+
transfer: {
|
|
426
|
+
device: hostname(),
|
|
427
|
+
files,
|
|
428
|
+
resources: [],
|
|
429
|
+
to: { sessionId: message.sessionId, device: hostname() },
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
})
|
|
433
|
+
.finally(() => this.#copies.delete(message.id))
|
|
434
|
+
.catch(() => {});
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
323
437
|
case "cancel": {
|
|
438
|
+
const copying = this.#copies.get(message.id);
|
|
439
|
+
if (copying) {
|
|
440
|
+
copying.abort(new Error(message.reason));
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
if (this.#histories.has(message.id)) {
|
|
444
|
+
this.#finishHistory(message.id);
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
324
447
|
const queued = this.#queue.findIndex(
|
|
325
448
|
(request) => request.id === message.id,
|
|
326
449
|
);
|
|
@@ -370,6 +493,125 @@ export class LocalSession {
|
|
|
370
493
|
}
|
|
371
494
|
}
|
|
372
495
|
|
|
496
|
+
async #request(
|
|
497
|
+
request: SessionRequest,
|
|
498
|
+
signal?: AbortSignal,
|
|
499
|
+
): Promise<SessionResult> {
|
|
500
|
+
signal?.throwIfAborted();
|
|
501
|
+
const connection = this.#connection;
|
|
502
|
+
if (!connection?.connected) throw new Error("Chappie is not connected");
|
|
503
|
+
const id = this.#nextRequestId++;
|
|
504
|
+
const completion = Promise.withResolvers<SessionResult>();
|
|
505
|
+
const onAbort = (): void => {
|
|
506
|
+
completion.reject(signal?.reason);
|
|
507
|
+
void connection.send({ type: "cancelRequest", id }).catch(() => {});
|
|
508
|
+
};
|
|
509
|
+
this.#requests.set(id, completion);
|
|
510
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
511
|
+
void connection
|
|
512
|
+
.send({ type: "request", id, request })
|
|
513
|
+
.catch(completion.reject);
|
|
514
|
+
try {
|
|
515
|
+
return await completion.promise;
|
|
516
|
+
} finally {
|
|
517
|
+
this.#requests.delete(id);
|
|
518
|
+
signal?.removeEventListener("abort", onAbort);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async *#readChunks(
|
|
523
|
+
resource: ResourceDescriptor,
|
|
524
|
+
signal: AbortSignal,
|
|
525
|
+
): AsyncGenerator<Uint8Array> {
|
|
526
|
+
for (let offset = 0; offset < resource.size; ) {
|
|
527
|
+
const result = await this.#request(
|
|
528
|
+
{
|
|
529
|
+
type: "readResource",
|
|
530
|
+
sessionId: resourceSessionId(resource.uri),
|
|
531
|
+
uri: resource.uri,
|
|
532
|
+
offset,
|
|
533
|
+
},
|
|
534
|
+
signal,
|
|
535
|
+
);
|
|
536
|
+
if (!("resource" in result))
|
|
537
|
+
throw new Error("Pi session returned no resource");
|
|
538
|
+
const data = Buffer.from(result.resource.blob, "base64");
|
|
539
|
+
if (data.length === 0)
|
|
540
|
+
throw new Error(
|
|
541
|
+
`Source ended before ${resource.size} bytes: ${resource.name}`,
|
|
542
|
+
);
|
|
543
|
+
yield data;
|
|
544
|
+
offset += data.length;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
#cancelRequests(error: Error): void {
|
|
549
|
+
for (const pending of this.#requests.values()) pending.reject(error);
|
|
550
|
+
this.#requests.clear();
|
|
551
|
+
for (const controller of this.#copies.values()) controller.abort(error);
|
|
552
|
+
this.#copies.clear();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async #readHistory(
|
|
556
|
+
request: HistoryRequest["request"],
|
|
557
|
+
wait = request.range.wait,
|
|
558
|
+
): Promise<void> {
|
|
559
|
+
try {
|
|
560
|
+
const context = this.#context;
|
|
561
|
+
if (context?.sessionManager.getSessionId() !== request.sessionId)
|
|
562
|
+
throw new Error("The requested Pi session is no longer active");
|
|
563
|
+
const history = historyResult(
|
|
564
|
+
context.sessionManager.getBranch(),
|
|
565
|
+
request.sessionId,
|
|
566
|
+
request.range,
|
|
567
|
+
);
|
|
568
|
+
if (wait && !request.range.before && history.count === 0) {
|
|
569
|
+
if (!this.#histories.has(request.id)) {
|
|
570
|
+
this.#histories.set(request.id, {
|
|
571
|
+
request,
|
|
572
|
+
timeout: setTimeout(() => {
|
|
573
|
+
void this.#readHistory(request, false).catch(() => {});
|
|
574
|
+
}, 30_000),
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
this.#finishHistory(request.id);
|
|
580
|
+
if (!request.range.observer) {
|
|
581
|
+
this.#notify(
|
|
582
|
+
`${chatLabel(request)} read history: ${history.count} entries`,
|
|
583
|
+
"info",
|
|
584
|
+
{ event: "history", ...source(request.chatId, request.requestId) },
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
await this.#connection?.send({
|
|
588
|
+
type: "result",
|
|
589
|
+
id: request.id,
|
|
590
|
+
cwd: context.cwd,
|
|
591
|
+
history,
|
|
592
|
+
});
|
|
593
|
+
} catch (error) {
|
|
594
|
+
this.#finishHistory(request.id);
|
|
595
|
+
await this.#sendError(
|
|
596
|
+
request.id,
|
|
597
|
+
error instanceof Error ? error.message : String(error),
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
#historyChanged(): void {
|
|
603
|
+
for (const { request } of this.#histories.values()) {
|
|
604
|
+
void this.#readHistory(request).catch(() => {});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
#finishHistory(id: number): void {
|
|
609
|
+
const pending = this.#histories.get(id);
|
|
610
|
+
if (!pending) return;
|
|
611
|
+
clearTimeout(pending.timeout);
|
|
612
|
+
this.#histories.delete(id);
|
|
613
|
+
}
|
|
614
|
+
|
|
373
615
|
#inspection(): SessionInspection {
|
|
374
616
|
const activeTools = new Set(this.#pi.getActiveTools());
|
|
375
617
|
this.#collectInputs();
|
|
@@ -384,9 +626,11 @@ export class LocalSession {
|
|
|
384
626
|
};
|
|
385
627
|
}
|
|
386
628
|
|
|
387
|
-
async #
|
|
629
|
+
async #globalAgents(): Promise<{ path: string } | undefined> {
|
|
630
|
+
const path = resolve(this.#agentDir, "AGENTS.md");
|
|
388
631
|
try {
|
|
389
|
-
|
|
632
|
+
await access(path);
|
|
633
|
+
return { path };
|
|
390
634
|
} catch (error) {
|
|
391
635
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
392
636
|
throw error;
|
|
@@ -450,6 +694,7 @@ export class LocalSession {
|
|
|
450
694
|
context: ExtensionContext,
|
|
451
695
|
): Promise<void> {
|
|
452
696
|
this.#context = context;
|
|
697
|
+
this.#historyChanged();
|
|
453
698
|
const active = this.#active;
|
|
454
699
|
if (!active || message !== active.message) return;
|
|
455
700
|
const sessionId = active.session.id;
|
package/src/state.ts
CHANGED
|
@@ -5,7 +5,6 @@ import type { QuestionAnswer, QuestionRecord } from "./questions.ts";
|
|
|
5
5
|
|
|
6
6
|
interface StateFile {
|
|
7
7
|
bindings?: Record<string, string>;
|
|
8
|
-
codes?: Record<string, string>;
|
|
9
8
|
deliveries?: DeliveryRecord[];
|
|
10
9
|
questions?: QuestionRecord[];
|
|
11
10
|
}
|
|
@@ -14,7 +13,6 @@ export class State {
|
|
|
14
13
|
readonly #path: string;
|
|
15
14
|
readonly #temporaryPath: string;
|
|
16
15
|
readonly #bindings = new Map<string, string>();
|
|
17
|
-
readonly #codes = new Map<string, string>();
|
|
18
16
|
readonly #deliveries = new Map<string, DeliveryRecord>();
|
|
19
17
|
readonly #questions = new Map<string, QuestionRecord>();
|
|
20
18
|
#writes = Promise.resolve();
|
|
@@ -36,9 +34,6 @@ export class State {
|
|
|
36
34
|
for (const [chatId, sessionId] of Object.entries(state.bindings ?? {})) {
|
|
37
35
|
if (typeof sessionId === "string") this.#bindings.set(chatId, sessionId);
|
|
38
36
|
}
|
|
39
|
-
for (const [sessionId, code] of Object.entries(state.codes ?? {})) {
|
|
40
|
-
this.#codes.set(sessionId, code);
|
|
41
|
-
}
|
|
42
37
|
for (const delivery of state.deliveries ?? []) {
|
|
43
38
|
if (delivery?.id) this.#deliveries.set(delivery.id, delivery);
|
|
44
39
|
}
|
|
@@ -46,16 +41,6 @@ export class State {
|
|
|
46
41
|
this.#questions.set(question.id, question);
|
|
47
42
|
}
|
|
48
43
|
|
|
49
|
-
code(sessionId: string): string | undefined {
|
|
50
|
-
return this.#codes.get(sessionId);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
setCode(sessionId: string, code: string | undefined): Promise<void> {
|
|
54
|
-
if (code === undefined) this.#codes.delete(sessionId);
|
|
55
|
-
else this.#codes.set(sessionId, code);
|
|
56
|
-
return this.#save();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
44
|
binding(chatId: string): string | undefined {
|
|
60
45
|
return this.#bindings.get(chatId);
|
|
61
46
|
}
|
|
@@ -163,10 +148,9 @@ export class State {
|
|
|
163
148
|
#save(): Promise<void> {
|
|
164
149
|
const saved = this.#writes.then(async () => {
|
|
165
150
|
const bindings = Object.fromEntries(this.#bindings);
|
|
166
|
-
const codes = Object.fromEntries(this.#codes);
|
|
167
151
|
await writeFile(
|
|
168
152
|
this.#temporaryPath,
|
|
169
|
-
`${JSON.stringify({ bindings,
|
|
153
|
+
`${JSON.stringify({ bindings, deliveries: [...this.#deliveries.values()], questions: [...this.#questions.values()] }, null, 2)}\n`,
|
|
170
154
|
{
|
|
171
155
|
mode: 0o600,
|
|
172
156
|
},
|