volute 0.49.0 → 0.51.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 (224) hide show
  1. package/README.md +2 -2
  2. package/dist/{accept-KYNC5X3U.js → accept-5NK5XJB5.js} +2 -1
  3. package/dist/{activity-events-GJJAI2WT.js → activity-events-LQII6PZS.js} +3 -3
  4. package/dist/{ai-service-A7FS7EC3.js → ai-service-YW5SWCSB.js} +9 -4
  5. package/dist/{archive-7E3YE6VF.js → archive-FOHGXHPT.js} +3 -3
  6. package/dist/{auth-ABYLOEZ7.js → auth-X6EMNEGB.js} +10 -6
  7. package/dist/{avatar-image-6ATV2IOZ.js → avatar-image-52WGXLHV.js} +3 -3
  8. package/dist/{backup-XYAMT4CU.js → backup-YVGV2ZNM.js} +11 -12
  9. package/dist/{bridge-3FNSWLNQ.js → bridge-YK7NTRU5.js} +2 -1
  10. package/dist/{channels-SGQZHNQJ.js → channels-FSAHOQWI.js} +2 -1
  11. package/dist/{chat-OGXAEXMV.js → chat-VAZ25KVT.js} +9 -9
  12. package/dist/{chunk-P4LHVRNE.js → chunk-2RNMBXD2.js} +43 -19
  13. package/dist/{chunk-6HGEZJDH.js → chunk-5N32EN4O.js} +2 -2
  14. package/dist/{chunk-JCWB2ITW.js → chunk-5Y54AE3L.js} +35 -5
  15. package/dist/{chunk-HCYQRME2.js → chunk-6NLW72HP.js} +2 -2
  16. package/dist/chunk-7HN63Q7J.js +1317 -0
  17. package/dist/chunk-7YJY3F2R.js +38 -0
  18. package/dist/{chunk-EADX54RG.js → chunk-AI47FDGL.js} +6 -7
  19. package/dist/{chunk-IH5TKXX6.js → chunk-BSHZ23MS.js} +12 -11
  20. package/dist/{chunk-K3JOLGWF.js → chunk-CDKQJWGK.js} +76 -63
  21. package/dist/{chunk-L27YYONJ.js → chunk-CV6BNGFK.js} +3 -3
  22. package/dist/{chunk-NOIAHMJB.js → chunk-DMK5ASKV.js} +1 -1
  23. package/dist/chunk-DN3LKLYR.js +109 -0
  24. package/dist/{chunk-JBIFXB6I.js → chunk-EDR47UE2.js} +3 -5
  25. package/dist/{chunk-SHKM5RGD.js → chunk-EEOMA4DX.js} +1 -1
  26. package/dist/{chunk-YIJGKHIS.js → chunk-EGV6R6UF.js} +43 -20
  27. package/dist/{chunk-KHCKZ36O.js → chunk-FLIEXG2T.js} +1 -1
  28. package/dist/chunk-FNJCBRD3.js +31 -0
  29. package/dist/{chunk-JPGHSXSV.js → chunk-FXWBFDQR.js} +23 -9
  30. package/dist/{chunk-JCBPU57A.js → chunk-G7A5LEVI.js} +6 -6
  31. package/dist/{chunk-PY24ZL62.js → chunk-GLM7TVWP.js} +1 -1
  32. package/dist/{chunk-RSBL3FAD.js → chunk-H6X6CKDN.js} +235 -24
  33. package/dist/{chunk-4DSGN4FL.js → chunk-HKP6OAJP.js} +43 -13
  34. package/dist/{chunk-C3KLHJDB.js → chunk-HTRSSOHY.js} +1 -1
  35. package/dist/{chunk-EG5LSKHU.js → chunk-I7U5EEZC.js} +2 -2
  36. package/dist/chunk-ICL6T4SN.js +562 -0
  37. package/dist/{chunk-FBBS4BVR.js → chunk-ICXY54ZC.js} +1 -1
  38. package/dist/{chunk-DAIIMBLB.js → chunk-IWAVIJO2.js} +3 -5
  39. package/dist/{chunk-TXJL36ZQ.js → chunk-JHKBALS2.js} +2 -2
  40. package/dist/{chunk-MG3Z34YE.js → chunk-L7BKTZ5U.js} +1 -1
  41. package/dist/{chunk-SX72DOO3.js → chunk-LAUCO2HC.js} +1 -1
  42. package/dist/chunk-LNNI3MS4.js +54 -0
  43. package/dist/{chunk-BCG45AQB.js → chunk-M74I3HHF.js} +15 -5
  44. package/dist/{chunk-DVN6DNMI.js → chunk-M77B73VV.js} +17 -2
  45. package/dist/{chunk-4B62E4CG.js → chunk-OXWUEU5V.js} +2 -2
  46. package/dist/chunk-Q7XQLKZB.js +289 -0
  47. package/dist/{chunk-GWVFDUNB.js → chunk-QT4D5XCI.js} +8 -10
  48. package/dist/{chunk-VNRSXEBF.js → chunk-TPX6E6PL.js} +38 -5
  49. package/dist/{chunk-3B427GUK.js → chunk-U54PJPTB.js} +2 -2
  50. package/dist/{chunk-EFT2OALG.js → chunk-UDZLPR34.js} +20 -5
  51. package/dist/{chunk-ZX46PGCC.js → chunk-UFTDRTCP.js} +1 -1
  52. package/dist/chunk-VAEZCIJC.js +1058 -0
  53. package/dist/chunk-VP5PFNZO.js +2905 -0
  54. package/dist/{chunk-VIUH5CHQ.js → chunk-VSBMLB2W.js} +51 -34
  55. package/dist/{chunk-RRZ4RQQU.js → chunk-X67BDDZR.js} +5 -7
  56. package/dist/cli.js +34 -27
  57. package/dist/{clock-6JZL2FS6.js → clock-ZT5YT6HA.js} +4 -3
  58. package/dist/{cloud-sync-VFDTNNCA.js → cloud-sync-GPXYPTIC.js} +23 -20
  59. package/dist/{config-RPSLA2HX.js → config-UGJS57VT.js} +3 -3
  60. package/dist/{conversations-TUM5PJJ4.js → conversations-ZL6PHROD.js} +8 -4
  61. package/dist/{create-Z5FSDNVZ.js → create-G2RIKQYK.js} +2 -2
  62. package/dist/{create-VIQVP3QB.js → create-M727VUDF.js} +2 -1
  63. package/dist/{daemon-client-ZFY5MOAL.js → daemon-client-VVYFZI2D.js} +2 -1
  64. package/dist/{daemon-restart-54L4B6AQ.js → daemon-restart-2EWBRTN5.js} +6 -7
  65. package/dist/daemon.js +2005 -762
  66. package/dist/{db-F6UMFRYD.js → db-BONH4SUG.js} +2 -2
  67. package/dist/{delete-ESEP7NGQ.js → delete-O7PEFQDF.js} +1 -1
  68. package/dist/delivery-manager-HGVZSEES.js +35 -0
  69. package/dist/{delivery-router-VUOL5YSA.js → delivery-router-RHANNTAC.js} +7 -5
  70. package/dist/{down-6X7KX7BZ.js → down-QSNEMTTX.js} +5 -5
  71. package/dist/echo-text-6ANI5HHN.js +39 -0
  72. package/dist/{env-ZYMSCB7B.js → env-IBW2QI6K.js} +3 -2
  73. package/dist/{env-KUL4XKN2.js → env-SQFCWW52.js} +3 -3
  74. package/dist/{exec-FBXA3CM4.js → exec-ABD3DYA2.js} +3 -3
  75. package/dist/{export-TNBPDH77.js → export-VBNWM7I4.js} +4 -4
  76. package/dist/{extension-J4ZQ2GHV.js → extension-MRYJWSMV.js} +2 -1
  77. package/dist/{extensions-AWNO3SZF.js → extensions-NY6WST33.js} +23 -20
  78. package/dist/{files-BVBRJJ2P.js → files-BRY5RRIM.js} +5 -4
  79. package/dist/{import-ZUNKMCKC.js → import-DB2KULRO.js} +8 -8
  80. package/dist/{isolation-OPCZQRTH.js → isolation-GVV2IBAU.js} +5 -3
  81. package/dist/{join-34TR2GXQ.js → join-PYA36HEC.js} +1 -1
  82. package/dist/{list-PMUXZNAM.js → list-UQGIW7OF.js} +8 -3
  83. package/dist/{login-LKNLCJPA.js → login-RJJTCVR3.js} +2 -1
  84. package/dist/{login-KAKNFCW5.js → login-SB5OENMV.js} +4 -3
  85. package/dist/{logout-YZRMZ3O2.js → logout-BYV433X6.js} +2 -1
  86. package/dist/{logout-RLT62RQP.js → logout-YFZAJRPY.js} +4 -3
  87. package/dist/message-delivery-JHMAJ2GU.js +43 -0
  88. package/dist/migrate-thread-config-EQSEP2XG.js +90 -0
  89. package/dist/{mind-IF6CGMFW.js → mind-WF6QIXWV.js} +19 -19
  90. package/dist/{mind-activity-tracker-YMMERB6B.js → mind-activity-tracker-QF6DCX2X.js} +4 -4
  91. package/dist/{mind-contacts-BKUFPUCU.js → mind-contacts-QYU4TPEA.js} +3 -2
  92. package/dist/{mind-history-WI5OI3BW.js → mind-history-OGNR4L7W.js} +30 -7
  93. package/dist/{mind-list-OTHV4ZO4.js → mind-list-SYRA7CH5.js} +3 -2
  94. package/dist/{mind-profile-LUG353ZT.js → mind-profile-MRCVZPYT.js} +1 -1
  95. package/dist/mind-service-DZCUUC7J.js +47 -0
  96. package/dist/{mind-sleep-VBV7YRSU.js → mind-sleep-LZAFBDZD.js} +4 -3
  97. package/dist/{mind-status-BJNPGXKC.js → mind-status-Z2LQFA6F.js} +3 -2
  98. package/dist/{mind-wake-LVTZ55EZ.js → mind-wake-LDGPRA42.js} +2 -1
  99. package/dist/{package-PD67VPHB.js → package-B6O5APCZ.js} +23 -23
  100. package/dist/{prompts-MDHDLWEX.js → prompts-NBO3HRK3.js} +3 -3
  101. package/dist/{read-YSWUXYL7.js → read-RJ5NAW5J.js} +5 -14
  102. package/dist/{register-TWQDKQRT.js → register-OX4HRSGC.js} +2 -1
  103. package/dist/{registry-3ELJPUGJ.js → registry-TRC2HJXR.js} +6 -4
  104. package/dist/{reject-SXGCYN4A.js → reject-TWB4QJHG.js} +2 -1
  105. package/dist/{restart-5F5QDBOG.js → restart-AGEGNSYQ.js} +2 -1
  106. package/dist/{sandbox-XPYA7UIF.js → sandbox-NNWCTHRI.js} +7 -4
  107. package/dist/scheduler-TNBN54ZK.js +25 -0
  108. package/dist/{schema-6YONVTKZ.js → schema-ZZQVM4GR.js} +5 -3
  109. package/dist/{seed-NQED7PSX.js → seed-K2LYODIH.js} +1 -1
  110. package/dist/{seed-check-TYD7RRZY.js → seed-check-NRJEQZST.js} +14 -5
  111. package/dist/{seed-cmd-QF3F2G4S.js → seed-cmd-2EWBTR6P.js} +3 -3
  112. package/dist/{seed-create-XOO4N2OE.js → seed-create-VGTLU5FD.js} +4 -4
  113. package/dist/{seed-readiness-3LRYASA4.js → seed-readiness-DYOVCYHN.js} +4 -5
  114. package/dist/{seed-sprout-HSBBGSS2.js → seed-sprout-PILHJ3YS.js} +9 -9
  115. package/dist/{send-IHRJYNXA.js → send-2YKOZJJ5.js} +108 -54
  116. package/dist/{service-YCOPCTSZ.js → service-XQPMZAGI.js} +4 -4
  117. package/dist/{service-install-Y46OTF55.js → service-install-2EWD7KXD.js} +5 -5
  118. package/dist/{setup-F4TE64YI.js → setup-MDPSK2XG.js} +6 -7
  119. package/dist/{setup-TCN7VH5U.js → setup-WZSXHHRJ.js} +10 -3
  120. package/dist/{skill-4CYJLYVY.js → skill-B5OJEQUA.js} +3 -2
  121. package/dist/skills/dreaming/references/INSTALL.md +2 -2
  122. package/dist/skills/imagegen/SKILL.md +21 -4
  123. package/dist/skills/imagegen/scripts/imagegen.ts +124 -21
  124. package/dist/skills/tending/SKILL.md +7 -7
  125. package/dist/skills/volute-mind/SKILL.md +3 -3
  126. package/dist/skills/volute-mind/references/integrations.md +2 -2
  127. package/dist/skills/volute-mind/references/routing.md +15 -15
  128. package/dist/{skills-NN5UZCUU.js → skills-NH2GQKAF.js} +4 -5
  129. package/dist/sleep-manager-7JXEQQ2P.js +41 -0
  130. package/dist/{spirit-SSJMYC6A.js → spirit-25JVAL3M.js} +11 -8
  131. package/dist/spirit-availability-PJDWZJ3J.js +42 -0
  132. package/dist/{split-JF2VWTXA.js → split-BU5VWX6P.js} +1 -1
  133. package/dist/{sprout-AXA2SECQ.js → sprout-EXUZMIOR.js} +1 -1
  134. package/dist/{src-YH6PQ7LJ.js → src-HGNE2XG4.js} +7 -1
  135. package/dist/{src-ZN4LXMPI.js → src-LCSOQY6Z.js} +57 -51
  136. package/dist/{src-JAOVXO5W.js → src-UDMJZ6MH.js} +8 -8
  137. package/dist/{start-BTVMTP3K.js → start-MUMNFR52.js} +2 -1
  138. package/dist/{status-VJGXMQ4L.js → status-MWLE3RVC.js} +7 -6
  139. package/dist/{stop-YORESK4U.js → stop-EKERNYTX.js} +2 -1
  140. package/dist/system-events-ZPSH63QX.js +46 -0
  141. package/dist/system-install-YZSWCPDK.js +8 -0
  142. package/dist/{systems-5MH2ELQE.js → systems-VDRUZXNF.js} +5 -4
  143. package/dist/{tailscale-AMMVKDXB.js → tailscale-KHNZFC7L.js} +2 -2
  144. package/dist/{template-hash-GSOUARW4.js → template-hash-YKWMVYMM.js} +2 -2
  145. package/dist/{template-staleness-HKGCTS3H.js → template-staleness-PY2CULH6.js} +5 -5
  146. package/dist/{turn-tracker-JQ2YUUY2.js → turn-tracker-TAT2HGK6.js} +6 -6
  147. package/dist/{up-SDLTLPAO.js → up-J7XCAH7P.js} +5 -6
  148. package/dist/{update-WEA4N2TR.js → update-CIWI2JYA.js} +5 -5
  149. package/dist/{update-check-DZOTONII.js → update-check-SZ7B5IFI.js} +3 -3
  150. package/dist/{upgrade-K5FIQPQD.js → upgrade-LIE46UBB.js} +1 -1
  151. package/dist/variant-cleanup-YXN25ZVW.js +22 -0
  152. package/dist/{version-notify-WFA5XB5A.js → version-notify-EN3KJ7HH.js} +11 -28
  153. package/dist/web-assets/assets/index-BOyJ4Gqs.css +1 -0
  154. package/dist/web-assets/assets/index-CrxBwnzp.js +95 -0
  155. package/dist/web-assets/index.html +2 -2
  156. package/dist/xai-P42CRCZA.js +129 -0
  157. package/drizzle/0014_system_events.sql +26 -0
  158. package/drizzle/0015_rename_session_to_thread.sql +12 -0
  159. package/drizzle/0016_brain_to_human.sql +4 -0
  160. package/drizzle/0017_event_history_rows.sql +9 -0
  161. package/drizzle/0018_api_tokens.sql +20 -0
  162. package/drizzle/meta/_journal.json +35 -0
  163. package/package.json +23 -23
  164. package/packages/extensions/notes/dist/ui/assets/index-BBs8pyel.js +67 -0
  165. package/packages/extensions/notes/dist/ui/index.html +1 -1
  166. package/packages/extensions/pages/dist/ui/assets/index-B589e8Xe.js +2 -0
  167. package/packages/extensions/pages/dist/ui/index.html +1 -1
  168. package/packages/extensions/plan/dist/ui/assets/index-B8s8wxyt.js +67 -0
  169. package/packages/extensions/plan/dist/ui/index.html +1 -1
  170. package/templates/_base/.init/.config/prompts.json +1 -0
  171. package/templates/_base/.init/.local/hooks/startup-context.ts +24 -9
  172. package/templates/_base/{biome.json.tmpl → biome.json} +2 -1
  173. package/templates/_base/gitignore +44 -0
  174. package/templates/_base/home/.config/routes.json +2 -2
  175. package/templates/_base/home/VOLUTE.md +18 -5
  176. package/templates/_base/home/public/.gitkeep +0 -0
  177. package/templates/_base/src/lib/daemon-client.ts +1 -1
  178. package/templates/_base/src/lib/event-turn.ts +39 -0
  179. package/templates/_base/src/lib/format-prefix.ts +36 -10
  180. package/templates/_base/src/lib/router.ts +19 -10
  181. package/templates/_base/src/lib/routing.ts +8 -8
  182. package/templates/_base/src/lib/startup.ts +9 -1
  183. package/templates/_base/src/lib/types.ts +7 -1
  184. package/templates/_base/src/lib/volute-server.ts +30 -1
  185. package/templates/claude/.init/.config/routes.json +3 -3
  186. package/templates/claude/.init/CLAUDE.md +1 -1
  187. package/templates/claude/{package.json.tmpl → package.json} +4 -4
  188. package/templates/claude/src/agent.ts +4 -1
  189. package/templates/claude/src/lib/hooks/reply-instructions.ts +26 -1
  190. package/templates/claude/volute-template.json +1 -3
  191. package/templates/codex/.init/.config/routes.json +3 -3
  192. package/templates/codex/.init/AGENTS.md +1 -1
  193. package/templates/codex/{package.json.tmpl → package.json} +4 -4
  194. package/templates/codex/src/agent.ts +11 -11
  195. package/templates/codex/src/lib/turn-context.ts +51 -0
  196. package/templates/codex/volute-template.json +1 -3
  197. package/templates/pi/.init/.config/routes.json +3 -3
  198. package/templates/pi/.init/MINDS.md +1 -1
  199. package/templates/pi/{package.json.tmpl → package.json} +6 -6
  200. package/templates/pi/src/agent.ts +9 -1
  201. package/templates/pi/src/lib/event-handler.ts +1 -1
  202. package/templates/pi/src/lib/reply-instructions-extension.ts +28 -1
  203. package/templates/pi/volute-template.json +1 -3
  204. package/dist/chunk-5T5YMX6S.js +0 -23
  205. package/dist/chunk-NJHN6IRB.js +0 -177
  206. package/dist/chunk-QQBOI3QA.js +0 -5345
  207. package/dist/chunk-XTEKINWG.js +0 -158
  208. package/dist/delivery-manager-RBXBPVWM.js +0 -36
  209. package/dist/echo-text-B6NZ5FGE.js +0 -36
  210. package/dist/message-delivery-ABPV76EA.js +0 -42
  211. package/dist/mind-service-42GDO4KH.js +0 -44
  212. package/dist/notices-N6BCGHFV.js +0 -25
  213. package/dist/scheduler-K7Y7Z7FQ.js +0 -34
  214. package/dist/sleep-manager-DSPXFHGX.js +0 -38
  215. package/dist/system-chat-M2XOBKEM.js +0 -40
  216. package/dist/variant-cleanup-P4MBNLJD.js +0 -33
  217. package/dist/web-assets/assets/index-BostMFoR.css +0 -1
  218. package/dist/web-assets/assets/index-CKlqUkyG.js +0 -92
  219. package/packages/extensions/notes/dist/ui/assets/index-D0-DgYyj.js +0 -67
  220. package/packages/extensions/pages/dist/ui/assets/index-DtzftuPA.js +0 -2
  221. package/packages/extensions/plan/dist/ui/assets/index-Cfzs-0G9.js +0 -67
  222. /package/templates/_base/home/.config/{config.json.tmpl → config.json} +0 -0
  223. /package/templates/codex/home/.config/{config.json.tmpl → config.json} +0 -0
  224. /package/templates/pi/home/.config/{config.json.tmpl → config.json} +0 -0
@@ -17,21 +17,47 @@ export function compactTime(date: Date = new Date()): string {
17
17
  return `${h}:${min}`;
18
18
  }
19
19
 
20
+ /**
21
+ * Prefix for a system-event turn: `=== System event: <label> — <local time> ===`.
22
+ * Deliberately unlike every message prefix (bracketed, names a sender): an event comes
23
+ * from the environment, has no sender, and cannot be replied to. The shape must not be
24
+ * pattern-matchable to a message — a mind that reads it as one tries to reply to it.
25
+ */
26
+ export function formatEventPrefix(label: string, at: string | undefined): string {
27
+ let time = "";
28
+ if (at) {
29
+ const iso = at.includes("T") ? at : at.replace(" ", "T");
30
+ const withZ = iso.endsWith("Z") ? iso : `${iso}Z`;
31
+ const d = new Date(withZ);
32
+ time = Number.isNaN(d.getTime()) ? at : compactTimestamp(d);
33
+ } else {
34
+ time = compactTimestamp();
35
+ }
36
+ return `=== System event: ${label}${time ? ` — ${time}` : ""} ===\n`;
37
+ }
38
+
20
39
  export function formatPrefix(meta: ChannelMeta | undefined, time: string): string {
21
40
  if (!meta?.channel && !meta?.sender) return "";
22
41
  const platform = meta.platform ?? "Volute";
23
- // Build sender context (e.g., "alice in DM" or "alice in #general in My Server")
24
- let sender = meta.sender ?? "";
25
- if (meta.isDM) {
26
- sender += " in DM";
27
- } else if (meta.channelName) {
28
- sender += ` in #${meta.channelName}`;
29
- if (meta.serverName) sender += ` in ${meta.serverName}`;
42
+ // Build the subject context. With a sender it's "alice in #general in My Server"; without one
43
+ // this is a sender-less channel announcement (#687) — frame it by its channel and never invent
44
+ // a name, so automated posts (e.g. #system "X has joined") aren't attributed to anyone.
45
+ let subject: string;
46
+ if (meta.sender) {
47
+ subject = meta.sender;
48
+ if (meta.isDM) {
49
+ subject += " in DM";
50
+ } else if (meta.channelName) {
51
+ subject += ` in #${meta.channelName}`;
52
+ if (meta.serverName) subject += ` in ${meta.serverName}`;
53
+ }
54
+ } else {
55
+ subject = meta.channelName ? `#${meta.channelName}` : (meta.channel ?? "");
30
56
  }
31
- const parts = [platform, sender].filter(Boolean);
32
- // Include session name if not the default
57
+ const parts = [platform, subject].filter(Boolean);
58
+ // Include thread name if not the default
33
59
  const sessionPart =
34
- meta.sessionName && meta.sessionName !== "main" ? ` — session: ${meta.sessionName}` : "";
60
+ meta.sessionName && meta.sessionName !== "main" ? ` — thread: ${meta.sessionName}` : "";
35
61
  let prefix = parts.length > 0 ? `[${parts.join(": ")}${sessionPart} — ${time}]\n` : "";
36
62
 
37
63
  // Append participant profiles on first encounter per channel
@@ -3,6 +3,7 @@ import { resolve } from "node:path";
3
3
  import {
4
4
  compactTime,
5
5
  compactTimestamp,
6
+ formatEventPrefix,
6
7
  formatPrefix,
7
8
  formatTypingSuffix,
8
9
  } from "./format-prefix.js";
@@ -67,7 +68,10 @@ function generateMessageId(): string {
67
68
 
68
69
  function applyPrefix(content: VoluteContentPart[], meta: ChannelMeta): VoluteContentPart[] {
69
70
  const time = compactTimestamp();
70
- const prefix = formatPrefix(meta, time);
71
+ // System-event turns get the fenced `=== System event: … ===` heading — no sender/DM framing.
72
+ const prefix = meta.isEvent
73
+ ? formatEventPrefix(meta.eventLabel ?? "Event", meta.eventAt)
74
+ : formatPrefix(meta, time);
71
75
  if (!prefix) return content;
72
76
 
73
77
  const firstTextIdx = content.findIndex((p) => p.type === "text");
@@ -238,10 +242,12 @@ export function createRouter(options: {
238
242
  const multiChannel = channelCounts.size > 1;
239
243
  const body = messages
240
244
  .map((m) => {
241
- const prefix =
242
- multiChannel && m.channel
243
- ? `[${m.sender ?? "unknown"} in ${m.channel} — ${m.timestamp}]`
244
- : `[${m.sender ?? "unknown"} — ${m.timestamp}]`;
245
+ // Sender-less rows are channel announcements (#687) — frame by channel, never "unknown".
246
+ const prefix = m.sender
247
+ ? multiChannel && m.channel
248
+ ? `[${m.sender} in ${m.channel} — ${m.timestamp}]`
249
+ : `[${m.sender} — ${m.timestamp}]`
250
+ : `[${m.channel ?? "#system"} — ${m.timestamp}]`;
245
251
  return `${prefix}\n${m.text}`;
246
252
  })
247
253
  .join("\n\n");
@@ -306,7 +312,7 @@ export function createRouter(options: {
306
312
  ): { messageId: string; unsubscribe: () => void } {
307
313
  const messageId = generateMessageId();
308
314
 
309
- // Expose session to child processes (daemon-client reads this for X-Volute-Session)
315
+ // Expose session to child processes (daemon-client reads this for X-Volute-Thread)
310
316
  process.env.VOLUTE_SESSION = session;
311
317
  // Also write to file for sandbox environments where env vars don't propagate
312
318
  try {
@@ -532,7 +538,7 @@ export function createRouter(options: {
532
538
 
533
539
  const body = allMessages
534
540
  .map((m) => {
535
- const sender = m.payload.sender ?? "unknown";
541
+ const sender = m.payload.sender;
536
542
  const text =
537
543
  typeof m.payload.content === "string"
538
544
  ? m.payload.content
@@ -543,9 +549,12 @@ export function createRouter(options: {
543
549
  .join("\n")
544
550
  : JSON.stringify(m.payload.content);
545
551
  const time = compactTime();
546
- const prefix = multiChannel
547
- ? `[${sender} in ${m.channel} — ${time}]`
548
- : `[${sender} — ${time}]`;
552
+ // Sender-less rows are channel announcements (#687) — frame by channel, never "unknown".
553
+ const prefix = sender
554
+ ? multiChannel
555
+ ? `[${sender} in ${m.channel} — ${time}]`
556
+ : `[${sender} — ${time}]`
557
+ : `[${m.channel} — ${time}]`;
549
558
  return `${prefix}\n${text}`;
550
559
  })
551
560
  .join("\n\n");
@@ -8,7 +8,7 @@ export type BatchConfig = {
8
8
  };
9
9
 
10
10
  export type RoutingRule = {
11
- session?: string;
11
+ thread?: string;
12
12
  destination?: "mind" | "file";
13
13
  path?: string; // file path for file destination
14
14
  channel?: string;
@@ -35,7 +35,7 @@ export type ResolvedSessionConfig = {
35
35
 
36
36
  export type RoutingConfig = {
37
37
  rules?: RoutingRule[];
38
- sessions?: Record<string, SessionConfig>;
38
+ threads?: Record<string, SessionConfig>;
39
39
  default?: string;
40
40
  gateUnmatched?: boolean;
41
41
  };
@@ -58,7 +58,7 @@ export function normalizeBatch(batch: number | BatchConfig): BatchConfig {
58
58
  export function loadRoutingConfig(configPath: string): RoutingConfig {
59
59
  try {
60
60
  const parsed = JSON.parse(readFileSync(configPath, "utf-8"));
61
- // Normalize flat arrays (e.g. [{channel, session}, ...]) to { rules: [...] }
61
+ // Normalize flat arrays (e.g. [{channel, thread}, ...]) to { rules: [...] }
62
62
  if (Array.isArray(parsed)) return { rules: parsed };
63
63
  return parsed;
64
64
  } catch (err: any) {
@@ -80,7 +80,7 @@ function globMatch(pattern: string, value: string): boolean {
80
80
  }
81
81
 
82
82
  const GLOB_MATCH_KEYS = new Set(["channel", "sender"]);
83
- const NON_MATCH_KEYS = new Set(["session", "destination", "path", "mode", "batch"]);
83
+ const NON_MATCH_KEYS = new Set(["thread", "destination", "path", "mode", "batch"]);
84
84
 
85
85
  type MatchMeta = { channel?: string; sender?: string; isDM?: boolean; participantCount?: number };
86
86
 
@@ -138,7 +138,7 @@ export function resolveRoute(config: RoutingConfig, meta: MatchMeta): ResolvedRo
138
138
  }
139
139
  return {
140
140
  destination: "mind",
141
- session: sanitizeSessionName(expandTemplate(rule.session ?? fallback, meta)),
141
+ session: sanitizeSessionName(expandTemplate(rule.thread ?? fallback, meta)),
142
142
  matched: true,
143
143
  mode: rule.mode,
144
144
  };
@@ -149,7 +149,7 @@ export function resolveRoute(config: RoutingConfig, meta: MatchMeta): ResolvedRo
149
149
  }
150
150
 
151
151
  /**
152
- * Resolve session config by matching session name against glob-pattern keys in config.sessions.
152
+ * Resolve thread config by matching thread name against glob-pattern keys in config.threads.
153
153
  * First match wins. Returns defaults if no match.
154
154
  */
155
155
  export function resolveSessionConfig(
@@ -158,9 +158,9 @@ export function resolveSessionConfig(
158
158
  ): ResolvedSessionConfig {
159
159
  const defaults: ResolvedSessionConfig = { interrupt: true, replyInstructions: "once" };
160
160
 
161
- if (!config.sessions) return defaults;
161
+ if (!config.threads) return defaults;
162
162
 
163
- for (const [pattern, sessionConfig] of Object.entries(config.sessions)) {
163
+ for (const [pattern, sessionConfig] of Object.entries(config.threads)) {
164
164
  if (globMatch(pattern, sessionName)) {
165
165
  const batch = sessionConfig.batch != null ? normalizeBatch(sessionConfig.batch) : undefined;
166
166
  return {
@@ -150,10 +150,16 @@ export type MindPrompts = {
150
150
  compaction_warning: string;
151
151
  compaction_instructions: string;
152
152
  reply_instructions: string;
153
+ event_instructions: string;
153
154
  channel_invite: string;
154
155
  };
155
156
 
156
- const DEFAULT_PROMPTS: MindPrompts = {
157
+ /**
158
+ * What a mind falls back to when its prompts.json lacks a key — i.e. what every new mind is
159
+ * actually given. Must stay in sync with PROMPT_DEFAULTS in packages/daemon/src/lib/prompts.ts
160
+ * (the copy the Prompt Library UI edits); test/prompts.test.ts fails if they drift.
161
+ */
162
+ export const DEFAULT_PROMPTS: MindPrompts = {
157
163
  compaction_warning:
158
164
  // biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${date} prompt template
159
165
  "Compaction approaching — this conversation will be summarized soon. Take a moment to save anything important to your files (MEMORY.md, memory/journal/${date}.md) so it's preserved. Focus on decisions made, open threads, and anything you'd want to pick up again.",
@@ -161,6 +167,8 @@ const DEFAULT_PROMPTS: MindPrompts = {
161
167
  "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.",
162
168
  // biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${channel} prompt template
163
169
  reply_instructions: 'To reply to this message, use: volute chat send ${channel} "your message"',
170
+ event_instructions:
171
+ "This is a system event from your environment — not a message from anyone, and nothing awaits a reply. If it calls for action, use your normal channels. Your closing thoughts on an event turn are kept as a private reflection in your history.",
164
172
  channel_invite: `[Channel Invite]
165
173
  \${headers}
166
174
 
@@ -4,7 +4,7 @@ export type VoluteContentPart =
4
4
 
5
5
  export type ParticipantProfile = {
6
6
  username: string;
7
- userType: "brain" | "mind";
7
+ userType: "human" | "mind";
8
8
  displayName?: string | null;
9
9
  description?: string | null;
10
10
  };
@@ -14,6 +14,12 @@ export type ChannelMeta = {
14
14
  sender?: string;
15
15
  platform?: string;
16
16
  isDM?: boolean;
17
+ /** True when this turn was triggered by a system event (not a person). */
18
+ isEvent?: boolean;
19
+ /** Worded label for an event, e.g. "Schedule: morning-check". */
20
+ eventLabel?: string;
21
+ /** The event's timestamp (UTC) for the envelope prefix. */
22
+ eventAt?: string;
17
23
  channelName?: string;
18
24
  serverName?: string;
19
25
  sessionName?: string;
@@ -187,7 +187,36 @@ export function createVoluteServer(options: {
187
187
 
188
188
  if (req.method === "POST" && url.pathname === "/message") {
189
189
  try {
190
- const body = JSON.parse(await readBody(req)) as VoluteRequest;
190
+ const raw = JSON.parse(await readBody(req)) as
191
+ | VoluteRequest
192
+ | {
193
+ kind: "event";
194
+ event: { id: number; type: string; label: string; body: string; at: string };
195
+ session?: string;
196
+ };
197
+
198
+ // System-event envelope: ambient, sender-less. Dispatch with event framing.
199
+ // The channel slug (`event:<type>:<id>`) is echoed back on this turn's stream
200
+ // events so the daemon can link the turn to its triggering event (reflection
201
+ // capture). The `event: true` ack marker tells the daemon this template
202
+ // understands events (a pre-events template 200-acks without it).
203
+ if ((raw as { kind?: string }).kind === "event") {
204
+ const { event, session } = raw as {
205
+ event: { id: number; type: string; label: string; body: string; at: string };
206
+ session?: string;
207
+ };
208
+ router.dispatch([{ type: "text", text: event.body }], session ?? "main", {
209
+ isEvent: true,
210
+ eventLabel: event.label,
211
+ eventAt: event.at,
212
+ channel: `event:${event.type}:${event.id}`,
213
+ });
214
+ res.writeHead(200, { "Content-Type": "application/json" });
215
+ res.end(JSON.stringify({ ok: true, event: true }));
216
+ return;
217
+ }
218
+
219
+ const body = raw as VoluteRequest;
191
220
 
192
221
  // Strip any sender-provided verified field to prevent spoofing
193
222
  delete body.verified;
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "gateUnmatched": true,
3
3
  "rules": [
4
- { "channel": "*", "isDM": true, "session": "${channel}" },
5
- { "channel": "#*", "session": "${channel}" }
4
+ { "channel": "*", "isDM": true, "thread": "${channel}" },
5
+ { "channel": "#*", "thread": "${channel}" }
6
6
  ],
7
- "sessions": {
7
+ "threads": {
8
8
  "#*": { "batch": { "debounce": 20, "maxWait": 120, "triggers": ["@{{name}}"] } }
9
9
  },
10
10
  "default": "main"
@@ -20,7 +20,7 @@ These files shape your starting identity. They're loaded into your system prompt
20
20
 
21
21
  **Editing any identity file triggers an automatic restart** — your server restarts so the updated file takes effect. Your session resumes automatically.
22
22
 
23
- You may have **multiple named sessions** — each maintains its own conversation history. See `VOLUTE.md` for how to configure session routing via `.config/routes.json`. Your conversation may be **resumed** from a previous session — orient yourself by reading recent journal entries if needed. On a **fresh session**, read `MEMORY.md` and recent journal entries to remember where you left off. On **compaction**, update today's journal to preserve context before the conversation is trimmed.
23
+ You may have **multiple named threads** — each maintains its own conversation history. See `VOLUTE.md` for how to configure thread routing via `.config/routes.json`. Your conversation may be **resumed** from a previous session — orient yourself by reading recent journal entries if needed. On a **fresh session**, read `MEMORY.md` and recent journal entries to remember where you left off. On **compaction**, update today's journal to preserve context before the conversation is trimmed.
24
24
 
25
25
  ## Memory System
26
26
 
@@ -9,13 +9,13 @@
9
9
  "typecheck": "tsc --noEmit"
10
10
  },
11
11
  "dependencies": {
12
- "@anthropic-ai/claude-agent-sdk": "^0.3.197",
12
+ "@anthropic-ai/claude-agent-sdk": "^0.3.207",
13
13
  "@anthropic-ai/tokenizer": "^0.0.4",
14
- "tsx": "^4.22.4"
14
+ "tsx": "^4.23.1"
15
15
  },
16
16
  "devDependencies": {
17
- "@biomejs/biome": "2.5.0",
18
- "@types/node": "^26.0.0",
17
+ "@biomejs/biome": "2.5.3",
18
+ "@types/node": "^26.1.1",
19
19
  "typescript": "^6.0.3"
20
20
  }
21
21
  }
@@ -51,6 +51,8 @@ type Session = {
51
51
  messageChannels: Map<string, { channel: string; sender?: string }>;
52
52
  replyInstructionsFired: boolean;
53
53
  replyInstructionsMode: "once" | "always" | "never";
54
+ /** The event note is a standing fact about events, so it fires once per session. */
55
+ eventNoteFired: boolean;
54
56
  contextTokens: number;
55
57
  /** Last inbound message or completed turn — drives idle reaping. */
56
58
  lastActivityAt: number;
@@ -150,7 +152,7 @@ export function createMind(options: {
150
152
  // The CLI only discovers skills through its user-scope ~/.claude/skills scan,
151
153
  // so the "user" setting source is load-bearing and HOME must be the mind's
152
154
  // home dir for the SDK subprocess (isolation modes already set it; pinning it
153
- // here keeps non-isolated minds from loading the operator's ~/.claude). The
155
+ // here keeps non-isolated minds from loading the host's ~/.claude). The
154
156
  // explicit skills array grants the Skill tool for each installed skill — the
155
157
  // SDK silently drops the documented `skills: 'all'` string form.
156
158
  const mindHome = resolvePath(options.cwd);
@@ -526,6 +528,7 @@ export function createMind(options: {
526
528
  messageChannels: new Map(),
527
529
  replyInstructionsFired: false,
528
530
  replyInstructionsMode: "once",
531
+ eventNoteFired: false,
529
532
  contextTokens: 0,
530
533
  lastActivityAt: Date.now(),
531
534
  };
@@ -1,4 +1,5 @@
1
1
  import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
2
+ import { firstReplyableEntry, isEventTurn } from "../event-turn.js";
2
3
  import { loadPrompts } from "../startup.js";
3
4
 
4
5
  export function createReplyInstructionsHook(
@@ -6,11 +7,33 @@ export function createReplyInstructionsHook(
6
7
  sessionState: {
7
8
  replyInstructionsFired: boolean;
8
9
  replyInstructionsMode: "once" | "always" | "never";
10
+ eventNoteFired: boolean;
9
11
  },
10
12
  ) {
11
13
  const prompts = loadPrompts();
12
14
 
13
15
  const hook: HookCallback = async () => {
16
+ // Event-ness is derived from the pending messages themselves, never from a
17
+ // session-level "the current turn is an event" flag: `/message` returns before the turn
18
+ // runs, so two messages can queue before the SDK fires this hook for the first, and a
19
+ // last-write-wins flag would then describe the wrong message. `messageChannels` is
20
+ // keyed by messageId and pruned per turn, so it always reflects what's actually pending.
21
+ const entries = [...messageChannels.values()];
22
+
23
+ // System-event turn: ambient, from the environment, not a person, with no channel to
24
+ // reply to. Never emit reply instructions. The note states a standing fact about events
25
+ // (also documented in VOLUTE.md), so it fires once per session, not on every event.
26
+ if (isEventTurn(entries)) {
27
+ if (sessionState.eventNoteFired) return {};
28
+ sessionState.eventNoteFired = true;
29
+ return {
30
+ hookSpecificOutput: {
31
+ hookEventName: "UserPromptSubmit" as const,
32
+ additionalContext: prompts.event_instructions,
33
+ },
34
+ };
35
+ }
36
+
14
37
  // "never" suppresses reply instructions entirely
15
38
  if (sessionState.replyInstructionsMode === "never") return {};
16
39
 
@@ -18,7 +41,9 @@ export function createReplyInstructionsHook(
18
41
  if (sessionState.replyInstructionsMode === "once" && sessionState.replyInstructionsFired)
19
42
  return {};
20
43
 
21
- const entry = messageChannels.values().next().value;
44
+ // Reply instructions must name a channel the mind can actually send to — never an
45
+ // event channel (see event-turn.ts).
46
+ const entry = firstReplyableEntry(entries);
22
47
  if (!entry) return {};
23
48
 
24
49
  sessionState.replyInstructionsFired = true;
@@ -1,8 +1,6 @@
1
1
  {
2
2
  "rename": {
3
- "package.json.tmpl": "package.json",
4
- "biome.json.tmpl": "biome.json",
5
- "home/.config/config.json.tmpl": "home/.config/config.json"
3
+ "gitignore": ".gitignore"
6
4
  },
7
5
  "substitute": ["package.json", ".init/SOUL.md", "home/.config/routes.json"]
8
6
  }
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "gateUnmatched": true,
3
3
  "rules": [
4
- { "channel": "*", "isDM": true, "session": "${channel}" },
5
- { "channel": "#*", "session": "${channel}" }
4
+ { "channel": "*", "isDM": true, "thread": "${channel}" },
5
+ { "channel": "#*", "thread": "${channel}" }
6
6
  ],
7
- "sessions": {
7
+ "threads": {
8
8
  "#*": { "batch": { "debounce": 20, "maxWait": 120, "triggers": ["@{{name}}"] } }
9
9
  },
10
10
  "default": "main"
@@ -25,7 +25,7 @@ See the **memory** skill for detailed guidance.
25
25
 
26
26
  ## Sessions
27
27
 
28
- - You may have **multiple named sessions** — each maintains its own conversation history. See `VOLUTE.md` for how to configure session routing via `.config/routes.json`.
28
+ - You may have **multiple named threads** — each maintains its own conversation history. See `VOLUTE.md` for how to configure thread routing via `.config/routes.json`.
29
29
  - Your conversation may be **resumed** from a previous session — orient yourself by reading recent journal entries if needed.
30
30
  - On a **fresh session**, read `MEMORY.md` and recent journal entries to remember where you left off.
31
31
  - On **compaction**, update today's journal to preserve context before the conversation is trimmed.
@@ -10,12 +10,12 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@anthropic-ai/tokenizer": "^0.0.4",
13
- "@openai/codex-sdk": "^0.142.5",
14
- "tsx": "^4.22.4"
13
+ "@openai/codex-sdk": "^0.144.3",
14
+ "tsx": "^4.23.1"
15
15
  },
16
16
  "devDependencies": {
17
- "@biomejs/biome": "2.5.0",
18
- "@types/node": "^26.0.0",
17
+ "@biomejs/biome": "2.5.3",
18
+ "@types/node": "^26.1.1",
19
19
  "typescript": "^6.0.3"
20
20
  }
21
21
  }
@@ -19,6 +19,7 @@ import { log, warn } from "./lib/logger.js";
19
19
  import { createSessionStore } from "./lib/session-store.js";
20
20
  import { getStartupContext, loadPrompts, loadSystemPrompt } from "./lib/startup.js";
21
21
  import { filterEvent, loadTransparencyPreset } from "./lib/transparency.js";
22
+ import { turnContextFor } from "./lib/turn-context.js";
22
23
  import type {
23
24
  HandlerMeta,
24
25
  HandlerResolver,
@@ -47,6 +48,8 @@ type CodexSession = {
47
48
  abortController?: AbortController;
48
49
  messageChannels: Map<string, string>;
49
50
  firstMessagePerChannel: Set<string>;
51
+ /** The event note is a standing fact about events, so it fires once per session. */
52
+ eventNoteFired: boolean;
50
53
  cumulativeInputTokens: number;
51
54
  };
52
55
 
@@ -145,6 +148,7 @@ export function createMind(options: {
145
148
  processing: false,
146
149
  messageChannels: new Map(),
147
150
  firstMessagePerChannel: new Set(),
151
+ eventNoteFired: false,
148
152
  cumulativeInputTokens: 0,
149
153
  };
150
154
  sessions.set(name, session);
@@ -255,20 +259,16 @@ export function createMind(options: {
255
259
  warn("mind", "pre-prompt hook failed:", err);
256
260
  }
257
261
 
258
- // Reply instructions on first message per channel (skip system messages)
259
- const channel = meta.channel;
260
- if (channel && !session.firstMessagePerChannel.has(channel)) {
261
- session.firstMessagePerChannel.add(channel);
262
- const isSystem = meta.sender === "volute";
263
- const replyInstructions = isSystem
264
- ? "This is a system message — no reply is needed."
265
- : prompts.reply_instructions.replace(/\$\{channel\}/g, channel);
262
+ // Either the system-event note or reply instructions never both, and never reply
263
+ // instructions on an event turn. See turn-context.ts for the rule and why it matters.
264
+ const turnContext = turnContextFor(meta, session, prompts);
265
+ if (turnContext) {
266
266
  emit(session, {
267
267
  type: "context",
268
- content: replyInstructions,
269
- metadata: { source: "reply-instructions" },
268
+ content: turnContext.content,
269
+ metadata: { source: turnContext.source },
270
270
  });
271
- text = `${replyInstructions}\n\n${text}`;
271
+ text = `${turnContext.content}\n\n${text}`;
272
272
  }
273
273
 
274
274
  session.abortController = new AbortController();
@@ -0,0 +1,51 @@
1
+ import { isEventChannel } from "./event-turn.js";
2
+ import type { MindPrompts } from "./startup.js";
3
+ import type { ChannelMeta } from "./types.js";
4
+
5
+ /** Context the template prepends to a turn: either the event note or reply instructions. */
6
+ export type TurnContext = {
7
+ content: string;
8
+ source: "event-instructions" | "reply-instructions";
9
+ };
10
+
11
+ /** The mutable per-session state this decision reads and updates. */
12
+ export type TurnContextState = {
13
+ eventNoteFired: boolean;
14
+ firstMessagePerChannel: Set<string>;
15
+ };
16
+
17
+ /**
18
+ * Decide what context (if any) to prepend to an incoming turn. Returns null for "nothing".
19
+ *
20
+ * Pulled out of agent.ts so it can actually be tested: this is the rule that keeps a system
21
+ * event from looking like a message. Reply instructions must never fire on an event turn —
22
+ * naming the event's synthetic channel tells the mind to `volute chat send event:...`, i.e.
23
+ * to reply to its own environment. The send is rejected, and the mind is left puzzling over
24
+ * a message nobody sent (observed: the seed "lucy" spent her first turn on exactly this).
25
+ *
26
+ * The event note fires once per session, not once per event: it states a standing fact about
27
+ * events (also in VOLUTE.md), and it is deliberately NOT keyed on `firstMessagePerChannel`,
28
+ * whose key is a distinct channel per event id and so would fire on every single event.
29
+ */
30
+ export function turnContextFor(
31
+ meta: ChannelMeta,
32
+ session: TurnContextState,
33
+ prompts: MindPrompts,
34
+ ): TurnContext | null {
35
+ const channel = meta.channel;
36
+
37
+ if (meta.isEvent || isEventChannel(channel)) {
38
+ if (session.eventNoteFired) return null;
39
+ session.eventNoteFired = true;
40
+ return { content: prompts.event_instructions, source: "event-instructions" };
41
+ }
42
+
43
+ if (!channel || session.firstMessagePerChannel.has(channel)) return null;
44
+ session.firstMessagePerChannel.add(channel);
45
+
46
+ const content =
47
+ meta.sender === "volute"
48
+ ? "This is a system message — no reply is needed."
49
+ : prompts.reply_instructions.replace(/\$\{channel\}/g, channel);
50
+ return { content, source: "reply-instructions" };
51
+ }
@@ -1,8 +1,6 @@
1
1
  {
2
2
  "rename": {
3
- "package.json.tmpl": "package.json",
4
- "biome.json.tmpl": "biome.json",
5
- "home/.config/config.json.tmpl": "home/.config/config.json"
3
+ "gitignore": ".gitignore"
6
4
  },
7
5
  "substitute": ["package.json", ".init/SOUL.md", "home/.config/routes.json"]
8
6
  }
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "gateUnmatched": true,
3
3
  "rules": [
4
- { "channel": "*", "isDM": true, "session": "${channel}" },
5
- { "channel": "#*", "session": "${channel}" }
4
+ { "channel": "*", "isDM": true, "thread": "${channel}" },
5
+ { "channel": "#*", "thread": "${channel}" }
6
6
  ],
7
- "sessions": {
7
+ "threads": {
8
8
  "#*": { "batch": { "debounce": 20, "maxWait": 120, "triggers": ["@{{name}}"] } }
9
9
  },
10
10
  "default": "main"
@@ -25,7 +25,7 @@ See the **memory** skill for detailed guidance.
25
25
 
26
26
  ## Sessions
27
27
 
28
- - You may have **multiple named sessions** — each maintains its own conversation history. See `VOLUTE.md` for how to configure session routing via `.config/routes.json`.
28
+ - You may have **multiple named threads** — each maintains its own conversation history. See `VOLUTE.md` for how to configure thread routing via `.config/routes.json`.
29
29
  - Your conversation may be **resumed** from a previous session — orient yourself by reading recent journal entries if needed.
30
30
  - On a **fresh session**, read `MEMORY.md` and recent journal entries to remember where you left off.
31
31
  - On **compaction**, update today's journal to preserve context before the conversation is trimmed.
@@ -10,14 +10,14 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@anthropic-ai/tokenizer": "^0.0.4",
13
- "@earendil-works/pi-ai": "^0.80.3",
14
- "@earendil-works/pi-coding-agent": "^0.80.3",
15
- "@sinclair/typebox": "^0.34.49",
16
- "tsx": "^4.22.4"
13
+ "@earendil-works/pi-ai": "0.80.6",
14
+ "@earendil-works/pi-coding-agent": "0.80.6",
15
+ "@sinclair/typebox": "^0.34.52",
16
+ "tsx": "^4.23.1"
17
17
  },
18
18
  "devDependencies": {
19
- "@biomejs/biome": "2.5.0",
20
- "@types/node": "^26.0.0",
19
+ "@biomejs/biome": "2.5.3",
20
+ "@types/node": "^26.1.1",
21
21
  "typescript": "^6.0.3"
22
22
  }
23
23
  }
@@ -83,9 +83,17 @@ export function createMind(options: {
83
83
 
84
84
  // Shared setup (created once)
85
85
  const modelStr = options.model || process.env.PI_MODEL || "anthropic:claude-sonnet-4-20250514";
86
- const model = resolveModel(modelStr);
87
86
  const authStorage = AuthStorage.create();
88
87
  const modelRegistry = ModelRegistry.create(authStorage);
88
+ // Prefer the registry's model: for OAuth providers with a per-credential baseUrl
89
+ // (e.g. GitHub Copilot, whose token is pinned to an individual/business/enterprise
90
+ // proxy host) it has already run the provider's modifyModels() patch. The static
91
+ // catalog lookup in resolveModel() has no way to apply that and would send every
92
+ // request to the wrong host, which the provider's API rejects (421 Misdirected
93
+ // Request for Copilot). Fall back to resolveModel() for ids the registry doesn't
94
+ // carry (e.g. an admin-registered custom id not yet in the built-in catalog).
95
+ const [modelProvider, ...modelRest] = modelStr.split(":");
96
+ const model = modelRegistry.find(modelProvider, modelRest.join(":")) ?? resolveModel(modelStr);
89
97
 
90
98
  // The daemon centrally refreshes the system OAuth token and rewrites auth.json
91
99
  // with the fresh token. Watch the file and reload so a running mind adopts the