volute 0.48.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -328
- package/dist/{accept-KYNC5X3U.js → accept-5NK5XJB5.js} +2 -1
- package/dist/{activity-events-MIOK3IOU.js → activity-events-H7YI3BTF.js} +4 -4
- package/dist/{ai-service-643TNFPH.js → ai-service-V4CTNGI3.js} +6 -5
- package/dist/{archive-6Z4GFQPH.js → archive-ORM3ENHI.js} +3 -3
- package/dist/{auth-A4VGHADI.js → auth-DT7GY7EZ.js} +11 -7
- package/dist/{avatar-image-Y45R7LW5.js → avatar-image-QEPAP3A5.js} +4 -4
- package/dist/{backup-UDH4GMSW.js → backup-IE6PMXJO.js} +17 -18
- package/dist/{bridge-3FNSWLNQ.js → bridge-YK7NTRU5.js} +2 -1
- package/dist/{channels-SGQZHNQJ.js → channels-FSAHOQWI.js} +2 -1
- package/dist/{chat-X43VYHH2.js → chat-W5K6HCBE.js} +9 -9
- package/dist/{chunk-LTLFOSA4.js → chunk-3BAFMGCJ.js} +3 -3
- package/dist/{chunk-XVBSFLM7.js → chunk-4CN7OCJE.js} +2 -2
- package/dist/{chunk-HPGN6XRT.js → chunk-574M6I7F.js} +39 -6
- package/dist/{chunk-FJ2MZ5PH.js → chunk-5FVANDON.js} +2 -2
- package/dist/{chunk-2MJ7EOMT.js → chunk-5IIVOWSP.js} +28 -19
- package/dist/{chunk-4FCL4DVZ.js → chunk-5PKFJHCR.js} +2 -2
- package/dist/{chunk-MOERAQXH.js → chunk-5TPC4KCV.js} +38 -31
- package/dist/{chunk-YNBXEQAP.js → chunk-632FJ23O.js} +24 -20
- package/dist/{chunk-T4VS7UDW.js → chunk-636GFQNE.js} +12 -12
- package/dist/{chunk-EIQDZC43.js → chunk-6BM2I736.js} +25 -6
- package/dist/{chunk-WARH6GIV.js → chunk-74T3CEXK.js} +17 -15
- package/dist/{chunk-OQJMTHEH.js → chunk-77IZUVBB.js} +2 -2
- package/dist/{chunk-USD45JKR.js → chunk-7DXLIYPY.js} +77 -64
- package/dist/chunk-7FDWPQCU.js +289 -0
- package/dist/chunk-7OZWKHA3.js +548 -0
- package/dist/{chunk-IJPMIEVE.js → chunk-7UWXAJMK.js} +1 -1
- package/dist/{chunk-SZJCJVXV.js → chunk-7XDOTN4R.js} +2 -2
- package/dist/{chunk-YKA7DCVW.js → chunk-AFSLNUTT.js} +1 -1
- package/dist/{chunk-DGGZVZTM.js → chunk-AJWFDIEY.js} +17 -2
- package/dist/chunk-ALIVXDM4.js +1014 -0
- package/dist/chunk-CAIYV33P.js +523 -0
- package/dist/{chunk-MTRMQFCQ.js → chunk-E5S3ILVA.js} +2 -2
- package/dist/{chunk-SHKM5RGD.js → chunk-EEOMA4DX.js} +1 -1
- package/dist/chunk-EQKSRVDE.js +2857 -0
- package/dist/chunk-FNJCBRD3.js +31 -0
- package/dist/chunk-HW6Q7AMB.js +113 -0
- package/dist/chunk-JI67D7O2.js +1222 -0
- package/dist/{chunk-3YUBFTDN.js → chunk-KAEZOVP6.js} +1 -1
- package/dist/chunk-LNNI3MS4.js +54 -0
- package/dist/{chunk-BCG45AQB.js → chunk-M74I3HHF.js} +15 -5
- package/dist/chunk-MLZBBLMT.js +109 -0
- package/dist/{chunk-5T5YMX6S.js → chunk-PW65BXY5.js} +5 -0
- package/dist/{chunk-2OIYNHWO.js → chunk-QFVGPKJI.js} +1 -1
- package/dist/{chunk-T2TP6ZC6.js → chunk-QUSMYGNQ.js} +6 -0
- package/dist/{chunk-HNGFKPNS.js → chunk-SFRYRW37.js} +85 -36
- package/dist/{chunk-7KRIHYLR.js → chunk-SSNZ6ZNN.js} +2 -2
- package/dist/{chunk-HUNDXZBZ.js → chunk-TGJHGVBZ.js} +48 -19
- package/dist/{chunk-PJVPDY2Z.js → chunk-U3EJJ7JN.js} +2 -2
- package/dist/{chunk-ODDGGLM6.js → chunk-UC6BNMGT.js} +43 -13
- package/dist/{chunk-Q7HOAVFR.js → chunk-WBBKYUGF.js} +4 -6
- package/dist/{chunk-WQ46E33M.js → chunk-X67VT4QO.js} +2 -2
- package/dist/{chunk-MJRXFUMS.js → chunk-YQOYZ6G7.js} +3 -5
- package/dist/{chunk-VZRA5TYO.js → chunk-ZUG76D3P.js} +36 -6
- package/dist/cli.js +34 -27
- package/dist/{clock-CNRF6F7T.js → clock-LH7UFNLJ.js} +5 -4
- package/dist/{cloud-sync-WHKCB4CD.js → cloud-sync-IDJFVDEP.js} +23 -20
- package/dist/{config-RPSLA2HX.js → config-UGJS57VT.js} +3 -3
- package/dist/{conversations-C7IKYSHD.js → conversations-S2232Y5I.js} +9 -5
- package/dist/{create-GJKHABJB.js → create-2M7DAHQX.js} +2 -2
- package/dist/{create-VIQVP3QB.js → create-M727VUDF.js} +2 -1
- package/dist/{daemon-client-ZFY5MOAL.js → daemon-client-VVYFZI2D.js} +2 -1
- package/dist/{daemon-restart-7KUHVZIN.js → daemon-restart-2AAGYFHE.js} +6 -7
- package/dist/daemon.js +2166 -833
- package/dist/{db-FO6LXYCW.js → db-QAY45Z56.js} +2 -2
- package/dist/{delete-ESEP7NGQ.js → delete-O7PEFQDF.js} +1 -1
- package/dist/delivery-manager-B4OKWD2P.js +33 -0
- package/dist/{delivery-router-3QFG2ZEX.js → delivery-router-JE7SHYNU.js} +10 -6
- package/dist/{down-G6IADJXV.js → down-D4IRNEMJ.js} +5 -5
- package/dist/echo-text-WYAAODBV.js +39 -0
- package/dist/{env-C7Z55QCJ.js → env-ABM5IMA7.js} +8 -7
- package/dist/{env-6TGPO5WG.js → env-AZK5X4W7.js} +3 -3
- package/dist/{exec-QIFYZG4C.js → exec-2H65LMTA.js} +3 -3
- package/dist/{export-C2K3NIRD.js → export-3E3EL6W3.js} +4 -4
- package/dist/{extension-J4ZQ2GHV.js → extension-MRYJWSMV.js} +2 -1
- package/dist/{extensions-GEKF47A4.js → extensions-7KP3X6G7.js} +23 -20
- package/dist/{files-LXQSQOFG.js → files-6BMKVG5G.js} +5 -4
- package/dist/{import-SX3ZXHB4.js → import-UPNTEBBT.js} +9 -9
- package/dist/{isolation-MCT4ZYLT.js → isolation-OKDFM6WA.js} +5 -3
- package/dist/{join-34TR2GXQ.js → join-PYA36HEC.js} +1 -1
- package/dist/{list-PMUXZNAM.js → list-4BVGH7AN.js} +8 -3
- package/dist/{login-CL4IT2ED.js → login-MOXVLULE.js} +7 -6
- package/dist/{login-ARRZ4ORJ.js → login-RJJTCVR3.js} +4 -3
- package/dist/{logout-YZRMZ3O2.js → logout-BYV433X6.js} +2 -1
- package/dist/{logout-HZYJCYCA.js → logout-DWD3OQVT.js} +4 -3
- package/dist/message-delivery-3456PXB7.js +43 -0
- package/dist/migrate-thread-config-4A5LW36Z.js +90 -0
- package/dist/{mind-46AOK373.js → mind-NYRALSV2.js} +19 -19
- package/dist/{mind-activity-tracker-YVZPC4A5.js → mind-activity-tracker-L7GCYXQR.js} +5 -5
- package/dist/{mind-contacts-GV7XYL5D.js → mind-contacts-QYU4TPEA.js} +8 -7
- package/dist/{mind-history-TTGA5BHZ.js → mind-history-NXXOOTX6.js} +34 -11
- package/dist/{mind-list-OTHV4ZO4.js → mind-list-SYRA7CH5.js} +3 -2
- package/dist/{mind-profile-LUG353ZT.js → mind-profile-MRCVZPYT.js} +1 -1
- package/dist/mind-service-5TA7UJAT.js +47 -0
- package/dist/{mind-sleep-DV7KP255.js → mind-sleep-TH6BRQ5X.js} +5 -4
- package/dist/{mind-status-BJNPGXKC.js → mind-status-Z2LQFA6F.js} +3 -2
- package/dist/{mind-wake-LVTZ55EZ.js → mind-wake-LDGPRA42.js} +2 -1
- package/dist/{package-RTN2PS7O.js → package-XYSV7NUT.js} +27 -26
- package/dist/prompts-QKFPX6DQ.js +24 -0
- package/dist/{read-YSWUXYL7.js → read-ZXGUT4QC.js} +5 -3
- package/dist/{register-42VBJKTP.js → register-OX4HRSGC.js} +4 -3
- package/dist/{registry-UMMDRPRM.js → registry-4UF6OCOE.js} +10 -4
- package/dist/{reject-SXGCYN4A.js → reject-TWB4QJHG.js} +2 -1
- package/dist/{restart-YYEGZJDD.js → restart-AGEGNSYQ.js} +6 -5
- package/dist/{sandbox-5VQ454XP.js → sandbox-EGEU5GGE.js} +8 -5
- package/dist/scheduler-UV6RUJ7P.js +25 -0
- package/dist/{schema-VEVO7HG6.js → schema-C2GIPD4V.js} +3 -3
- package/dist/{seed-F2HXZQU4.js → seed-3IMBOAGK.js} +1 -1
- package/dist/{seed-check-TYD7RRZY.js → seed-check-NRJEQZST.js} +14 -5
- package/dist/{seed-cmd-MLCTGSX5.js → seed-cmd-U73XVQMI.js} +3 -3
- package/dist/{seed-create-KQ7OILWB.js → seed-create-FBLNVSUP.js} +4 -4
- package/dist/{seed-readiness-RBPGOLSZ.js → seed-readiness-QRA46ZJQ.js} +4 -5
- package/dist/{seed-sprout-HVYCFJLA.js → seed-sprout-3B4SZX7J.js} +12 -12
- package/dist/{send-TD2X5U3K.js → send-NBRZJXKX.js} +34 -12
- package/dist/{service-CVZMQYK6.js → service-XMXAXGJD.js} +4 -4
- package/dist/{service-install-VCRMFWX5.js → service-install-MOKTEG7B.js} +5 -5
- package/dist/{setup-5ECWTYKG.js → setup-CAJFOR2N.js} +12 -3
- package/dist/{setup-AE4CVUC4.js → setup-JGYAXXW7.js} +6 -7
- package/dist/{skill-UUM4JZRF.js → skill-NXIHDQCG.js} +7 -6
- package/dist/skills/dreaming/references/INSTALL.md +2 -2
- package/dist/skills/imagegen/SKILL.md +21 -4
- package/dist/skills/imagegen/scripts/imagegen.ts +124 -21
- package/dist/skills/tending/SKILL.md +7 -7
- package/dist/skills/volute-mind/SKILL.md +6 -6
- package/dist/skills/volute-mind/references/integrations.md +2 -2
- package/dist/skills/volute-mind/references/routing.md +19 -18
- package/dist/skills/volute-mind/references/variants.md +48 -14
- package/dist/{skills-B3SVDCEB.js → skills-5MSMT7PT.js} +7 -6
- package/dist/sleep-manager-LIEPO6WG.js +41 -0
- package/dist/spirit-IL5EMWYZ.js +34 -0
- package/dist/spirit-availability-YFEGJNJ2.js +42 -0
- package/dist/{split-SKZ4Y2D6.js → split-BU5VWX6P.js} +5 -2
- package/dist/{sprout-KQ43BWZJ.js → sprout-UPXGJ5HX.js} +1 -1
- package/dist/{src-YH6PQ7LJ.js → src-HGNE2XG4.js} +7 -1
- package/dist/{src-WLJE73NP.js → src-LCSOQY6Z.js} +60 -54
- package/dist/{src-JAOVXO5W.js → src-UDMJZ6MH.js} +8 -8
- package/dist/{start-O6UKK7BG.js → start-MUMNFR52.js} +4 -3
- package/dist/{status-F4PGREUU.js → status-IL67CYM4.js} +7 -6
- package/dist/{stop-UBX6X5RA.js → stop-EKERNYTX.js} +6 -5
- package/dist/system-events-A6IGCEC3.js +46 -0
- package/dist/system-install-YZSWCPDK.js +8 -0
- package/dist/{systems-JJNGQHAO.js → systems-VDRUZXNF.js} +5 -4
- package/dist/{tailscale-MCLY5GYE.js → tailscale-SJUW2ETR.js} +3 -3
- package/dist/{template-staleness-EMQORSGG.js → template-staleness-T27VCGYA.js} +4 -4
- package/dist/{turn-tracker-KLPV3PAU.js → turn-tracker-6EI2EL7G.js} +7 -7
- package/dist/{up-RVGDE6CQ.js → up-CLL4665A.js} +5 -6
- package/dist/{update-OXQEGGXD.js → update-HVAUCQPN.js} +5 -5
- package/dist/{update-check-266R4ZSY.js → update-check-2MGBBAFF.js} +3 -3
- package/dist/{upgrade-K5FIQPQD.js → upgrade-LIE46UBB.js} +1 -1
- package/dist/variant-cleanup-HJ7Z75R6.js +22 -0
- package/dist/{version-notify-73YURTFR.js → version-notify-7BUHWZNT.js} +11 -28
- package/dist/web-assets/assets/index-ClsUcXiW.js +95 -0
- package/dist/web-assets/assets/index-Dz3wTbtR.css +1 -0
- package/dist/web-assets/index.html +2 -2
- package/dist/xai-P42CRCZA.js +129 -0
- package/drizzle/0013_variant_purpose.sql +3 -0
- package/drizzle/0014_system_events.sql +26 -0
- package/drizzle/0015_rename_session_to_thread.sql +12 -0
- package/drizzle/0016_brain_to_human.sql +4 -0
- package/drizzle/0017_event_history_rows.sql +9 -0
- package/drizzle/meta/_journal.json +35 -0
- package/package.json +27 -26
- package/packages/extensions/notes/dist/ui/assets/index-BBs8pyel.js +67 -0
- package/packages/extensions/notes/dist/ui/index.html +1 -1
- package/packages/extensions/pages/dist/ui/assets/index-B589e8Xe.js +2 -0
- package/packages/extensions/pages/dist/ui/index.html +1 -1
- package/packages/extensions/plan/dist/ui/assets/index-B8s8wxyt.js +67 -0
- package/packages/extensions/plan/dist/ui/index.html +1 -1
- package/templates/_base/.init/.config/prompts.json +1 -0
- package/templates/_base/.init/.local/hooks/startup-context.ts +34 -2
- package/templates/_base/home/.config/routes.json +2 -2
- package/templates/_base/home/VOLUTE.md +30 -5
- package/templates/_base/src/lib/daemon-client.ts +28 -8
- package/templates/_base/src/lib/event-turn.ts +39 -0
- package/templates/_base/src/lib/format-prefix.ts +21 -2
- package/templates/_base/src/lib/router.ts +6 -2
- package/templates/_base/src/lib/routing.ts +8 -8
- package/templates/_base/src/lib/startup.ts +9 -1
- package/templates/_base/src/lib/types.ts +7 -1
- package/templates/_base/src/lib/volute-server.ts +30 -1
- package/templates/claude/.init/.config/routes.json +3 -3
- package/templates/claude/.init/CLAUDE.md +1 -1
- package/templates/claude/package.json.tmpl +4 -4
- package/templates/claude/src/agent.ts +5 -2
- package/templates/claude/src/lib/hooks/identity-reload.ts +12 -3
- package/templates/claude/src/lib/hooks/reply-instructions.ts +26 -1
- package/templates/codex/.init/.config/routes.json +3 -3
- package/templates/codex/.init/AGENTS.md +1 -1
- package/templates/codex/package.json.tmpl +4 -4
- package/templates/codex/src/agent.ts +11 -11
- package/templates/codex/src/lib/turn-context.ts +51 -0
- package/templates/pi/.init/.config/routes.json +3 -3
- package/templates/pi/.init/MINDS.md +1 -1
- package/templates/pi/package.json.tmpl +6 -6
- package/templates/pi/src/agent.ts +9 -1
- package/templates/pi/src/lib/event-handler.ts +47 -6
- package/templates/pi/src/lib/reply-instructions-extension.ts +28 -1
- package/dist/chunk-5KXCM5AW.js +0 -169
- package/dist/chunk-IYCV34IU.js +0 -158
- package/dist/chunk-VB2QXUJQ.js +0 -5260
- package/dist/chunk-WW6C3PLV.js +0 -280
- package/dist/delivery-manager-HZTT5SJR.js +0 -36
- package/dist/echo-text-H3SH337Q.js +0 -36
- package/dist/message-delivery-GPQ5SE5N.js +0 -40
- package/dist/mind-service-2ARGJNKP.js +0 -44
- package/dist/notices-2KU5NTRY.js +0 -25
- package/dist/scheduler-I7JZCT47.js +0 -34
- package/dist/sleep-manager-Z27R3WTW.js +0 -38
- package/dist/spirit-WXS3HCLN.js +0 -25
- package/dist/system-chat-63ABFF6V.js +0 -40
- package/dist/web-assets/assets/index-WdZ60R6c.css +0 -1
- package/dist/web-assets/assets/index-taX6Ezfa.js +0 -92
- package/packages/extensions/notes/dist/ui/assets/index-D0-DgYyj.js +0 -67
- package/packages/extensions/pages/dist/ui/assets/index-DtzftuPA.js +0 -2
- package/packages/extensions/plan/dist/ui/assets/index-Cfzs-0G9.js +0 -67
|
@@ -17,6 +17,25 @@ 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";
|
|
@@ -29,9 +48,9 @@ export function formatPrefix(meta: ChannelMeta | undefined, time: string): strin
|
|
|
29
48
|
if (meta.serverName) sender += ` in ${meta.serverName}`;
|
|
30
49
|
}
|
|
31
50
|
const parts = [platform, sender].filter(Boolean);
|
|
32
|
-
// Include
|
|
51
|
+
// Include thread name if not the default
|
|
33
52
|
const sessionPart =
|
|
34
|
-
meta.sessionName && meta.sessionName !== "main" ? ` —
|
|
53
|
+
meta.sessionName && meta.sessionName !== "main" ? ` — thread: ${meta.sessionName}` : "";
|
|
35
54
|
let prefix = parts.length > 0 ? `[${parts.join(": ")}${sessionPart} — ${time}]\n` : "";
|
|
36
55
|
|
|
37
56
|
// 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
|
-
|
|
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");
|
|
@@ -306,7 +310,7 @@ export function createRouter(options: {
|
|
|
306
310
|
): { messageId: string; unsubscribe: () => void } {
|
|
307
311
|
const messageId = generateMessageId();
|
|
308
312
|
|
|
309
|
-
// Expose session to child processes (daemon-client reads this for X-Volute-
|
|
313
|
+
// Expose session to child processes (daemon-client reads this for X-Volute-Thread)
|
|
310
314
|
process.env.VOLUTE_SESSION = session;
|
|
311
315
|
// Also write to file for sandbox environments where env vars don't propagate
|
|
312
316
|
try {
|
|
@@ -8,7 +8,7 @@ export type BatchConfig = {
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
export type RoutingRule = {
|
|
11
|
-
|
|
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
|
-
|
|
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,
|
|
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(["
|
|
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.
|
|
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
|
|
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.
|
|
161
|
+
if (!config.threads) return defaults;
|
|
162
162
|
|
|
163
|
-
for (const [pattern, sessionConfig] of Object.entries(config.
|
|
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
|
-
|
|
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: "
|
|
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
|
|
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, "
|
|
5
|
-
{ "channel": "#*", "
|
|
4
|
+
{ "channel": "*", "isDM": true, "thread": "${channel}" },
|
|
5
|
+
{ "channel": "#*", "thread": "${channel}" }
|
|
6
6
|
],
|
|
7
|
-
"
|
|
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
|
|
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.
|
|
12
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.207",
|
|
13
13
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
14
|
-
"tsx": "^4.
|
|
14
|
+
"tsx": "^4.23.1"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@biomejs/biome": "2.5.
|
|
18
|
-
"@types/node": "^26.
|
|
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
|
|
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);
|
|
@@ -346,7 +348,7 @@ export function createMind(options: {
|
|
|
346
348
|
// Mind's turn after compaction warning is done — abort the stream to run /compact
|
|
347
349
|
log("mind", `session "${session.name}": aborting stream for compaction`);
|
|
348
350
|
streamAbort.abort(new CompactionAbort());
|
|
349
|
-
} else if (identityReload.
|
|
351
|
+
} else if (identityReload.shouldRequestReload()) {
|
|
350
352
|
options.onIdentityReload?.();
|
|
351
353
|
}
|
|
352
354
|
},
|
|
@@ -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
|
};
|
|
@@ -5,6 +5,7 @@ const IDENTITY_FILES = ["SOUL.md", "MEMORY.md", "VOLUTE.md"];
|
|
|
5
5
|
|
|
6
6
|
export function createIdentityReloadHook(cwd: string) {
|
|
7
7
|
let reloadNeeded = false;
|
|
8
|
+
let reloadRequested = false;
|
|
8
9
|
|
|
9
10
|
const hook: HookCallback = async (input) => {
|
|
10
11
|
const filePath = (input as { tool_input?: { file_path?: string } }).tool_input?.file_path;
|
|
@@ -18,9 +19,17 @@ export function createIdentityReloadHook(cwd: string) {
|
|
|
18
19
|
return {};
|
|
19
20
|
};
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
// Returns true at most once per process. A reload requests a daemon restart,
|
|
23
|
+
// which normally kills this process — but if that restart fails (e.g. the
|
|
24
|
+
// request errors), the process survives with reloadNeeded still set. Latching
|
|
25
|
+
// on reloadRequested stops us from re-firing the restart on every later turn.
|
|
26
|
+
function shouldRequestReload(): boolean {
|
|
27
|
+
if (reloadNeeded && !reloadRequested) {
|
|
28
|
+
reloadRequested = true;
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
23
32
|
}
|
|
24
33
|
|
|
25
|
-
return { hook,
|
|
34
|
+
return { hook, shouldRequestReload };
|
|
26
35
|
}
|
|
@@ -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
|
-
|
|
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,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"gateUnmatched": true,
|
|
3
3
|
"rules": [
|
|
4
|
-
{ "channel": "*", "isDM": true, "
|
|
5
|
-
{ "channel": "#*", "
|
|
4
|
+
{ "channel": "*", "isDM": true, "thread": "${channel}" },
|
|
5
|
+
{ "channel": "#*", "thread": "${channel}" }
|
|
6
6
|
],
|
|
7
|
-
"
|
|
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
|
|
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.
|
|
14
|
-
"tsx": "^4.
|
|
13
|
+
"@openai/codex-sdk": "^0.144.3",
|
|
14
|
+
"tsx": "^4.23.1"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@biomejs/biome": "2.5.
|
|
18
|
-
"@types/node": "^26.
|
|
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
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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:
|
|
269
|
-
metadata: { source:
|
|
268
|
+
content: turnContext.content,
|
|
269
|
+
metadata: { source: turnContext.source },
|
|
270
270
|
});
|
|
271
|
-
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,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"gateUnmatched": true,
|
|
3
3
|
"rules": [
|
|
4
|
-
{ "channel": "*", "isDM": true, "
|
|
5
|
-
{ "channel": "#*", "
|
|
4
|
+
{ "channel": "*", "isDM": true, "thread": "${channel}" },
|
|
5
|
+
{ "channel": "#*", "thread": "${channel}" }
|
|
6
6
|
],
|
|
7
|
-
"
|
|
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
|
|
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.
|
|
14
|
-
"@earendil-works/pi-coding-agent": "^0.80.
|
|
15
|
-
"@sinclair/typebox": "^0.34.
|
|
16
|
-
"tsx": "^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.
|
|
20
|
-
"@types/node": "^26.
|
|
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
|