volute 0.45.1 → 0.45.2
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/{activity-events-OMCSM7XW.js → activity-events-BUR2RYTJ.js} +2 -2
- package/dist/{ai-service-534WUSDL.js → ai-service-UODCCE4O.js} +3 -3
- package/dist/{archive-6R4TZMYI.js → archive-WS2L2VIQ.js} +2 -2
- package/dist/{auth-FRZJVQCS.js → auth-FLAEXXZK.js} +3 -3
- package/dist/{avatar-image-BNWEXSA2.js → avatar-image-6KY6UU4D.js} +3 -3
- package/dist/{backup-OZXHL3JS.js → backup-TST5C2YN.js} +7 -7
- package/dist/{chat-I4EDDIQC.js → chat-76MRWWQX.js} +2 -2
- package/dist/{chunk-ZIR7HSAX.js → chunk-4JOQZPCE.js} +1 -1
- package/dist/{chunk-2BONPXAF.js → chunk-6EMWJZBK.js} +1 -1
- package/dist/{chunk-ZMIMSIIT.js → chunk-7C44R625.js} +1 -1
- package/dist/{chunk-Y6OPHHGM.js → chunk-7ZUB63PS.js} +4 -4
- package/dist/{chunk-LAXUUY7O.js → chunk-A7WQZKOU.js} +2 -2
- package/dist/{chunk-H7IYFRAL.js → chunk-ALKYQULU.js} +10 -5
- package/dist/{chunk-KP3KWNG6.js → chunk-CDPBTTZO.js} +2 -2
- package/dist/{chunk-SQL7M4HJ.js → chunk-DRJ24GXK.js} +2 -2
- package/dist/{chunk-HVU45COX.js → chunk-EJVPTVTI.js} +1 -1
- package/dist/{chunk-322KE457.js → chunk-F7H6ATM4.js} +1 -1
- package/dist/{chunk-5AUKCNPE.js → chunk-FR6S2KNN.js} +8 -8
- package/dist/{chunk-KVKP6VQ5.js → chunk-FXORA5JG.js} +12 -3
- package/dist/{chunk-3ERIL673.js → chunk-H2X54EHC.js} +1 -1
- package/dist/{chunk-TSK5PI4L.js → chunk-IHKGWTGT.js} +1 -1
- package/dist/{chunk-OXXTPILE.js → chunk-JZ62SYWR.js} +1 -1
- package/dist/{chunk-77BVGCTZ.js → chunk-NYC5U6K7.js} +9 -9
- package/dist/{chunk-JSRMEY52.js → chunk-OP4JG2BS.js} +5 -4
- package/dist/{chunk-CU6OFXMM.js → chunk-PFZBNKUH.js} +9 -0
- package/dist/{chunk-QMJJWTNL.js → chunk-QCN24XEN.js} +1 -1
- package/dist/{chunk-IUXTYXFI.js → chunk-QNQMSWZE.js} +3 -3
- package/dist/{chunk-UMI4UIJG.js → chunk-R77O2WMK.js} +1 -1
- package/dist/{chunk-IEWZ7VD7.js → chunk-RRNXJOKB.js} +2 -2
- package/dist/{chunk-FHWUQZXE.js → chunk-RXEEYDIB.js} +1 -1
- package/dist/{chunk-CYI534DJ.js → chunk-T6HYVHPG.js} +2 -2
- package/dist/{chunk-Q2XBKQ2D.js → chunk-TQJ6ZYVY.js} +2 -2
- package/dist/{chunk-V4GYPD5L.js → chunk-UYLGEDVD.js} +4 -4
- package/dist/{chunk-U4FNXDEJ.js → chunk-YT7PEESJ.js} +231 -98
- package/dist/{chunk-5GT7URXJ.js → chunk-ZDZISKCL.js} +1 -1
- package/dist/cli.js +16 -16
- package/dist/{cloud-sync-Y7LQPRE3.js → cloud-sync-3OJOPZRT.js} +16 -16
- package/dist/{conversations-G4LYMHAA.js → conversations-JZTD6KLR.js} +3 -3
- package/dist/{create-2YY7IPMS.js → create-IOV4GCRX.js} +1 -1
- package/dist/{daemon-restart-ABP54YN6.js → daemon-restart-TIHKVVVA.js} +6 -6
- package/dist/daemon.js +180 -137
- package/dist/{db-JFLCJCUG.js → db-MF2JNRUW.js} +1 -1
- package/dist/delivery-manager-VDVKVA46.js +32 -0
- package/dist/{delivery-router-6DE3BPHO.js → delivery-router-N4A7ZC5T.js} +2 -2
- package/dist/{down-XCAZXWLB.js → down-U2CZ23M4.js} +4 -4
- package/dist/echo-text-6JJOPUD4.js +32 -0
- package/dist/{exec-4GWHAHS3.js → exec-G3ZRST4Z.js} +2 -2
- package/dist/{export-ID7YUSLZ.js → export-GGIVO76O.js} +2 -2
- package/dist/{extensions-AYSPKE33.js → extensions-JONTW2HF.js} +16 -16
- package/dist/{files-BQ2WB5KC.js → files-4LN3EN7Q.js} +2 -2
- package/dist/{import-SKID37QX.js → import-42YNQBF4.js} +5 -5
- package/dist/{isolation-5MB3J73F.js → isolation-YMGZZ4E5.js} +2 -2
- package/dist/{join-RXPXGV4L.js → join-ZJV7W6MO.js} +4 -0
- package/dist/{login-LM4NF4ZB.js → login-M6OJUPQ2.js} +1 -1
- package/dist/{logout-CTJVIZGA.js → logout-WQBWO77G.js} +1 -1
- package/dist/message-delivery-3C65ZI6L.js +34 -0
- package/dist/{mind-KLHC7XYR.js → mind-DWB6XYCH.js} +6 -6
- package/dist/{mind-activity-tracker-BGHJE7XA.js → mind-activity-tracker-POBLU3LC.js} +3 -3
- package/dist/mind-service-3F4I46CI.js +36 -0
- package/dist/{package-BMHV7UHN.js → package-A7KXT67Q.js} +1 -1
- package/dist/{registry-NUJAGLQJ.js → registry-OUZBZ3Q4.js} +5 -1
- package/dist/{sandbox-72MWV4JL.js → sandbox-ZJ7QA6Z6.js} +3 -3
- package/dist/scheduler-P6DXLLXI.js +30 -0
- package/dist/{seed-4MKQ5QKG.js → seed-HCP5NYKH.js} +1 -1
- package/dist/{seed-cmd-LCXMR7VV.js → seed-cmd-ILOGNIFB.js} +2 -2
- package/dist/{seed-create-N2BF5LQX.js → seed-create-OURS2LS4.js} +3 -3
- package/dist/{seed-sprout-6ZDP23YI.js → seed-sprout-ELDFDAN6.js} +7 -7
- package/dist/{send-O6W53VBM.js → send-AAIOPJAV.js} +2 -2
- package/dist/{service-AQVPUANO.js → service-UOCLXDWR.js} +3 -3
- package/dist/{service-install-4QNUDKLM.js → service-install-4HHFDDWY.js} +4 -4
- package/dist/{setup-3O6O2JZ7.js → setup-DQM5VPY4.js} +6 -6
- package/dist/{setup-73YBJZQL.js → setup-Y2EJI3GG.js} +2 -2
- package/dist/skills/imagegen/scripts/imagegen.ts +100 -40
- package/dist/skills/volute-mind/SKILL.md +5 -1
- package/dist/{skills-ND4ZFYUW.js → skills-BKINFL7W.js} +4 -4
- package/dist/sleep-manager-HCUH4PM4.js +34 -0
- package/dist/{spirit-II3HOERM.js → spirit-DBTIW7P4.js} +7 -7
- package/dist/{sprout-4KUG76AQ.js → sprout-W3KC7UGK.js} +1 -1
- package/dist/{src-XFY3XDD6.js → src-OJHARHTF.js} +28 -17
- package/dist/{status-E5DQNCAO.js → status-SWRVGXBI.js} +4 -4
- package/dist/system-chat-XOALE2RD.js +36 -0
- package/dist/{tailscale-QAWVKQKU.js → tailscale-3XOKOW7T.js} +1 -1
- package/dist/{turn-tracker-QQIVVXBI.js → turn-tracker-2W6Q2NSR.js} +4 -4
- package/dist/{up-LG56MPBI.js → up-FTK7CTO3.js} +5 -5
- package/dist/{update-7TYVH5FM.js → update-SWAYZRWJ.js} +4 -4
- package/dist/{update-check-VSCSJ7KX.js → update-check-PQ2MZ47K.js} +2 -2
- package/dist/{version-notify-CODJOUW2.js → version-notify-3XMAIQTE.js} +17 -17
- package/dist/{volute-config-TS62GS6A.js → volute-config-A52FHWKM.js} +5 -1
- package/dist/web-assets/assets/{index-Dac11sKB.js → index-Bc2m8gRC.js} +15 -15
- package/dist/web-assets/index.html +1 -1
- package/package.json +1 -1
- package/templates/_base/src/lib/file-handler.ts +3 -3
- package/templates/_base/src/lib/router.ts +21 -25
- package/templates/_base/src/lib/startup.ts +2 -0
- package/templates/_base/src/lib/types.ts +2 -2
- package/templates/claude/src/agent.ts +75 -10
- package/templates/claude/src/lib/message-channel.ts +23 -0
- package/templates/claude/src/lib/session-reaper.ts +37 -0
- package/templates/claude/src/server.ts +1 -0
- package/templates/codex/src/agent.ts +18 -6
- package/templates/pi/src/agent.ts +18 -7
- package/dist/delivery-manager-R5STGLXB.js +0 -32
- package/dist/echo-text-NQXYYYCU.js +0 -32
- package/dist/message-delivery-IUAXRPFE.js +0 -34
- package/dist/mind-service-BF2LUQMH.js +0 -36
- package/dist/scheduler-YHJRRV6E.js +0 -30
- package/dist/sleep-manager-YAHDOIU5.js +0 -34
- package/dist/system-chat-E65T2HSW.js +0 -34
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
9
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
10
10
|
<link href="https://fonts.googleapis.com/css2?family=Averia+Serif+Libre:wght@300;400;700&family=Fira+Code:wght@300;400;500;600&family=Averia+Sans+Libre:ital,wght@0,300;0,400;0,500;0,600;1,400&display=swap" rel="stylesheet" />
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-Bc2m8gRC.js"></script>
|
|
12
12
|
<link rel="stylesheet" crossorigin href="/assets/index-B1JBQv2m.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
package/package.json
CHANGED
|
@@ -25,11 +25,11 @@ export function createFileHandlerResolver(cwd: string): HandlerResolver {
|
|
|
25
25
|
const resolvedCwd = resolve(cwd);
|
|
26
26
|
|
|
27
27
|
return (filePath: string): MessageHandler => ({
|
|
28
|
-
handle(content: VoluteContentPart[], meta: HandlerMeta, listener
|
|
28
|
+
handle(content: VoluteContentPart[], meta: HandlerMeta, listener?: Listener): () => void {
|
|
29
29
|
const resolved = resolve(resolvedCwd, filePath);
|
|
30
30
|
if (!resolved.startsWith(`${resolvedCwd}/`) && resolved !== resolvedCwd) {
|
|
31
31
|
log("file", `rejected path traversal: ${filePath}`);
|
|
32
|
-
queueMicrotask(() => listener({ type: "done", messageId: meta.messageId }));
|
|
32
|
+
queueMicrotask(() => listener?.({ type: "done", messageId: meta.messageId }));
|
|
33
33
|
return () => {};
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -44,7 +44,7 @@ export function createFileHandlerResolver(cwd: string): HandlerResolver {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
// Emit done asynchronously so unsubscribe is assigned before listener fires
|
|
47
|
-
queueMicrotask(() => listener({ type: "done", messageId: meta.messageId }));
|
|
47
|
+
queueMicrotask(() => listener?.({ type: "done", messageId: meta.messageId }));
|
|
48
48
|
return () => {};
|
|
49
49
|
},
|
|
50
50
|
});
|
|
@@ -191,7 +191,11 @@ export function createRouter(options: {
|
|
|
191
191
|
instructions: string | undefined,
|
|
192
192
|
sessionName: string,
|
|
193
193
|
): VoluteContentPart[] {
|
|
194
|
-
if (!instructions
|
|
194
|
+
if (!instructions) return content;
|
|
195
|
+
// Ephemeral $new sessions get a unique name dispatched exactly once, so we
|
|
196
|
+
// always prepend and never track them — tracking would leak an entry per message.
|
|
197
|
+
if (sessionName.startsWith("new-")) return prependInstructions(content, instructions);
|
|
198
|
+
if (instructedSessions.has(sessionName)) return content;
|
|
195
199
|
instructedSessions.add(sessionName);
|
|
196
200
|
return prependInstructions(content, instructions);
|
|
197
201
|
}
|
|
@@ -256,9 +260,9 @@ export function createRouter(options: {
|
|
|
256
260
|
const messageId = generateMessageId();
|
|
257
261
|
const handler = options.mindHandler(buffer.sessionName);
|
|
258
262
|
|
|
259
|
-
// Batch flushes are fire-and-forget — no HTTP response is waiting, so listener
|
|
263
|
+
// Batch flushes are fire-and-forget — no HTTP response is waiting, so no listener
|
|
260
264
|
try {
|
|
261
|
-
handler.handle(content, { sessionName: buffer.sessionName, messageId }
|
|
265
|
+
handler.handle(content, { sessionName: buffer.sessionName, messageId });
|
|
262
266
|
} catch (err) {
|
|
263
267
|
log("router", `error flushing batch for session ${buffer.sessionName}:`, err);
|
|
264
268
|
return;
|
|
@@ -301,8 +305,6 @@ export function createRouter(options: {
|
|
|
301
305
|
listener?: Listener,
|
|
302
306
|
): { messageId: string; unsubscribe: () => void } {
|
|
303
307
|
const messageId = generateMessageId();
|
|
304
|
-
const noop = () => {};
|
|
305
|
-
const safeListener = listener ?? noop;
|
|
306
308
|
|
|
307
309
|
// Expose session to child processes (daemon-client reads this for X-Volute-Session)
|
|
308
310
|
process.env.VOLUTE_SESSION = session;
|
|
@@ -342,7 +344,7 @@ export function createRouter(options: {
|
|
|
342
344
|
interrupt,
|
|
343
345
|
replyInstructions: sessionConfig.replyInstructions,
|
|
344
346
|
},
|
|
345
|
-
|
|
347
|
+
listener,
|
|
346
348
|
);
|
|
347
349
|
return { messageId, unsubscribe };
|
|
348
350
|
}
|
|
@@ -368,7 +370,6 @@ export function createRouter(options: {
|
|
|
368
370
|
|
|
369
371
|
const messageId = generateMessageId();
|
|
370
372
|
const noop = () => {};
|
|
371
|
-
const safeListener = listener ?? noop;
|
|
372
373
|
|
|
373
374
|
// Gate unmatched channels (default: gate unless explicitly disabled)
|
|
374
375
|
if (!resolved.matched && config.gateUnmatched !== false) {
|
|
@@ -380,7 +381,7 @@ export function createRouter(options: {
|
|
|
380
381
|
if (options.fileHandler) {
|
|
381
382
|
const formatted = applyPrefix(content, meta);
|
|
382
383
|
const fileHandler = options.fileHandler(filePath);
|
|
383
|
-
fileHandler.handle(formatted, { ...meta, messageId }
|
|
384
|
+
fileHandler.handle(formatted, { ...meta, messageId });
|
|
384
385
|
}
|
|
385
386
|
|
|
386
387
|
// First message from this channel — send invite notification
|
|
@@ -389,18 +390,14 @@ export function createRouter(options: {
|
|
|
389
390
|
const notification = formatInviteNotification(meta, filePath, text);
|
|
390
391
|
const notifContent: VoluteContentPart[] = [{ type: "text", text: notification }];
|
|
391
392
|
const handler = options.mindHandler("main");
|
|
392
|
-
handler.handle(
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
interrupt: true,
|
|
398
|
-
},
|
|
399
|
-
noop,
|
|
400
|
-
);
|
|
393
|
+
handler.handle(notifContent, {
|
|
394
|
+
sessionName: "main",
|
|
395
|
+
messageId: generateMessageId(),
|
|
396
|
+
interrupt: true,
|
|
397
|
+
});
|
|
401
398
|
}
|
|
402
399
|
|
|
403
|
-
queueMicrotask(() =>
|
|
400
|
+
queueMicrotask(() => listener?.({ type: "done", messageId }));
|
|
404
401
|
return { messageId, unsubscribe: noop };
|
|
405
402
|
}
|
|
406
403
|
|
|
@@ -411,7 +408,7 @@ export function createRouter(options: {
|
|
|
411
408
|
const escaped = mindName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
412
409
|
const pattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
413
410
|
if (!pattern.test(text)) {
|
|
414
|
-
queueMicrotask(() =>
|
|
411
|
+
queueMicrotask(() => listener?.({ type: "done", messageId }));
|
|
415
412
|
return { messageId, unsubscribe: noop };
|
|
416
413
|
}
|
|
417
414
|
} else {
|
|
@@ -424,12 +421,12 @@ export function createRouter(options: {
|
|
|
424
421
|
if (options.fileHandler) {
|
|
425
422
|
const formatted = applyPrefix(content, meta);
|
|
426
423
|
const handler = options.fileHandler(resolved.path);
|
|
427
|
-
const unsubscribe = handler.handle(formatted, { ...meta, messageId },
|
|
424
|
+
const unsubscribe = handler.handle(formatted, { ...meta, messageId }, listener);
|
|
428
425
|
return { messageId, unsubscribe };
|
|
429
426
|
}
|
|
430
427
|
// No file handler configured — emit done and discard
|
|
431
428
|
log("router", `no file handler configured — discarding file-destined message`);
|
|
432
|
-
queueMicrotask(() =>
|
|
429
|
+
queueMicrotask(() => listener?.({ type: "done", messageId }));
|
|
433
430
|
return { messageId, unsubscribe: noop };
|
|
434
431
|
}
|
|
435
432
|
|
|
@@ -473,7 +470,7 @@ export function createRouter(options: {
|
|
|
473
470
|
scheduleBatchTimers(batchKey);
|
|
474
471
|
}
|
|
475
472
|
|
|
476
|
-
queueMicrotask(() =>
|
|
473
|
+
queueMicrotask(() => listener?.({ type: "done", messageId }));
|
|
477
474
|
return { messageId, unsubscribe: noop };
|
|
478
475
|
}
|
|
479
476
|
|
|
@@ -495,7 +492,7 @@ export function createRouter(options: {
|
|
|
495
492
|
interrupt: sessionConfig.interrupt,
|
|
496
493
|
replyInstructions: sessionConfig.replyInstructions,
|
|
497
494
|
},
|
|
498
|
-
|
|
495
|
+
listener,
|
|
499
496
|
);
|
|
500
497
|
return { messageId, unsubscribe };
|
|
501
498
|
}
|
|
@@ -562,10 +559,9 @@ export function createRouter(options: {
|
|
|
562
559
|
|
|
563
560
|
const messageId = generateMessageId();
|
|
564
561
|
const handler = options.mindHandler(session);
|
|
565
|
-
const noop = () => {};
|
|
566
562
|
|
|
567
563
|
try {
|
|
568
|
-
handler.handle(withInstructions, { sessionName: session, messageId }
|
|
564
|
+
handler.handle(withInstructions, { sessionName: session, messageId });
|
|
569
565
|
log("router", `dispatched batch for session ${session}: ${allMessages.length} messages`);
|
|
570
566
|
} catch (err) {
|
|
571
567
|
log("router", `error dispatching batch for session ${session}:`, err);
|
|
@@ -28,6 +28,8 @@ export type MindConfig = {
|
|
|
28
28
|
logLevel?: "error" | "warn" | "info" | "debug";
|
|
29
29
|
compactionMessage?: string;
|
|
30
30
|
compaction?: { maxContextTokens?: number };
|
|
31
|
+
/** Idle minutes before a session's SDK subprocess is reaped. 0 disables. Default 30. */
|
|
32
|
+
sessionIdleMinutes?: number;
|
|
31
33
|
subagents?: Record<string, SubagentConfig>;
|
|
32
34
|
// Template-specific config fields (claude, pi, codex)
|
|
33
35
|
maxThinkingTokens?: number;
|
|
@@ -50,9 +50,9 @@ export type VoluteEvent = { messageId?: string } & (
|
|
|
50
50
|
|
|
51
51
|
export type Listener = (event: VoluteEvent) => void;
|
|
52
52
|
|
|
53
|
-
/** A handler that processes a single routed message and emits events to
|
|
53
|
+
/** A handler that processes a single routed message and emits events to an optional listener callback. */
|
|
54
54
|
export type MessageHandler = {
|
|
55
|
-
handle(content: VoluteContentPart[], meta: HandlerMeta, listener
|
|
55
|
+
handle(content: VoluteContentPart[], meta: HandlerMeta, listener?: Listener): () => void; // returns unsubscribe
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
/** Resolves a key (session name, file path, etc.) to a MessageHandler. */
|
|
@@ -21,6 +21,7 @@ import { createPreCompactHook } from "./lib/hooks/pre-compact.js";
|
|
|
21
21
|
import { createReplyInstructionsHook } from "./lib/hooks/reply-instructions.js";
|
|
22
22
|
import { log } from "./lib/logger.js";
|
|
23
23
|
import { createMessageChannel } from "./lib/message-channel.js";
|
|
24
|
+
import { isSessionReapable } from "./lib/session-reaper.js";
|
|
24
25
|
import { createSessionStore } from "./lib/session-store.js";
|
|
25
26
|
import { loadPrompts, type SubagentConfig } from "./lib/startup.js";
|
|
26
27
|
import { consumeStream } from "./lib/stream-consumer.js";
|
|
@@ -45,6 +46,8 @@ type Session = {
|
|
|
45
46
|
replyInstructionsFired: boolean;
|
|
46
47
|
replyInstructionsMode: "once" | "always" | "never";
|
|
47
48
|
contextTokens: number;
|
|
49
|
+
/** Last inbound message or completed turn — drives idle reaping. */
|
|
50
|
+
lastActivityAt: number;
|
|
48
51
|
};
|
|
49
52
|
|
|
50
53
|
export function createMind(options: {
|
|
@@ -58,6 +61,8 @@ export function createMind(options: {
|
|
|
58
61
|
maxContextTokens?: number;
|
|
59
62
|
subagents?: Record<string, SubagentConfig>;
|
|
60
63
|
onIdentityReload?: () => Promise<void>;
|
|
64
|
+
/** Idle minutes before a session's SDK subprocess is reaped. 0 disables. Default 30. */
|
|
65
|
+
sessionIdleMinutes?: number;
|
|
61
66
|
}): {
|
|
62
67
|
resolve: HandlerResolver;
|
|
63
68
|
waitForCommits: () => Promise<void>;
|
|
@@ -320,6 +325,7 @@ export function createMind(options: {
|
|
|
320
325
|
// This turn's message is fully processed — drop it from the channel's
|
|
321
326
|
// in-flight set so a later compaction abort won't re-feed it.
|
|
322
327
|
session.channel.ack();
|
|
328
|
+
session.lastActivityAt = Date.now();
|
|
323
329
|
await autoCommit.flushFileChanges();
|
|
324
330
|
const wasCompacting = compactionTriggered.get(session.name);
|
|
325
331
|
compactionTriggered.set(session.name, false);
|
|
@@ -478,16 +484,20 @@ export function createMind(options: {
|
|
|
478
484
|
log("mind", `session "${session.name}": stream consumer error:`, retryErr);
|
|
479
485
|
await emitError(retryErr);
|
|
480
486
|
emitDone();
|
|
481
|
-
sessions.delete(session.name);
|
|
482
487
|
}
|
|
483
488
|
} else {
|
|
484
489
|
log("mind", `session "${session.name}": stream consumer error:`, err);
|
|
485
490
|
await emitError(err);
|
|
486
491
|
emitDone();
|
|
487
|
-
sessions.delete(session.name);
|
|
488
492
|
}
|
|
493
|
+
} finally {
|
|
494
|
+
// The stream consumer has ended (subprocess exit, error, or fresh-start
|
|
495
|
+
// abandonment) — drop the session so the sessions map doesn't retain dead
|
|
496
|
+
// entries. Matters most for ephemeral $new sessions once the idle reaper
|
|
497
|
+
// (#458) kills their subprocess, but also for any named session that ends.
|
|
498
|
+
sessions.delete(session.name);
|
|
499
|
+
log("mind", `session "${session.name}": stream consumer ended`);
|
|
489
500
|
}
|
|
490
|
-
log("mind", `session "${session.name}": stream consumer ended`);
|
|
491
501
|
})();
|
|
492
502
|
}
|
|
493
503
|
|
|
@@ -504,6 +514,7 @@ export function createMind(options: {
|
|
|
504
514
|
replyInstructionsFired: false,
|
|
505
515
|
replyInstructionsMode: "once",
|
|
506
516
|
contextTokens: 0,
|
|
517
|
+
lastActivityAt: Date.now(),
|
|
507
518
|
};
|
|
508
519
|
sessions.set(name, session);
|
|
509
520
|
|
|
@@ -526,18 +537,69 @@ export function createMind(options: {
|
|
|
526
537
|
return session;
|
|
527
538
|
}
|
|
528
539
|
|
|
540
|
+
// --- Idle session reaping ---
|
|
541
|
+
// Each session holds a resident SDK subprocess (~250MB) for its whole life.
|
|
542
|
+
// After the idle timeout, shut the subprocess down while keeping the session
|
|
543
|
+
// resumable: the session id is persisted, so the next inbound message
|
|
544
|
+
// transparently re-creates the session via getOrCreateSession's resume path.
|
|
545
|
+
const idleTimeoutMs = (options.sessionIdleMinutes ?? 30) * 60_000;
|
|
546
|
+
|
|
547
|
+
function reapSession(session: Session) {
|
|
548
|
+
log("mind", `session "${session.name}": idle — reaping SDK subprocess (resumable)`);
|
|
549
|
+
// Delete first so a racing inbound message spins up a fresh resumed session
|
|
550
|
+
// instead of reusing the one we're tearing down.
|
|
551
|
+
sessions.delete(session.name);
|
|
552
|
+
compactionTriggered.delete(session.name);
|
|
553
|
+
// Terminate the subprocess and end the input iterable so the stream consumer
|
|
554
|
+
// unwinds (its finally block also deletes from the map, now a no-op).
|
|
555
|
+
session.currentQuery?.close();
|
|
556
|
+
session.channel.close();
|
|
557
|
+
// Nothing should have raced in (isSessionReapable checked isEmpty), but if it
|
|
558
|
+
// did, re-dispatch into a fresh session so no input is dropped.
|
|
559
|
+
const pending = session.channel.recover();
|
|
560
|
+
if (pending.length > 0) {
|
|
561
|
+
const fresh = getOrCreateSession(session.name);
|
|
562
|
+
for (const msg of pending) fresh.channel.push(msg);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (idleTimeoutMs > 0) {
|
|
567
|
+
log("mind", `idle session reaper: ${idleTimeoutMs / 60_000} min timeout`);
|
|
568
|
+
const checkMs = Math.min(60_000, idleTimeoutMs);
|
|
569
|
+
const reaper = setInterval(() => {
|
|
570
|
+
const now = Date.now();
|
|
571
|
+
const stale = [...sessions.values()].filter((s) =>
|
|
572
|
+
isSessionReapable(s, now, idleTimeoutMs, (name) => !!compactionTriggered.get(name)),
|
|
573
|
+
);
|
|
574
|
+
for (const session of stale) reapSession(session);
|
|
575
|
+
}, checkMs);
|
|
576
|
+
reaper.unref?.();
|
|
577
|
+
}
|
|
578
|
+
|
|
529
579
|
// --- MessageHandler implementation ---
|
|
530
580
|
|
|
531
581
|
function createSessionHandler(sessionName: string): MessageHandler {
|
|
532
582
|
return {
|
|
533
|
-
handle(content: VoluteContentPart[], meta: HandlerMeta, listener
|
|
583
|
+
handle(content: VoluteContentPart[], meta: HandlerMeta, listener?: Listener): () => void {
|
|
534
584
|
const session = getOrCreateSession(sessionName);
|
|
535
585
|
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
586
|
+
// Only register a listener when a caller actually wants events. A per-message
|
|
587
|
+
// listener that's never removed would grow session.listeners without bound and
|
|
588
|
+
// make broadcastToSession O(messages-ever-received). The live dispatch path
|
|
589
|
+
// passes no listener, so this is usually a no-op.
|
|
590
|
+
let filteredListener: Listener | undefined;
|
|
591
|
+
if (listener) {
|
|
592
|
+
// Filter to only this messageId, and self-remove on the matching done so a
|
|
593
|
+
// caller that forgets to unsubscribe can't reintroduce the leak.
|
|
594
|
+
filteredListener = (event) => {
|
|
595
|
+
if (event.messageId !== meta.messageId) return;
|
|
596
|
+
listener(event);
|
|
597
|
+
if (event.type === "done" && filteredListener) {
|
|
598
|
+
session.listeners.delete(filteredListener);
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
session.listeners.add(filteredListener);
|
|
602
|
+
}
|
|
541
603
|
|
|
542
604
|
// Track channel/sender for reply instructions
|
|
543
605
|
if (meta.channel) {
|
|
@@ -559,6 +621,7 @@ export function createMind(options: {
|
|
|
559
621
|
}
|
|
560
622
|
|
|
561
623
|
// Push message into SDK
|
|
624
|
+
session.lastActivityAt = Date.now();
|
|
562
625
|
session.messageIds.push(meta.messageId);
|
|
563
626
|
session.channel.push({
|
|
564
627
|
type: "user",
|
|
@@ -567,7 +630,9 @@ export function createMind(options: {
|
|
|
567
630
|
parent_tool_use_id: null,
|
|
568
631
|
});
|
|
569
632
|
|
|
570
|
-
return () =>
|
|
633
|
+
return () => {
|
|
634
|
+
if (filteredListener) session.listeners.delete(filteredListener);
|
|
635
|
+
};
|
|
571
636
|
},
|
|
572
637
|
};
|
|
573
638
|
}
|
|
@@ -11,6 +11,14 @@ export type MessageChannel = {
|
|
|
11
11
|
* compaction abort so in-flight input is never lost.
|
|
12
12
|
*/
|
|
13
13
|
recover: () => SDKUserMessage[];
|
|
14
|
+
/**
|
|
15
|
+
* Terminate the input iterable: resolve any pending `next()` with `done: true`
|
|
16
|
+
* and make subsequent `next()` calls return done. The SDK sees end-of-input and
|
|
17
|
+
* shuts the query down. Used by the idle reaper to release the SDK subprocess.
|
|
18
|
+
*/
|
|
19
|
+
close: () => void;
|
|
20
|
+
/** True when nothing is queued or in flight (no pending work). */
|
|
21
|
+
isEmpty: () => boolean;
|
|
14
22
|
iterable: AsyncIterable<SDKUserMessage>;
|
|
15
23
|
};
|
|
16
24
|
|
|
@@ -23,6 +31,7 @@ export function createMessageChannel(): MessageChannel {
|
|
|
23
31
|
// re-feed the unprocessed ones instead of dropping them.
|
|
24
32
|
const inFlight: SDKUserMessage[] = [];
|
|
25
33
|
let resolve: ((value: IteratorResult<SDKUserMessage>) => void) | null = null;
|
|
34
|
+
let closed = false;
|
|
26
35
|
|
|
27
36
|
function deliver(msg: SDKUserMessage): IteratorResult<SDKUserMessage> {
|
|
28
37
|
inFlight.push(msg);
|
|
@@ -52,10 +61,24 @@ export function createMessageChannel(): MessageChannel {
|
|
|
52
61
|
}
|
|
53
62
|
return [...inFlight.splice(0), ...queue.splice(0)];
|
|
54
63
|
},
|
|
64
|
+
close() {
|
|
65
|
+
closed = true;
|
|
66
|
+
if (resolve) {
|
|
67
|
+
const r = resolve;
|
|
68
|
+
resolve = null;
|
|
69
|
+
r({ value: undefined as any, done: true });
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
isEmpty() {
|
|
73
|
+
return queue.length === 0 && inFlight.length === 0;
|
|
74
|
+
},
|
|
55
75
|
iterable: {
|
|
56
76
|
[Symbol.asyncIterator]() {
|
|
57
77
|
return {
|
|
58
78
|
next(): Promise<IteratorResult<SDKUserMessage>> {
|
|
79
|
+
if (closed) {
|
|
80
|
+
return Promise.resolve({ value: undefined as any, done: true });
|
|
81
|
+
}
|
|
59
82
|
if (queue.length > 0) {
|
|
60
83
|
return Promise.resolve(deliver(queue.shift()!));
|
|
61
84
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idle-session reaping decision logic.
|
|
3
|
+
*
|
|
4
|
+
* Each mind session keeps a resident Claude SDK subprocess alive for as long as
|
|
5
|
+
* the session lives. When a session has been idle past the configured timeout it
|
|
6
|
+
* can be reaped — its subprocess shut down — while staying resumable, since the
|
|
7
|
+
* session id is persisted and the next inbound message re-creates it via resume.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface ReapableSession {
|
|
11
|
+
name: string;
|
|
12
|
+
/** Timestamp (ms) of the last inbound message or completed turn. */
|
|
13
|
+
lastActivityAt: number;
|
|
14
|
+
/** Set while a turn is in progress; undefined between turns. */
|
|
15
|
+
currentMessageId?: string;
|
|
16
|
+
channel: { isEmpty(): boolean };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A session is reapable only when it has been idle longer than the timeout and
|
|
21
|
+
* has no work in progress: no turn running, no queued/in-flight input, and not
|
|
22
|
+
* mid-compaction (which owns its own abort/resume lifecycle).
|
|
23
|
+
*/
|
|
24
|
+
export function isSessionReapable(
|
|
25
|
+
session: ReapableSession,
|
|
26
|
+
now: number,
|
|
27
|
+
idleTimeoutMs: number,
|
|
28
|
+
isCompacting: (name: string) => boolean,
|
|
29
|
+
): boolean {
|
|
30
|
+
if (idleTimeoutMs <= 0) return false;
|
|
31
|
+
return (
|
|
32
|
+
session.currentMessageId === undefined &&
|
|
33
|
+
session.channel.isEmpty() &&
|
|
34
|
+
!isCompacting(session.name) &&
|
|
35
|
+
now - session.lastActivityAt > idleTimeoutMs
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -33,6 +33,7 @@ const mind = createMind({
|
|
|
33
33
|
sessionsDir,
|
|
34
34
|
compactionMessage: config.compactionMessage,
|
|
35
35
|
maxContextTokens: config.compaction?.maxContextTokens,
|
|
36
|
+
sessionIdleMinutes: config.sessionIdleMinutes,
|
|
36
37
|
subagents: config.subagents,
|
|
37
38
|
onIdentityReload: async () => {
|
|
38
39
|
log("server", "identity file changed — restarting to reload");
|
|
@@ -558,13 +558,23 @@ export function createMind(options: {
|
|
|
558
558
|
|
|
559
559
|
function createSessionHandler(sessionName: string): MessageHandler {
|
|
560
560
|
return {
|
|
561
|
-
handle(content: VoluteContentPart[], meta: HandlerMeta, listener
|
|
561
|
+
handle(content: VoluteContentPart[], meta: HandlerMeta, listener?: Listener): () => void {
|
|
562
562
|
const session = getOrCreateSession(sessionName);
|
|
563
563
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
564
|
+
// Only register a listener when a caller wants events. A per-message listener
|
|
565
|
+
// that's never removed grows session.listeners without bound and makes broadcast
|
|
566
|
+
// O(messages-ever-received). The live dispatch path passes no listener.
|
|
567
|
+
let filteredListener: Listener | undefined;
|
|
568
|
+
if (listener) {
|
|
569
|
+
filteredListener = (event) => {
|
|
570
|
+
if (event.messageId !== meta.messageId) return;
|
|
571
|
+
listener(event);
|
|
572
|
+
if (event.type === "done" && filteredListener) {
|
|
573
|
+
session.listeners.delete(filteredListener);
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
session.listeners.add(filteredListener);
|
|
577
|
+
}
|
|
568
578
|
|
|
569
579
|
// Track channel for reply instructions
|
|
570
580
|
if (meta.channel) {
|
|
@@ -586,7 +596,9 @@ export function createMind(options: {
|
|
|
586
596
|
broadcast(session, { type: "done" });
|
|
587
597
|
});
|
|
588
598
|
|
|
589
|
-
return () =>
|
|
599
|
+
return () => {
|
|
600
|
+
if (filteredListener) session.listeners.delete(filteredListener);
|
|
601
|
+
};
|
|
590
602
|
},
|
|
591
603
|
};
|
|
592
604
|
}
|
|
@@ -447,14 +447,23 @@ export function createMind(options: {
|
|
|
447
447
|
|
|
448
448
|
function createSessionHandler(sessionName: string): MessageHandler {
|
|
449
449
|
return {
|
|
450
|
-
handle(content: VoluteContentPart[], meta: HandlerMeta, listener
|
|
450
|
+
handle(content: VoluteContentPart[], meta: HandlerMeta, listener?: Listener): () => void {
|
|
451
451
|
const session = getOrCreateSession(sessionName);
|
|
452
452
|
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
453
|
+
// Only register a listener when a caller wants events. A per-message listener
|
|
454
|
+
// that's never removed grows session.listeners without bound and makes broadcast
|
|
455
|
+
// O(messages-ever-received). The live dispatch path passes no listener.
|
|
456
|
+
let filteredListener: Listener | undefined;
|
|
457
|
+
if (listener) {
|
|
458
|
+
filteredListener = (event) => {
|
|
459
|
+
if (event.messageId !== meta.messageId) return;
|
|
460
|
+
listener(event);
|
|
461
|
+
if (event.type === "done" && filteredListener) {
|
|
462
|
+
session.listeners.delete(filteredListener);
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
session.listeners.add(filteredListener);
|
|
466
|
+
}
|
|
458
467
|
|
|
459
468
|
// Track channel for reply instructions
|
|
460
469
|
if (meta.channel) {
|
|
@@ -500,7 +509,9 @@ export function createMind(options: {
|
|
|
500
509
|
broadcast(session, { type: "done" });
|
|
501
510
|
});
|
|
502
511
|
|
|
503
|
-
return () =>
|
|
512
|
+
return () => {
|
|
513
|
+
if (filteredListener) session.listeners.delete(filteredListener);
|
|
514
|
+
};
|
|
504
515
|
},
|
|
505
516
|
};
|
|
506
517
|
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
DeliveryManager,
|
|
4
|
-
getDeliveryManager,
|
|
5
|
-
initDeliveryManager,
|
|
6
|
-
tryGetDeliveryManager
|
|
7
|
-
} from "./chunk-U4FNXDEJ.js";
|
|
8
|
-
import "./chunk-5GT7URXJ.js";
|
|
9
|
-
import "./chunk-5AUKCNPE.js";
|
|
10
|
-
import "./chunk-QLWG3C2Z.js";
|
|
11
|
-
import "./chunk-Y6OPHHGM.js";
|
|
12
|
-
import "./chunk-JSRMEY52.js";
|
|
13
|
-
import "./chunk-CU6OFXMM.js";
|
|
14
|
-
import "./chunk-OXXTPILE.js";
|
|
15
|
-
import "./chunk-HVU45COX.js";
|
|
16
|
-
import "./chunk-ZIR7HSAX.js";
|
|
17
|
-
import "./chunk-V4GYPD5L.js";
|
|
18
|
-
import "./chunk-3ERIL673.js";
|
|
19
|
-
import "./chunk-TSK5PI4L.js";
|
|
20
|
-
import "./chunk-322KE457.js";
|
|
21
|
-
import "./chunk-CYI534DJ.js";
|
|
22
|
-
import "./chunk-ZMIMSIIT.js";
|
|
23
|
-
import "./chunk-T2TP6ZC6.js";
|
|
24
|
-
import "./chunk-H7IYFRAL.js";
|
|
25
|
-
import "./chunk-ZV7MFISX.js";
|
|
26
|
-
import "./chunk-K3NQKI34.js";
|
|
27
|
-
export {
|
|
28
|
-
DeliveryManager,
|
|
29
|
-
getDeliveryManager,
|
|
30
|
-
initDeliveryManager,
|
|
31
|
-
tryGetDeliveryManager
|
|
32
|
-
};
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
clearEchoTextCache,
|
|
4
|
-
echoTextToChannel
|
|
5
|
-
} from "./chunk-77BVGCTZ.js";
|
|
6
|
-
import "./chunk-FHWUQZXE.js";
|
|
7
|
-
import "./chunk-U4FNXDEJ.js";
|
|
8
|
-
import "./chunk-5GT7URXJ.js";
|
|
9
|
-
import "./chunk-5AUKCNPE.js";
|
|
10
|
-
import "./chunk-QLWG3C2Z.js";
|
|
11
|
-
import "./chunk-Y6OPHHGM.js";
|
|
12
|
-
import "./chunk-JSRMEY52.js";
|
|
13
|
-
import "./chunk-CU6OFXMM.js";
|
|
14
|
-
import "./chunk-OXXTPILE.js";
|
|
15
|
-
import "./chunk-HVU45COX.js";
|
|
16
|
-
import "./chunk-ZIR7HSAX.js";
|
|
17
|
-
import "./chunk-V4GYPD5L.js";
|
|
18
|
-
import "./chunk-Q2XBKQ2D.js";
|
|
19
|
-
import "./chunk-3ERIL673.js";
|
|
20
|
-
import "./chunk-TSK5PI4L.js";
|
|
21
|
-
import "./chunk-322KE457.js";
|
|
22
|
-
import "./chunk-CYI534DJ.js";
|
|
23
|
-
import "./chunk-ZMIMSIIT.js";
|
|
24
|
-
import "./chunk-T2TP6ZC6.js";
|
|
25
|
-
import "./chunk-LGNUFVMR.js";
|
|
26
|
-
import "./chunk-H7IYFRAL.js";
|
|
27
|
-
import "./chunk-ZV7MFISX.js";
|
|
28
|
-
import "./chunk-K3NQKI34.js";
|
|
29
|
-
export {
|
|
30
|
-
clearEchoTextCache,
|
|
31
|
-
echoTextToChannel
|
|
32
|
-
};
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
deliverMessage,
|
|
4
|
-
linkToolResultToTurn,
|
|
5
|
-
recordInbound,
|
|
6
|
-
recordOutbound,
|
|
7
|
-
resolveSleepAction
|
|
8
|
-
} from "./chunk-U4FNXDEJ.js";
|
|
9
|
-
import "./chunk-5GT7URXJ.js";
|
|
10
|
-
import "./chunk-5AUKCNPE.js";
|
|
11
|
-
import "./chunk-QLWG3C2Z.js";
|
|
12
|
-
import "./chunk-Y6OPHHGM.js";
|
|
13
|
-
import "./chunk-JSRMEY52.js";
|
|
14
|
-
import "./chunk-CU6OFXMM.js";
|
|
15
|
-
import "./chunk-OXXTPILE.js";
|
|
16
|
-
import "./chunk-HVU45COX.js";
|
|
17
|
-
import "./chunk-ZIR7HSAX.js";
|
|
18
|
-
import "./chunk-V4GYPD5L.js";
|
|
19
|
-
import "./chunk-3ERIL673.js";
|
|
20
|
-
import "./chunk-TSK5PI4L.js";
|
|
21
|
-
import "./chunk-322KE457.js";
|
|
22
|
-
import "./chunk-CYI534DJ.js";
|
|
23
|
-
import "./chunk-ZMIMSIIT.js";
|
|
24
|
-
import "./chunk-T2TP6ZC6.js";
|
|
25
|
-
import "./chunk-H7IYFRAL.js";
|
|
26
|
-
import "./chunk-ZV7MFISX.js";
|
|
27
|
-
import "./chunk-K3NQKI34.js";
|
|
28
|
-
export {
|
|
29
|
-
deliverMessage,
|
|
30
|
-
linkToolResultToTurn,
|
|
31
|
-
recordInbound,
|
|
32
|
-
recordOutbound,
|
|
33
|
-
resolveSleepAction
|
|
34
|
-
};
|