volute 0.54.0 → 0.56.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.
Files changed (204) hide show
  1. package/dist/{activity-events-LQII6PZS.js → activity-events-YASF46HS.js} +3 -3
  2. package/dist/{ai-service-YW5SWCSB.js → ai-service-3UFNG6GI.js} +3 -5
  3. package/dist/{archive-FOHGXHPT.js → archive-L2OFBGDQ.js} +2 -2
  4. package/dist/{auth-KRDSM55Q.js → auth-N2F5ASAB.js} +4 -4
  5. package/dist/auto-upgrade-O4MQPYQF.js +46 -0
  6. package/dist/{avatar-image-52WGXLHV.js → avatar-image-QEKKUFDF.js} +3 -3
  7. package/dist/{backup-QRNX6RK7.js → backup-TPM7QQBQ.js} +13 -13
  8. package/dist/{channels-CKCY64TD.js → channels-JP3GYTPX.js} +18 -18
  9. package/dist/{chat-ZUGNL5GS.js → chat-7YCF4ZU5.js} +3 -3
  10. package/dist/{chunk-QUSMYGNQ.js → chunk-2674KIGI.js} +1 -0
  11. package/dist/{chunk-444RR3YF.js → chunk-2B4E6SL7.js} +14 -14
  12. package/dist/chunk-2GATCSZK.js +520 -0
  13. package/dist/{chunk-I7U5EEZC.js → chunk-2LFTJNVH.js} +2 -2
  14. package/dist/{chunk-L7BKTZ5U.js → chunk-2QLWAYIU.js} +2 -2
  15. package/dist/{chunk-F6D766VQ.js → chunk-45MCH4G4.js} +2 -2
  16. package/dist/{chunk-OGPVJ53N.js → chunk-4RZS7XCV.js} +5 -5
  17. package/dist/{chunk-HKGYZVWN.js → chunk-6LICEQGP.js} +38 -4
  18. package/dist/{chunk-LAUCO2HC.js → chunk-APDEUAIE.js} +1 -1
  19. package/dist/{chunk-5Y54AE3L.js → chunk-CLH357FI.js} +2 -2
  20. package/dist/{chunk-OAYY3WF3.js → chunk-CVPCRWWC.js} +1 -12
  21. package/dist/{chunk-EGV6R6UF.js → chunk-E2SN3ZOJ.js} +5 -40
  22. package/dist/{chunk-LGXVGIWN.js → chunk-ELQU2GBT.js} +193 -69
  23. package/dist/{chunk-OT5SXF7L.js → chunk-EPRLTYJR.js} +28 -14
  24. package/dist/{chunk-ATZF2RNJ.js → chunk-EYFBT2KP.js} +15 -15
  25. package/dist/{chunk-U54PJPTB.js → chunk-FYMIGUJ5.js} +2 -2
  26. package/dist/{chunk-7XKPG66D.js → chunk-G7A5LEVI.js} +5 -5
  27. package/dist/{chunk-RGHF3OK5.js → chunk-GAAATVTM.js} +17 -17
  28. package/dist/{chunk-7LT3YZUR.js → chunk-I3OJ342S.js} +2 -2
  29. package/dist/{chunk-3LHUXY3E.js → chunk-IABDYN4N.js} +4 -4
  30. package/dist/{chunk-VDUCLVZI.js → chunk-IOMBP2T5.js} +28 -26
  31. package/dist/{chunk-YSRYCS3N.js → chunk-IQROTVBG.js} +2 -2
  32. package/dist/{chunk-PPSN2MLC.js → chunk-IRNM4LSF.js} +9 -6
  33. package/dist/{chunk-ZNHL5KQJ.js → chunk-KN3UHV7O.js} +8 -3
  34. package/dist/{chunk-CV6BNGFK.js → chunk-KXM4K46T.js} +4 -4
  35. package/dist/{chunk-Z4Q6TQWL.js → chunk-LRRPKZBD.js} +2 -2
  36. package/dist/{chunk-LNNI3MS4.js → chunk-MNKPAAQF.js} +9 -2
  37. package/dist/{chunk-OIOW3FPT.js → chunk-NA2NJXY4.js} +1 -1
  38. package/dist/{chunk-GLM7TVWP.js → chunk-OHHS5PSI.js} +1 -1
  39. package/dist/{chunk-CKPAVQNK.js → chunk-OPTA3BJ4.js} +36 -24
  40. package/dist/{chunk-X67BDDZR.js → chunk-P3GXOT2X.js} +48 -2
  41. package/dist/{chunk-WHL6PKYP.js → chunk-PAO7JZNW.js} +52 -48
  42. package/dist/{chunk-TPX6E6PL.js → chunk-Q5M5QFKY.js} +3 -3
  43. package/dist/{chunk-OYF25B62.js → chunk-T7UMSTH2.js} +3 -3
  44. package/dist/{chunk-H6X6CKDN.js → chunk-TAVIVN2I.js} +2 -32
  45. package/dist/{chunk-56IT2ZXF.js → chunk-TGYKFUCF.js} +9 -15
  46. package/dist/{chunk-5U5GLZSD.js → chunk-TZ7OHGNL.js} +2 -2
  47. package/dist/{chunk-UFTDRTCP.js → chunk-UMQP5TC5.js} +2 -2
  48. package/dist/{chunk-LKUGUQUW.js → chunk-UNTDRIV2.js} +5 -5
  49. package/dist/{chunk-CLCHOXLR.js → chunk-V2M6PNLE.js} +5 -5
  50. package/dist/{chunk-ICXY54ZC.js → chunk-WTZD6LFW.js} +2 -2
  51. package/dist/{chunk-FLIEXG2T.js → chunk-XZP3FJ7Q.js} +1 -1
  52. package/dist/{chunk-HTRSSOHY.js → chunk-YU5EYQAE.js} +1 -1
  53. package/dist/cli.js +26 -23
  54. package/dist/{clock-X7V3C7NL.js → clock-ZT5YT6HA.js} +2 -2
  55. package/dist/{cloud-sync-52YNJRMR.js → cloud-sync-RTUXMYU2.js} +23 -23
  56. package/dist/{conversations-ZL6PHROD.js → conversations-ERMMPMOS.js} +4 -4
  57. package/dist/{create-G2RIKQYK.js → create-IUCFJXCE.js} +1 -1
  58. package/dist/{daemon-restart-QWHN5VMU.js → daemon-restart-N2YOISNJ.js} +11 -6
  59. package/dist/daemon.js +230 -279
  60. package/dist/{db-BONH4SUG.js → db-BQFYIONA.js} +1 -1
  61. package/dist/{db-I4EH5SLF.js → db-JH3NTERX.js} +17 -3
  62. package/dist/delivery-manager-5EGCFN4K.js +41 -0
  63. package/dist/{delivery-notices-RZQ6GONP.js → delivery-notices-LZKZXWTX.js} +6 -6
  64. package/dist/{delivery-router-QS4NLJR6.js → delivery-router-U6Q22ZYU.js} +3 -3
  65. package/dist/doctor-225FYBAQ.js +454 -0
  66. package/dist/{down-UN2EDX2Y.js → down-3BQSPUAZ.js} +4 -4
  67. package/dist/echo-text-Y2DFTD7L.js +40 -0
  68. package/dist/{env-SQFCWW52.js → env-4YX5YNWZ.js} +2 -2
  69. package/dist/{exec-Z34TTUO3.js → exec-Y5VMDAVA.js} +2 -2
  70. package/dist/{export-VBNWM7I4.js → export-ASRHBMRG.js} +2 -2
  71. package/dist/{extensions-QT65JGV6.js → extensions-YEPEWURN.js} +27 -23
  72. package/dist/{files-PCKBQHAY.js → files-FASUDSZQ.js} +3 -3
  73. package/dist/{import-DB2KULRO.js → import-O35Y7DSF.js} +6 -6
  74. package/dist/{isolation-UQPDO34R.js → isolation-ZOAUXYXA.js} +2 -2
  75. package/dist/{login-57MMYILL.js → login-54TZQQP4.js} +5 -5
  76. package/dist/{logout-YFZAJRPY.js → logout-4G46Z4RU.js} +1 -1
  77. package/dist/message-delivery-HBTMGWFU.js +46 -0
  78. package/dist/{mind-CT6ANXK4.js → mind-IKMX7K7G.js} +8 -24
  79. package/dist/{mind-activity-tracker-QF6DCX2X.js → mind-activity-tracker-ABAEIMA3.js} +4 -4
  80. package/dist/mind-service-LRVOQG2T.js +48 -0
  81. package/dist/{mind-sleep-24HV4RCA.js → mind-sleep-LZAFBDZD.js} +2 -2
  82. package/dist/{package-F6S5NHY2.js → package-X5O7OD2E.js} +12 -13
  83. package/dist/{prompts-JECJIFPQ.js → prompts-ZNOE3KN3.js} +2 -2
  84. package/dist/{registry-TRC2HJXR.js → registry-BTQXLDW6.js} +1 -1
  85. package/dist/{sandbox-NNWCTHRI.js → sandbox-OUFAGN45.js} +3 -3
  86. package/dist/scheduler-G6BCIVGP.js +27 -0
  87. package/dist/{seed-cmd-6IMTO3LG.js → seed-cmd-BGAW3WDY.js} +2 -2
  88. package/dist/{seed-create-VNLDYRFK.js → seed-create-SMCPEUFQ.js} +3 -3
  89. package/dist/{seed-readiness-H5TTPAQH.js → seed-readiness-RBANMSWW.js} +3 -3
  90. package/dist/{seed-sprout-PPGYGK4G.js → seed-sprout-CSHV36N3.js} +9 -9
  91. package/dist/{send-UXUQKO4N.js → send-D3ADUBR5.js} +6 -6
  92. package/dist/{service-T4P5W6RS.js → service-SWEEBOOY.js} +3 -24
  93. package/dist/{service-install-R4SNYUVO.js → service-install-RCC26KDV.js} +4 -4
  94. package/dist/{setup-YUAQMDVZ.js → setup-L5NZG2U3.js} +5 -5
  95. package/dist/{setup-WZSXHHRJ.js → setup-R7Q4HFJP.js} +1 -5
  96. package/dist/skills/volute-mind/SKILL.md +8 -0
  97. package/dist/{skills-GMQ5UIIW.js → skills-BCCU4OJW.js} +4 -4
  98. package/dist/sleep-manager-25P44HAO.js +42 -0
  99. package/dist/{spirit-Q5VGFWUI.js → spirit-YPG2MU6W.js} +7 -7
  100. package/dist/spirit-availability-ZQM5IH4C.js +43 -0
  101. package/dist/{src-H3UCOP6B.js → src-246GSX7A.js} +5 -2
  102. package/dist/src-LOUB3WPR.js +4189 -0
  103. package/dist/{status-LCKTTIWS.js → status-BYPCJU6H.js} +4 -4
  104. package/dist/{system-events-Q7IVZ5T4.js → system-events-CWC7CBKP.js} +6 -4
  105. package/dist/{systems-HL7SGSW7.js → systems-VDRUZXNF.js} +2 -2
  106. package/dist/{tailscale-KHNZFC7L.js → tailscale-4XY7B5A2.js} +2 -2
  107. package/dist/{template-hash-YKWMVYMM.js → template-hash-RJRMF6DT.js} +2 -2
  108. package/dist/{template-staleness-PY2CULH6.js → template-staleness-CL4V7BT7.js} +5 -5
  109. package/dist/{turn-tracker-HZDPMSCC.js → turn-tracker-LZP5X6GZ.js} +8 -8
  110. package/dist/{up-XZDSJVMU.js → up-3AAN4GYY.js} +4 -4
  111. package/dist/{update-ILCPVNDD.js → update-2MWXRE2S.js} +4 -4
  112. package/dist/{update-check-SZ7B5IFI.js → update-check-PJOZXZYJ.js} +2 -2
  113. package/dist/variant-cleanup-K7RFMSH7.js +24 -0
  114. package/dist/{version-notify-VNVFX6UP.js → version-notify-QIDHR6WO.js} +12 -12
  115. package/dist/web-assets/assets/{index-CwtxGwe_.js → index-Bfvls20e.js} +17 -17
  116. package/dist/web-assets/assets/index-hPn_fkz3.css +1 -0
  117. package/dist/web-assets/index.html +2 -2
  118. package/drizzle/0000_crazy_epoch.sql +232 -0
  119. package/drizzle/README.md +42 -0
  120. package/drizzle/meta/0000_snapshot.json +1420 -3
  121. package/drizzle/meta/_journal.json +2 -128
  122. package/package.json +12 -13
  123. package/packages/extensions/intentions/dist/ui/assets/index-Bl5KEcli.js +67 -0
  124. package/packages/extensions/intentions/dist/ui/index.html +1 -1
  125. package/packages/extensions/pages/dist/ui/assets/index-BKxVQkA9.css +1 -0
  126. package/packages/extensions/pages/dist/ui/assets/index-BMt6sStP.js +68 -0
  127. package/packages/extensions/pages/dist/ui/index.html +2 -2
  128. package/packages/extensions/pages/skills/pages/SKILL.md +123 -4
  129. package/templates/_base/.init/.config/prompts.json +0 -1
  130. package/templates/_base/.init/.local/hooks/pre-prompt/turn-context.ts +30 -0
  131. package/templates/_base/src/lib/codex-session-seed.ts +6 -55
  132. package/templates/_base/src/lib/pi-session-seed.ts +8 -57
  133. package/templates/_base/src/lib/seed-note.ts +4 -4
  134. package/templates/_base/src/lib/session-seed.ts +8 -53
  135. package/templates/_base/src/lib/startup.ts +0 -20
  136. package/templates/claude/package.json +2 -2
  137. package/templates/claude/src/agent.ts +34 -102
  138. package/templates/claude/src/lib/hooks/pre-compact.ts +6 -6
  139. package/templates/claude/src/server.ts +0 -1
  140. package/templates/codex/package.json +2 -2
  141. package/templates/codex/src/agent.ts +23 -89
  142. package/templates/pi/package.json +1 -1
  143. package/templates/pi/src/agent.ts +26 -93
  144. package/templates/pi/src/server.ts +0 -1
  145. package/dist/auto-upgrade-HEVZKPTF.js +0 -46
  146. package/dist/chunk-AAPNU7GA.js +0 -168
  147. package/dist/delivery-manager-7R2YIQ2W.js +0 -41
  148. package/dist/echo-text-73CYQRL7.js +0 -40
  149. package/dist/message-delivery-TRKT4T2P.js +0 -46
  150. package/dist/migrate-name-placeholder-C7NCNLWF.js +0 -37
  151. package/dist/migrate-thread-config-EQSEP2XG.js +0 -90
  152. package/dist/mind-service-7EFIJ3ZA.js +0 -48
  153. package/dist/scheduler-73Q4MQ7Y.js +0 -27
  154. package/dist/seed-DRKAP3XV.js +0 -11
  155. package/dist/sleep-manager-KHI65FEX.js +0 -42
  156. package/dist/spirit-availability-G5FMWKRH.js +0 -43
  157. package/dist/sprout-PMTRL7QN.js +0 -11
  158. package/dist/src-3FIW7DLH.js +0 -2376
  159. package/dist/src-LTTQVN2Z.js +0 -832
  160. package/dist/variant-HHDTW74J.js +0 -62
  161. package/dist/variant-cleanup-QHGGY7ND.js +0 -24
  162. package/dist/web-assets/assets/index-DhM1yYRN.css +0 -1
  163. package/drizzle/0000_baseline.sql +0 -152
  164. package/drizzle/0001_add_conversation_private.sql +0 -1
  165. package/drizzle/0002_turns.sql +0 -21
  166. package/drizzle/0003_turn_feed_links.sql +0 -11
  167. package/drizzle/0004_spirits.sql +0 -5
  168. package/drizzle/0005_meta_summaries.sql +0 -15
  169. package/drizzle/0006_channels.sql +0 -17
  170. package/drizzle/0007_drop_conversation_name_title.sql +0 -11
  171. package/drizzle/0008_performance_indexes.sql +0 -6
  172. package/drizzle/0009_mind_notices.sql +0 -14
  173. package/drizzle/0010_delivery_queue_redrive.sql +0 -10
  174. package/drizzle/0011_drop_turn_summary_event_id.sql +0 -3
  175. package/drizzle/0012_channel_gates.sql +0 -17
  176. package/drizzle/0013_variant_purpose.sql +0 -3
  177. package/drizzle/0014_system_events.sql +0 -26
  178. package/drizzle/0015_rename_session_to_thread.sql +0 -12
  179. package/drizzle/0016_brain_to_human.sql +0 -4
  180. package/drizzle/0017_event_history_rows.sql +0 -9
  181. package/drizzle/0018_api_tokens.sql +0 -20
  182. package/drizzle/meta/0001_snapshot.json +0 -7
  183. package/drizzle/meta/0002_snapshot.json +0 -7
  184. package/drizzle/meta/0003_snapshot.json +0 -7
  185. package/drizzle/meta/0004_snapshot.json +0 -7
  186. package/drizzle/meta/0005_snapshot.json +0 -7
  187. package/drizzle/meta/0006_snapshot.json +0 -7
  188. package/drizzle/meta/0007_snapshot.json +0 -7
  189. package/packages/extensions/intentions/dist/ui/assets/index-CQz4aceM.js +0 -67
  190. package/packages/extensions/notes/dist/ui/assets/index-BBs8pyel.js +0 -67
  191. package/packages/extensions/notes/dist/ui/assets/index-D3cdui42.css +0 -1
  192. package/packages/extensions/notes/dist/ui/index.html +0 -14
  193. package/packages/extensions/notes/skills/notes/SKILL.md +0 -90
  194. package/packages/extensions/pages/dist/ui/assets/index-B589e8Xe.js +0 -2
  195. package/packages/extensions/pages/dist/ui/assets/index-BuXnE-rb.css +0 -1
  196. package/dist/{env-7AZFBPWX.js → env-IBW2QI6K.js} +5 -5
  197. package/dist/{login-4NUS72Z5.js → login-RJJTCVR3.js} +3 -3
  198. package/dist/{mind-contacts-PEXCDRXA.js → mind-contacts-KV64UDJS.js} +6 -6
  199. package/dist/{mind-history-RZMMCHMX.js → mind-history-H45GM2M2.js} +6 -6
  200. package/dist/{register-WNCSOTAU.js → register-OX4HRSGC.js} +3 -3
  201. package/dist/{restart-B4GNMVF2.js → restart-AGEGNSYQ.js} +4 -4
  202. package/dist/{skill-QYTBUDNQ.js → skill-B5OJEQUA.js} +4 -4
  203. package/dist/{start-BDHJVOVX.js → start-MUMNFR52.js} +3 -3
  204. package/dist/{stop-45QYSGYS.js → stop-EKERNYTX.js} +4 -4
@@ -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 from the pinned `cut` (falling back to a budget-based tail when the pin
421
- * can't be honored), writes it as a new synthetic rollout next to the source, and — for
422
- * persistent sessions archives the rotated-out thread pointer so the full transcript
423
- * stays findable. Returns the new thread id, or null if rotation can't proceed (the
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, cut, seedTokens } = opts;
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
- // Honor the pinned boundary the mind was warned about; only fall back to a
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 seeded tail from the pinned `cut` (falling back to a budget-based tail
383
- * when the pin can't be honored), writes it as a new session file in the same live
384
- * dir, and — for persistent sessions — archives the rotated-out file so the full
385
- * transcript stays findable. Returns the new session file **path** (the caller
386
- * switches the running SessionManager to it), or null if rotation can't proceed
387
- * (the caller then falls back to a fresh session). Never throws.
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, cut, seedTokens } = opts;
353
+ const { cwd, sessionsDir, name, sourcePath, seedTokens } = opts;
398
354
  try {
399
355
  const jsonl = readFileSync(sourcePath, "utf-8");
400
- // Honor the pinned boundary the mind was warned about; only fall back to a
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. No gap clause (the break is instantaneous — the
25
- * context limit, not a sleep). Points at `volute mind history` for the collapsed
26
- * turns, whose summaries the mind was asked to author at warn time.
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 rotated in place at the context limit. The turns above are your most recent, kept verbatim; earlier turns have been collapsed — your summaries of them are in your history (`volute mind history`). A fresh session continues from here.";
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
- * (used when reading a transcript that may still be mid-write, e.g. computing the
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 seeded tail from the pinned `cut` (falling back to a budget-based tail when the
332
- * pin can't be honored), writes it as a new synthetic session file next to the
333
- * source, and — for persistent sessions — archives the rotated-out pointer so the
334
- * full transcript stays findable. Returns the new session id, or null if rotation
335
- * can't proceed (the caller then falls back to a fresh session). Never throws.
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, cut, seedTokens } = opts;
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
- // Honor the pinned boundary the mind was warned about; only fall back to a
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.207",
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.3",
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
- * Rotation state machine (undefined = idle). The warning is pushed and the mind
95
- * takes a wrap-up turn with FULL context of the turns it's summarizing — before
96
- * the session rotates:
97
- * - "warned": a turn was mid-flight when we warned; it must finish before the
98
- * wrap-up turn (which consumes the warning) runs.
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
- rotationPhase?: RotationPhase;
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
- * Warn the mind that the session will rotate at the context limit and pin the
401
- * cut point it's told about. Computes which whole turns fit in `seedTokens`
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 warns + pins + enters the rotation machine
447
- // (unless already warned, or the rotation cap is hit) and blocks; second fire
448
- // allows the SDK's native compaction — the emergency backstop when rotation never
449
- // intervened (a hung turn, or a system prompt too large for rotation to relieve).
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.rotationPhase && session.consecutiveRotations < MAX_CONSECUTIVE_ROTATIONS) {
452
- warnAndPinRotation();
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.rotationPhase === "warned") {
475
- // The mid-flight turn finished; the queued warning runs next as the wrap-up
476
- // turn. Advance — do NOT abort yet, so the mind summarizes with full context.
477
- session.rotationPhase = "rotateAfterTurn";
478
- } else if (session.rotationPhase === "rotateAfterTurn") {
479
- // The wrap-up turn is done (summaries authored) — abort to rotate in place.
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.rotationPhase &&
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} — warning + pinning rotation`,
434
+ `session "${session.name}": ${tokens} tokens >= ${maxContextTokens} — rotation pending at turn end`,
500
435
  );
501
- warnAndPinRotation();
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 (from the pinned cut), then resume it.
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.rotationCut = null;
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
- // (the wrap-up warning plus anything that arrived mid-turn) so nothing is
606
- // dropped when the killed subprocess takes its buffer with it.
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.rotationPhase = undefined;
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)?.rotationPhase !== undefined,
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` (warn the mind + pin the rotation boundary, same
12
- * as the threshold path) and BLOCK the SDK's native compaction. The wrap-up
13
- * turn then ends and the session rotates.
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 warns + marks for rotation and blocks; second
36
- // pass allows the native compaction as the emergency backstop.
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 — warning mind + marking session for rotation");
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,