@adecore/agents 0.46.1 → 0.46.3
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/dist/chat/chat-core.d.ts +9 -0
- package/dist/chat/chat-core.js +86 -15
- package/dist/chat/payload-store.js +1 -1
- package/package.json +3 -3
- package/src/chat/chat-core.ts +110 -15
- package/src/chat/payload-store.ts +1 -1
package/dist/chat/chat-core.d.ts
CHANGED
|
@@ -82,6 +82,7 @@ export interface ChatCoreOptions {
|
|
|
82
82
|
};
|
|
83
83
|
accounts?: AccountLaunches;
|
|
84
84
|
hooks?: ChatCoreHooks;
|
|
85
|
+
unloadAfterMs?: number;
|
|
85
86
|
}
|
|
86
87
|
export declare class ChatCore {
|
|
87
88
|
protected readonly bookmarks: BookmarkStore | null;
|
|
@@ -123,6 +124,9 @@ export declare class ChatCore {
|
|
|
123
124
|
private readonly nameChat;
|
|
124
125
|
private readonly onInterruptedRun;
|
|
125
126
|
private readonly limitResume;
|
|
127
|
+
private readonly unloadAfterMs;
|
|
128
|
+
private readonly unloadTimers;
|
|
129
|
+
private readonly unloadedInfo;
|
|
126
130
|
constructor(options: ChatCoreOptions);
|
|
127
131
|
codexProcess(info: Pick<ChatInfo, 'cwd' | 'account'>): CodexProcessSpec;
|
|
128
132
|
claudeProjectsDirOf(info: Pick<ChatInfo, 'account'>): string;
|
|
@@ -257,6 +261,11 @@ export declare class ChatCore {
|
|
|
257
261
|
updatedAt: number;
|
|
258
262
|
}[];
|
|
259
263
|
list(): ChatInfo[];
|
|
264
|
+
unloadIdle(chatId: string): Promise<boolean>;
|
|
265
|
+
private unloadable;
|
|
266
|
+
private scheduleUnload;
|
|
267
|
+
private cancelUnload;
|
|
268
|
+
private forgetLoaded;
|
|
260
269
|
get(chatId: string): ChatSession | undefined;
|
|
261
270
|
resumeRun(chatId: string, turnId: string, attempt: number): Promise<void>;
|
|
262
271
|
takeUpAfterLimit(chatId: string, turnId: string): Promise<OutboxOutcome>;
|
package/dist/chat/chat-core.js
CHANGED
|
@@ -94,6 +94,10 @@ export class ChatCore {
|
|
|
94
94
|
nameChat;
|
|
95
95
|
onInterruptedRun;
|
|
96
96
|
limitResume;
|
|
97
|
+
unloadAfterMs;
|
|
98
|
+
unloadTimers = new Map();
|
|
99
|
+
// The last info of each chat that left memory, so `list` still answers what it was doing.
|
|
100
|
+
unloadedInfo = new Map();
|
|
97
101
|
constructor(options) {
|
|
98
102
|
this.uiQueries = options.intelligentUi ? new ChatUiQueries(options.intelligentUi) : null;
|
|
99
103
|
this.uiFenceLanguage = options.uiFenceLanguage;
|
|
@@ -111,6 +115,7 @@ export class ChatCore {
|
|
|
111
115
|
this.nameChat = options.nameChat ?? null;
|
|
112
116
|
this.onInterruptedRun = options.onInterruptedRun ?? null;
|
|
113
117
|
this.limitResume = options.limitResume ?? null;
|
|
118
|
+
this.unloadAfterMs = options.store && options.unloadAfterMs !== undefined ? options.unloadAfterMs : null;
|
|
114
119
|
this.hooks = options.hooks ?? {};
|
|
115
120
|
this.commands = {
|
|
116
121
|
...(options.command ? { claude: options.command } : {}),
|
|
@@ -325,12 +330,15 @@ export class ChatCore {
|
|
|
325
330
|
...(this.limitResume ? { limitResume: limitHooks(this.limitResume, payload.chatId) } : {})
|
|
326
331
|
});
|
|
327
332
|
this.chats.set(session.id, session);
|
|
333
|
+
this.unloadedInfo.delete(session.id);
|
|
328
334
|
if (stored) {
|
|
329
335
|
session.settleStored(selection, resume);
|
|
330
336
|
session.settleOwedResume();
|
|
331
337
|
await session.settleOrphanedSubagents();
|
|
332
338
|
}
|
|
333
339
|
await this.hooks.opened?.(session, stored ?? null);
|
|
340
|
+
// A chat loaded for someone who never attaches, such as a host reading it, leaves again on its own.
|
|
341
|
+
this.scheduleUnload(session.id);
|
|
334
342
|
return session.info;
|
|
335
343
|
}
|
|
336
344
|
async storedBackground(chatId) {
|
|
@@ -392,6 +400,7 @@ export class ChatCore {
|
|
|
392
400
|
// client is on the list: the text is either in the snapshot or in the stream, never both.
|
|
393
401
|
this.coalescers.get(chatId)?.flush();
|
|
394
402
|
clients.add(clientId);
|
|
403
|
+
this.cancelUnload(chatId);
|
|
395
404
|
const log = this.logs.get(chatId);
|
|
396
405
|
const events = since === undefined ? null : log.after(since);
|
|
397
406
|
if (events !== null) {
|
|
@@ -509,12 +518,14 @@ export class ChatCore {
|
|
|
509
518
|
detach(chatId, clientId) {
|
|
510
519
|
this.coalescers.get(chatId)?.flush();
|
|
511
520
|
this.attached.get(chatId)?.delete(clientId);
|
|
521
|
+
this.scheduleUnload(chatId);
|
|
512
522
|
}
|
|
513
523
|
detachAll(clientId) {
|
|
514
524
|
for (const [chatId, clients] of this.attached) {
|
|
515
525
|
if (clients.has(clientId)) {
|
|
516
526
|
this.coalescers.get(chatId)?.flush();
|
|
517
527
|
clients.delete(clientId);
|
|
528
|
+
this.scheduleUnload(chatId);
|
|
518
529
|
}
|
|
519
530
|
}
|
|
520
531
|
this.subagents.releaseClient(clientId);
|
|
@@ -613,6 +624,7 @@ export class ChatCore {
|
|
|
613
624
|
if (this.chats.has(chatId) || this.creating.has(chatId)) {
|
|
614
625
|
return false;
|
|
615
626
|
}
|
|
627
|
+
this.unloadedInfo.delete(chatId);
|
|
616
628
|
await Promise.all([
|
|
617
629
|
this.store?.delete(chatId),
|
|
618
630
|
this.attachments.removeAll(chatId),
|
|
@@ -897,21 +909,9 @@ export class ChatCore {
|
|
|
897
909
|
// A write already out would put the record back after the delete.
|
|
898
910
|
const writing = this.writes.get(chatId);
|
|
899
911
|
const ended = session.dispose();
|
|
900
|
-
this.
|
|
901
|
-
this.uiQueries?.forget(chatId);
|
|
902
|
-
this.coalescers.get(chatId)?.dispose();
|
|
903
|
-
this.coalescers.delete(chatId);
|
|
904
|
-
this.logs.get(chatId)?.close();
|
|
905
|
-
this.logs.delete(chatId);
|
|
906
|
-
this.cancelWaiting(chatId);
|
|
907
|
-
this.sizes.delete(chatId);
|
|
908
|
-
this.unlogged.delete(chatId);
|
|
909
|
-
this.writes.delete(chatId);
|
|
910
|
-
this.activity.delete(chatId);
|
|
911
|
-
this.chats.delete(chatId);
|
|
912
|
+
this.forgetLoaded(chatId);
|
|
912
913
|
this.attached.delete(chatId);
|
|
913
|
-
this.
|
|
914
|
-
this.announced.delete(chatId);
|
|
914
|
+
this.unloadedInfo.delete(chatId);
|
|
915
915
|
const snapshot = session.thread.snapshot();
|
|
916
916
|
this.hooks.forgotten?.(chatId);
|
|
917
917
|
await writing;
|
|
@@ -944,7 +944,75 @@ export class ChatCore {
|
|
|
944
944
|
return targets;
|
|
945
945
|
}
|
|
946
946
|
list() {
|
|
947
|
-
return [...this.chats.values()].map((session) => session.info);
|
|
947
|
+
return [...[...this.chats.values()].map((session) => session.info), ...this.unloadedInfo.values()];
|
|
948
|
+
}
|
|
949
|
+
/*
|
|
950
|
+
* Puts a chat nobody needs in memory back on disk alone: nobody reads it, it has no CLI, no turn,
|
|
951
|
+
* request, hold or move is under way, and no resume is owed. Answers whether it left. The next
|
|
952
|
+
* create, attach or note loads it again from its record, as after a restart.
|
|
953
|
+
*/
|
|
954
|
+
async unloadIdle(chatId) {
|
|
955
|
+
this.cancelUnload(chatId);
|
|
956
|
+
if (!this.unloadable(chatId)) {
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
await this.persistNow(chatId, true);
|
|
960
|
+
const session = this.chats.get(chatId);
|
|
961
|
+
// Someone may have attached or sent while the record was written.
|
|
962
|
+
if (!session || !this.unloadable(chatId)) {
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
this.forgetLoaded(chatId);
|
|
966
|
+
this.attached.delete(chatId);
|
|
967
|
+
this.unloadedInfo.set(chatId, session.info);
|
|
968
|
+
await session.dispose();
|
|
969
|
+
return true;
|
|
970
|
+
}
|
|
971
|
+
unloadable(chatId) {
|
|
972
|
+
const session = this.chats.get(chatId);
|
|
973
|
+
return (session !== undefined &&
|
|
974
|
+
this.store !== null &&
|
|
975
|
+
(this.attached.get(chatId)?.size ?? 0) === 0 &&
|
|
976
|
+
!this.creating.has(chatId) &&
|
|
977
|
+
!this.relocations.has(chatId) &&
|
|
978
|
+
session.pid === null &&
|
|
979
|
+
session.relocatable &&
|
|
980
|
+
session.info.resumeAt === undefined &&
|
|
981
|
+
(session.info.requests?.length ?? 0) === 0);
|
|
982
|
+
}
|
|
983
|
+
scheduleUnload(chatId) {
|
|
984
|
+
if (this.unloadAfterMs === null || (this.attached.get(chatId)?.size ?? 0) > 0) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
this.cancelUnload(chatId);
|
|
988
|
+
const timer = setTimeout(() => {
|
|
989
|
+
this.unloadTimers.delete(chatId);
|
|
990
|
+
void this.unloadIdle(chatId).catch((e) => console.error(`Unloading chat ${chatId} failed:`, errorText(e)));
|
|
991
|
+
}, this.unloadAfterMs);
|
|
992
|
+
timer.unref?.();
|
|
993
|
+
this.unloadTimers.set(chatId, timer);
|
|
994
|
+
}
|
|
995
|
+
cancelUnload(chatId) {
|
|
996
|
+
clearTimeout(this.unloadTimers.get(chatId));
|
|
997
|
+
this.unloadTimers.delete(chatId);
|
|
998
|
+
}
|
|
999
|
+
/* Drops what the core keeps of a loaded chat beside its session, which a kill and an unload share. */
|
|
1000
|
+
forgetLoaded(chatId) {
|
|
1001
|
+
this.subagents.releaseChat(chatId);
|
|
1002
|
+
this.uiQueries?.forget(chatId);
|
|
1003
|
+
this.coalescers.get(chatId)?.dispose();
|
|
1004
|
+
this.coalescers.delete(chatId);
|
|
1005
|
+
this.logs.get(chatId)?.close();
|
|
1006
|
+
this.logs.delete(chatId);
|
|
1007
|
+
this.cancelWaiting(chatId);
|
|
1008
|
+
this.cancelUnload(chatId);
|
|
1009
|
+
this.sizes.delete(chatId);
|
|
1010
|
+
this.unlogged.delete(chatId);
|
|
1011
|
+
this.writes.delete(chatId);
|
|
1012
|
+
this.activity.delete(chatId);
|
|
1013
|
+
this.chats.delete(chatId);
|
|
1014
|
+
this.listChanges.delete(chatId);
|
|
1015
|
+
this.announced.delete(chatId);
|
|
948
1016
|
}
|
|
949
1017
|
get(chatId) {
|
|
950
1018
|
return this.chats.get(chatId);
|
|
@@ -1032,6 +1100,7 @@ export class ChatCore {
|
|
|
1032
1100
|
}
|
|
1033
1101
|
for (const chatId of this.chats.keys()) {
|
|
1034
1102
|
this.cancelWaiting(chatId);
|
|
1103
|
+
this.cancelUnload(chatId);
|
|
1035
1104
|
}
|
|
1036
1105
|
// A thread that cannot be written still ends its CLI, whose process group would otherwise outlive the host.
|
|
1037
1106
|
const written = await Promise.allSettled([...this.chats.keys()].map((chatId) => this.persistNow(chatId, true)));
|
|
@@ -1173,6 +1242,8 @@ export class ChatCore {
|
|
|
1173
1242
|
for (const clientId of this.attached.get(chatId) ?? []) {
|
|
1174
1243
|
this.sinks.to(clientId, { event: 'chat.event', payload });
|
|
1175
1244
|
}
|
|
1245
|
+
// A chat nobody reads counts its time from what it said last, such as its CLI going down.
|
|
1246
|
+
this.scheduleUnload(chatId);
|
|
1176
1247
|
// After the thread, so a client reading it has already had this info and the status is a no-op there.
|
|
1177
1248
|
if (event.type === 'info' || event.type === 'reset') {
|
|
1178
1249
|
this.announceStatus(chatId, event.info);
|
|
@@ -8,7 +8,7 @@ import { CHAT_PAYLOAD_INLINE_BYTES, ChatCheckpointDiffSchema } from '@adecore/ag
|
|
|
8
8
|
import { isNotFound, writeAtomic, writeAtomicSync } from '../fs.js';
|
|
9
9
|
import { ChatError } from './errors.js';
|
|
10
10
|
/* How much of a value stored apart the item keeps: what a timeline shows before anyone asks for the rest. */
|
|
11
|
-
export const PAYLOAD_PREVIEW_BYTES =
|
|
11
|
+
export const PAYLOAD_PREVIEW_BYTES = 2 * 1024;
|
|
12
12
|
const FOLDER_SUFFIX = '.payloads';
|
|
13
13
|
const ID_PATTERN = /^[0-9a-f]{64}$/;
|
|
14
14
|
const CONTINUATION_MASK = 0xc0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agents",
|
|
3
|
-
"version": "0.46.
|
|
3
|
+
"version": "0.46.3",
|
|
4
4
|
"description": "Node-compatible agent chat hosts, providers, accounts, usage and durable task coordination.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -93,8 +93,8 @@
|
|
|
93
93
|
"test": "bun --conditions=source test"
|
|
94
94
|
},
|
|
95
95
|
"dependencies": {
|
|
96
|
-
"@adecore/agent-contracts": "0.46.
|
|
97
|
-
"@adecore/intelligent-ui": "0.46.
|
|
96
|
+
"@adecore/agent-contracts": "0.46.3",
|
|
97
|
+
"@adecore/intelligent-ui": "0.46.3",
|
|
98
98
|
"zod": "^4.6.7"
|
|
99
99
|
},
|
|
100
100
|
"devDependencies": {
|
package/src/chat/chat-core.ts
CHANGED
|
@@ -215,6 +215,12 @@ export interface ChatCoreOptions {
|
|
|
215
215
|
// The accounts a chat may run under; without them every chat runs under its CLI's default account.
|
|
216
216
|
accounts?: AccountLaunches;
|
|
217
217
|
hooks?: ChatCoreHooks;
|
|
218
|
+
/*
|
|
219
|
+
* How long a chat stays in memory once nobody reads it, its CLI is gone and nothing is under way;
|
|
220
|
+
* after that only its info stays, for `list`, until something loads it again. Without it, or
|
|
221
|
+
* without a store, a loaded chat stays until it is removed or the host stops.
|
|
222
|
+
*/
|
|
223
|
+
unloadAfterMs?: number;
|
|
218
224
|
}
|
|
219
225
|
|
|
220
226
|
/*
|
|
@@ -272,6 +278,10 @@ export class ChatCore {
|
|
|
272
278
|
private readonly nameChat: ChatCoreOptions['nameChat'] | null;
|
|
273
279
|
private readonly onInterruptedRun: ChatCoreOptions['onInterruptedRun'] | null;
|
|
274
280
|
private readonly limitResume: ChatCoreOptions['limitResume'] | null;
|
|
281
|
+
private readonly unloadAfterMs: number | null;
|
|
282
|
+
private readonly unloadTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
283
|
+
// The last info of each chat that left memory, so `list` still answers what it was doing.
|
|
284
|
+
private readonly unloadedInfo = new Map<string, ChatInfo>();
|
|
275
285
|
|
|
276
286
|
constructor(options: ChatCoreOptions) {
|
|
277
287
|
this.uiQueries = options.intelligentUi ? new ChatUiQueries(options.intelligentUi) : null;
|
|
@@ -290,6 +300,7 @@ export class ChatCore {
|
|
|
290
300
|
this.nameChat = options.nameChat ?? null;
|
|
291
301
|
this.onInterruptedRun = options.onInterruptedRun ?? null;
|
|
292
302
|
this.limitResume = options.limitResume ?? null;
|
|
303
|
+
this.unloadAfterMs = options.store && options.unloadAfterMs !== undefined ? options.unloadAfterMs : null;
|
|
293
304
|
this.hooks = options.hooks ?? {};
|
|
294
305
|
|
|
295
306
|
this.commands = {
|
|
@@ -549,6 +560,7 @@ export class ChatCore {
|
|
|
549
560
|
});
|
|
550
561
|
|
|
551
562
|
this.chats.set(session.id, session);
|
|
563
|
+
this.unloadedInfo.delete(session.id);
|
|
552
564
|
|
|
553
565
|
if (stored) {
|
|
554
566
|
session.settleStored(selection, resume);
|
|
@@ -557,6 +569,8 @@ export class ChatCore {
|
|
|
557
569
|
}
|
|
558
570
|
|
|
559
571
|
await this.hooks.opened?.(session, stored ?? null);
|
|
572
|
+
// A chat loaded for someone who never attaches, such as a host reading it, leaves again on its own.
|
|
573
|
+
this.scheduleUnload(session.id);
|
|
560
574
|
|
|
561
575
|
return session.info;
|
|
562
576
|
}
|
|
@@ -642,6 +656,7 @@ export class ChatCore {
|
|
|
642
656
|
// client is on the list: the text is either in the snapshot or in the stream, never both.
|
|
643
657
|
this.coalescers.get(chatId)?.flush();
|
|
644
658
|
clients.add(clientId);
|
|
659
|
+
this.cancelUnload(chatId);
|
|
645
660
|
const log = this.logs.get(chatId)!;
|
|
646
661
|
const events = since === undefined ? null : log.after(since);
|
|
647
662
|
|
|
@@ -797,6 +812,7 @@ export class ChatCore {
|
|
|
797
812
|
detach(chatId: string, clientId: string): void {
|
|
798
813
|
this.coalescers.get(chatId)?.flush();
|
|
799
814
|
this.attached.get(chatId)?.delete(clientId);
|
|
815
|
+
this.scheduleUnload(chatId);
|
|
800
816
|
}
|
|
801
817
|
|
|
802
818
|
detachAll(clientId: string): void {
|
|
@@ -804,6 +820,7 @@ export class ChatCore {
|
|
|
804
820
|
if (clients.has(clientId)) {
|
|
805
821
|
this.coalescers.get(chatId)?.flush();
|
|
806
822
|
clients.delete(clientId);
|
|
823
|
+
this.scheduleUnload(chatId);
|
|
807
824
|
}
|
|
808
825
|
}
|
|
809
826
|
|
|
@@ -931,6 +948,8 @@ export class ChatCore {
|
|
|
931
948
|
return false;
|
|
932
949
|
}
|
|
933
950
|
|
|
951
|
+
this.unloadedInfo.delete(chatId);
|
|
952
|
+
|
|
934
953
|
await Promise.all([
|
|
935
954
|
this.store?.delete(chatId),
|
|
936
955
|
this.attachments.removeAll(chatId),
|
|
@@ -1295,21 +1314,9 @@ export class ChatCore {
|
|
|
1295
1314
|
// A write already out would put the record back after the delete.
|
|
1296
1315
|
const writing = this.writes.get(chatId);
|
|
1297
1316
|
const ended = session.dispose();
|
|
1298
|
-
this.
|
|
1299
|
-
this.uiQueries?.forget(chatId);
|
|
1300
|
-
this.coalescers.get(chatId)?.dispose();
|
|
1301
|
-
this.coalescers.delete(chatId);
|
|
1302
|
-
this.logs.get(chatId)?.close();
|
|
1303
|
-
this.logs.delete(chatId);
|
|
1304
|
-
this.cancelWaiting(chatId);
|
|
1305
|
-
this.sizes.delete(chatId);
|
|
1306
|
-
this.unlogged.delete(chatId);
|
|
1307
|
-
this.writes.delete(chatId);
|
|
1308
|
-
this.activity.delete(chatId);
|
|
1309
|
-
this.chats.delete(chatId);
|
|
1317
|
+
this.forgetLoaded(chatId);
|
|
1310
1318
|
this.attached.delete(chatId);
|
|
1311
|
-
this.
|
|
1312
|
-
this.announced.delete(chatId);
|
|
1319
|
+
this.unloadedInfo.delete(chatId);
|
|
1313
1320
|
const snapshot = session.thread.snapshot();
|
|
1314
1321
|
this.hooks.forgotten?.(chatId);
|
|
1315
1322
|
await writing;
|
|
@@ -1349,7 +1356,91 @@ export class ChatCore {
|
|
|
1349
1356
|
}
|
|
1350
1357
|
|
|
1351
1358
|
list(): ChatInfo[] {
|
|
1352
|
-
return [...this.chats.values()].map((session) => session.info);
|
|
1359
|
+
return [...[...this.chats.values()].map((session) => session.info), ...this.unloadedInfo.values()];
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/*
|
|
1363
|
+
* Puts a chat nobody needs in memory back on disk alone: nobody reads it, it has no CLI, no turn,
|
|
1364
|
+
* request, hold or move is under way, and no resume is owed. Answers whether it left. The next
|
|
1365
|
+
* create, attach or note loads it again from its record, as after a restart.
|
|
1366
|
+
*/
|
|
1367
|
+
async unloadIdle(chatId: string): Promise<boolean> {
|
|
1368
|
+
this.cancelUnload(chatId);
|
|
1369
|
+
|
|
1370
|
+
if (!this.unloadable(chatId)) {
|
|
1371
|
+
return false;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
await this.persistNow(chatId, true);
|
|
1375
|
+
const session = this.chats.get(chatId);
|
|
1376
|
+
|
|
1377
|
+
// Someone may have attached or sent while the record was written.
|
|
1378
|
+
if (!session || !this.unloadable(chatId)) {
|
|
1379
|
+
return false;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
this.forgetLoaded(chatId);
|
|
1383
|
+
this.attached.delete(chatId);
|
|
1384
|
+
this.unloadedInfo.set(chatId, session.info);
|
|
1385
|
+
await session.dispose();
|
|
1386
|
+
|
|
1387
|
+
return true;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
private unloadable(chatId: string): boolean {
|
|
1391
|
+
const session = this.chats.get(chatId);
|
|
1392
|
+
|
|
1393
|
+
return (
|
|
1394
|
+
session !== undefined &&
|
|
1395
|
+
this.store !== null &&
|
|
1396
|
+
(this.attached.get(chatId)?.size ?? 0) === 0 &&
|
|
1397
|
+
!this.creating.has(chatId) &&
|
|
1398
|
+
!this.relocations.has(chatId) &&
|
|
1399
|
+
session.pid === null &&
|
|
1400
|
+
session.relocatable &&
|
|
1401
|
+
session.info.resumeAt === undefined &&
|
|
1402
|
+
(session.info.requests?.length ?? 0) === 0
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
private scheduleUnload(chatId: string): void {
|
|
1407
|
+
if (this.unloadAfterMs === null || (this.attached.get(chatId)?.size ?? 0) > 0) {
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
this.cancelUnload(chatId);
|
|
1412
|
+
|
|
1413
|
+
const timer = setTimeout(() => {
|
|
1414
|
+
this.unloadTimers.delete(chatId);
|
|
1415
|
+
void this.unloadIdle(chatId).catch((e) => console.error(`Unloading chat ${chatId} failed:`, errorText(e)));
|
|
1416
|
+
}, this.unloadAfterMs);
|
|
1417
|
+
|
|
1418
|
+
timer.unref?.();
|
|
1419
|
+
this.unloadTimers.set(chatId, timer);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
private cancelUnload(chatId: string): void {
|
|
1423
|
+
clearTimeout(this.unloadTimers.get(chatId));
|
|
1424
|
+
this.unloadTimers.delete(chatId);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/* Drops what the core keeps of a loaded chat beside its session, which a kill and an unload share. */
|
|
1428
|
+
private forgetLoaded(chatId: string): void {
|
|
1429
|
+
this.subagents.releaseChat(chatId);
|
|
1430
|
+
this.uiQueries?.forget(chatId);
|
|
1431
|
+
this.coalescers.get(chatId)?.dispose();
|
|
1432
|
+
this.coalescers.delete(chatId);
|
|
1433
|
+
this.logs.get(chatId)?.close();
|
|
1434
|
+
this.logs.delete(chatId);
|
|
1435
|
+
this.cancelWaiting(chatId);
|
|
1436
|
+
this.cancelUnload(chatId);
|
|
1437
|
+
this.sizes.delete(chatId);
|
|
1438
|
+
this.unlogged.delete(chatId);
|
|
1439
|
+
this.writes.delete(chatId);
|
|
1440
|
+
this.activity.delete(chatId);
|
|
1441
|
+
this.chats.delete(chatId);
|
|
1442
|
+
this.listChanges.delete(chatId);
|
|
1443
|
+
this.announced.delete(chatId);
|
|
1353
1444
|
}
|
|
1354
1445
|
|
|
1355
1446
|
get(chatId: string): ChatSession | undefined {
|
|
@@ -1460,6 +1551,7 @@ export class ChatCore {
|
|
|
1460
1551
|
|
|
1461
1552
|
for (const chatId of this.chats.keys()) {
|
|
1462
1553
|
this.cancelWaiting(chatId);
|
|
1554
|
+
this.cancelUnload(chatId);
|
|
1463
1555
|
}
|
|
1464
1556
|
|
|
1465
1557
|
// A thread that cannot be written still ends its CLI, whose process group would otherwise outlive the host.
|
|
@@ -1649,6 +1741,9 @@ export class ChatCore {
|
|
|
1649
1741
|
this.sinks.to(clientId, { event: 'chat.event', payload });
|
|
1650
1742
|
}
|
|
1651
1743
|
|
|
1744
|
+
// A chat nobody reads counts its time from what it said last, such as its CLI going down.
|
|
1745
|
+
this.scheduleUnload(chatId);
|
|
1746
|
+
|
|
1652
1747
|
// After the thread, so a client reading it has already had this info and the status is a no-op there.
|
|
1653
1748
|
if (event.type === 'info' || event.type === 'reset') {
|
|
1654
1749
|
this.announceStatus(chatId, event.info);
|
|
@@ -9,7 +9,7 @@ import { isNotFound, writeAtomic, writeAtomicSync } from '../fs.ts';
|
|
|
9
9
|
import { ChatError } from './errors.ts';
|
|
10
10
|
|
|
11
11
|
/* How much of a value stored apart the item keeps: what a timeline shows before anyone asks for the rest. */
|
|
12
|
-
export const PAYLOAD_PREVIEW_BYTES =
|
|
12
|
+
export const PAYLOAD_PREVIEW_BYTES = 2 * 1024;
|
|
13
13
|
|
|
14
14
|
const FOLDER_SUFFIX = '.payloads';
|
|
15
15
|
const ID_PATTERN = /^[0-9a-f]{64}$/;
|