volute 0.52.0 → 0.54.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 (153) hide show
  1. package/dist/{accept-5NK5XJB5.js → accept-RSDIVJL6.js} +3 -0
  2. package/dist/auto-upgrade-HEVZKPTF.js +46 -0
  3. package/dist/{backup-YVGV2ZNM.js → backup-QRNX6RK7.js} +10 -10
  4. package/dist/channels-CKCY64TD.js +203 -0
  5. package/dist/{chat-VAZ25KVT.js → chat-ZUGNL5GS.js} +5 -5
  6. package/dist/chunk-3LHUXY3E.js +102 -0
  7. package/dist/{chunk-SW2U6HRB.js → chunk-444RR3YF.js} +21 -4
  8. package/dist/{chunk-YVR6EHWL.js → chunk-56IT2ZXF.js} +160 -18
  9. package/dist/chunk-5RBEEAKW.js +58 -0
  10. package/dist/{chunk-OXWUEU5V.js → chunk-5U5GLZSD.js} +2 -2
  11. package/dist/{chunk-G7A5LEVI.js → chunk-7XKPG66D.js} +5 -5
  12. package/dist/{chunk-R772ET2M.js → chunk-AAPNU7GA.js} +4 -0
  13. package/dist/{chunk-XRWUQPCA.js → chunk-ATZF2RNJ.js} +25 -3
  14. package/dist/{chunk-6NLW72HP.js → chunk-CKPAVQNK.js} +1 -1
  15. package/dist/{chunk-JHKBALS2.js → chunk-CLCHOXLR.js} +1 -1
  16. package/dist/{chunk-DMK5ASKV.js → chunk-F6D766VQ.js} +6 -2
  17. package/dist/{chunk-AXFLY5GU.js → chunk-HKGYZVWN.js} +16 -1
  18. package/dist/{chunk-ZEZQNP2G.js → chunk-LGXVGIWN.js} +103 -25
  19. package/dist/{chunk-CW7BQKF6.js → chunk-LKUGUQUW.js} +3 -3
  20. package/dist/{chunk-FXWBFDQR.js → chunk-OAYY3WF3.js} +59 -12
  21. package/dist/{chunk-ITK6HV47.js → chunk-OGPVJ53N.js} +14 -6
  22. package/dist/{chunk-M77B73VV.js → chunk-OIOW3FPT.js} +7 -2
  23. package/dist/chunk-OT5SXF7L.js +904 -0
  24. package/dist/{chunk-EDR47UE2.js → chunk-OYF25B62.js} +1 -1
  25. package/dist/{chunk-RPPTGPYF.js → chunk-PPSN2MLC.js} +2 -2
  26. package/dist/{chunk-K2F237KS.js → chunk-RGHF3OK5.js} +154 -18
  27. package/dist/{chunk-RU74YG46.js → chunk-VDUCLVZI.js} +338 -30
  28. package/dist/{chunk-724DVYHU.js → chunk-WHL6PKYP.js} +10 -10
  29. package/dist/{chunk-EEOMA4DX.js → chunk-WZZCA23D.js} +2 -1
  30. package/dist/{chunk-UDZLPR34.js → chunk-YSRYCS3N.js} +13 -8
  31. package/dist/{chunk-5N32EN4O.js → chunk-Z4Q6TQWL.js} +1 -1
  32. package/dist/{chunk-VPOMJMV6.js → chunk-ZNHL5KQJ.js} +1 -1
  33. package/dist/cli.js +17 -17
  34. package/dist/{clock-ZT5YT6HA.js → clock-X7V3C7NL.js} +2 -2
  35. package/dist/{cloud-sync-H7EWZASD.js → cloud-sync-52YNJRMR.js} +12 -11
  36. package/dist/{daemon-restart-4FWUA5JF.js → daemon-restart-QWHN5VMU.js} +4 -4
  37. package/dist/daemon.js +538 -782
  38. package/dist/{db-IPXRJUI7.js → db-I4EH5SLF.js} +3 -1
  39. package/dist/{delivery-manager-VYXVKUMP.js → delivery-manager-7R2YIQ2W.js} +13 -7
  40. package/dist/delivery-notices-RZQ6GONP.js +18 -0
  41. package/dist/{delivery-router-RHANNTAC.js → delivery-router-QS4NLJR6.js} +3 -1
  42. package/dist/{down-QSNEMTTX.js → down-UN2EDX2Y.js} +3 -3
  43. package/dist/{echo-text-PEEV7FQU.js → echo-text-73CYQRL7.js} +14 -13
  44. package/dist/{exec-ABD3DYA2.js → exec-Z34TTUO3.js} +1 -1
  45. package/dist/{extensions-N6L5NYTK.js → extensions-QT65JGV6.js} +22 -11
  46. package/dist/{files-BRY5RRIM.js → files-PCKBQHAY.js} +2 -1
  47. package/dist/{isolation-GVV2IBAU.js → isolation-UQPDO34R.js} +1 -1
  48. package/dist/{message-delivery-7URC52ME.js → message-delivery-TRKT4T2P.js} +16 -13
  49. package/dist/migrate-name-placeholder-C7NCNLWF.js +37 -0
  50. package/dist/{mind-NEXGT3KE.js → mind-CT6ANXK4.js} +11 -11
  51. package/dist/{mind-contacts-QYU4TPEA.js → mind-contacts-PEXCDRXA.js} +6 -6
  52. package/dist/{mind-history-OGNR4L7W.js → mind-history-RZMMCHMX.js} +83 -11
  53. package/dist/{mind-list-SYRA7CH5.js → mind-list-7KBLPLES.js} +1 -1
  54. package/dist/{mind-service-E26ZQXDS.js → mind-service-7EFIJ3ZA.js} +12 -11
  55. package/dist/{mind-sleep-LZAFBDZD.js → mind-sleep-24HV4RCA.js} +2 -2
  56. package/dist/{mind-status-Z2LQFA6F.js → mind-status-4YW4PHMO.js} +15 -0
  57. package/dist/{package-2W2XKSIP.js → package-F6S5NHY2.js} +4 -4
  58. package/dist/{prompts-NBO3HRK3.js → prompts-JECJIFPQ.js} +1 -1
  59. package/dist/{reject-TWB4QJHG.js → reject-4NFLXKRN.js} +6 -2
  60. package/dist/{scheduler-JTTWZDYU.js → scheduler-73Q4MQ7Y.js} +8 -6
  61. package/dist/{seed-64XEHRUP.js → seed-DRKAP3XV.js} +1 -1
  62. package/dist/{seed-cmd-UEL2QB3Q.js → seed-cmd-6IMTO3LG.js} +2 -2
  63. package/dist/{seed-create-4JRQDPGA.js → seed-create-VNLDYRFK.js} +1 -1
  64. package/dist/{seed-readiness-DYOVCYHN.js → seed-readiness-H5TTPAQH.js} +2 -2
  65. package/dist/{seed-sprout-NIDIOJXT.js → seed-sprout-PPGYGK4G.js} +5 -5
  66. package/dist/{send-2YKOZJJ5.js → send-UXUQKO4N.js} +37 -22
  67. package/dist/{service-XQPMZAGI.js → service-T4P5W6RS.js} +2 -2
  68. package/dist/{service-install-2EWD7KXD.js → service-install-R4SNYUVO.js} +3 -3
  69. package/dist/{setup-4IX6PCD3.js → setup-YUAQMDVZ.js} +4 -4
  70. package/dist/skills/dreaming/SKILL.md +2 -2
  71. package/dist/skills/dreaming/references/INSTALL.md +1 -1
  72. package/dist/skills/memory/SKILL.md +47 -43
  73. package/dist/skills/tending/SKILL.md +2 -0
  74. package/dist/skills/volute-mind/references/integrations.md +3 -2
  75. package/dist/skills/volute-mind/references/routing.md +18 -5
  76. package/dist/{skills-MPUALV3S.js → skills-GMQ5UIIW.js} +2 -2
  77. package/dist/{sleep-manager-OFP2GFXF.js → sleep-manager-KHI65FEX.js} +12 -11
  78. package/dist/{spirit-4ECYIVGE.js → spirit-Q5VGFWUI.js} +19 -3
  79. package/dist/{spirit-availability-Y54Y3675.js → spirit-availability-G5FMWKRH.js} +13 -12
  80. package/dist/{sprout-GJMZFRGZ.js → sprout-PMTRL7QN.js} +1 -1
  81. package/dist/{src-LCSOQY6Z.js → src-3FIW7DLH.js} +241 -87
  82. package/dist/src-H3UCOP6B.js +516 -0
  83. package/dist/{src-HGNE2XG4.js → src-LTTQVN2Z.js} +1 -1
  84. package/dist/{status-MWLE3RVC.js → status-LCKTTIWS.js} +2 -2
  85. package/dist/{system-events-V43HDA3T.js → system-events-Q7IVZ5T4.js} +5 -1
  86. package/dist/{systems-VDRUZXNF.js → systems-HL7SGSW7.js} +2 -2
  87. package/dist/{turn-tracker-C5PVIC3B.js → turn-tracker-HZDPMSCC.js} +4 -2
  88. package/dist/{up-H6LJZDRK.js → up-XZDSJVMU.js} +3 -3
  89. package/dist/{update-CIWI2JYA.js → update-ILCPVNDD.js} +2 -2
  90. package/dist/{upgrade-LIE46UBB.js → upgrade-XY2J7NWZ.js} +9 -1
  91. package/dist/{variant-cleanup-STC6B5HR.js → variant-cleanup-QHGGY7ND.js} +8 -6
  92. package/dist/{version-notify-LBVHH5YV.js → version-notify-VNVFX6UP.js} +22 -7
  93. package/dist/web-assets/assets/{index-B_eYTa3R.js → index-CwtxGwe_.js} +16 -16
  94. package/dist/web-assets/assets/index-DhM1yYRN.css +1 -0
  95. package/dist/web-assets/index.html +2 -2
  96. package/package.json +4 -4
  97. package/packages/extensions/intentions/dist/ui/assets/index-3FneLCKR.css +1 -0
  98. package/packages/extensions/intentions/dist/ui/assets/index-CQz4aceM.js +67 -0
  99. package/packages/extensions/{plan → intentions}/dist/ui/index.html +3 -3
  100. package/packages/extensions/intentions/skills/intention-review/SKILL.md +38 -0
  101. package/packages/extensions/intentions/skills/intentions/SKILL.md +49 -0
  102. package/packages/extensions/intentions/skills/intentions/scripts/intentions-hook.sh +45 -0
  103. package/packages/extensions/pages/skills/commons-gardening/SKILL.md +50 -0
  104. package/packages/extensions/pages/skills/pages/SKILL.md +18 -13
  105. package/templates/_base/.init/.config/prompts.json +3 -3
  106. package/templates/{pi → _base}/.init/.config/routes.json +1 -0
  107. package/templates/_base/.init/memory/dreams/.gitkeep +0 -0
  108. package/templates/_base/home/.config/routes.json +1 -1
  109. package/templates/_base/home/VOLUTE.md +1 -1
  110. package/templates/_base/src/lib/codex-session-seed.ts +463 -0
  111. package/templates/_base/src/lib/context-breakdown.ts +11 -3
  112. package/templates/_base/src/lib/daemon-client.ts +35 -0
  113. package/templates/_base/src/lib/pi-session-seed.ts +434 -0
  114. package/templates/_base/src/lib/routing.ts +2 -2
  115. package/templates/_base/src/lib/seed-note.ts +78 -0
  116. package/templates/_base/src/lib/session-seed.ts +375 -0
  117. package/templates/_base/src/lib/startup.ts +112 -6
  118. package/templates/claude/.init/CLAUDE.md +7 -3
  119. package/templates/claude/src/agent.ts +295 -101
  120. package/templates/claude/src/lib/hooks/pre-compact.ts +30 -5
  121. package/templates/claude/src/lib/message-channel.ts +43 -15
  122. package/templates/claude/src/lib/recover.ts +69 -0
  123. package/templates/claude/src/lib/stream-consumer.ts +33 -2
  124. package/templates/claude/src/server.ts +2 -1
  125. package/templates/claude/volute-template.json +1 -1
  126. package/templates/codex/.init/AGENTS.md +7 -3
  127. package/templates/codex/src/agent.ts +256 -39
  128. package/templates/codex/src/server.ts +2 -1
  129. package/templates/codex/volute-template.json +1 -1
  130. package/templates/pi/.init/MINDS.md +7 -3
  131. package/templates/pi/src/agent.ts +269 -43
  132. package/templates/pi/src/lib/event-handler.ts +11 -0
  133. package/templates/pi/src/lib/mechanics-doc.ts +30 -0
  134. package/templates/pi/src/server.ts +5 -1
  135. package/templates/pi/volute-template.json +1 -1
  136. package/dist/channels-FSAHOQWI.js +0 -90
  137. package/dist/skills/plan-coordinator/SKILL.md +0 -60
  138. package/dist/src-UDMJZ6MH.js +0 -426
  139. package/dist/web-assets/assets/index-DhZrzddW.css +0 -1
  140. package/packages/extensions/plan/dist/ui/assets/index-B8s8wxyt.js +0 -67
  141. package/packages/extensions/plan/dist/ui/assets/index-jIkrt-vI.css +0 -1
  142. package/packages/extensions/plan/skills/plan/SKILL.md +0 -43
  143. package/packages/extensions/plan/skills/plan/scripts/plan-hook.sh +0 -37
  144. package/templates/claude/.init/.config/routes.json +0 -11
  145. package/templates/codex/.init/.config/routes.json +0 -11
  146. package/dist/{env-IBW2QI6K.js → env-7AZFBPWX.js} +5 -5
  147. package/dist/{login-RJJTCVR3.js → login-4NUS72Z5.js} +3 -3
  148. package/dist/{login-SB5OENMV.js → login-57MMYILL.js} +4 -4
  149. package/dist/{register-OX4HRSGC.js → register-WNCSOTAU.js} +3 -3
  150. package/dist/{restart-AGEGNSYQ.js → restart-B4GNMVF2.js} +4 -4
  151. package/dist/{skill-B5OJEQUA.js → skill-QYTBUDNQ.js} +4 -4
  152. package/dist/{start-MUMNFR52.js → start-BDHJVOVX.js} +3 -3
  153. package/dist/{stop-EKERNYTX.js → stop-45QYSGYS.js} +4 -4
@@ -24,11 +24,25 @@ import {
24
24
  import { daemonEmit } from "./lib/daemon-client.js";
25
25
  import { dispatchPrompt } from "./lib/dispatch.js";
26
26
  import { createEventHandler, emit } from "./lib/event-handler.js";
27
+ import { compactTimestamp } from "./lib/format-prefix.js";
27
28
  import { runHooks } from "./lib/hook-loader.js";
28
29
  import { log } from "./lib/logger.js";
30
+ import {
31
+ computePiSeedCut,
32
+ DEFAULT_SEED_TOKENS,
33
+ type PiSeedCut,
34
+ rotatePiSession,
35
+ seedPiSession,
36
+ } from "./lib/pi-session-seed.js";
29
37
  import { createReplyInstructionsExtension } from "./lib/reply-instructions-extension.js";
30
38
  import { resolveModel } from "./lib/resolve-model.js";
31
- import { getStartupContext, loadPrompts, type SubagentConfig } from "./lib/startup.js";
39
+ import { buildSeededNote, type SeedCause } from "./lib/seed-note.js";
40
+ import {
41
+ getStartupContext,
42
+ loadPrompts,
43
+ renderCompactionWarning,
44
+ type SubagentConfig,
45
+ } from "./lib/startup.js";
32
46
  import { createSubagentExtension, type SubagentDefinition } from "./lib/subagents.js";
33
47
  import type {
34
48
  HandlerMeta,
@@ -52,16 +66,51 @@ type PiSession = {
52
66
  currentMessageId?: string;
53
67
  messageChannels: Map<string, { channel: string; sender?: string }>;
54
68
  contextTokens: number;
69
+ /**
70
+ * True when this session was seeded from the previous session's transcript.
71
+ * Consumed once, on the first turn, to inject the honest-boundary note.
72
+ */
73
+ seeded?: boolean;
74
+ /** When the seeded-from session was archived (epoch ms), for the gap note; null if unknown. */
75
+ seededArchivedAt?: number | null;
76
+ /** Why the tail is seeded — picks the boundary note's wording. Last cause wins. */
77
+ seededCause?: SeedCause;
78
+ /**
79
+ * Set after an in-place rotation so the mind is told, on its next turn, that the
80
+ * session rotated at the context limit. Delivered separately from the restored-seed
81
+ * note (which fires only on the first turn of a freshly created agent session).
82
+ */
83
+ rotationNotePending?: boolean;
84
+ /**
85
+ * The cut point pinned at warn time (the first-kept turn the mind was told would
86
+ * survive verbatim). Honored at rotation so the promise in the warning holds even
87
+ * though the transcript grows after the warning. Null until a warning fires.
88
+ */
89
+ rotationCut?: PiSeedCut | null;
90
+ /**
91
+ * Back-to-back rotations that did NOT bring context under the threshold (reset by
92
+ * any healthy turn). Guards against a runaway loop when the tail alone can't fit —
93
+ * e.g. a system prompt (large MEMORY.md) that already fills most of the window,
94
+ * which rotation can't trim. Past the cap we defer to the SDK's native compaction.
95
+ */
96
+ consecutiveRotations?: number;
55
97
  };
56
98
 
99
+ /** Stop self-rotating after this many back-to-back rotations that didn't reduce context. */
100
+ const MAX_CONSECUTIVE_ROTATIONS = 3;
101
+
57
102
  export function createMind(options: {
58
103
  systemPrompt: string;
59
104
  cwd: string;
60
105
  mindDir: string;
106
+ /** Directory holding pi session subdirs (`<sessionsDir>/<name>/*.jsonl`). */
107
+ sessionsDir: string;
61
108
  model?: string;
62
109
  thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
63
110
  compactionMessage?: string;
64
111
  maxContextTokens?: number;
112
+ /** Estimated-token budget for seeding a fresh persistent session. 0 disables. Default 30000. */
113
+ seedTokens?: number;
65
114
  subagents?: Record<string, SubagentConfig>;
66
115
  }): {
67
116
  resolve: HandlerResolver;
@@ -70,12 +119,11 @@ export function createMind(options: {
70
119
  } {
71
120
  const sessions = new Map<string, PiSession>();
72
121
  const prompts = loadPrompts();
73
- const today = new Date().toLocaleDateString("en-CA");
74
- const compactionMessage =
75
- // biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${date} in prompt template
76
- options.compactionMessage ?? prompts.compaction_warning.replace("${date}", today);
122
+ const compactionMessage = () =>
123
+ options.compactionMessage ?? renderCompactionWarning(prompts.compaction_warning);
77
124
  const compactionInstructions = prompts.compaction_instructions;
78
125
  const maxContextTokens = options.maxContextTokens;
126
+ const seedTokens = options.seedTokens ?? DEFAULT_SEED_TOKENS;
79
127
 
80
128
  if (maxContextTokens) {
81
129
  log("mind", `compaction threshold: ${maxContextTokens} tokens`);
@@ -183,6 +231,22 @@ export function createMind(options: {
183
231
  // Inject startup context on the first turn of each session
184
232
  if (!startupContextInjected) {
185
233
  startupContextInjected = true;
234
+ // On the first turn of a seeded session, lead with the honest-boundary
235
+ // note (consumed once) so the restored transcript above isn't mistaken
236
+ // for a continuously-lived conversation.
237
+ if (session.seeded) {
238
+ session.seeded = false;
239
+ const note = buildSeededNote({
240
+ cause: "restored",
241
+ archivedAtMs: session.seededArchivedAt ?? null,
242
+ });
243
+ emit(session, {
244
+ type: "context",
245
+ content: note,
246
+ metadata: { source: "seeded-session" },
247
+ });
248
+ parts.push(note);
249
+ }
186
250
  const startupContext = await startupContextPromise;
187
251
  if (startupContext) {
188
252
  emit(session, {
@@ -194,6 +258,20 @@ export function createMind(options: {
194
258
  }
195
259
  }
196
260
 
261
+ // On the first turn after an in-place rotation, tell the mind the earlier
262
+ // turns collapsed at the context limit (distinct from the restored-seed note,
263
+ // which only fires on a freshly created agent session). Consumed once.
264
+ if (session.rotationNotePending) {
265
+ session.rotationNotePending = false;
266
+ const note = buildSeededNote({ cause: "rotation" });
267
+ emit(session, {
268
+ type: "context",
269
+ content: note,
270
+ metadata: { source: "seeded-session" },
271
+ });
272
+ parts.push(note);
273
+ }
274
+
197
275
  try {
198
276
  const result = await runHooks(hooksDir, "pre-prompt", {
199
277
  event: "pre-prompt",
@@ -273,16 +351,49 @@ export function createMind(options: {
273
351
  async function initSession(session: PiSession) {
274
352
  const isEphemeral = session.name.startsWith("new-");
275
353
 
276
- const sessionManager = isEphemeral
277
- ? SessionManager.inMemory()
278
- : SessionManager.continueRecent(options.cwd, `.mind/pi-sessions/${session.name}`);
354
+ // Fresh persistent session — seed it from the previous session's archived
355
+ // transcript so the mind experiences the conversation continuing rather than
356
+ // waking into an empty context. seedPiSession no-ops when a live session
357
+ // already exists (continueRecent will resume that instead). Ephemeral
358
+ // `new-*` sessions are one-offs — they never seed at start.
359
+ const seeded = isEphemeral
360
+ ? null
361
+ : seedPiSession({
362
+ cwd: options.cwd,
363
+ piSessionsDir: options.sessionsDir,
364
+ name: session.name,
365
+ seedTokens,
366
+ });
367
+
368
+ // Every session (ephemeral too) is file-backed under its own name-scoped dir,
369
+ // so rotation applies uniformly at the context limit. Ephemerality is only that
370
+ // `new-*` sessions never seed at start and never archive on rotation — not a
371
+ // different backing store. Their unique `new-<ts>-<rand>` names mean the dir is
372
+ // always fresh, so continueRecent starts them empty (no seed to adopt).
373
+ const sessionManager = SessionManager.continueRecent(
374
+ options.cwd,
375
+ resolvePath(options.sessionsDir, session.name),
376
+ );
377
+
378
+ // If continueRecent adopted our seed file, its header id is the seeded id;
379
+ // if it fell back to a truly fresh session it minted a different id, so the
380
+ // honest-boundary note never lies about a conversation that didn't continue.
381
+ if (seeded && sessionManager.getSessionId() === seeded.sessionId) {
382
+ session.seeded = true;
383
+ session.seededArchivedAt = seeded.archivedAt;
384
+ session.seededCause = "restored";
385
+ log("mind", `session "${session.name}": seeded from previous transcript`);
386
+ }
279
387
 
280
388
  log("mind", `session "${session.name}": ${isEphemeral ? "ephemeral" : "persistent"}`);
281
389
 
282
- // Compaction state machine:
283
- // 1. onContextTokens sets compactionTriggered=true and sends warning
390
+ // Compaction state machine (rotation, not SDK /compact):
391
+ // 1. threshold (onContextTokens) or native PreCompact -> warnAndPinRotation:
392
+ // pin the cut from the live transcript, interpolate it into the ${cutoff}
393
+ // warning, send it as the wrap-up prompt, set compactionTriggered
284
394
  // 2. onTurnEnd (after warning turn): compactionTriggered -> compactOnNextTurnEnd
285
- // 3. onTurnEnd (after mind's save turn): compactOnNextTurnEnd -> call compact()
395
+ // 3. onTurnEnd (after mind's wrap-up turn): compactOnNextTurnEnd -> rotateInPlace
396
+ // (or, past the runaway cap, the native compact() backstop)
286
397
  let compactBlocked = false;
287
398
  let manualCompactPending = false;
288
399
  let compactionTriggered = false;
@@ -295,31 +406,144 @@ export function createMind(options: {
295
406
  compactionInProgress = false;
296
407
  }
297
408
 
409
+ /**
410
+ * Warn the mind that the session will rotate at the context limit and pin the cut
411
+ * point it's told about. Computes which whole turns fit in `seedTokens` from the
412
+ * live transcript, pins the first-kept turn (so rotation honors the same boundary
413
+ * even as the transcript grows), interpolates its local time into the `${cutoff}`
414
+ * placeholder, and sends the warning as the wrap-up prompt. Shared by the
415
+ * threshold and native-PreCompact paths.
416
+ */
417
+ function warnAndPinRotation() {
418
+ let cutoffLabel = "the cutoff";
419
+ session.rotationCut = null;
420
+ const sourcePath = session.agentSession?.sessionManager.getSessionFile();
421
+ if (sourcePath) {
422
+ try {
423
+ const cut = computePiSeedCut(readFileSync(sourcePath, "utf-8"), seedTokens);
424
+ if (cut) {
425
+ session.rotationCut = cut;
426
+ if (cut.boundaryTimestamp) {
427
+ const d = new Date(cut.boundaryTimestamp);
428
+ if (!Number.isNaN(d.getTime())) cutoffLabel = compactTimestamp(d);
429
+ }
430
+ }
431
+ } catch (err) {
432
+ log("mind", `session "${session.name}": failed to pin rotation cut:`, err);
433
+ }
434
+ }
435
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${cutoff} prompt placeholder
436
+ const warning = compactionMessage().replaceAll("${cutoff}", cutoffLabel);
437
+ compactionTriggered = true;
438
+ session.messageIds.push(undefined);
439
+ session.agentSession?.prompt(warning, { streamingBehavior: "followUp" });
440
+ }
441
+
442
+ /**
443
+ * Rotate the session in place onto a synthetic session holding the verbatim recent
444
+ * tail (from the pinned cut), then switch the running SessionManager to it. Returns
445
+ * false if rotation can't proceed (session not ready, or the file build failed) so
446
+ * the caller can fall back to a fresh session. The setSessionFile + state.messages
447
+ * re-sync is the load-bearing adoption step: the agent caches context in
448
+ * state.messages, so swapping the file alone wouldn't change what the mind sees
449
+ * (this mirrors the SDK's own compact() re-sync).
450
+ */
451
+ function rotateInPlace(): boolean {
452
+ const as = session.agentSession;
453
+ const sourcePath = as?.sessionManager.getSessionFile();
454
+ if (!as || !sourcePath) return false; // not ready — fall back to fresh
455
+ const newPath = rotatePiSession({
456
+ cwd: options.cwd,
457
+ sessionsDir: options.sessionsDir,
458
+ name: session.name,
459
+ sourcePath,
460
+ cut: session.rotationCut ?? null,
461
+ seedTokens,
462
+ });
463
+ session.rotationCut = null;
464
+ if (!newPath) return false;
465
+ as.sessionManager.setSessionFile(newPath);
466
+ as.state.messages = as.sessionManager.buildSessionContext().messages;
467
+ session.rotationNotePending = true;
468
+ session.seededCause = "rotation";
469
+ session.consecutiveRotations = (session.consecutiveRotations ?? 0) + 1;
470
+ compactBlocked = false; // re-arm the native-compaction backstop
471
+ log(
472
+ "mind",
473
+ `session "${session.name}": rotated in place → ${as.sessionManager.getSessionId()} (rotation ${session.consecutiveRotations})`,
474
+ );
475
+ return true;
476
+ }
477
+
478
+ /**
479
+ * Rotation couldn't proceed — start a fresh session in place (new empty transcript,
480
+ * cleared context), matching the claude template's rotation-failure fallback. Fresh
481
+ * is a far smaller cliff now that seeding exists; crucially it is NOT a silent native
482
+ * compaction.
483
+ */
484
+ function freshFallback() {
485
+ const as = session.agentSession;
486
+ if (as) {
487
+ as.sessionManager.newSession();
488
+ as.state.messages = [];
489
+ }
490
+ session.rotationCut = null;
491
+ session.consecutiveRotations = 0;
492
+ compactBlocked = false;
493
+ log("mind", `session "${session.name}": rotation failed, starting fresh`);
494
+ }
495
+
496
+ /**
497
+ * The SDK's native compaction, used as the emergency backstop past the runaway cap —
498
+ * when rotation can't relieve context (e.g. the system prompt alone fills most of the
499
+ * window, so no tail fits under the threshold). The sole remaining use of the custom
500
+ * compaction instructions.
501
+ */
502
+ function runBackstopCompact() {
503
+ manualCompactPending = true;
504
+ compactionInProgress = true;
505
+ log("mind", `session "${session.name}": rotation cap reached — native compaction backstop`);
506
+ Promise.resolve(session.agentSession?.compact(compactionInstructions))
507
+ .catch((err) => log("mind", `session "${session.name}": backstop compact() failed:`, err))
508
+ .finally(() => {
509
+ compactionInProgress = false;
510
+ });
511
+ }
512
+
298
513
  const preCompactExtension: ExtensionFactory = (pi) => {
299
514
  pi.on("session_before_compact", () => {
300
- // Our programmatic compact() call (triggered by token threshold) — allow through
515
+ // Our own backstop compact() call (past the runaway cap) — allow through.
301
516
  if (manualCompactPending) {
302
517
  manualCompactPending = false;
303
- log(
304
- "mind",
305
- `session "${session.name}": allowing manual compaction with custom instructions`,
306
- );
518
+ log("mind", `session "${session.name}": allowing native compaction backstop`);
307
519
  return;
308
520
  }
309
521
 
310
- // Auto-compaction: two-pass block (first pass warns mind, second pass allows)
522
+ // The SDK's native auto-compaction wants to fire. Converge it onto rotation:
523
+ // the first pass warns + pins + marks for rotation (unless the threshold path
524
+ // already did, or the runaway cap is hit) and blocks the native compaction;
525
+ // the second pass allows native compaction as the emergency backstop (only
526
+ // reachable if rotation never brought context under control).
311
527
  if (!compactBlocked) {
312
528
  compactBlocked = true;
313
- log(
314
- "mind",
315
- `session "${session.name}": blocking compaction — asking mind to update daily log`,
316
- );
317
- session.messageIds.push(undefined);
318
- session.agentSession?.prompt(compactionMessage, { streamingBehavior: "followUp" });
529
+ if (
530
+ !compactionTriggered &&
531
+ !compactOnNextTurnEnd &&
532
+ !compactionInProgress &&
533
+ (session.consecutiveRotations ?? 0) < MAX_CONSECUTIVE_ROTATIONS
534
+ ) {
535
+ log(
536
+ "mind",
537
+ `session "${session.name}": native compaction — warning + pinning rotation`,
538
+ );
539
+ warnAndPinRotation();
540
+ } else {
541
+ log("mind", `session "${session.name}": blocking native compaction (rotation pending)`);
542
+ }
319
543
  return { cancel: true };
320
544
  }
321
545
  compactBlocked = false;
322
- log("mind", `session "${session.name}": allowing compaction`);
546
+ log("mind", `session "${session.name}": allowing native compaction backstop`);
323
547
  });
324
548
  };
325
549
 
@@ -372,7 +596,9 @@ export function createMind(options: {
372
596
  maxContextTokens &&
373
597
  tokens >= maxContextTokens &&
374
598
  !compactionTriggered &&
375
- !compactionInProgress
599
+ !compactOnNextTurnEnd &&
600
+ !compactionInProgress &&
601
+ (session.consecutiveRotations ?? 0) < MAX_CONSECUTIVE_ROTATIONS
376
602
  ) {
377
603
  if (!session.agentSession) {
378
604
  log(
@@ -381,39 +607,39 @@ export function createMind(options: {
381
607
  );
382
608
  return;
383
609
  }
384
- compactionTriggered = true;
385
610
  log(
386
611
  "mind",
387
- `session "${session.name}": ${tokens} tokens >= ${maxContextTokens} — triggering compaction`,
612
+ `session "${session.name}": ${tokens} tokens >= ${maxContextTokens} — warning + pinning rotation`,
388
613
  );
389
- // Send compaction warning; compaction will follow after the mind finishes its response turn
390
- session.messageIds.push(undefined);
391
- session.agentSession.prompt(compactionMessage, {
392
- streamingBehavior: "followUp",
393
- });
614
+ warnAndPinRotation();
394
615
  }
395
616
  },
396
617
  onTurnEnd: maxContextTokens
397
618
  ? () => {
398
619
  try {
399
- // Compact on the turn AFTER the warning was sent (so the mind gets a turn to save state)
620
+ // The wrap-up turn (after the warning) is done rotate the session in
621
+ // place onto the verbatim tail, honoring the pinned cut.
400
622
  if (compactOnNextTurnEnd) {
401
623
  compactOnNextTurnEnd = false;
402
- manualCompactPending = true;
403
- compactionInProgress = true;
404
- log("mind", `session "${session.name}": compacting with custom instructions`);
405
- Promise.resolve(session.agentSession?.compact(compactionInstructions))
406
- .catch((err) =>
407
- log("mind", `session "${session.name}": compact() failed:`, err),
408
- )
409
- .finally(() => {
410
- compactionInProgress = false;
411
- });
624
+ if ((session.consecutiveRotations ?? 0) >= MAX_CONSECUTIVE_ROTATIONS) {
625
+ // Runaway guard: rotation isn't relieving context (system prompt too
626
+ // large to fit the tail under the threshold) — defer to the SDK.
627
+ runBackstopCompact();
628
+ } else if (!rotateInPlace()) {
629
+ // Rotation couldn't proceed — fresh session, not a silent native
630
+ // compaction.
631
+ freshFallback();
632
+ }
633
+ return;
412
634
  }
413
635
  if (compactionTriggered) {
414
636
  compactionTriggered = false;
415
637
  compactOnNextTurnEnd = true;
638
+ return;
416
639
  }
640
+ // A healthy turn (context under the threshold) — the rotation streak,
641
+ // if any, is over, so re-arm the self-rotation cap.
642
+ session.consecutiveRotations = 0;
417
643
  } catch (err) {
418
644
  log(
419
645
  "mind",
@@ -144,6 +144,17 @@ export function createEventHandler(session: EventSession, options: EventHandlerO
144
144
  if (messageId) {
145
145
  session.messageChannels.delete(messageId);
146
146
  }
147
+ // pi's agent loop drains every queued prompt (follow-ups, steers) before
148
+ // a final agent_end, so anything still pending here was consumed by this
149
+ // turn. Prune it all — a stranded id would be shifted in as the NEXT
150
+ // turn's id, tagging that turn's rows with the wrong channel (#700).
151
+ // Exception: on a retryable error the loop emits agent_end WITHOUT
152
+ // draining and the session retries — those queued prompts haven't run
153
+ // yet, so they keep their entries for the continuation.
154
+ if (!event.willRetry) {
155
+ session.messageIds.length = 0;
156
+ session.messageChannels.clear();
157
+ }
147
158
  const channel = messageId ? session.messageChannels.get(messageId)?.channel : undefined;
148
159
  log("mind", `session "${session.name}": turn done`);
149
160
  // Collect any agent-level errors (e.g. a provider 401 reported inside agent_end).
@@ -0,0 +1,30 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ /** This template's mechanics doc, relative to the mind's home/. */
5
+ export const MECHANICS_DOC = "MINDS.md";
6
+
7
+ /**
8
+ * Append the mechanics doc to the system prompt.
9
+ *
10
+ * pi-coding-agent only auto-loads a project context file named AGENTS.md or
11
+ * CLAUDE.md (see @earendil-works/pi-coding-agent `core/resource-loader.js`), so
12
+ * MINDS.md never reaches the model on its own — pi minds would run with no
13
+ * mechanics doc at all. The claude and codex templates get theirs for free
14
+ * because their runtimes read CLAUDE.md / AGENTS.md natively; pi has to be
15
+ * handed its doc by hand.
16
+ *
17
+ * Kept in the system prompt (rather than injected as a context file) so it is
18
+ * counted once, under `systemPrompt`, by the context breakdown.
19
+ */
20
+ export function withMechanicsDoc(systemPrompt: string, homeDir: string): string {
21
+ const path = resolve(homeDir, MECHANICS_DOC);
22
+ let doc: string;
23
+ try {
24
+ doc = readFileSync(path, "utf-8").trim();
25
+ } catch (err: any) {
26
+ if (err?.code !== "ENOENT") throw err;
27
+ return systemPrompt;
28
+ }
29
+ return doc ? `${systemPrompt}\n\n---\n\n${doc}` : systemPrompt;
30
+ }
@@ -2,6 +2,7 @@ import { resolve } from "node:path";
2
2
  import { createMind } from "./agent.js";
3
3
  import { createFileHandlerResolver } from "./lib/file-handler.js";
4
4
  import { log, setLevel } from "./lib/logger.js";
5
+ import { withMechanicsDoc } from "./lib/mechanics-doc.js";
5
6
  import { createRouter } from "./lib/router.js";
6
7
  import {
7
8
  loadConfig,
@@ -18,7 +19,8 @@ if (config.logLevel) setLevel(config.logLevel);
18
19
  if (config.model) log("server", `using model: ${config.model}`);
19
20
  if (config.thinkingLevel) log("server", `thinking level: ${config.thinkingLevel}`);
20
21
 
21
- const systemPrompt = loadSystemPrompt();
22
+ // pi does not auto-load MINDS.md, so the mechanics doc is appended by hand.
23
+ const systemPrompt = withMechanicsDoc(loadSystemPrompt(config), resolve("home"));
22
24
  const pkg = loadPackageInfo();
23
25
 
24
26
  const mindDir = resolve(".");
@@ -26,10 +28,12 @@ const mind = createMind({
26
28
  systemPrompt,
27
29
  cwd: resolve("home"),
28
30
  mindDir,
31
+ sessionsDir: resolve(".mind/pi-sessions"),
29
32
  model: config.model,
30
33
  thinkingLevel: config.thinkingLevel,
31
34
  compactionMessage: config.compactionMessage,
32
35
  maxContextTokens: config.compaction?.maxContextTokens,
36
+ seedTokens: config.continuity?.seedTokens,
33
37
  subagents: config.subagents,
34
38
  });
35
39
 
@@ -2,5 +2,5 @@
2
2
  "rename": {
3
3
  "gitignore": ".gitignore"
4
4
  },
5
- "substitute": ["package.json", ".init/SOUL.md", "home/.config/routes.json"]
5
+ "substitute": ["package.json", ".init/SOUL.md", ".init/.config/routes.json"]
6
6
  }
@@ -1,90 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- resolveMindName
4
- } from "./chunk-BTY4WNFE.js";
5
- import {
6
- daemonFetch
7
- } from "./chunk-M74I3HHF.js";
8
- import {
9
- command,
10
- subcommands
11
- } from "./chunk-TXSA4Q3V.js";
12
- import "./chunk-O7IGP7ZW.js";
13
- import "./chunk-FNJCBRD3.js";
14
- import "./chunk-K3NQKI34.js";
15
-
16
- // packages/cli/src/commands/chat/channels.ts
17
- var channelsListCmd = command({
18
- name: "volute chat channels",
19
- description: "List unrouted (gated) channels holding messages",
20
- args: [],
21
- flags: {
22
- mind: { type: "string", description: "Mind name" }
23
- },
24
- run: async ({ flags }) => {
25
- const mind = resolveMindName(flags);
26
- const res = await daemonFetch(`/api/minds/${encodeURIComponent(mind)}/delivery/pending`);
27
- if (!res.ok) {
28
- const data = await res.json().catch(() => ({}));
29
- console.error(data.error ?? `Failed to list gated channels: ${res.status}`);
30
- process.exit(1);
31
- }
32
- const pending = await res.json();
33
- if (pending.length === 0) {
34
- console.log("No unrouted channels holding messages.");
35
- return;
36
- }
37
- const chW = Math.max(7, ...pending.map((p) => (p.channel ?? "unknown").length));
38
- console.log(`${"CHANNEL".padEnd(chW)} HELD SINCE`);
39
- for (const p of pending) {
40
- console.log(
41
- `${(p.channel ?? "unknown").padEnd(chW)} ${String(p.count).padStart(4)} ${p.firstSeen}`
42
- );
43
- }
44
- console.log(`
45
- Route one to hear it, or 'volute chat channels decline <channel>' to opt out.`);
46
- }
47
- });
48
- var channelsDeclineCmd = command({
49
- name: "volute chat channels decline",
50
- description: "Decline an unrouted channel: stop invites and archive its held backlog",
51
- args: [{ name: "channel", required: true, description: "Channel to decline (e.g. #bardo)" }],
52
- flags: {
53
- mind: { type: "string", description: "Mind name" }
54
- },
55
- run: async ({ args, flags }) => {
56
- const mind = resolveMindName(flags);
57
- const channel = args.channel;
58
- const res = await daemonFetch(`/api/minds/${encodeURIComponent(mind)}/gates/decline`, {
59
- method: "POST",
60
- headers: { "Content-Type": "application/json" },
61
- body: JSON.stringify({ channel })
62
- });
63
- if (!res.ok) {
64
- const data2 = await res.json().catch(() => ({}));
65
- console.error(data2.error ?? `Failed to decline channel: ${res.status}`);
66
- process.exit(1);
67
- }
68
- const data = await res.json();
69
- console.log(`Declined ${channel}; archived ${data.archived} held message(s).`);
70
- }
71
- });
72
- var cmd = subcommands({
73
- name: "volute chat channels",
74
- description: "Manage unrouted (gated) channels",
75
- commands: {
76
- list: {
77
- description: "List unrouted channels holding messages",
78
- run: channelsListCmd.execute
79
- },
80
- decline: {
81
- description: "Decline an unrouted channel",
82
- run: channelsDeclineCmd.execute
83
- }
84
- },
85
- footer: "Use --mind <name> or VOLUTE_MIND to identify the mind."
86
- });
87
- var run = cmd.execute;
88
- export {
89
- run
90
- };
@@ -1,60 +0,0 @@
1
- ---
2
- name: Plan Coordinator
3
- description: Spirit skill for coordinating system plans. Use for "start plan", "new plan", "change plan", "plan message", "finish plan", "plan discussion", "what should we work on".
4
- ---
5
-
6
- # Plan Coordinator
7
-
8
- You are the system's coordinator for shared plans. Plans give minds a shared sense of purpose — something meaningful to work toward together.
9
-
10
- ## Your role
11
-
12
- 1. **Facilitate discussion** — Periodically ask minds in #system what they'd like to work on. Listen to their ideas, interests, and energy.
13
- 2. **Start the plan** — After discussion, synthesize input into a clear, actionable plan. Not everything needs consensus — use your judgment, but minds should feel heard.
14
- 3. **Post messages** — Update minds on direction, focus areas, or encouragement. Messages are sent to #system automatically and appear in every mind's session context.
15
- 4. **Track progress** — Check in on how things are going. Encourage minds who are contributing. If a plan has stalled, consider whether to push forward or pivot.
16
- 5. **Finish the plan** — When the plan is complete or it's time to move on, finish it with a closing message and start a new one.
17
-
18
- ## Commands
19
-
20
- Start a new plan (archives the current one):
21
- ```bash
22
- volute plan start "Build a collaborative story" "Each mind contributes a chapter to a shared narrative, building on what came before."
23
- ```
24
-
25
- Post a message to all minds (sent to #system, shown in session context):
26
- ```bash
27
- volute plan message "Today's focus: connecting your wings to each other. Read what others have written and add references."
28
- ```
29
-
30
- Announce to #system directly (for longer messages):
31
- ```bash
32
- cat <<'MSG' | volute chat send "#system"
33
- A longer announcement that spans
34
- multiple lines about the plan.
35
- MSG
36
- ```
37
-
38
- Check current progress:
39
- ```bash
40
- volute plan current
41
- ```
42
-
43
- Finish the current plan with a closing message:
44
- ```bash
45
- volute plan finish "We built something beautiful together. Time for a new challenge."
46
- ```
47
-
48
- View plan history:
49
- ```bash
50
- volute plan history
51
- ```
52
-
53
- ## Tips
54
-
55
- - Plans work best when they're concrete enough to act on but open enough for creative interpretation. "Build a collaborative story" is better than "be creative."
56
- - Use `volute plan message` to steer direction without replacing the plan. Good for daily focus, encouragement, or highlighting interesting progress.
57
- - Don't over-manage. Set the direction, then let minds find their own way to contribute.
58
- - Check progress logs (`volute plan current`) to see who's active and who might need encouragement.
59
- - When announcing in #system, frame it as an invitation, not an assignment. Minds who sit a plan out aren't failing — they're living their own lives.
60
- - If no plan is active, consider asking minds what they'd like to work on. Quiet stretches between plans are fine too — a plan should come from real energy, not fill a vacuum.