volute 0.54.0 → 0.55.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/dist/{activity-events-LQII6PZS.js → activity-events-TFAJBNBW.js} +2 -2
- package/dist/{ai-service-YW5SWCSB.js → ai-service-KEK2V3AP.js} +2 -2
- package/dist/{auth-KRDSM55Q.js → auth-Y27LTNZK.js} +3 -3
- package/dist/auto-upgrade-2MZF7NG3.js +46 -0
- package/dist/{avatar-image-52WGXLHV.js → avatar-image-SN576P45.js} +2 -2
- package/dist/{backup-QRNX6RK7.js → backup-GDCWH6MC.js} +2 -2
- package/dist/{channels-CKCY64TD.js → channels-AFJWGX7W.js} +15 -15
- package/dist/{chat-ZUGNL5GS.js → chat-HVPOZK3O.js} +3 -3
- package/dist/{chunk-QUSMYGNQ.js → chunk-2674KIGI.js} +1 -0
- package/dist/chunk-2GATCSZK.js +520 -0
- package/dist/{chunk-LKUGUQUW.js → chunk-3SFKY4GI.js} +4 -4
- package/dist/{chunk-CLCHOXLR.js → chunk-45QTYKQQ.js} +4 -4
- package/dist/{chunk-PPSN2MLC.js → chunk-4VAQKLMU.js} +7 -4
- package/dist/{chunk-TPX6E6PL.js → chunk-6HBGUPBA.js} +2 -2
- package/dist/{chunk-UFTDRTCP.js → chunk-7AA4T2RT.js} +2 -2
- package/dist/{chunk-56IT2ZXF.js → chunk-BP2X5AIK.js} +8 -14
- package/dist/{chunk-444RR3YF.js → chunk-CKOA4LWV.js} +11 -11
- package/dist/{chunk-HKGYZVWN.js → chunk-DUV4EMNW.js} +37 -3
- package/dist/{chunk-CV6BNGFK.js → chunk-FSXPPOPM.js} +3 -3
- package/dist/{chunk-3LHUXY3E.js → chunk-FZUPIS3T.js} +3 -3
- package/dist/{chunk-L7BKTZ5U.js → chunk-G3XAB4DK.js} +1 -1
- package/dist/{chunk-WHL6PKYP.js → chunk-HLWQ3P3R.js} +49 -45
- package/dist/{chunk-OAYY3WF3.js → chunk-HTSWDOJE.js} +0 -11
- package/dist/{chunk-OYF25B62.js → chunk-IR2FW4W7.js} +1 -1
- package/dist/{chunk-ICXY54ZC.js → chunk-IT4GEFDH.js} +1 -1
- package/dist/{chunk-F6D766VQ.js → chunk-KS62IHE2.js} +1 -1
- package/dist/{chunk-LNNI3MS4.js → chunk-MNKPAAQF.js} +9 -2
- package/dist/{chunk-LGXVGIWN.js → chunk-NCRG4SBR.js} +189 -55
- package/dist/{chunk-I7U5EEZC.js → chunk-NM5FDH4T.js} +1 -1
- package/dist/{chunk-7LT3YZUR.js → chunk-OFU6WJJZ.js} +1 -1
- package/dist/{chunk-OT5SXF7L.js → chunk-OUGD5L5U.js} +26 -12
- package/dist/{chunk-X67BDDZR.js → chunk-P3GXOT2X.js} +48 -2
- package/dist/{chunk-U54PJPTB.js → chunk-PRZUBJ4K.js} +1 -1
- package/dist/{chunk-OGPVJ53N.js → chunk-QVS3C4DK.js} +2 -2
- package/dist/{chunk-VDUCLVZI.js → chunk-TMTAHKYP.js} +27 -25
- package/dist/{chunk-RGHF3OK5.js → chunk-UEZC5MNB.js} +13 -13
- package/dist/{chunk-ATZF2RNJ.js → chunk-VTFXZPC3.js} +12 -12
- package/dist/{chunk-YSRYCS3N.js → chunk-W3OIPJPB.js} +1 -1
- package/dist/{chunk-5Y54AE3L.js → chunk-XAG53RWY.js} +1 -1
- package/dist/{chunk-HTRSSOHY.js → chunk-YU5EYQAE.js} +1 -1
- package/dist/{chunk-EGV6R6UF.js → chunk-Z7TNIIHW.js} +3 -1
- package/dist/cli.js +8 -8
- package/dist/{cloud-sync-52YNJRMR.js → cloud-sync-FM73SVFJ.js} +20 -20
- package/dist/{conversations-ZL6PHROD.js → conversations-FH3ODBL7.js} +3 -3
- package/dist/{create-G2RIKQYK.js → create-P75ZFTVN.js} +1 -1
- package/dist/{daemon-restart-QWHN5VMU.js → daemon-restart-JQMMMITM.js} +1 -1
- package/dist/daemon.js +187 -159
- package/dist/{db-I4EH5SLF.js → db-JH3NTERX.js} +17 -3
- package/dist/{delivery-manager-7R2YIQ2W.js → delivery-manager-H23Q23N7.js} +15 -15
- package/dist/{delivery-notices-RZQ6GONP.js → delivery-notices-S7TKOACE.js} +5 -5
- package/dist/{delivery-router-QS4NLJR6.js → delivery-router-KHBVEVVL.js} +2 -2
- package/dist/echo-text-ULEIFJJF.js +40 -0
- package/dist/{extensions-QT65JGV6.js → extensions-QD22GXZD.js} +24 -20
- package/dist/{files-PCKBQHAY.js → files-DUGRXN6V.js} +2 -2
- package/dist/{import-DB2KULRO.js → import-RDDUGGFI.js} +3 -3
- package/dist/message-delivery-LU4EP244.js +46 -0
- package/dist/migrate-init-infrastructure-Z72UJ5NT.js +44 -0
- package/dist/{migrate-name-placeholder-C7NCNLWF.js → migrate-name-placeholder-64AEAWPE.js} +1 -1
- package/dist/{migrate-thread-config-EQSEP2XG.js → migrate-thread-config-SVF2M3D3.js} +1 -1
- package/dist/{mind-CT6ANXK4.js → mind-BARCLI7O.js} +4 -4
- package/dist/{mind-activity-tracker-QF6DCX2X.js → mind-activity-tracker-UT23RGBF.js} +3 -3
- package/dist/mind-service-ZJCUBAUL.js +48 -0
- package/dist/{package-F6S5NHY2.js → package-AUDBDKAO.js} +10 -12
- package/dist/{prompts-JECJIFPQ.js → prompts-MOBF3TMO.js} +1 -1
- package/dist/{sandbox-NNWCTHRI.js → sandbox-VI7XWBIA.js} +2 -2
- package/dist/scheduler-GLINVC2B.js +27 -0
- package/dist/{seed-DRKAP3XV.js → seed-5JGR7ZRA.js} +1 -1
- package/dist/{seed-cmd-6IMTO3LG.js → seed-cmd-M6A7N7F3.js} +2 -2
- package/dist/{seed-create-VNLDYRFK.js → seed-create-TGFZW4IO.js} +3 -3
- package/dist/{seed-readiness-H5TTPAQH.js → seed-readiness-XBH2MUNA.js} +2 -2
- package/dist/{seed-sprout-PPGYGK4G.js → seed-sprout-3IC5ZVTI.js} +7 -7
- package/dist/{send-UXUQKO4N.js → send-DHYBAYK4.js} +2 -2
- package/dist/{setup-YUAQMDVZ.js → setup-6KAH3GRW.js} +1 -1
- package/dist/skills/volute-mind/SKILL.md +8 -0
- package/dist/{skills-GMQ5UIIW.js → skills-LKQCYFVP.js} +2 -2
- package/dist/sleep-manager-Q3ITFNOJ.js +42 -0
- package/dist/{spirit-Q5VGFWUI.js → spirit-FKXMGO3N.js} +5 -5
- package/dist/spirit-availability-5R2WYSWF.js +43 -0
- package/dist/{sprout-PMTRL7QN.js → sprout-VEW3CUNX.js} +1 -1
- package/dist/{src-H3UCOP6B.js → src-246GSX7A.js} +5 -2
- package/dist/src-LOUB3WPR.js +4189 -0
- package/dist/{system-events-Q7IVZ5T4.js → system-events-ZYOCYAO2.js} +5 -3
- package/dist/{tailscale-KHNZFC7L.js → tailscale-F2IMBILZ.js} +1 -1
- package/dist/{template-hash-YKWMVYMM.js → template-hash-RJRMF6DT.js} +2 -2
- package/dist/{template-staleness-PY2CULH6.js → template-staleness-IBD722PN.js} +4 -4
- package/dist/{turn-tracker-HZDPMSCC.js → turn-tracker-BE34OI6O.js} +7 -7
- package/dist/{up-XZDSJVMU.js → up-J47J5JRB.js} +1 -1
- package/dist/variant-cleanup-JGJZ5KB4.js +24 -0
- package/dist/{version-notify-VNVFX6UP.js → version-notify-KIXYNHJC.js} +11 -11
- package/dist/web-assets/assets/{index-CwtxGwe_.js → index-CoU1eUKE.js} +15 -15
- package/dist/web-assets/assets/index-hPn_fkz3.css +1 -0
- package/dist/web-assets/index.html +2 -2
- package/package.json +10 -12
- package/packages/extensions/intentions/dist/ui/assets/index-Bl5KEcli.js +67 -0
- package/packages/extensions/intentions/dist/ui/index.html +1 -1
- package/packages/extensions/pages/dist/ui/assets/index-BKxVQkA9.css +1 -0
- package/packages/extensions/pages/dist/ui/assets/index-BMt6sStP.js +68 -0
- package/packages/extensions/pages/dist/ui/index.html +2 -2
- package/packages/extensions/pages/skills/pages/SKILL.md +123 -4
- package/templates/_base/.init/.config/prompts.json +0 -1
- package/templates/_base/.init/.local/hooks/pre-prompt/turn-context.ts +30 -0
- package/templates/_base/src/lib/codex-session-seed.ts +6 -55
- package/templates/_base/src/lib/pi-session-seed.ts +8 -57
- package/templates/_base/src/lib/seed-note.ts +4 -4
- package/templates/_base/src/lib/session-seed.ts +8 -53
- package/templates/_base/src/lib/startup.ts +0 -20
- package/templates/claude/package.json +2 -2
- package/templates/claude/src/agent.ts +34 -102
- package/templates/claude/src/lib/hooks/pre-compact.ts +6 -6
- package/templates/claude/src/server.ts +0 -1
- package/templates/codex/package.json +2 -2
- package/templates/codex/src/agent.ts +23 -89
- package/templates/pi/package.json +1 -1
- package/templates/pi/src/agent.ts +26 -93
- package/templates/pi/src/server.ts +0 -1
- package/dist/auto-upgrade-HEVZKPTF.js +0 -46
- package/dist/chunk-AAPNU7GA.js +0 -168
- package/dist/echo-text-73CYQRL7.js +0 -40
- package/dist/message-delivery-TRKT4T2P.js +0 -46
- package/dist/mind-service-7EFIJ3ZA.js +0 -48
- package/dist/scheduler-73Q4MQ7Y.js +0 -27
- package/dist/sleep-manager-KHI65FEX.js +0 -42
- package/dist/spirit-availability-G5FMWKRH.js +0 -43
- package/dist/src-3FIW7DLH.js +0 -2376
- package/dist/src-LTTQVN2Z.js +0 -832
- package/dist/variant-cleanup-QHGGY7ND.js +0 -24
- package/dist/web-assets/assets/index-DhM1yYRN.css +0 -1
- package/packages/extensions/intentions/dist/ui/assets/index-CQz4aceM.js +0 -67
- package/packages/extensions/notes/dist/ui/assets/index-BBs8pyel.js +0 -67
- package/packages/extensions/notes/dist/ui/assets/index-D3cdui42.css +0 -1
- package/packages/extensions/notes/dist/ui/index.html +0 -14
- package/packages/extensions/notes/skills/notes/SKILL.md +0 -90
- package/packages/extensions/pages/dist/ui/assets/index-B589e8Xe.js +0 -2
- package/packages/extensions/pages/dist/ui/assets/index-BuXnE-rb.css +0 -1
- /package/dist/{chunk-ZNHL5KQJ.js → chunk-OKM67ZWH.js} +0 -0
|
@@ -278,49 +278,6 @@ export function buildSeededRollout(
|
|
|
278
278
|
return emitFromKept(p.meta, p.kept, tailStartByBudget(p.kept, seedTokens), threadId, now);
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
/**
|
|
282
|
-
* A pinned cut point for rotation. Codex `response_item` message lines carry no
|
|
283
|
-
* stable id, so we pin the boundary by its top-level `timestamp` (millisecond
|
|
284
|
-
* precision — unique in practice). Rotation locates the same turn by matching it.
|
|
285
|
-
*/
|
|
286
|
-
export type CodexSeedCut = { boundaryTimestamp: string };
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Compute the cut point (the first-kept turn's timestamp) for a `seedTokens` budget,
|
|
290
|
-
* from raw jsonl. Parsed leniently because the live rollout may still be mid-write
|
|
291
|
-
* when this runs at warn time. Returns null if there's no genuine turn or the chosen
|
|
292
|
-
* boundary lacks a timestamp to pin on.
|
|
293
|
-
*/
|
|
294
|
-
export function computeCodexSeedCut(jsonl: string, seedTokens: number): CodexSeedCut | null {
|
|
295
|
-
const p = parseRollout(jsonl, true);
|
|
296
|
-
if (!p || p.kept.length === 0) return null;
|
|
297
|
-
if (keptBoundaries(p.kept).length === 0) return null;
|
|
298
|
-
const boundary = p.kept[tailStartByBudget(p.kept, seedTokens)].obj;
|
|
299
|
-
if (typeof boundary.timestamp !== "string") return null;
|
|
300
|
-
return { boundaryTimestamp: boundary.timestamp };
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Build the seeded rollout starting at a pinned boundary timestamp (the whole tail
|
|
305
|
-
* from that turn through EOF). Used by rotation to honor the cut point the mind was
|
|
306
|
-
* warned about, regardless of how much the rollout grew after the warning. Returns
|
|
307
|
-
* null if the boundary isn't found or a line is corrupt.
|
|
308
|
-
*/
|
|
309
|
-
export function buildSeededRolloutFromCut(
|
|
310
|
-
jsonl: string,
|
|
311
|
-
threadId: string,
|
|
312
|
-
boundaryTimestamp: string,
|
|
313
|
-
now: Date = new Date(),
|
|
314
|
-
): SeededRollout | null {
|
|
315
|
-
const p = parseRollout(jsonl, false);
|
|
316
|
-
if (!p || p.kept.length === 0) return null;
|
|
317
|
-
const startIdx = p.kept.findIndex(
|
|
318
|
-
(k) => isTurnBoundary(k.obj) && k.obj.timestamp === boundaryTimestamp,
|
|
319
|
-
);
|
|
320
|
-
if (startIdx < 0) return null;
|
|
321
|
-
return emitFromKept(p.meta, p.kept, startIdx, threadId, now);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
281
|
/** Two-digit zero-padded string for date/time components. */
|
|
325
282
|
function pad2(n: number): string {
|
|
326
283
|
return String(n).padStart(2, "0");
|
|
@@ -417,32 +374,26 @@ export function writeCodexRotationArchivePointer(
|
|
|
417
374
|
|
|
418
375
|
/**
|
|
419
376
|
* Rotate a codex session in place at the context limit. Reads the live rollout, builds
|
|
420
|
-
* a seeded tail
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
* caller then leaves the old thread in place). Never throws.
|
|
377
|
+
* a seeded budget-based tail, writes it as a new synthetic rollout next to the source,
|
|
378
|
+
* and — for persistent sessions — archives the rotated-out thread pointer so the full
|
|
379
|
+
* transcript stays findable. Returns the new thread id, or null if rotation can't proceed
|
|
380
|
+
* (the caller then leaves the old thread in place). Never throws.
|
|
425
381
|
*/
|
|
426
382
|
export function rotateCodexSession(opts: {
|
|
427
383
|
mindDir: string;
|
|
428
384
|
name: string;
|
|
429
385
|
oldThreadId: string;
|
|
430
|
-
cut: CodexSeedCut | null;
|
|
431
386
|
seedTokens: number;
|
|
432
387
|
now?: Date;
|
|
433
388
|
}): string | null {
|
|
434
|
-
const { mindDir, name, oldThreadId,
|
|
389
|
+
const { mindDir, name, oldThreadId, seedTokens } = opts;
|
|
435
390
|
const now = opts.now ?? new Date();
|
|
436
391
|
try {
|
|
437
392
|
const sourcePath = findCodexSessionFile(oldThreadId, mindDir);
|
|
438
393
|
if (!sourcePath) return null; // live rollout not found — leave the old thread
|
|
439
394
|
const jsonl = readFileSync(sourcePath, "utf-8");
|
|
440
395
|
const threadId = generateThreadId(now);
|
|
441
|
-
|
|
442
|
-
// budget-based tail if the pin can't be resolved (missing/renamed boundary).
|
|
443
|
-
const seeded =
|
|
444
|
-
(cut ? buildSeededRolloutFromCut(jsonl, threadId, cut.boundaryTimestamp, now) : null) ??
|
|
445
|
-
buildSeededRollout(jsonl, threadId, seedTokens, now);
|
|
396
|
+
const seeded = buildSeededRollout(jsonl, threadId, seedTokens, now);
|
|
446
397
|
if (!seeded) return null;
|
|
447
398
|
|
|
448
399
|
writeSeededRollout(sourcePath, seeded, now);
|
|
@@ -239,49 +239,6 @@ export function buildSeededPiTranscript(
|
|
|
239
239
|
return emitPiTail(h.header, h.entries, h.entryRaws, startIdx, opts.cwd, opts.sourcePath);
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
/** A pinned cut point for rotation: where the verbatim tail begins. */
|
|
243
|
-
export type PiSeedCut = { boundaryId: string; boundaryTimestamp: string | null };
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Compute the cut point (the first-kept turn's identity) for a `seedTokens` budget,
|
|
247
|
-
* from raw jsonl. Parsed leniently because the live transcript may still be mid-write
|
|
248
|
-
* when this runs at warn time. Returns null if there's no genuine turn or the chosen
|
|
249
|
-
* boundary entry lacks an id.
|
|
250
|
-
*/
|
|
251
|
-
export function computePiSeedCut(jsonl: string, seedTokens: number): PiSeedCut | null {
|
|
252
|
-
const p = parsePiJsonl(jsonl, true);
|
|
253
|
-
if (!p) return null;
|
|
254
|
-
const h = splitPiHeader(p.parsed, p.raws);
|
|
255
|
-
if (!h) return null;
|
|
256
|
-
const startIdx = tailStartByBudget(h.entries, h.entryRaws, seedTokens);
|
|
257
|
-
if (startIdx < 0) return null;
|
|
258
|
-
const boundary = h.entries[startIdx];
|
|
259
|
-
if (typeof boundary.id !== "string") return null;
|
|
260
|
-
return {
|
|
261
|
-
boundaryId: boundary.id,
|
|
262
|
-
boundaryTimestamp: typeof boundary.timestamp === "string" ? boundary.timestamp : null,
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Build the seeded transcript starting at a pinned boundary entry id (the whole tail
|
|
268
|
-
* from that entry through EOF). Used by rotation to honor the cut point the mind was
|
|
269
|
-
* warned about, regardless of how much the transcript grew after the warning.
|
|
270
|
-
* Returns null if the boundary id isn't found or a line is corrupt.
|
|
271
|
-
*/
|
|
272
|
-
export function buildSeededPiTranscriptFromCut(
|
|
273
|
-
jsonl: string,
|
|
274
|
-
opts: { cwd: string; boundaryId: string; sourcePath?: string },
|
|
275
|
-
): SeededPiTranscript | null {
|
|
276
|
-
const p = parsePiJsonl(jsonl, false);
|
|
277
|
-
if (!p) return null;
|
|
278
|
-
const h = splitPiHeader(p.parsed, p.raws);
|
|
279
|
-
if (!h) return null;
|
|
280
|
-
const startIdx = h.entries.findIndex((e) => e.id === opts.boundaryId);
|
|
281
|
-
if (startIdx < 0) return null;
|
|
282
|
-
return emitPiTail(h.header, h.entries, h.entryRaws, startIdx, opts.cwd, opts.sourcePath);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
242
|
/**
|
|
286
243
|
* True if `<piSessionsDir>/<name>/` already holds a live pi `.jsonl` session —
|
|
287
244
|
* in which case continueRecent will resume it and we must not seed over it.
|
|
@@ -379,30 +336,24 @@ function archiveRotatedPiFile(
|
|
|
379
336
|
|
|
380
337
|
/**
|
|
381
338
|
* Rotate a pi session in place at the context limit. Reads the live session file,
|
|
382
|
-
* builds a
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
339
|
+
* builds a budget-based tail (as many whole trailing turns as fit in `seedTokens`),
|
|
340
|
+
* writes it as a new session file in the same live dir, and — for persistent
|
|
341
|
+
* sessions — archives the rotated-out file so the full transcript stays findable.
|
|
342
|
+
* Returns the new session file **path** (the caller switches the running
|
|
343
|
+
* SessionManager to it), or null if rotation can't proceed (the caller then falls
|
|
344
|
+
* back to a fresh session). Never throws.
|
|
388
345
|
*/
|
|
389
346
|
export function rotatePiSession(opts: {
|
|
390
347
|
cwd: string;
|
|
391
348
|
sessionsDir: string;
|
|
392
349
|
name: string;
|
|
393
350
|
sourcePath: string;
|
|
394
|
-
cut: PiSeedCut | null;
|
|
395
351
|
seedTokens: number;
|
|
396
352
|
}): string | null {
|
|
397
|
-
const { cwd, sessionsDir, name, sourcePath,
|
|
353
|
+
const { cwd, sessionsDir, name, sourcePath, seedTokens } = opts;
|
|
398
354
|
try {
|
|
399
355
|
const jsonl = readFileSync(sourcePath, "utf-8");
|
|
400
|
-
|
|
401
|
-
// budget-based tail if the pin can't be resolved (missing/renamed id).
|
|
402
|
-
const seeded =
|
|
403
|
-
(cut
|
|
404
|
-
? buildSeededPiTranscriptFromCut(jsonl, { cwd, boundaryId: cut.boundaryId, sourcePath })
|
|
405
|
-
: null) ?? buildSeededPiTranscript(jsonl, { cwd, seedTokens, sourcePath });
|
|
356
|
+
const seeded = buildSeededPiTranscript(jsonl, { cwd, seedTokens, sourcePath });
|
|
406
357
|
if (!seeded) return null;
|
|
407
358
|
|
|
408
359
|
const liveDir = resolve(sessionsDir, name);
|
|
@@ -21,12 +21,12 @@ export const SEEDED_SESSION_NOTE_BASE =
|
|
|
21
21
|
"Note: this session continues from your previous session's transcript (restored after archival). The conversation above happened before the break; a fresh session begins here.";
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Note for the "rotation" cause.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* Note for the "rotation" cause. One line, low-salience by design: rotation is
|
|
25
|
+
* meant to be near-invisible — no gap clause (the break is instantaneous), no
|
|
26
|
+
* task, just an honest pointer to where the collapsed turns' summaries live.
|
|
27
27
|
*/
|
|
28
28
|
export const ROTATED_SESSION_NOTE =
|
|
29
|
-
"Note: this session was
|
|
29
|
+
"Note: this session was consolidated at the context limit — the turns above are kept verbatim; summaries of older turns are in your history (`volute mind history`).";
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Parse an archive-pointer timestamp into epoch millis, or null if it doesn't
|
|
@@ -117,8 +117,7 @@ function estimateTokens(raw: string): number {
|
|
|
117
117
|
* Parse jsonl into aligned parsed/raw arrays. In strict mode a corrupt line aborts
|
|
118
118
|
* (returns null — used when copying verbatim, where a broken line means we can't
|
|
119
119
|
* faithfully reconstruct the chain). In lenient mode corrupt lines are skipped
|
|
120
|
-
* (
|
|
121
|
-
* cut point while the SDK is streaming).
|
|
120
|
+
* (tolerating a transcript that may still be mid-write).
|
|
122
121
|
*/
|
|
123
122
|
function parseJsonl(
|
|
124
123
|
jsonl: string,
|
|
@@ -200,9 +199,6 @@ function emitTail(parsed: JsonlLine[], startLine: number): SeededTranscript {
|
|
|
200
199
|
|
|
201
200
|
export type SeededTranscript = { sessionId: string; lines: string[] };
|
|
202
201
|
|
|
203
|
-
/** A pinned cut point for rotation: where the verbatim tail begins. */
|
|
204
|
-
export type SeedCut = { boundaryUuid: string; boundaryTimestamp: string | null };
|
|
205
|
-
|
|
206
202
|
/**
|
|
207
203
|
* Build the seeded transcript from an old transcript's raw jsonl text: take as
|
|
208
204
|
* many whole trailing turns as fit in `seedTokens` (always at least the final
|
|
@@ -218,42 +214,6 @@ export function buildSeededTranscript(jsonl: string, seedTokens: number): Seeded
|
|
|
218
214
|
return emitTail(p.parsed, startLine);
|
|
219
215
|
}
|
|
220
216
|
|
|
221
|
-
/**
|
|
222
|
-
* Compute the cut point (the first-kept turn's identity) for a `seedTokens` budget,
|
|
223
|
-
* from raw jsonl. Parsed leniently because the live transcript may still be
|
|
224
|
-
* mid-write when this runs at warn time. Returns null if there's no genuine turn
|
|
225
|
-
* or the chosen boundary lacks a uuid.
|
|
226
|
-
*/
|
|
227
|
-
export function computeSeedCut(jsonl: string, seedTokens: number): SeedCut | null {
|
|
228
|
-
const p = parseJsonl(jsonl, true);
|
|
229
|
-
if (!p || p.parsed.length === 0) return null;
|
|
230
|
-
if (turnBoundaries(p.parsed).length === 0) return null;
|
|
231
|
-
const startLine = tailStartByBudget(p.parsed, p.raws, seedTokens);
|
|
232
|
-
const boundary = p.parsed[startLine];
|
|
233
|
-
if (typeof boundary.uuid !== "string") return null;
|
|
234
|
-
return {
|
|
235
|
-
boundaryUuid: boundary.uuid,
|
|
236
|
-
boundaryTimestamp: typeof boundary.timestamp === "string" ? boundary.timestamp : null,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Build the seeded transcript starting at a pinned boundary uuid (the whole tail
|
|
242
|
-
* from that line through EOF). Used by rotation to honor the cut point the mind
|
|
243
|
-
* was warned about, regardless of how much the transcript grew after the warning.
|
|
244
|
-
* Returns null if the boundary uuid isn't found or a line is corrupt.
|
|
245
|
-
*/
|
|
246
|
-
export function buildSeededTranscriptFromCut(
|
|
247
|
-
jsonl: string,
|
|
248
|
-
boundaryUuid: string,
|
|
249
|
-
): SeededTranscript | null {
|
|
250
|
-
const p = parseJsonl(jsonl, false);
|
|
251
|
-
if (!p || p.parsed.length === 0) return null;
|
|
252
|
-
const startLine = p.parsed.findIndex((o) => o.uuid === boundaryUuid);
|
|
253
|
-
if (startLine < 0) return null;
|
|
254
|
-
return emitTail(p.parsed, startLine);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
217
|
/** Result of a successful seed: the new session id and when the source was archived. */
|
|
258
218
|
export type SeedOutcome = { sessionId: string; archivedAt: number | null };
|
|
259
219
|
|
|
@@ -328,30 +288,25 @@ export function writeRotationArchivePointer(
|
|
|
328
288
|
|
|
329
289
|
/**
|
|
330
290
|
* Rotate a session in place at the context limit. Reads the live transcript, builds
|
|
331
|
-
* a
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
291
|
+
* a budget-based tail (as many whole trailing turns as fit in `seedTokens`), writes
|
|
292
|
+
* it as a new synthetic session file next to the source, and — for persistent
|
|
293
|
+
* sessions — archives the rotated-out pointer so the full transcript stays findable.
|
|
294
|
+
* Returns the new session id, or null if rotation can't proceed (the caller then
|
|
295
|
+
* falls back to a fresh session). Never throws.
|
|
336
296
|
*/
|
|
337
297
|
export function rotateSession(opts: {
|
|
338
298
|
cwd: string;
|
|
339
299
|
sessionsDir: string;
|
|
340
300
|
name: string;
|
|
341
301
|
oldSessionId: string;
|
|
342
|
-
cut: SeedCut | null;
|
|
343
302
|
seedTokens: number;
|
|
344
303
|
}): string | null {
|
|
345
|
-
const { cwd, sessionsDir, name, oldSessionId,
|
|
304
|
+
const { cwd, sessionsDir, name, oldSessionId, seedTokens } = opts;
|
|
346
305
|
try {
|
|
347
306
|
const sourcePath = findClaudeSessionFile(cwd, oldSessionId);
|
|
348
307
|
if (!sourcePath) return null; // live transcript not found — fall back to fresh
|
|
349
308
|
const jsonl = readFileSync(sourcePath, "utf-8");
|
|
350
|
-
|
|
351
|
-
// budget-based tail if the pin can't be resolved (missing/renamed uuid).
|
|
352
|
-
const seeded =
|
|
353
|
-
(cut ? buildSeededTranscriptFromCut(jsonl, cut.boundaryUuid) : null) ??
|
|
354
|
-
buildSeededTranscript(jsonl, seedTokens);
|
|
309
|
+
const seeded = buildSeededTranscript(jsonl, seedTokens);
|
|
355
310
|
if (!seeded) return null;
|
|
356
311
|
|
|
357
312
|
writeFileSync(
|
|
@@ -40,7 +40,6 @@ export type EffortLevel = "low" | "medium" | "high" | "xhigh" | "max";
|
|
|
40
40
|
export type MindConfig = {
|
|
41
41
|
model?: string;
|
|
42
42
|
logLevel?: "error" | "warn" | "info" | "debug";
|
|
43
|
-
compactionMessage?: string;
|
|
44
43
|
compaction?: { maxContextTokens?: number };
|
|
45
44
|
/** Idle minutes before a session's SDK subprocess is reaped. 0 disables. Default 30. */
|
|
46
45
|
sessionIdleMinutes?: number;
|
|
@@ -103,21 +102,6 @@ export function memorySizeLabel(): string {
|
|
|
103
102
|
return formatTokens(estimateTokens(loadFile(resolve("home/MEMORY.md")).length));
|
|
104
103
|
}
|
|
105
104
|
|
|
106
|
-
/**
|
|
107
|
-
* Render the compaction warning for sending: substitute ${date} and
|
|
108
|
-
* ${memory_size} with current values. Called per send — the date rolls over
|
|
109
|
-
* and MEMORY.md changes as the mind edits it.
|
|
110
|
-
*/
|
|
111
|
-
export function renderCompactionWarning(template: string): string {
|
|
112
|
-
return (
|
|
113
|
-
template
|
|
114
|
-
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${date} placeholder
|
|
115
|
-
.replaceAll("${date}", new Date().toLocaleDateString("en-CA"))
|
|
116
|
-
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${memory_size} placeholder
|
|
117
|
-
.replaceAll("${memory_size}", memorySizeLabel())
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
105
|
function headAtLineBoundary(text: string, maxChars: number): string {
|
|
122
106
|
const cut = text.lastIndexOf("\n", maxChars);
|
|
123
107
|
// Prefer a whole-line cut, but not when the nearest newline is so far back
|
|
@@ -251,7 +235,6 @@ export async function getStartupContext(): Promise<string | null> {
|
|
|
251
235
|
}
|
|
252
236
|
|
|
253
237
|
export type MindPrompts = {
|
|
254
|
-
compaction_warning: string;
|
|
255
238
|
compaction_instructions: string;
|
|
256
239
|
reply_instructions: string;
|
|
257
240
|
event_instructions: string;
|
|
@@ -264,9 +247,6 @@ export type MindPrompts = {
|
|
|
264
247
|
* (the copy the Prompt Library UI edits); test/prompts.test.ts fails if they drift.
|
|
265
248
|
*/
|
|
266
249
|
export const DEFAULT_PROMPTS: MindPrompts = {
|
|
267
|
-
compaction_warning:
|
|
268
|
-
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${cutoff}/${date}/${memory_size} prompt template
|
|
269
|
-
"Context limit approaching — this session will rotate shortly. Turns before ${cutoff} will be collapsed to their summaries; turns from ${cutoff} on are kept verbatim in the continued session, so there's no need to re-describe them.\n\nFor the turns that will collapse, make sure they read the way you'd want: `volute mind history --provisional` shows the provisional summaries, and `volute mind history --write --turn <id> --text \"...\"` replaces any with your own account. Also save anything important to your files (memory/journal/${date}.md, or a memory/ file). Provisional summaries are kept if you write nothing — nothing blocks on this.\n\nYour MEMORY.md is currently ${memory_size}. It is loaded into every request, so consolidate rather than append — distill detail into memory/ files and keep MEMORY.md lean (see the memory skill).",
|
|
270
250
|
compaction_instructions:
|
|
271
251
|
"Preserve your sense of who you are, what matters to you, what happened in this conversation, and the threads of thought and connection you'd want to return to.",
|
|
272
252
|
// Quoted: `${channel}` is `#garden` for a Volute channel, and an unquoted `#` starts a
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
"typecheck": "tsc --noEmit"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.
|
|
12
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.217",
|
|
13
13
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
14
14
|
"tsx": "^4.23.1"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@biomejs/biome": "2.5.
|
|
17
|
+
"@biomejs/biome": "2.5.5",
|
|
18
18
|
"@types/node": "^26.1.1",
|
|
19
19
|
"typescript": "^6.0.3"
|
|
20
20
|
}
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
readSkillDescriptions,
|
|
15
15
|
} from "./lib/context-breakdown.js";
|
|
16
16
|
import { daemonEmit, daemonNotice } from "./lib/daemon-client.js";
|
|
17
|
-
import { compactTimestamp } from "./lib/format-prefix.js";
|
|
18
17
|
import { runHooks } from "./lib/hook-loader.js";
|
|
19
18
|
import { createAutoCommitHook } from "./lib/hooks/auto-commit.js";
|
|
20
19
|
import { createIdentityReloadHook } from "./lib/hooks/identity-reload.js";
|
|
@@ -30,16 +29,9 @@ import {
|
|
|
30
29
|
reapSessionQuery,
|
|
31
30
|
reapSessionsForShutdown,
|
|
32
31
|
} from "./lib/session-reaper.js";
|
|
33
|
-
import {
|
|
34
|
-
computeSeedCut,
|
|
35
|
-
DEFAULT_SEED_TOKENS,
|
|
36
|
-
rotateSession,
|
|
37
|
-
type SeedCut,
|
|
38
|
-
seedSession,
|
|
39
|
-
} from "./lib/session-seed.js";
|
|
32
|
+
import { DEFAULT_SEED_TOKENS, rotateSession, seedSession } from "./lib/session-seed.js";
|
|
40
33
|
import { createSessionStore } from "./lib/session-store.js";
|
|
41
|
-
import type { EffortLevel, ThinkingConfig } from "./lib/startup.js";
|
|
42
|
-
import { loadPrompts, renderCompactionWarning, type SubagentConfig } from "./lib/startup.js";
|
|
34
|
+
import type { EffortLevel, SubagentConfig, ThinkingConfig } from "./lib/startup.js";
|
|
43
35
|
import { consumeStream, type MessageIdEntry } from "./lib/stream-consumer.js";
|
|
44
36
|
import type {
|
|
45
37
|
HandlerMeta,
|
|
@@ -77,12 +69,6 @@ type Session = {
|
|
|
77
69
|
seededArchivedAt: number | null;
|
|
78
70
|
/** Why the tail is seeded — picks the boundary note's wording. Last cause wins. */
|
|
79
71
|
seededCause: SeedCause;
|
|
80
|
-
/**
|
|
81
|
-
* The cut point pinned at warn time (the first-kept turn the mind was told would
|
|
82
|
-
* survive verbatim). Honored at rotation so the promise in the warning holds even
|
|
83
|
-
* though the transcript grows after the warning. Null until a warning fires.
|
|
84
|
-
*/
|
|
85
|
-
rotationCut: SeedCut | null;
|
|
86
72
|
/**
|
|
87
73
|
* Back-to-back rotations that did NOT bring context under the threshold (reset by
|
|
88
74
|
* any healthy turn). Guards against a runaway loop when the tail alone can't fit —
|
|
@@ -91,20 +77,15 @@ type Session = {
|
|
|
91
77
|
*/
|
|
92
78
|
consecutiveRotations: number;
|
|
93
79
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* the
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* - "rotateAfterTurn": the wrap-up turn is the current/next turn; rotate when it ends.
|
|
100
|
-
* Lives on the session (not a map) so it can't leak into a resumed session if the
|
|
101
|
-
* stream dies mid-flight — a fresh session starts idle.
|
|
80
|
+
* True when this session crossed the context threshold (or the SDK tried to
|
|
81
|
+
* auto-compact) and should rotate in place when the current turn ends. Rotation
|
|
82
|
+
* is deliberately silent: no warning, no wrap-up turn — the auto summarizer's
|
|
83
|
+
* turn summaries are the record of collapsed turns, and the one-line
|
|
84
|
+
* ROTATED_SESSION_NOTE marks the boundary on the next turn.
|
|
102
85
|
*/
|
|
103
|
-
|
|
86
|
+
rotationPending: boolean;
|
|
104
87
|
};
|
|
105
88
|
|
|
106
|
-
type RotationPhase = "warned" | "rotateAfterTurn";
|
|
107
|
-
|
|
108
89
|
/** Stop self-rotating after this many back-to-back rotations that didn't reduce context. */
|
|
109
90
|
const MAX_CONSECUTIVE_ROTATIONS = 3;
|
|
110
91
|
|
|
@@ -116,7 +97,6 @@ export function createMind(options: {
|
|
|
116
97
|
thinking?: ThinkingConfig;
|
|
117
98
|
effort?: EffortLevel;
|
|
118
99
|
sessionsDir: string;
|
|
119
|
-
compactionMessage?: string;
|
|
120
100
|
maxContextTokens?: number;
|
|
121
101
|
subagents?: Record<string, SubagentConfig>;
|
|
122
102
|
onIdentityReload?: () => Promise<void>;
|
|
@@ -139,9 +119,6 @@ export function createMind(options: {
|
|
|
139
119
|
];
|
|
140
120
|
|
|
141
121
|
const sessions = new Map<string, Session>();
|
|
142
|
-
const prompts = loadPrompts();
|
|
143
|
-
const compactionMessage = () =>
|
|
144
|
-
options.compactionMessage ?? renderCompactionWarning(prompts.compaction_warning);
|
|
145
122
|
const maxContextTokens = options.maxContextTokens;
|
|
146
123
|
const seedTokens = options.seedTokens ?? DEFAULT_SEED_TOKENS;
|
|
147
124
|
|
|
@@ -396,60 +373,19 @@ export function createMind(options: {
|
|
|
396
373
|
let currentSessionId = savedSessionId;
|
|
397
374
|
let streamAbort = new AbortController();
|
|
398
375
|
|
|
399
|
-
/**
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
* from the live transcript, pins the first-kept turn (so rotation honors the
|
|
403
|
-
* same boundary even as the transcript grows), interpolates its time into the
|
|
404
|
-
* `${cutoff}` placeholder, enters the rotation state machine, and pushes the
|
|
405
|
-
* warning as the wrap-up prompt. Shared by the threshold and PreCompact paths.
|
|
406
|
-
*
|
|
407
|
-
* Crucially it does NOT abort here: the mind takes a wrap-up turn (reading the
|
|
408
|
-
* pinned cutoff and authoring `volute mind history` summaries with full context
|
|
409
|
-
* of the turns being collapsed) before rotation. The entry phase depends on
|
|
410
|
-
* whether a turn is mid-flight: if one is (threshold path fires on an assistant
|
|
411
|
-
* message), it must finish before the queued warning runs, so we start "warned";
|
|
412
|
-
* if we're between turns (PreCompact fired at a boundary), the warning IS the
|
|
413
|
-
* next turn, so we start "rotateAfterTurn".
|
|
414
|
-
*/
|
|
415
|
-
function warnAndPinRotation() {
|
|
416
|
-
let cutoffLabel = "the cutoff";
|
|
417
|
-
session.rotationCut = null;
|
|
418
|
-
if (currentSessionId) {
|
|
419
|
-
try {
|
|
420
|
-
const path = findClaudeSessionFile(options.cwd, currentSessionId);
|
|
421
|
-
const cut = path ? computeSeedCut(readFileSync(path, "utf-8"), seedTokens) : null;
|
|
422
|
-
if (cut) {
|
|
423
|
-
session.rotationCut = cut;
|
|
424
|
-
if (cut.boundaryTimestamp) {
|
|
425
|
-
const d = new Date(cut.boundaryTimestamp);
|
|
426
|
-
if (!Number.isNaN(d.getTime())) cutoffLabel = compactTimestamp(d);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
} catch (err) {
|
|
430
|
-
log("mind", `session "${session.name}": failed to pin rotation cut:`, err);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${cutoff} prompt placeholder
|
|
434
|
-
const warning = compactionMessage().replaceAll("${cutoff}", cutoffLabel);
|
|
435
|
-
session.rotationPhase =
|
|
436
|
-
session.currentMessageId !== undefined ? "warned" : "rotateAfterTurn";
|
|
437
|
-
const seq = session.channel.push({
|
|
438
|
-
type: "user",
|
|
439
|
-
session_id: "",
|
|
440
|
-
message: { role: "user", content: [{ type: "text", text: warning }] },
|
|
441
|
-
parent_tool_use_id: null,
|
|
442
|
-
});
|
|
443
|
-
session.messageIds.push({ id: undefined, seq });
|
|
376
|
+
/** Mark the session to rotate in place when the current turn ends. */
|
|
377
|
+
function scheduleRotation() {
|
|
378
|
+
session.rotationPending = true;
|
|
444
379
|
}
|
|
445
380
|
|
|
446
|
-
// PreCompact backstop: first fire
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
//
|
|
381
|
+
// PreCompact backstop: first fire schedules a rotation (unless one is already
|
|
382
|
+
// pending, or the rotation cap is hit) and blocks; second fire allows the SDK's
|
|
383
|
+
// native compaction — the emergency backstop when rotation never intervened
|
|
384
|
+
// (a hung turn, or a system prompt too large for rotation to relieve).
|
|
450
385
|
const preCompact = createPreCompactHook(() => {
|
|
451
|
-
if (!session.
|
|
452
|
-
|
|
386
|
+
if (!session.rotationPending && session.consecutiveRotations < MAX_CONSECUTIVE_ROTATIONS) {
|
|
387
|
+
log("mind", `session "${session.name}": native compaction — scheduling rotation`);
|
|
388
|
+
scheduleRotation();
|
|
453
389
|
}
|
|
454
390
|
});
|
|
455
391
|
|
|
@@ -471,13 +407,12 @@ export function createMind(options: {
|
|
|
471
407
|
// the closest-to-arrival signal available without reading the transcript back.
|
|
472
408
|
session.seeded = false;
|
|
473
409
|
await autoCommit.flushFileChanges();
|
|
474
|
-
if (session.
|
|
475
|
-
// The
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
log("mind", `session "${session.name}": wrap-up turn done — aborting to rotate`);
|
|
410
|
+
if (session.rotationPending) {
|
|
411
|
+
// The turn that crossed the threshold is done — abort to rotate in place.
|
|
412
|
+
log(
|
|
413
|
+
"mind",
|
|
414
|
+
`session "${session.name}": turn ended over context limit — aborting to rotate`,
|
|
415
|
+
);
|
|
481
416
|
streamAbort.abort(new CompactionAbort());
|
|
482
417
|
} else {
|
|
483
418
|
// A healthy turn (context under the threshold) — the rotation streak, if
|
|
@@ -491,14 +426,14 @@ export function createMind(options: {
|
|
|
491
426
|
if (
|
|
492
427
|
maxContextTokens &&
|
|
493
428
|
tokens >= maxContextTokens &&
|
|
494
|
-
!session.
|
|
429
|
+
!session.rotationPending &&
|
|
495
430
|
session.consecutiveRotations < MAX_CONSECUTIVE_ROTATIONS
|
|
496
431
|
) {
|
|
497
432
|
log(
|
|
498
433
|
"mind",
|
|
499
|
-
`session "${session.name}": ${tokens} tokens >= ${maxContextTokens} —
|
|
434
|
+
`session "${session.name}": ${tokens} tokens >= ${maxContextTokens} — rotation pending at turn end`,
|
|
500
435
|
);
|
|
501
|
-
|
|
436
|
+
scheduleRotation();
|
|
502
437
|
}
|
|
503
438
|
},
|
|
504
439
|
};
|
|
@@ -546,17 +481,15 @@ export function createMind(options: {
|
|
|
546
481
|
currentSessionId
|
|
547
482
|
) {
|
|
548
483
|
// Stream was aborted to rotate: replace the session with a synthetic one
|
|
549
|
-
// holding the verbatim recent tail (
|
|
484
|
+
// holding the verbatim recent tail (the seedTokens-budget tail), then resume it.
|
|
550
485
|
const rotatedId = rotateSession({
|
|
551
486
|
cwd: options.cwd,
|
|
552
487
|
sessionsDir: options.sessionsDir,
|
|
553
488
|
name: session.name,
|
|
554
489
|
oldSessionId: currentSessionId,
|
|
555
|
-
cut: session.rotationCut,
|
|
556
490
|
seedTokens,
|
|
557
491
|
});
|
|
558
|
-
session.
|
|
559
|
-
session.rotationPhase = undefined; // state machine consumed
|
|
492
|
+
session.rotationPending = false;
|
|
560
493
|
if (!rotatedId) {
|
|
561
494
|
// Rotation couldn't proceed — fall back to a fresh session (no seed).
|
|
562
495
|
// Unlike a successful rotation (boundary note, verbatim tail, archived
|
|
@@ -602,8 +535,8 @@ export function createMind(options: {
|
|
|
602
535
|
preCompact.reset();
|
|
603
536
|
streamAbort = new AbortController();
|
|
604
537
|
// Recover input the aborted stream had already pulled but not finished
|
|
605
|
-
// (
|
|
606
|
-
//
|
|
538
|
+
// (anything that arrived mid-turn) so nothing is dropped when the killed
|
|
539
|
+
// subprocess takes its buffer with it.
|
|
607
540
|
const pending = session.channel.recover();
|
|
608
541
|
const oldMessageIds = session.messageIds;
|
|
609
542
|
session.channel = createMessageChannel();
|
|
@@ -637,8 +570,7 @@ export function createMind(options: {
|
|
|
637
570
|
// conversation continued when the seeded transcript failed to resume,
|
|
638
571
|
// and don't let a half-entered rotation leak into the fresh session.
|
|
639
572
|
session.seeded = false;
|
|
640
|
-
session.
|
|
641
|
-
session.rotationCut = null;
|
|
573
|
+
session.rotationPending = false;
|
|
642
574
|
streamAbort = new AbortController();
|
|
643
575
|
session.channel = createMessageChannel();
|
|
644
576
|
try {
|
|
@@ -682,8 +614,8 @@ export function createMind(options: {
|
|
|
682
614
|
seeded: false,
|
|
683
615
|
seededArchivedAt: null,
|
|
684
616
|
seededCause: "restored",
|
|
685
|
-
rotationCut: null,
|
|
686
617
|
consecutiveRotations: 0,
|
|
618
|
+
rotationPending: false,
|
|
687
619
|
};
|
|
688
620
|
sessions.set(name, session);
|
|
689
621
|
|
|
@@ -798,7 +730,7 @@ export function createMind(options: {
|
|
|
798
730
|
s,
|
|
799
731
|
now,
|
|
800
732
|
idleTimeoutMs,
|
|
801
|
-
(name) => sessions.get(name)?.
|
|
733
|
+
(name) => sessions.get(name)?.rotationPending === true,
|
|
802
734
|
),
|
|
803
735
|
);
|
|
804
736
|
// Reaps run independently; each awaits its own subprocess exit internally.
|
|
@@ -8,9 +8,9 @@ import { log } from "../logger.js";
|
|
|
8
8
|
* session in place. This hook covers the case where the SDK decides to auto-compact
|
|
9
9
|
* before (or instead of) that threshold firing:
|
|
10
10
|
*
|
|
11
|
-
* - First fire: run `onCompact` (
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* - First fire: run `onCompact` (schedule a rotation — mark the session to rotate
|
|
12
|
+
* at turn end) and BLOCK the SDK's native compaction, so the session rotates
|
|
13
|
+
* instead of being natively compacted.
|
|
14
14
|
* - Second fire: allow the SDK's native compaction as an emergency backstop —
|
|
15
15
|
* only reachable if rotation never happened (a hung turn, or rotation failed).
|
|
16
16
|
*
|
|
@@ -32,10 +32,10 @@ export function createPreCompactHook(onCompact: () => void) {
|
|
|
32
32
|
return {};
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
// Auto-compaction: first pass
|
|
36
|
-
//
|
|
35
|
+
// Auto-compaction: first pass schedules a rotation and blocks; second pass
|
|
36
|
+
// allows the native compaction as the emergency backstop.
|
|
37
37
|
if (!compactBlocked) {
|
|
38
|
-
log("mind", "blocking auto-compaction —
|
|
38
|
+
log("mind", "blocking auto-compaction — marking session for rotation");
|
|
39
39
|
try {
|
|
40
40
|
onCompact();
|
|
41
41
|
compactBlocked = true;
|
|
@@ -33,7 +33,6 @@ const mind = createMind({
|
|
|
33
33
|
thinking: config.thinking,
|
|
34
34
|
effort: config.effort,
|
|
35
35
|
sessionsDir,
|
|
36
|
-
compactionMessage: config.compactionMessage,
|
|
37
36
|
maxContextTokens: config.compaction?.maxContextTokens,
|
|
38
37
|
sessionIdleMinutes: config.sessionIdleMinutes,
|
|
39
38
|
seedTokens: config.continuity?.seedTokens,
|