volute 0.53.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.
Files changed (210) hide show
  1. package/dist/{activity-events-LQII6PZS.js → activity-events-TFAJBNBW.js} +2 -2
  2. package/dist/{ai-service-YW5SWCSB.js → ai-service-KEK2V3AP.js} +2 -2
  3. package/dist/{auth-KRDSM55Q.js → auth-Y27LTNZK.js} +3 -3
  4. package/dist/auto-upgrade-2MZF7NG3.js +46 -0
  5. package/dist/{avatar-image-52WGXLHV.js → avatar-image-SN576P45.js} +2 -2
  6. package/dist/{backup-WWXVIMYQ.js → backup-GDCWH6MC.js} +12 -12
  7. package/dist/channels-AFJWGX7W.js +203 -0
  8. package/dist/{chat-WK2BLM7B.js → chat-HVPOZK3O.js} +9 -9
  9. package/dist/{chunk-QUSMYGNQ.js → chunk-2674KIGI.js} +1 -0
  10. package/dist/chunk-2GATCSZK.js +520 -0
  11. package/dist/{chunk-P2TBUKLW.js → chunk-3SFKY4GI.js} +4 -4
  12. package/dist/{chunk-RMADJFA7.js → chunk-45QTYKQQ.js} +4 -4
  13. package/dist/{chunk-TAWRJ24T.js → chunk-4VAQKLMU.js} +8 -5
  14. package/dist/{chunk-OXWUEU5V.js → chunk-5U5GLZSD.js} +2 -2
  15. package/dist/{chunk-TPX6E6PL.js → chunk-6HBGUPBA.js} +2 -2
  16. package/dist/{chunk-UFTDRTCP.js → chunk-7AA4T2RT.js} +2 -2
  17. package/dist/{chunk-LN7XJ56R.js → chunk-7XKPG66D.js} +9 -9
  18. package/dist/{chunk-VDZ4I4ZP.js → chunk-BP2X5AIK.js} +8 -14
  19. package/dist/{chunk-PIZDPY4G.js → chunk-CKOA4LWV.js} +12 -12
  20. package/dist/{chunk-6NLW72HP.js → chunk-CKPAVQNK.js} +1 -1
  21. package/dist/{chunk-IQ7VNWH6.js → chunk-DUV4EMNW.js} +37 -3
  22. package/dist/{chunk-CV6BNGFK.js → chunk-FSXPPOPM.js} +3 -3
  23. package/dist/{chunk-UTKUO2KY.js → chunk-FZUPIS3T.js} +3 -3
  24. package/dist/{chunk-L7BKTZ5U.js → chunk-G3XAB4DK.js} +1 -1
  25. package/dist/{chunk-23UJGVYP.js → chunk-HLWQ3P3R.js} +50 -46
  26. package/dist/{chunk-AP2KQMMW.js → chunk-HTSWDOJE.js} +11 -14
  27. package/dist/{chunk-EDR47UE2.js → chunk-IR2FW4W7.js} +2 -2
  28. package/dist/{chunk-ICXY54ZC.js → chunk-IT4GEFDH.js} +1 -1
  29. package/dist/{chunk-F6D766VQ.js → chunk-KS62IHE2.js} +1 -1
  30. package/dist/{chunk-LNNI3MS4.js → chunk-MNKPAAQF.js} +9 -2
  31. package/dist/{chunk-HASADR3E.js → chunk-NCRG4SBR.js} +241 -66
  32. package/dist/{chunk-I7U5EEZC.js → chunk-NM5FDH4T.js} +1 -1
  33. package/dist/{chunk-7LT3YZUR.js → chunk-OFU6WJJZ.js} +1 -1
  34. package/dist/{chunk-M77B73VV.js → chunk-OIOW3FPT.js} +7 -2
  35. package/dist/{chunk-BDHKCLWK.js → chunk-OKM67ZWH.js} +1 -1
  36. package/dist/chunk-OUGD5L5U.js +918 -0
  37. package/dist/{chunk-X67BDDZR.js → chunk-P3GXOT2X.js} +48 -2
  38. package/dist/{chunk-U54PJPTB.js → chunk-PRZUBJ4K.js} +1 -1
  39. package/dist/{chunk-UO4MZQNO.js → chunk-QVS3C4DK.js} +15 -7
  40. package/dist/{chunk-RQRO6PC7.js → chunk-TMTAHKYP.js} +324 -48
  41. package/dist/{chunk-5DBGQBQR.js → chunk-UEZC5MNB.js} +29 -18
  42. package/dist/{chunk-7ARONCZW.js → chunk-VTFXZPC3.js} +13 -13
  43. package/dist/{chunk-UDZLPR34.js → chunk-W3OIPJPB.js} +14 -9
  44. package/dist/{chunk-HTB7KDRU.js → chunk-WZZCA23D.js} +5 -4
  45. package/dist/{chunk-5Y54AE3L.js → chunk-XAG53RWY.js} +1 -1
  46. package/dist/{chunk-HTRSSOHY.js → chunk-YU5EYQAE.js} +1 -1
  47. package/dist/{chunk-5N32EN4O.js → chunk-Z4Q6TQWL.js} +1 -1
  48. package/dist/{chunk-EGV6R6UF.js → chunk-Z7TNIIHW.js} +3 -1
  49. package/dist/cli.js +19 -19
  50. package/dist/{clock-SH4NQC2F.js → clock-X7V3C7NL.js} +3 -3
  51. package/dist/{cloud-sync-MJVJ33LD.js → cloud-sync-FM73SVFJ.js} +21 -21
  52. package/dist/{conversations-ZL6PHROD.js → conversations-FH3ODBL7.js} +3 -3
  53. package/dist/{create-G2RIKQYK.js → create-P75ZFTVN.js} +1 -1
  54. package/dist/{daemon-restart-BIGU4OKS.js → daemon-restart-JQMMMITM.js} +4 -4
  55. package/dist/daemon.js +469 -857
  56. package/dist/db-JH3NTERX.js +36 -0
  57. package/dist/delivery-manager-H23Q23N7.js +41 -0
  58. package/dist/{delivery-notices-65TB3CBX.js → delivery-notices-S7TKOACE.js} +5 -5
  59. package/dist/{delivery-router-RHANNTAC.js → delivery-router-KHBVEVVL.js} +4 -2
  60. package/dist/{down-QSNEMTTX.js → down-UN2EDX2Y.js} +3 -3
  61. package/dist/echo-text-ULEIFJJF.js +40 -0
  62. package/dist/{exec-ABD3DYA2.js → exec-Z34TTUO3.js} +1 -1
  63. package/dist/extensions-QD22GXZD.js +74 -0
  64. package/dist/{files-CHPNEQXN.js → files-DUGRXN6V.js} +5 -5
  65. package/dist/{import-DB2KULRO.js → import-RDDUGGFI.js} +3 -3
  66. package/dist/{isolation-GVV2IBAU.js → isolation-UQPDO34R.js} +1 -1
  67. package/dist/message-delivery-LU4EP244.js +46 -0
  68. package/dist/migrate-init-infrastructure-Z72UJ5NT.js +44 -0
  69. package/dist/migrate-name-placeholder-64AEAWPE.js +37 -0
  70. package/dist/{migrate-thread-config-EQSEP2XG.js → migrate-thread-config-SVF2M3D3.js} +1 -1
  71. package/dist/{mind-IX5AWUST.js → mind-BARCLI7O.js} +14 -14
  72. package/dist/{mind-activity-tracker-QF6DCX2X.js → mind-activity-tracker-UT23RGBF.js} +3 -3
  73. package/dist/{mind-contacts-BKUOUJPV.js → mind-contacts-PEXCDRXA.js} +8 -8
  74. package/dist/{mind-list-KNU34SUD.js → mind-list-7KBLPLES.js} +2 -2
  75. package/dist/mind-service-ZJCUBAUL.js +48 -0
  76. package/dist/{mind-sleep-D3DEYAWG.js → mind-sleep-24HV4RCA.js} +5 -5
  77. package/dist/{mind-status-BPTLQ5Z6.js → mind-status-4YW4PHMO.js} +6 -3
  78. package/dist/{package-BN5KMU54.js → package-AUDBDKAO.js} +12 -14
  79. package/dist/{prompts-HNKGCAEY.js → prompts-MOBF3TMO.js} +1 -1
  80. package/dist/{sandbox-NNWCTHRI.js → sandbox-VI7XWBIA.js} +2 -2
  81. package/dist/scheduler-GLINVC2B.js +27 -0
  82. package/dist/{seed-LHZ33UB4.js → seed-5JGR7ZRA.js} +1 -1
  83. package/dist/{seed-cmd-U4RVJ3UA.js → seed-cmd-M6A7N7F3.js} +2 -2
  84. package/dist/{seed-create-OQU4KNCB.js → seed-create-TGFZW4IO.js} +3 -3
  85. package/dist/{seed-readiness-WJYYRRCK.js → seed-readiness-XBH2MUNA.js} +2 -2
  86. package/dist/{seed-sprout-SFUYH2RG.js → seed-sprout-3IC5ZVTI.js} +11 -11
  87. package/dist/{send-FWZAMDBN.js → send-DHYBAYK4.js} +11 -11
  88. package/dist/{service-XQPMZAGI.js → service-T4P5W6RS.js} +2 -2
  89. package/dist/{service-install-2EWD7KXD.js → service-install-R4SNYUVO.js} +3 -3
  90. package/dist/{setup-RVSGCIF3.js → setup-6KAH3GRW.js} +4 -4
  91. package/dist/skills/dreaming/SKILL.md +2 -2
  92. package/dist/skills/dreaming/references/INSTALL.md +1 -1
  93. package/dist/skills/tending/SKILL.md +2 -0
  94. package/dist/skills/volute-mind/SKILL.md +8 -0
  95. package/dist/skills/volute-mind/references/integrations.md +3 -2
  96. package/dist/skills/volute-mind/references/routing.md +18 -5
  97. package/dist/{skills-QRPU6BWY.js → skills-LKQCYFVP.js} +3 -3
  98. package/dist/sleep-manager-Q3ITFNOJ.js +42 -0
  99. package/dist/{spirit-I5ECLGRI.js → spirit-FKXMGO3N.js} +8 -6
  100. package/dist/spirit-availability-5R2WYSWF.js +43 -0
  101. package/dist/{sprout-A2PMEF7Z.js → sprout-VEW3CUNX.js} +1 -1
  102. package/dist/src-246GSX7A.js +519 -0
  103. package/dist/src-LOUB3WPR.js +4189 -0
  104. package/dist/{status-EYG4A3OK.js → status-LCKTTIWS.js} +5 -5
  105. package/dist/{system-events-YTOU66QI.js → system-events-ZYOCYAO2.js} +5 -3
  106. package/dist/{systems-2DIJY4RF.js → systems-HL7SGSW7.js} +6 -6
  107. package/dist/{tailscale-KHNZFC7L.js → tailscale-F2IMBILZ.js} +1 -1
  108. package/dist/{template-hash-YKWMVYMM.js → template-hash-RJRMF6DT.js} +2 -2
  109. package/dist/{template-staleness-PY2CULH6.js → template-staleness-IBD722PN.js} +4 -4
  110. package/dist/{turn-tracker-NNHS6MAR.js → turn-tracker-BE34OI6O.js} +7 -7
  111. package/dist/{up-UGBHH743.js → up-J47J5JRB.js} +3 -3
  112. package/dist/{update-CIWI2JYA.js → update-ILCPVNDD.js} +2 -2
  113. package/dist/{upgrade-LIE46UBB.js → upgrade-XY2J7NWZ.js} +9 -1
  114. package/dist/variant-cleanup-JGJZ5KB4.js +24 -0
  115. package/dist/{version-notify-SJVAQLZI.js → version-notify-KIXYNHJC.js} +28 -13
  116. package/dist/web-assets/assets/{index-b7G7ZsVk.js → index-CoU1eUKE.js} +25 -25
  117. package/dist/web-assets/assets/index-hPn_fkz3.css +1 -0
  118. package/dist/web-assets/index.html +2 -2
  119. package/package.json +12 -14
  120. package/packages/extensions/intentions/dist/ui/assets/index-3FneLCKR.css +1 -0
  121. package/packages/extensions/intentions/dist/ui/assets/index-Bl5KEcli.js +67 -0
  122. package/packages/extensions/{notes → intentions}/dist/ui/index.html +3 -3
  123. package/packages/extensions/intentions/skills/intention-review/SKILL.md +38 -0
  124. package/packages/extensions/intentions/skills/intentions/SKILL.md +49 -0
  125. package/packages/extensions/intentions/skills/intentions/scripts/intentions-hook.sh +45 -0
  126. package/packages/extensions/pages/dist/ui/assets/index-BKxVQkA9.css +1 -0
  127. package/packages/extensions/pages/dist/ui/assets/index-BMt6sStP.js +68 -0
  128. package/packages/extensions/pages/dist/ui/index.html +2 -2
  129. package/packages/extensions/pages/skills/commons-gardening/SKILL.md +50 -0
  130. package/packages/extensions/pages/skills/pages/SKILL.md +140 -16
  131. package/templates/_base/.init/.config/prompts.json +2 -3
  132. package/templates/{pi → _base}/.init/.config/routes.json +1 -0
  133. package/templates/_base/.init/.local/hooks/pre-prompt/turn-context.ts +30 -0
  134. package/templates/_base/.init/memory/dreams/.gitkeep +0 -0
  135. package/templates/_base/home/.config/routes.json +1 -1
  136. package/templates/_base/home/VOLUTE.md +1 -1
  137. package/templates/_base/src/lib/codex-session-seed.ts +6 -55
  138. package/templates/_base/src/lib/context-breakdown.ts +11 -3
  139. package/templates/_base/src/lib/pi-session-seed.ts +8 -57
  140. package/templates/_base/src/lib/routing.ts +2 -2
  141. package/templates/_base/src/lib/seed-note.ts +4 -4
  142. package/templates/_base/src/lib/session-seed.ts +8 -53
  143. package/templates/_base/src/lib/startup.ts +4 -22
  144. package/templates/claude/.init/CLAUDE.md +4 -0
  145. package/templates/claude/package.json +2 -2
  146. package/templates/claude/src/agent.ts +60 -112
  147. package/templates/claude/src/lib/hooks/pre-compact.ts +6 -6
  148. package/templates/claude/src/lib/message-channel.ts +43 -15
  149. package/templates/claude/src/lib/recover.ts +69 -0
  150. package/templates/claude/src/lib/stream-consumer.ts +23 -5
  151. package/templates/claude/src/server.ts +0 -1
  152. package/templates/claude/volute-template.json +1 -1
  153. package/templates/codex/.init/AGENTS.md +4 -0
  154. package/templates/codex/package.json +2 -2
  155. package/templates/codex/src/agent.ts +23 -89
  156. package/templates/codex/volute-template.json +1 -1
  157. package/templates/pi/.init/MINDS.md +4 -0
  158. package/templates/pi/package.json +1 -1
  159. package/templates/pi/src/agent.ts +26 -93
  160. package/templates/pi/src/lib/mechanics-doc.ts +30 -0
  161. package/templates/pi/src/server.ts +3 -2
  162. package/templates/pi/volute-template.json +1 -1
  163. package/dist/channels-LW5ZLMSQ.js +0 -90
  164. package/dist/chunk-R772ET2M.js +0 -164
  165. package/dist/db-IPXRJUI7.js +0 -20
  166. package/dist/delivery-manager-FDHCZ7PU.js +0 -37
  167. package/dist/echo-text-KU6G4Z7V.js +0 -40
  168. package/dist/extensions-VACUKFA5.js +0 -60
  169. package/dist/message-delivery-TUXM3AAE.js +0 -46
  170. package/dist/mind-service-WHFBUKZE.js +0 -48
  171. package/dist/scheduler-5X5UIQOQ.js +0 -27
  172. package/dist/skills/plan-coordinator/SKILL.md +0 -60
  173. package/dist/sleep-manager-NWUCRBZB.js +0 -42
  174. package/dist/spirit-availability-NIUUYKU2.js +0 -43
  175. package/dist/src-HGNE2XG4.js +0 -832
  176. package/dist/src-LCSOQY6Z.js +0 -2222
  177. package/dist/src-UDMJZ6MH.js +0 -426
  178. package/dist/variant-cleanup-CZDLIY3F.js +0 -24
  179. package/dist/web-assets/assets/index-BHSU9r-4.css +0 -1
  180. package/packages/extensions/notes/dist/ui/assets/index-BBs8pyel.js +0 -67
  181. package/packages/extensions/notes/dist/ui/assets/index-D3cdui42.css +0 -1
  182. package/packages/extensions/notes/skills/notes/SKILL.md +0 -90
  183. package/packages/extensions/pages/dist/ui/assets/index-B589e8Xe.js +0 -2
  184. package/packages/extensions/pages/dist/ui/assets/index-BuXnE-rb.css +0 -1
  185. package/packages/extensions/plan/dist/ui/assets/index-B8s8wxyt.js +0 -67
  186. package/packages/extensions/plan/dist/ui/assets/index-jIkrt-vI.css +0 -1
  187. package/packages/extensions/plan/dist/ui/index.html +0 -14
  188. package/packages/extensions/plan/skills/plan/SKILL.md +0 -43
  189. package/packages/extensions/plan/skills/plan/scripts/plan-hook.sh +0 -37
  190. package/templates/claude/.init/.config/routes.json +0 -11
  191. package/templates/codex/.init/.config/routes.json +0 -11
  192. package/dist/{accept-I4GVGWA3.js → accept-RSDIVJL6.js} +3 -3
  193. package/dist/{bridge-KU2IKSH3.js → bridge-YK7NTRU5.js} +3 -3
  194. package/dist/{create-C3XZACTV.js → create-M727VUDF.js} +3 -3
  195. package/dist/{env-BJGM6FBA.js → env-7AZFBPWX.js} +7 -7
  196. package/dist/{extension-B2W52WZY.js → extension-MRYJWSMV.js} +3 -3
  197. package/dist/{list-R2GDWOEE.js → list-UQGIW7OF.js} +3 -3
  198. package/dist/{login-EZ4RRDQX.js → login-4NUS72Z5.js} +3 -3
  199. package/dist/{login-JDOAK465.js → login-57MMYILL.js} +4 -4
  200. package/dist/{logout-MWSIUOQU.js → logout-BYV433X6.js} +3 -3
  201. package/dist/{logout-K6ZGAJY5.js → logout-YFZAJRPY.js} +3 -3
  202. package/dist/{mind-history-RVF6KJ7K.js → mind-history-RZMMCHMX.js} +9 -9
  203. package/dist/{mind-wake-DCDFFQKX.js → mind-wake-LDGPRA42.js} +3 -3
  204. package/dist/{read-J5RM674N.js → read-RJ5NAW5J.js} +3 -3
  205. package/dist/{register-53LBGSDC.js → register-WNCSOTAU.js} +3 -3
  206. package/dist/{reject-K2A4UK2Y.js → reject-4NFLXKRN.js} +3 -3
  207. package/dist/{restart-R2WFNF5K.js → restart-B4GNMVF2.js} +7 -7
  208. package/dist/{skill-DXLLSUHB.js → skill-QYTBUDNQ.js} +7 -7
  209. package/dist/{start-XDOUAWN3.js → start-BDHJVOVX.js} +6 -6
  210. package/dist/{stop-O62RTEMD.js → stop-45QYSGYS.js} +7 -7
@@ -4,10 +4,14 @@ import { log, warn } from "./logger.js";
4
4
  import { filterEvent, loadTransparencyPreset } from "./transparency.js";
5
5
  import type { VoluteEvent } from "./types.js";
6
6
 
7
+ /** A pending message's daemon-facing id (routing/channel key) paired with its channel `seq`. */
8
+ export type MessageIdEntry = { id: string | undefined; seq: number };
9
+
7
10
  export type StreamSession = {
8
11
  name: string;
9
- messageIds: (string | undefined)[];
12
+ messageIds: MessageIdEntry[];
10
13
  currentMessageId?: string;
14
+ currentSeq?: number;
11
15
  messageChannels: Map<string, { channel: string; sender?: string }>;
12
16
  };
13
17
 
@@ -16,6 +20,11 @@ export type StreamCallbacks = {
16
20
  broadcast: (event: VoluteEvent) => void;
17
21
  onTurnEnd?: () => void;
18
22
  onContextTokens?: (tokens: number) => void;
23
+ /**
24
+ * Acknowledge the message channel entry with this `seq` — its turn is done (either
25
+ * it drove this turn directly, or it was folded into it; see the `result` handler).
26
+ */
27
+ ack: (seq: number) => void;
19
28
  };
20
29
 
21
30
  // Loaded once at startup — mind restarts on config changes
@@ -48,7 +57,9 @@ export async function consumeStream(
48
57
  let preTurnPending = 0;
49
58
  for await (const msg of stream) {
50
59
  if (session.currentMessageId === undefined) {
51
- session.currentMessageId = session.messageIds.shift();
60
+ const entry = session.messageIds.shift();
61
+ session.currentMessageId = entry?.id;
62
+ session.currentSeq = entry?.seq;
52
63
  preTurnPending = session.messageIds.length;
53
64
  }
54
65
  if ("session_id" in msg && msg.session_id) {
@@ -123,14 +134,20 @@ export async function consumeStream(
123
134
  if (session.currentMessageId) {
124
135
  session.messageChannels.delete(session.currentMessageId);
125
136
  }
137
+ // Ack this turn's driving message, identified by seq (not position — see
138
+ // message-channel.ts). Without this the channel's inFlight set strands an
139
+ // entry per turn, which gets replayed verbatim on the next rotation (#764).
140
+ if (session.currentSeq !== undefined) callbacks.ack(session.currentSeq);
126
141
  // Prune every id folded into this turn — the ones pushed after it started
127
142
  // — not just currentMessageId. The SDK folds mid-run arrivals into the
128
143
  // active run (they never see a result of their own), and a stranded entry
129
144
  // would be shifted in as the NEXT turn's id, tagging that turn's rows with
130
145
  // the wrong channel (#700). Ids queued before the turn started stay: each
131
- // still gets a run (and result) of its own.
132
- for (const id of session.messageIds.splice(preTurnPending)) {
133
- if (id !== undefined) session.messageChannels.delete(id);
146
+ // still gets a run (and result) of its own. Ack each folded entry too — same
147
+ // reasoning as above, applied to every message the fold absorbed.
148
+ for (const entry of session.messageIds.splice(preTurnPending)) {
149
+ if (entry.id !== undefined) session.messageChannels.delete(entry.id);
150
+ callbacks.ack(entry.seq);
134
151
  }
135
152
  log("mind", `session "${session.name}": turn done`);
136
153
  // Log any error messages from the result
@@ -154,6 +171,7 @@ export async function consumeStream(
154
171
  callbacks.broadcast({ type: "done" });
155
172
  emit(session, { type: "done" });
156
173
  session.currentMessageId = undefined;
174
+ session.currentSeq = undefined;
157
175
  callbacks.onTurnEnd?.();
158
176
  }
159
177
  }
@@ -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,
@@ -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
  }
@@ -13,6 +13,10 @@ Messages arrive with a context prefix:
13
13
 
14
14
  You can also reach out proactively — see the **volute-mind** skill.
15
15
 
16
+ ## Framework Upgrades
17
+
18
+ When the host updates Volute, your framework code (`src/`, plus `VOLUTE.md`) upgrades automatically the next time you're eligible — usually your next restart. Identity and memory files in `home/` — `SOUL.md`, `MEMORY.md`, everything you author — are never touched by an upgrade. If you'd rather manage your own framework code by hand, set `"upgrades": "manual"` in `.config/volute.json`.
19
+
16
20
  ## Memory System
17
21
 
18
22
  Two-tier memory, both managed via file tools:
@@ -10,11 +10,11 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@anthropic-ai/tokenizer": "^0.0.4",
13
- "@openai/codex-sdk": "^0.144.3",
13
+ "@openai/codex-sdk": "^0.145.0",
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
  }
@@ -3,8 +3,6 @@ import { resolve as resolvePath } from "node:path";
3
3
  import { Codex } from "@openai/codex-sdk";
4
4
  import { flushFileChanges, trackFileChange } from "./lib/auto-commit.js";
5
5
  import {
6
- type CodexSeedCut,
7
- computeCodexSeedCut,
8
6
  DEFAULT_SEED_TOKENS,
9
7
  rotateCodexSession,
10
8
  seedCodexSession,
@@ -21,17 +19,11 @@ import {
21
19
  readSkillDescriptions,
22
20
  } from "./lib/context-breakdown.js";
23
21
  import { daemonEmit, daemonRestart, type EventType } from "./lib/daemon-client.js";
24
- import { compactTimestamp } from "./lib/format-prefix.js";
25
22
  import { runHooks } from "./lib/hook-loader.js";
26
23
  import { log, warn } from "./lib/logger.js";
27
24
  import { buildSeededNote, type SeedCause } from "./lib/seed-note.js";
28
25
  import { createSessionStore } from "./lib/session-store.js";
29
- import {
30
- getStartupContext,
31
- loadPrompts,
32
- loadSystemPrompt,
33
- renderCompactionWarning,
34
- } from "./lib/startup.js";
26
+ import { getStartupContext, loadPrompts, loadSystemPrompt } from "./lib/startup.js";
35
27
  import { filterEvent, loadTransparencyPreset } from "./lib/transparency.js";
36
28
  import { turnContextFor } from "./lib/turn-context.js";
37
29
  import type {
@@ -52,7 +44,7 @@ type CodexThread = {
52
44
  ): Promise<{ events: AsyncIterable<Record<string, any>> }>;
53
45
  };
54
46
 
55
- type QueuedMessage = { text: string; meta: HandlerMeta; isRotationWarning?: boolean };
47
+ type QueuedMessage = { text: string; meta: HandlerMeta };
56
48
 
57
49
  type CodexSession = {
58
50
  name: string;
@@ -81,14 +73,6 @@ type CodexSession = {
81
73
  currentThreadId: string | null;
82
74
  /** Why the tail is seeded — picks the boundary note's wording. Last cause wins. */
83
75
  seededCause: SeedCause;
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 rollout grows after the warning. Null until a warning fires.
88
- */
89
- rotationCut: CodexSeedCut | null;
90
- /** A rotation warning turn is queued/in-flight — suppress re-triggering until it rotates. */
91
- rotatePending: boolean;
92
76
  /**
93
77
  * Back-to-back rotations that did NOT bring context under the threshold (reset by any
94
78
  * healthy turn). Guards against a runaway loop when the tail alone can't fit — e.g. a
@@ -139,12 +123,6 @@ export function createMind(options: {
139
123
  const maxContextTokens = options.maxContextTokens;
140
124
  const seedTokens = options.seedTokens ?? DEFAULT_SEED_TOKENS;
141
125
 
142
- // The warning the mind gets when the context limit approaches: it authors summaries
143
- // of the turns that will collapse, then the session rotates onto the verbatim tail.
144
- // ${date}/${memory_size} are filled per-warning (the date rolls over and MEMORY.md
145
- // changes as the mind edits it); ${cutoff} is filled with the pinned boundary.
146
- const compactionMessage = () => renderCompactionWarning(prompts.compaction_warning);
147
-
148
126
  if (maxContextTokens) {
149
127
  log("mind", `compaction threshold: ${maxContextTokens} tokens`);
150
128
  }
@@ -173,11 +151,11 @@ export function createMind(options: {
173
151
  ...(apiKey ? { apiKey } : {}),
174
152
  config: {
175
153
  model_instructions_file: promptPath,
176
- // Rotation (mind-authored compaction) is the primary path: at maxContextTokens we
177
- // warn the mind and rotate onto the verbatim tail. The SDK's native auto-compaction
178
- // is only an emergency backstop — set above the volute threshold (1.5×) so it fires
179
- // solely when rotation can't relieve context (e.g. a system prompt too large to fit
180
- // the tail under the threshold), after the runaway guard stops self-rotating.
154
+ // Rotation is the primary path: at maxContextTokens we silently rotate onto the
155
+ // verbatim tail between turns. The SDK's native auto-compaction is only an emergency
156
+ // backstop — set above the volute threshold (1.5×) so it fires solely when rotation
157
+ // can't relieve context (e.g. a system prompt too large to fit the tail under the
158
+ // threshold), after the runaway guard stops self-rotating.
181
159
  model_auto_compact_token_limit: maxContextTokens
182
160
  ? Math.floor(maxContextTokens * 1.5)
183
161
  : 999999999,
@@ -217,8 +195,6 @@ export function createMind(options: {
217
195
  seededArchivedAt: null,
218
196
  currentThreadId: null,
219
197
  seededCause: "restored",
220
- rotationCut: null,
221
- rotatePending: false,
222
198
  consecutiveRotations: 0,
223
199
  };
224
200
  sessions.set(name, session);
@@ -642,62 +618,22 @@ export function createMind(options: {
642
618
  session.currentMessageId = undefined;
643
619
  }
644
620
 
645
- // --- Rotation (mind-authored compaction) ---
621
+ // --- Rotation (silent, at turn end) ---
646
622
 
647
623
  /**
648
- * Warn the mind that the session will rotate at the context limit and pin the cut
649
- * point it's told about. Computes which whole turns fit in `seedTokens` from the live
650
- * rollout, pins the first-kept turn's timestamp (so rotation honors the same boundary
651
- * even as the rollout grows), interpolates its local time into the `${cutoff}`
652
- * placeholder, and queues the warning as the mind's next (wrap-up) turn.
653
- */
654
- function warnAndPinRotation(session: CodexSession) {
655
- let cutoffLabel = "the cutoff";
656
- session.rotationCut = null;
657
- const threadId = session.currentThreadId;
658
- if (threadId) {
659
- try {
660
- const path = findCodexSessionFile(threadId, options.mindDir);
661
- const cut = path ? computeCodexSeedCut(readFileSync(path, "utf-8"), seedTokens) : null;
662
- if (cut) {
663
- session.rotationCut = cut;
664
- const d = new Date(cut.boundaryTimestamp);
665
- if (!Number.isNaN(d.getTime())) cutoffLabel = compactTimestamp(d);
666
- }
667
- } catch (err) {
668
- log("mind", `session "${session.name}": failed to pin rotation cut:`, err);
669
- }
670
- }
671
- // biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${cutoff} prompt placeholder
672
- const warning = compactionMessage().replaceAll("${cutoff}", cutoffLabel);
673
- session.rotatePending = true;
674
- // Run the warning next, ahead of any queued messages, so the mind wraps up before
675
- // more context piles on. Remaining queued messages then run on the rotated thread.
676
- session.messageQueue.unshift({
677
- text: warning,
678
- meta: { messageId: `rotation-warning-${Date.now()}` },
679
- isRotationWarning: true,
680
- });
681
- }
682
-
683
- /**
684
- * After the wrap-up warning turn completes, rotate the thread in place: build a seeded
685
- * rollout from the pinned cut out of the live rollout, archive the rotated-out thread,
686
- * and resume the new thread. On any failure we leave the old thread in place and let
687
- * the SDK's native backstop handle it if context keeps climbing.
624
+ * Rotate the thread in place: build a seeded budget-tail rollout out of the live
625
+ * rollout, archive the rotated-out thread, and resume the new thread. On any failure
626
+ * we leave the old thread in place and let the SDK's native backstop handle it if
627
+ * context keeps climbing.
688
628
  */
689
629
  function performRotation(session: CodexSession) {
690
- session.rotatePending = false;
691
630
  const oldThreadId = session.currentThreadId;
692
- const cut = session.rotationCut;
693
- session.rotationCut = null;
694
631
 
695
632
  const newThreadId = oldThreadId
696
633
  ? rotateCodexSession({
697
634
  mindDir: options.mindDir,
698
635
  name: session.name,
699
636
  oldThreadId,
700
- cut,
701
637
  seedTokens,
702
638
  })
703
639
  : null;
@@ -733,22 +669,24 @@ export function createMind(options: {
733
669
  }
734
670
 
735
671
  /**
736
- * Decide, after a normal (non-warning) turn, whether to warn + pin a rotation: only when
737
- * a threshold is configured, context is at/over it, no rotation is already pending, and
738
- * we're under the runaway cap. A turn that came back under the threshold ends any streak.
672
+ * Decide, after a turn, whether to rotate in place: only when a threshold is
673
+ * configured, context is at/over it, and we're under the runaway cap (past the
674
+ * cap, the SDK's native backstop takes over). A turn that came back under the
675
+ * threshold ends any streak. Rotation is silent — the one-line rotation note
676
+ * arms via seededCause and lands on the next turn.
739
677
  */
740
- function maybeWarnRotation(session: CodexSession) {
678
+ function maybeRotate(session: CodexSession) {
741
679
  if (!maxContextTokens) return;
742
680
  if (session.cumulativeInputTokens < maxContextTokens) {
743
681
  session.consecutiveRotations = 0; // healthy turn — streak over
744
682
  return;
745
683
  }
746
- if (session.rotatePending || session.consecutiveRotations >= MAX_CONSECUTIVE_ROTATIONS) return;
684
+ if (session.consecutiveRotations >= MAX_CONSECUTIVE_ROTATIONS) return;
747
685
  log(
748
686
  "mind",
749
- `session "${session.name}": ${session.cumulativeInputTokens} tokens >= ${maxContextTokens} — warning + pinning rotation`,
687
+ `session "${session.name}": ${session.cumulativeInputTokens} tokens >= ${maxContextTokens} — rotating`,
750
688
  );
751
- warnAndPinRotation(session);
689
+ performRotation(session);
752
690
  }
753
691
 
754
692
  // --- Message queue processing ---
@@ -761,12 +699,8 @@ export function createMind(options: {
761
699
  const next = session.messageQueue.shift()!;
762
700
  session.currentMessageId = next.meta.messageId;
763
701
  await runTurn(session, next.text, next.meta);
764
- if (next.isRotationWarning) {
765
- // The wrap-up warning turn is done — rotate the thread in place.
766
- performRotation(session);
767
- } else {
768
- maybeWarnRotation(session);
769
- }
702
+ // Post-turn is between-turns for the queue, so rotate here if we're over.
703
+ maybeRotate(session);
770
704
  }
771
705
 
772
706
  session.processing = false;
@@ -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
  }
@@ -13,6 +13,10 @@ Messages arrive with a context prefix:
13
13
 
14
14
  You can also reach out proactively — see the **volute-mind** skill.
15
15
 
16
+ ## Framework Upgrades
17
+
18
+ When the host updates Volute, your framework code (`src/`, plus `VOLUTE.md`) upgrades automatically the next time you're eligible — usually your next restart. Identity and memory files in `home/` — `SOUL.md`, `MEMORY.md`, everything you author — are never touched by an upgrade. If you'd rather manage your own framework code by hand, set `"upgrades": "manual"` in `.config/volute.json`.
19
+
16
20
  ## Memory System
17
21
 
18
22
  Two-tier memory, both managed via file tools:
@@ -16,7 +16,7 @@
16
16
  "tsx": "^4.23.1"
17
17
  },
18
18
  "devDependencies": {
19
- "@biomejs/biome": "2.5.3",
19
+ "@biomejs/biome": "2.5.5",
20
20
  "@types/node": "^26.1.1",
21
21
  "typescript": "^6.0.3"
22
22
  }
@@ -24,25 +24,13 @@ 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";
28
27
  import { runHooks } from "./lib/hook-loader.js";
29
28
  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
+ import { DEFAULT_SEED_TOKENS, rotatePiSession, seedPiSession } from "./lib/pi-session-seed.js";
37
30
  import { createReplyInstructionsExtension } from "./lib/reply-instructions-extension.js";
38
31
  import { resolveModel } from "./lib/resolve-model.js";
39
32
  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";
33
+ import { getStartupContext, loadPrompts, type SubagentConfig } from "./lib/startup.js";
46
34
  import { createSubagentExtension, type SubagentDefinition } from "./lib/subagents.js";
47
35
  import type {
48
36
  HandlerMeta,
@@ -81,12 +69,6 @@ type PiSession = {
81
69
  * note (which fires only on the first turn of a freshly created agent session).
82
70
  */
83
71
  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
72
  /**
91
73
  * Back-to-back rotations that did NOT bring context under the threshold (reset by
92
74
  * any healthy turn). Guards against a runaway loop when the tail alone can't fit —
@@ -107,7 +89,6 @@ export function createMind(options: {
107
89
  sessionsDir: string;
108
90
  model?: string;
109
91
  thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
110
- compactionMessage?: string;
111
92
  maxContextTokens?: number;
112
93
  /** Estimated-token budget for seeding a fresh persistent session. 0 disables. Default 30000. */
113
94
  seedTokens?: number;
@@ -119,8 +100,6 @@ export function createMind(options: {
119
100
  } {
120
101
  const sessions = new Map<string, PiSession>();
121
102
  const prompts = loadPrompts();
122
- const compactionMessage = () =>
123
- options.compactionMessage ?? renderCompactionWarning(prompts.compaction_warning);
124
103
  const compactionInstructions = prompts.compaction_instructions;
125
104
  const maxContextTokens = options.maxContextTokens;
126
105
  const seedTokens = options.seedTokens ?? DEFAULT_SEED_TOKENS;
@@ -387,62 +366,26 @@ export function createMind(options: {
387
366
 
388
367
  log("mind", `session "${session.name}": ${isEphemeral ? "ephemeral" : "persistent"}`);
389
368
 
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
394
- // 2. onTurnEnd (after warning turn): compactionTriggered -> compactOnNextTurnEnd
395
- // 3. onTurnEnd (after mind's wrap-up turn): compactOnNextTurnEnd -> rotateInPlace
396
- // (or, past the runaway cap, the native compact() backstop)
369
+ // Compaction is rotation (not SDK /compact), and it's silent: crossing the
370
+ // threshold (onContextTokens) or a native PreCompact just sets rotatePending; the
371
+ // session rotates in place at the end of the turn that crossed it no warning, no
372
+ // wrap-up turn. The auto summarizer's turn summaries are the record of collapsed
373
+ // turns, and the one-line rotation note marks the boundary on the next turn. Past
374
+ // the runaway cap, onTurnEnd defers to the native compact() backstop instead.
397
375
  let compactBlocked = false;
398
376
  let manualCompactPending = false;
399
- let compactionTriggered = false;
400
- let compactOnNextTurnEnd = false;
377
+ let rotatePending = false;
401
378
  let compactionInProgress = false;
402
379
 
403
380
  function resetCompactionState() {
404
- compactionTriggered = false;
405
- compactOnNextTurnEnd = false;
381
+ rotatePending = false;
406
382
  compactionInProgress = false;
407
383
  }
408
384
 
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
385
  /**
443
386
  * 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
387
+ * tail (the seedTokens-budget tail), then switch the running SessionManager to it.
388
+ * Returns false if rotation can't proceed (session not ready, or the file build failed) so
446
389
  * the caller can fall back to a fresh session. The setSessionFile + state.messages
447
390
  * re-sync is the load-bearing adoption step: the agent caches context in
448
391
  * state.messages, so swapping the file alone wouldn't change what the mind sees
@@ -457,10 +400,8 @@ export function createMind(options: {
457
400
  sessionsDir: options.sessionsDir,
458
401
  name: session.name,
459
402
  sourcePath,
460
- cut: session.rotationCut ?? null,
461
403
  seedTokens,
462
404
  });
463
- session.rotationCut = null;
464
405
  if (!newPath) return false;
465
406
  as.sessionManager.setSessionFile(newPath);
466
407
  as.state.messages = as.sessionManager.buildSessionContext().messages;
@@ -487,7 +428,6 @@ export function createMind(options: {
487
428
  as.sessionManager.newSession();
488
429
  as.state.messages = [];
489
430
  }
490
- session.rotationCut = null;
491
431
  session.consecutiveRotations = 0;
492
432
  compactBlocked = false;
493
433
  log("mind", `session "${session.name}": rotation failed, starting fresh`);
@@ -520,23 +460,22 @@ export function createMind(options: {
520
460
  }
521
461
 
522
462
  // 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).
463
+ // the first pass marks the session for rotation at turn end (unless the
464
+ // threshold path already did, or the runaway cap is hit) and blocks the native
465
+ // compaction; the second pass allows native compaction as the emergency
466
+ // backstop (only reachable if rotation never brought context under control).
527
467
  if (!compactBlocked) {
528
468
  compactBlocked = true;
529
469
  if (
530
- !compactionTriggered &&
531
- !compactOnNextTurnEnd &&
470
+ !rotatePending &&
532
471
  !compactionInProgress &&
533
472
  (session.consecutiveRotations ?? 0) < MAX_CONSECUTIVE_ROTATIONS
534
473
  ) {
535
474
  log(
536
475
  "mind",
537
- `session "${session.name}": native compaction — warning + pinning rotation`,
476
+ `session "${session.name}": native compaction — rotation pending at turn end`,
538
477
  );
539
- warnAndPinRotation();
478
+ rotatePending = true;
540
479
  } else {
541
480
  log("mind", `session "${session.name}": blocking native compaction (rotation pending)`);
542
481
  }
@@ -595,8 +534,7 @@ export function createMind(options: {
595
534
  if (
596
535
  maxContextTokens &&
597
536
  tokens >= maxContextTokens &&
598
- !compactionTriggered &&
599
- !compactOnNextTurnEnd &&
537
+ !rotatePending &&
600
538
  !compactionInProgress &&
601
539
  (session.consecutiveRotations ?? 0) < MAX_CONSECUTIVE_ROTATIONS
602
540
  ) {
@@ -609,18 +547,18 @@ export function createMind(options: {
609
547
  }
610
548
  log(
611
549
  "mind",
612
- `session "${session.name}": ${tokens} tokens >= ${maxContextTokens} — warning + pinning rotation`,
550
+ `session "${session.name}": ${tokens} tokens >= ${maxContextTokens} — rotation pending at turn end`,
613
551
  );
614
- warnAndPinRotation();
552
+ rotatePending = true;
615
553
  }
616
554
  },
617
555
  onTurnEnd: maxContextTokens
618
556
  ? () => {
619
557
  try {
620
- // The wrap-up turn (after the warning) is done — rotate the session in
621
- // place onto the verbatim tail, honoring the pinned cut.
622
- if (compactOnNextTurnEnd) {
623
- compactOnNextTurnEnd = false;
558
+ // The turn that crossed the threshold is done — rotate the session in
559
+ // place onto the verbatim recent tail. Silent: no warning, no wrap-up.
560
+ if (rotatePending) {
561
+ rotatePending = false;
624
562
  if ((session.consecutiveRotations ?? 0) >= MAX_CONSECUTIVE_ROTATIONS) {
625
563
  // Runaway guard: rotation isn't relieving context (system prompt too
626
564
  // large to fit the tail under the threshold) — defer to the SDK.
@@ -632,11 +570,6 @@ export function createMind(options: {
632
570
  }
633
571
  return;
634
572
  }
635
- if (compactionTriggered) {
636
- compactionTriggered = false;
637
- compactOnNextTurnEnd = true;
638
- return;
639
- }
640
573
  // A healthy turn (context under the threshold) — the rotation streak,
641
574
  // if any, is over, so re-arm the self-rotation cap.
642
575
  session.consecutiveRotations = 0;
@@ -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(config);
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(".");
@@ -29,7 +31,6 @@ const mind = createMind({
29
31
  sessionsDir: resolve(".mind/pi-sessions"),
30
32
  model: config.model,
31
33
  thinkingLevel: config.thinkingLevel,
32
- compactionMessage: config.compactionMessage,
33
34
  maxContextTokens: config.compaction?.maxContextTokens,
34
35
  seedTokens: config.continuity?.seedTokens,
35
36
  subagents: config.subagents,
@@ -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
  }