volute 0.49.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 +2 -2
- package/dist/{accept-KYNC5X3U.js → accept-5NK5XJB5.js} +2 -1
- package/dist/{activity-events-GJJAI2WT.js → activity-events-H7YI3BTF.js} +3 -3
- package/dist/{ai-service-A7FS7EC3.js → ai-service-V4CTNGI3.js} +5 -4
- package/dist/{archive-7E3YE6VF.js → archive-ORM3ENHI.js} +3 -3
- package/dist/{auth-ABYLOEZ7.js → auth-DT7GY7EZ.js} +10 -6
- package/dist/{avatar-image-6ATV2IOZ.js → avatar-image-QEPAP3A5.js} +3 -3
- package/dist/{backup-XYAMT4CU.js → backup-IE6PMXJO.js} +11 -12
- package/dist/{bridge-3FNSWLNQ.js → bridge-YK7NTRU5.js} +2 -1
- package/dist/{channels-SGQZHNQJ.js → channels-FSAHOQWI.js} +2 -1
- package/dist/{chat-OGXAEXMV.js → chat-W5K6HCBE.js} +9 -9
- package/dist/{chunk-3B427GUK.js → chunk-3BAFMGCJ.js} +2 -2
- package/dist/{chunk-4B62E4CG.js → chunk-4CN7OCJE.js} +2 -2
- package/dist/{chunk-VNRSXEBF.js → chunk-574M6I7F.js} +38 -5
- package/dist/{chunk-HCYQRME2.js → chunk-5FVANDON.js} +2 -2
- package/dist/{chunk-P4LHVRNE.js → chunk-5IIVOWSP.js} +27 -19
- package/dist/{chunk-TXJL36ZQ.js → chunk-5PKFJHCR.js} +2 -2
- package/dist/{chunk-JPGHSXSV.js → chunk-5TPC4KCV.js} +23 -9
- package/dist/{chunk-YIJGKHIS.js → chunk-632FJ23O.js} +24 -20
- package/dist/{chunk-IH5TKXX6.js → chunk-636GFQNE.js} +11 -11
- package/dist/{chunk-EFT2OALG.js → chunk-6BM2I736.js} +20 -5
- package/dist/{chunk-JCBPU57A.js → chunk-74T3CEXK.js} +6 -6
- package/dist/{chunk-EG5LSKHU.js → chunk-77IZUVBB.js} +2 -2
- package/dist/{chunk-K3JOLGWF.js → chunk-7DXLIYPY.js} +76 -63
- package/dist/chunk-7FDWPQCU.js +289 -0
- package/dist/chunk-7OZWKHA3.js +548 -0
- package/dist/{chunk-PY24ZL62.js → chunk-7UWXAJMK.js} +1 -1
- package/dist/{chunk-MG3Z34YE.js → chunk-7XDOTN4R.js} +1 -1
- package/dist/{chunk-SX72DOO3.js → chunk-AFSLNUTT.js} +1 -1
- package/dist/{chunk-DVN6DNMI.js → chunk-AJWFDIEY.js} +17 -2
- package/dist/chunk-ALIVXDM4.js +1014 -0
- package/dist/{chunk-RSBL3FAD.js → chunk-CAIYV33P.js} +234 -24
- package/dist/{chunk-ZX46PGCC.js → chunk-E5S3ILVA.js} +1 -1
- 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-EADX54RG.js → chunk-HW6Q7AMB.js} +4 -4
- package/dist/chunk-JI67D7O2.js +1222 -0
- package/dist/{chunk-KHCKZ36O.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-NOIAHMJB.js → chunk-QFVGPKJI.js} +1 -1
- package/dist/{chunk-VIUH5CHQ.js → chunk-SFRYRW37.js} +50 -33
- package/dist/{chunk-L27YYONJ.js → chunk-SSNZ6ZNN.js} +1 -1
- package/dist/{chunk-GWVFDUNB.js → chunk-TGJHGVBZ.js} +8 -10
- package/dist/{chunk-6HGEZJDH.js → chunk-U3EJJ7JN.js} +2 -2
- package/dist/{chunk-4DSGN4FL.js → chunk-UC6BNMGT.js} +43 -13
- package/dist/{chunk-JBIFXB6I.js → chunk-WBBKYUGF.js} +3 -5
- package/dist/{chunk-FBBS4BVR.js → chunk-X67VT4QO.js} +1 -1
- package/dist/{chunk-DAIIMBLB.js → chunk-YQOYZ6G7.js} +3 -5
- package/dist/{chunk-JCWB2ITW.js → chunk-ZUG76D3P.js} +35 -5
- package/dist/cli.js +34 -27
- package/dist/{clock-6JZL2FS6.js → clock-LH7UFNLJ.js} +4 -3
- package/dist/{cloud-sync-VFDTNNCA.js → cloud-sync-IDJFVDEP.js} +22 -19
- package/dist/{config-RPSLA2HX.js → config-UGJS57VT.js} +3 -3
- package/dist/{conversations-TUM5PJJ4.js → conversations-S2232Y5I.js} +8 -4
- package/dist/{create-Z5FSDNVZ.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-54L4B6AQ.js → daemon-restart-2AAGYFHE.js} +6 -7
- package/dist/daemon.js +1625 -529
- package/dist/{db-F6UMFRYD.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-VUOL5YSA.js → delivery-router-JE7SHYNU.js} +7 -5
- package/dist/{down-6X7KX7BZ.js → down-D4IRNEMJ.js} +5 -5
- package/dist/echo-text-WYAAODBV.js +39 -0
- package/dist/{env-ZYMSCB7B.js → env-ABM5IMA7.js} +3 -2
- package/dist/{env-KUL4XKN2.js → env-AZK5X4W7.js} +3 -3
- package/dist/{exec-FBXA3CM4.js → exec-2H65LMTA.js} +3 -3
- package/dist/{export-TNBPDH77.js → export-3E3EL6W3.js} +4 -4
- package/dist/{extension-J4ZQ2GHV.js → extension-MRYJWSMV.js} +2 -1
- package/dist/{extensions-AWNO3SZF.js → extensions-7KP3X6G7.js} +22 -19
- package/dist/{files-BVBRJJ2P.js → files-6BMKVG5G.js} +5 -4
- package/dist/{import-ZUNKMCKC.js → import-UPNTEBBT.js} +8 -8
- package/dist/{isolation-OPCZQRTH.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-KAKNFCW5.js → login-MOXVLULE.js} +4 -3
- package/dist/{login-LKNLCJPA.js → login-RJJTCVR3.js} +2 -1
- package/dist/{logout-YZRMZ3O2.js → logout-BYV433X6.js} +2 -1
- package/dist/{logout-RLT62RQP.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-IF6CGMFW.js → mind-NYRALSV2.js} +19 -19
- package/dist/{mind-activity-tracker-YMMERB6B.js → mind-activity-tracker-L7GCYXQR.js} +4 -4
- package/dist/{mind-contacts-BKUFPUCU.js → mind-contacts-QYU4TPEA.js} +3 -2
- package/dist/{mind-history-WI5OI3BW.js → mind-history-NXXOOTX6.js} +30 -7
- 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-VBV7YRSU.js → mind-sleep-TH6BRQ5X.js} +4 -3
- 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-PD67VPHB.js → package-XYSV7NUT.js} +23 -23
- package/dist/{prompts-MDHDLWEX.js → prompts-QKFPX6DQ.js} +3 -3
- package/dist/{read-YSWUXYL7.js → read-ZXGUT4QC.js} +5 -3
- package/dist/{register-TWQDKQRT.js → register-OX4HRSGC.js} +2 -1
- package/dist/{registry-3ELJPUGJ.js → registry-4UF6OCOE.js} +6 -4
- package/dist/{reject-SXGCYN4A.js → reject-TWB4QJHG.js} +2 -1
- package/dist/{restart-5F5QDBOG.js → restart-AGEGNSYQ.js} +2 -1
- package/dist/{sandbox-XPYA7UIF.js → sandbox-EGEU5GGE.js} +7 -4
- package/dist/scheduler-UV6RUJ7P.js +25 -0
- package/dist/{schema-6YONVTKZ.js → schema-C2GIPD4V.js} +3 -3
- package/dist/{seed-NQED7PSX.js → seed-3IMBOAGK.js} +1 -1
- package/dist/{seed-check-TYD7RRZY.js → seed-check-NRJEQZST.js} +14 -5
- package/dist/{seed-cmd-QF3F2G4S.js → seed-cmd-U73XVQMI.js} +3 -3
- package/dist/{seed-create-XOO4N2OE.js → seed-create-FBLNVSUP.js} +4 -4
- package/dist/{seed-readiness-3LRYASA4.js → seed-readiness-QRA46ZJQ.js} +4 -5
- package/dist/{seed-sprout-HSBBGSS2.js → seed-sprout-3B4SZX7J.js} +9 -9
- package/dist/{send-IHRJYNXA.js → send-NBRZJXKX.js} +32 -10
- package/dist/{service-YCOPCTSZ.js → service-XMXAXGJD.js} +4 -4
- package/dist/{service-install-Y46OTF55.js → service-install-MOKTEG7B.js} +5 -5
- package/dist/{setup-TCN7VH5U.js → setup-CAJFOR2N.js} +10 -3
- package/dist/{setup-F4TE64YI.js → setup-JGYAXXW7.js} +6 -7
- package/dist/{skill-4CYJLYVY.js → skill-NXIHDQCG.js} +3 -2
- 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 +3 -3
- package/dist/skills/volute-mind/references/integrations.md +2 -2
- package/dist/skills/volute-mind/references/routing.md +15 -15
- package/dist/{skills-NN5UZCUU.js → skills-5MSMT7PT.js} +4 -5
- package/dist/sleep-manager-LIEPO6WG.js +41 -0
- package/dist/{spirit-SSJMYC6A.js → spirit-IL5EMWYZ.js} +10 -7
- package/dist/spirit-availability-YFEGJNJ2.js +42 -0
- package/dist/{split-JF2VWTXA.js → split-BU5VWX6P.js} +1 -1
- package/dist/{sprout-AXA2SECQ.js → sprout-UPXGJ5HX.js} +1 -1
- package/dist/{src-YH6PQ7LJ.js → src-HGNE2XG4.js} +7 -1
- package/dist/{src-ZN4LXMPI.js → src-LCSOQY6Z.js} +57 -51
- package/dist/{src-JAOVXO5W.js → src-UDMJZ6MH.js} +8 -8
- package/dist/{start-BTVMTP3K.js → start-MUMNFR52.js} +2 -1
- package/dist/{status-VJGXMQ4L.js → status-IL67CYM4.js} +7 -6
- package/dist/{stop-YORESK4U.js → stop-EKERNYTX.js} +2 -1
- package/dist/system-events-A6IGCEC3.js +46 -0
- package/dist/system-install-YZSWCPDK.js +8 -0
- package/dist/{systems-5MH2ELQE.js → systems-VDRUZXNF.js} +5 -4
- package/dist/{tailscale-AMMVKDXB.js → tailscale-SJUW2ETR.js} +2 -2
- package/dist/{template-staleness-HKGCTS3H.js → template-staleness-T27VCGYA.js} +3 -3
- package/dist/{turn-tracker-JQ2YUUY2.js → turn-tracker-6EI2EL7G.js} +6 -6
- package/dist/{up-SDLTLPAO.js → up-CLL4665A.js} +5 -6
- package/dist/{update-WEA4N2TR.js → update-HVAUCQPN.js} +5 -5
- package/dist/{update-check-DZOTONII.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-WFA5XB5A.js → version-notify-7BUHWZNT.js} +10 -27
- 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/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 +28 -0
- package/package.json +23 -23
- 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 +24 -9
- package/templates/_base/home/.config/routes.json +2 -2
- package/templates/_base/home/VOLUTE.md +18 -5
- package/templates/_base/src/lib/daemon-client.ts +1 -1
- 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 +4 -1
- 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 +1 -1
- package/templates/pi/src/lib/reply-instructions-extension.ts +28 -1
- package/dist/chunk-NJHN6IRB.js +0 -177
- package/dist/chunk-QQBOI3QA.js +0 -5345
- package/dist/chunk-XTEKINWG.js +0 -158
- package/dist/delivery-manager-RBXBPVWM.js +0 -36
- package/dist/echo-text-B6NZ5FGE.js +0 -36
- package/dist/message-delivery-ABPV76EA.js +0 -42
- package/dist/mind-service-42GDO4KH.js +0 -44
- package/dist/notices-N6BCGHFV.js +0 -25
- package/dist/scheduler-K7Y7Z7FQ.js +0 -34
- package/dist/sleep-manager-DSPXFHGX.js +0 -38
- package/dist/system-chat-M2XOBKEM.js +0 -40
- package/dist/variant-cleanup-P4MBNLJD.js +0 -33
- package/dist/web-assets/assets/index-BostMFoR.css +0 -1
- package/dist/web-assets/assets/index-CKlqUkyG.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
|
@@ -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);
|
|
@@ -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
|
-
|
|
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
|
|
@@ -147,7 +147,7 @@ export function createEventHandler(session: EventSession, options: EventHandlerO
|
|
|
147
147
|
const channel = messageId ? session.messageChannels.get(messageId)?.channel : undefined;
|
|
148
148
|
log("mind", `session "${session.name}": turn done`);
|
|
149
149
|
// Collect any agent-level errors (e.g. a provider 401 reported inside agent_end).
|
|
150
|
-
// Warn-logging alone left the failure invisible at every
|
|
150
|
+
// Warn-logging alone left the failure invisible at every host surface (#619);
|
|
151
151
|
// we emit a turn_error to the daemon too — mirroring the dispatch-rejection path in
|
|
152
152
|
// agent.ts — so classify() tags it and chat status + lastError surface it. Dedupe
|
|
153
153
|
// identical messages so a turn with several failed messages records one notice.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { EventSession } from "./event-handler.js";
|
|
3
|
+
import { firstReplyableEntry, isEventTurn } from "./event-turn.js";
|
|
3
4
|
import { log } from "./logger.js";
|
|
4
5
|
import { loadPrompts } from "./startup.js";
|
|
5
6
|
|
|
@@ -14,11 +15,37 @@ export function createReplyInstructionsExtension(
|
|
|
14
15
|
const prompts = loadPrompts();
|
|
15
16
|
return (pi) => {
|
|
16
17
|
let fired = false;
|
|
18
|
+
let eventNoteFired = false;
|
|
17
19
|
pi.on("before_agent_start", () => {
|
|
18
20
|
try {
|
|
21
|
+
const entries = [...messageChannels.values()];
|
|
22
|
+
|
|
23
|
+
// System-event turn: ambient, from the environment, with no channel to reply to.
|
|
24
|
+
// The note fires once per session (see event-turn.ts for why).
|
|
25
|
+
if (isEventTurn(entries)) {
|
|
26
|
+
if (eventNoteFired) return {};
|
|
27
|
+
eventNoteFired = true;
|
|
28
|
+
const content = prompts.event_instructions;
|
|
29
|
+
if (emitContext && session) {
|
|
30
|
+
emitContext(session, {
|
|
31
|
+
type: "context",
|
|
32
|
+
content,
|
|
33
|
+
metadata: { source: "event-instructions" },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
message: {
|
|
38
|
+
customType: "event-instructions",
|
|
39
|
+
content,
|
|
40
|
+
display: true,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
19
45
|
if (fired) return {};
|
|
20
46
|
|
|
21
|
-
|
|
47
|
+
// Reply instructions must name a channel the mind can actually send to.
|
|
48
|
+
const entry = firstReplyableEntry(entries);
|
|
22
49
|
if (!entry) return {};
|
|
23
50
|
|
|
24
51
|
fired = true;
|