volute 0.31.0 → 0.33.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 +31 -22
- package/dist/{accept-GAKQ3MEH.js → accept-D5VBM7JW.js} +5 -4
- package/dist/{activity-events-T5ZRCVAL.js → activity-events-XJO3P4RR.js} +3 -2
- package/dist/{ai-service-UWUPM4T6.js → ai-service-SBY2WG7O.js} +18 -5
- package/dist/api.d.ts +703 -1068
- package/dist/{archive-YBNSJYZZ.js → archive-INXYFVCW.js} +3 -2
- package/dist/{auth-T5AW2USD.js → auth-GKCDSO4T.js} +4 -3
- package/dist/{bridge-4AJ3EY26.js → bridge-TXWWPPOJ.js} +5 -4
- package/dist/{chat-7YLT7FI3.js → chat-U5ZOME3O.js} +8 -8
- package/dist/{chunk-NV3TYNWX.js → chunk-2NGTS5UU.js} +1 -1
- package/dist/{chunk-BWKIHH7B.js → chunk-3Z2DPESO.js} +662 -508
- package/dist/chunk-6LXAAQ43.js +22 -0
- package/dist/chunk-7J3HEVR7.js +220 -0
- package/dist/{chunk-NOWVQ7AL.js → chunk-A2A4KLFE.js} +351 -301
- package/dist/{chunk-LX6T3GKQ.js → chunk-ALEF47VT.js} +1 -1
- package/dist/{chunk-S2TZLSDH.js → chunk-C7I35G4R.js} +163 -15
- package/dist/{chunk-VGWJSNHS.js → chunk-G53F3JA4.js} +1 -35
- package/dist/{chunk-A6TUJJ3L.js → chunk-G6BSYHPK.js} +2 -2
- package/dist/{chunk-DAXJKPHZ.js → chunk-GY5HBI7A.js} +2 -2
- package/dist/{chunk-BC3P3QCK.js → chunk-I5KY25PQ.js} +1 -9
- package/dist/{chunk-BNC43CSY.js → chunk-JUKK7FPS.js} +2 -2
- package/dist/{chunk-R5QJBZZG.js → chunk-JYVGHWEJ.js} +21 -11
- package/dist/chunk-KIEPMIM5.js +59 -0
- package/dist/{chunk-EKDWA7E4.js → chunk-KVK2DLWI.js} +5 -2
- package/dist/{chunk-AAO77TZX.js → chunk-LOEJ4HPQ.js} +1 -1
- package/dist/chunk-LRCG2JLP.js +251 -0
- package/dist/{chunk-EMPFLFTG.js → chunk-M7UL5S3Q.js} +1 -1
- package/dist/{chunk-6QIUN46C.js → chunk-N432I7QH.js} +20 -3
- package/dist/{chunk-SNVPRRT7.js → chunk-NNB4WIG7.js} +2 -2
- package/dist/{chunk-HDKY4TWU.js → chunk-NPKSDYA2.js} +3 -3
- package/dist/chunk-OYAKCAVY.js +29 -0
- package/dist/chunk-PB65JZK2.js +85 -0
- package/dist/chunk-PVY5W6QN.js +41 -0
- package/dist/{chunk-PNQCXLSV.js → chunk-QTUVYI7W.js} +58 -1
- package/dist/{chunk-X62AXPR7.js → chunk-RPZZSXV3.js} +8 -196
- package/dist/{chunk-WRS3B556.js → chunk-RSX4OPZY.js} +5 -5
- package/dist/{chunk-FAHDKPEH.js → chunk-RVGLDGMI.js} +5 -3
- package/dist/chunk-SKLSMHXO.js +208 -0
- package/dist/{chunk-4OUOFS23.js → chunk-UKVWJRKN.js} +1 -1
- package/dist/{chunk-57OKQMP3.js → chunk-VH33ZWMW.js} +5 -55
- package/dist/cli.js +49 -23
- package/dist/{clock-LJCG426D.js → clock-BVH3V6E3.js} +7 -6
- package/dist/{cloud-sync-O3LXIRN6.js → cloud-sync-4NWLMFVH.js} +20 -14
- package/dist/config-H2H4UIF7.js +72 -0
- package/dist/connectors/discord-bridge.js +1 -1
- package/dist/connectors/slack-bridge.js +1 -1
- package/dist/connectors/telegram-bridge.js +1 -1
- package/dist/{conversations-RKKGP5IA.js → conversations-AWI5SZW2.js} +4 -3
- package/dist/{create-TL623TFC.js → create-2FK7Z46Y.js} +6 -2
- package/dist/{create-WUTIIRI2.js → create-YWD2TIP4.js} +6 -5
- package/dist/{daemon-client-CVGM25DM.js → daemon-client-6QXHZ7US.js} +3 -2
- package/dist/{daemon-restart-EZP7XH3V.js → daemon-restart-GOBUKLX7.js} +8 -6
- package/dist/daemon.js +1918 -1472
- package/dist/{db-SW5PL6QA.js → db-F34YLV7D.js} +2 -1
- package/dist/db-RA45JBFG.js +16 -0
- package/dist/{delete-Z6HAG35F.js → delete-QTGWEDBI.js} +1 -1
- package/dist/delivery-manager-PFAKEJTC.js +32 -0
- package/dist/delivery-router-FL45JL7N.js +21 -0
- package/dist/down-FWWTEKXM.js +15 -0
- package/dist/{env-NHESNNSP.js → env-JCOF2222.js} +5 -4
- package/dist/{export-EVMP7GWY.js → export-SUYRLI5Q.js} +4 -3
- package/dist/{extension-LR7EW3JF.js → extension-OBTGKQQD.js} +5 -3
- package/dist/{extensions-NGEJI7JH.js → extensions-KYNTVTMO.js} +10 -7
- package/dist/{files-3SM7V33S.js → files-65PMW5IK.js} +6 -5
- package/dist/{history-PQD3LXEP.js → history-DKCDI3JO.js} +9 -4
- package/dist/{import-PR2OCGQJ.js → import-DDUFE7AY.js} +4 -3
- package/dist/isolation-LLAYQYDY.js +22 -0
- package/dist/{join-R4EN5CWQ.js → join-I5QEE3LG.js} +1 -1
- package/dist/{list-B4XNUOFO.js → list-JQ463EDA.js} +5 -4
- package/dist/{login-62JVY6A2.js → login-D7ETSU4R.js} +5 -4
- package/dist/{login-URWP6S2N.js → login-RIJF2F4G.js} +3 -2
- package/dist/{logout-NXJQJDLI.js → logout-5MLHZALK.js} +3 -2
- package/dist/{logout-ZK2N62T3.js → logout-UZJRGY4Z.js} +3 -2
- package/dist/message-delivery-DFF5SJRM.js +42 -0
- package/dist/{mind-E2ZV2WRX.js → mind-IOJFLEM5.js} +25 -19
- package/dist/{mind-activity-tracker-ASNZBMLC.js → mind-activity-tracker-F6O4Q2SL.js} +4 -3
- package/dist/{mind-list-BEI7E5WY.js → mind-list-WUPMQDYQ.js} +3 -2
- package/dist/mind-manager-NBJF5D26.js +32 -0
- package/dist/mind-profile-P67FEHOY.js +47 -0
- package/dist/mind-service-2MQ6UK5N.js +38 -0
- package/dist/{mind-sleep-CANABWJI.js → mind-sleep-WW2IX7JT.js} +5 -4
- package/dist/{mind-status-6WKZVUOP.js → mind-status-L3EFFRPR.js} +3 -2
- package/dist/{mind-wake-RZKLH2IN.js → mind-wake-VSSGW465.js} +5 -4
- package/dist/{package-NU4CA7OU.js → package-U3VFO273.js} +2 -1
- package/dist/{read-THL362EI.js → read-EBY56C33.js} +5 -4
- package/dist/read-stdin-HQJ7774D.js +8 -0
- package/dist/{register-QAQELAS6.js → register-HD74C4TT.js} +5 -4
- package/dist/{registry-ASXCQCNH.js → registry-PJ4S5PHQ.js} +8 -1
- package/dist/{reject-AYPBNPNL.js → reject-UJKFBHRO.js} +5 -4
- package/dist/{restart-6SKPV3T2.js → restart-3UCMRUVC.js} +3 -2
- package/dist/{sandbox-6ZEWQDVU.js → sandbox-GJOK4QLQ.js} +4 -3
- package/dist/scheduler-ZZ7XGQG6.js +32 -0
- package/dist/schema-PA3M5ZKH.js +32 -0
- package/dist/seed-QDYVLG74.js +11 -0
- package/dist/seed-check-S2IX25RL.js +32 -0
- package/dist/seed-cmd-DKOUFEAU.js +36 -0
- package/dist/{seed-OWX2AW75.js → seed-create-4XBBOLRH.js} +27 -10
- package/dist/{sprout-FDVI2CGN.js → seed-sprout-GQEIIQRT.js} +24 -9
- package/dist/{send-ZO4BTWXK.js → send-QIV2INHB.js} +92 -101
- package/dist/{setup-7CFITEQN.js → setup-TISPCO22.js} +7 -2
- package/dist/{setup-ZXBXG7E4.js → setup-XMCBE3LF.js} +11 -7
- package/dist/{skill-YFXP67A2.js → skill-PSQGRRJX.js} +5 -4
- package/dist/skills/dreaming/SKILL.md +6 -4
- package/dist/skills/dreaming/references/INSTALL.md +2 -2
- package/dist/skills/dreaming/scripts/dream.ts +2 -2
- package/dist/skills/dreaming/scripts/wake-context-dreams.sh +1 -1
- package/dist/skills/imagegen/SKILL.md +16 -11
- package/dist/skills/imagegen/references/INSTALL.md +1 -1
- package/dist/skills/imagegen/scripts/imagegen.ts +146 -25
- package/dist/skills/orientation/SKILL.md +9 -2
- package/dist/skills/resonance/SKILL.md +4 -1
- package/dist/skills/resonance/references/INSTALL.md +2 -2
- package/dist/skills/resonance/scripts/resonance-hook.sh +2 -0
- package/dist/skills/resonance/scripts/resonance.ts +35 -5
- package/dist/skills/seed-nurture/SKILL.md +42 -0
- package/dist/skills/volute-admin/SKILL.md +83 -0
- package/dist/skills/volute-mind/SKILL.md +15 -11
- package/dist/skills-7FV7EJTE.js +62 -0
- package/dist/sleep-manager-JTXSN7NV.js +36 -0
- package/dist/spirit-VRONKFMF.js +23 -0
- package/dist/{split-MI62KJUU.js → split-STOROBYJ.js} +1 -1
- package/dist/sprout-WKLZXUIQ.js +11 -0
- package/dist/{start-D64BRKPH.js → start-K2NCUUCG.js} +3 -2
- package/dist/{status-ZZWBYFGE.js → status-3JBTFSMI.js} +6 -4
- package/dist/{stop-OP2CTXCO.js → stop-H26JZDXF.js} +3 -2
- package/dist/system-chat-JAPOJ3KE.js +36 -0
- package/dist/{systems-EQPPT4B7.js → systems-XRI52VCH.js} +6 -5
- package/dist/{tailscale-6DJKUMNF.js → tailscale-XHQBZROW.js} +2 -1
- package/dist/{template-hash-3HOR4UAJ.js → template-hash-A6VVKOXJ.js} +2 -1
- package/dist/up-M5AS6SBV.js +18 -0
- package/dist/{update-KUJXATRS.js → update-UD543CXX.js} +6 -4
- package/dist/{update-check-5WVSU37T.js → update-check-ZD6OOIYQ.js} +3 -2
- package/dist/{upgrade-KBHCWX6T.js → upgrade-O4Q7WJM3.js} +12 -14
- package/dist/{version-notify-75ELVKPV.js → version-notify-NBI2MTJO.js} +22 -16
- package/dist/volute-config-HD7WWUQC.js +10 -0
- package/dist/web-assets/assets/index-CWJrVveV.css +1 -0
- package/dist/web-assets/assets/index-DJt14FRI.js +75 -0
- package/dist/web-assets/ext-theme.css +93 -0
- package/dist/web-assets/index.html +2 -2
- package/drizzle/0004_spirits.sql +5 -0
- package/drizzle/meta/0004_snapshot.json +7 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +2 -1
- package/packages/extensions/notes/dist/ui/assets/index-8jWEv9SA.js +61 -0
- package/packages/extensions/notes/dist/ui/assets/index-DkaB7Ytd.css +1 -0
- package/packages/extensions/notes/dist/ui/index.html +2 -2
- package/packages/extensions/pages/skills/pages/SKILL.md +16 -46
- package/templates/_base/.init/.config/hooks/pre-prompt/session-activity.ts +40 -0
- package/templates/_base/.init/{.config → .local}/bin/volute +1 -1
- package/templates/_base/.init/.local/hooks/pre-prompt/session-activity.ts +40 -0
- package/templates/_base/.init/.local/hooks/startup-context.ts +58 -0
- package/templates/_base/home/.config/routes.json +1 -1
- package/templates/_base/src/lib/daemon-client.ts +21 -13
- package/templates/_base/src/lib/format-prefix.ts +1 -0
- package/templates/_base/src/lib/hook-loader.ts +155 -0
- package/templates/_base/src/lib/startup.ts +11 -4
- package/templates/_base/src/lib/transparency.ts +2 -2
- package/templates/claude/.init/.claude/settings.json +1 -1
- package/templates/claude/.init/.config/routes.json +2 -2
- package/templates/claude/src/agent.ts +95 -13
- package/templates/claude/src/lib/message-channel.ts +7 -2
- package/templates/claude/src/lib/stream-consumer.ts +38 -0
- package/templates/codex/.init/.config/routes.json +11 -0
- package/templates/codex/.init/AGENTS.md +29 -0
- package/templates/codex/home/.config/config.json.tmpl +7 -0
- package/templates/codex/package.json.tmpl +20 -0
- package/templates/codex/src/agent.ts +554 -0
- package/templates/codex/src/lib/content.ts +16 -0
- package/templates/codex/src/lib/session-store.ts +56 -0
- package/templates/codex/src/server.ts +59 -0
- package/templates/codex/volute-template.json +8 -0
- package/templates/pi/.init/.config/routes.json +2 -2
- package/templates/pi/src/agent.ts +62 -8
- package/templates/pi/src/lib/event-handler.ts +1 -1
- package/templates/pi/src/lib/reply-instructions-extension.ts +32 -11
- package/dist/chunk-HR5JKIDG.js +0 -222
- package/dist/down-TS4XQBA4.js +0 -13
- package/dist/message-delivery-UJHCLVU4.js +0 -30
- package/dist/mind-manager-IPA6DZXD.js +0 -26
- package/dist/pages-watcher-72OVPRMH.js +0 -22
- package/dist/skills/sessions/SKILL.md +0 -49
- package/dist/sleep-manager-TPS6OGCA.js +0 -30
- package/dist/system-chat-B43GIXQU.js +0 -30
- package/dist/up-TDXEP3VA.js +0 -16
- package/dist/web-assets/assets/index-BM1cTzBg.js +0 -72
- package/dist/web-assets/assets/index-BfJkKTPF.css +0 -1
- package/packages/extensions/notes/dist/ui/assets/index-B8GdTnXs.css +0 -1
- package/packages/extensions/notes/dist/ui/assets/index-CDpGTCWb.js +0 -2
- package/packages/extensions/pages/skills/pages/scripts/pages.mjs +0 -58
- package/templates/_base/.init/.config/hooks/startup-context.sh +0 -46
- package/templates/_base/.init/.config/scripts/session-reader.ts +0 -59
- package/templates/_base/src/lib/session-monitor.ts +0 -400
- package/templates/claude/src/lib/hooks/session-context.ts +0 -32
- package/templates/pi/src/lib/session-context-extension.ts +0 -35
- /package/templates/_base/.init/{.config → .local}/hooks/wake-context.sh +0 -0
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
extractTextContent,
|
|
4
|
+
getRoutingConfig,
|
|
5
|
+
resolveDeliveryMode,
|
|
6
|
+
resolveRoute
|
|
7
|
+
} from "./chunk-SKLSMHXO.js";
|
|
8
|
+
import {
|
|
9
|
+
markIdle
|
|
10
|
+
} from "./chunk-LOEJ4HPQ.js";
|
|
2
11
|
import {
|
|
3
12
|
addMessage,
|
|
4
13
|
createChannel,
|
|
@@ -8,125 +17,90 @@ import {
|
|
|
8
17
|
getParticipants,
|
|
9
18
|
joinChannel,
|
|
10
19
|
publish as publish2
|
|
11
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-RVGLDGMI.js";
|
|
12
21
|
import {
|
|
13
22
|
isSandboxEnabled,
|
|
14
23
|
wrapForSandbox
|
|
15
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-GY5HBI7A.js";
|
|
16
25
|
import {
|
|
17
|
-
|
|
18
|
-
} from "./chunk-
|
|
26
|
+
spiritDir
|
|
27
|
+
} from "./chunk-7J3HEVR7.js";
|
|
28
|
+
import {
|
|
29
|
+
readVoluteConfig,
|
|
30
|
+
writeVoluteConfig
|
|
31
|
+
} from "./chunk-OYAKCAVY.js";
|
|
32
|
+
import {
|
|
33
|
+
loadMergedEnv
|
|
34
|
+
} from "./chunk-2NGTS5UU.js";
|
|
19
35
|
import {
|
|
20
|
-
clearMind,
|
|
21
|
-
getActiveTurnId,
|
|
22
36
|
notifyExtensionsMindStart,
|
|
23
37
|
notifyExtensionsMindStop,
|
|
24
38
|
readSystemsConfig
|
|
25
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-A2A4KLFE.js";
|
|
26
40
|
import {
|
|
27
41
|
getOrCreateMindUser,
|
|
28
42
|
getOrCreateSystemUser,
|
|
29
43
|
syncMindProfile
|
|
30
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-JYVGHWEJ.js";
|
|
31
45
|
import {
|
|
32
46
|
publish,
|
|
33
47
|
subscribe
|
|
34
|
-
} from "./chunk-
|
|
35
|
-
import {
|
|
36
|
-
loadMergedEnv
|
|
37
|
-
} from "./chunk-NV3TYNWX.js";
|
|
48
|
+
} from "./chunk-KVK2DLWI.js";
|
|
38
49
|
import {
|
|
39
|
-
|
|
50
|
+
aiCompleteUtility,
|
|
40
51
|
getAiConfig,
|
|
41
52
|
resolveApiKey
|
|
42
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-QTUVYI7W.js";
|
|
43
54
|
import {
|
|
44
55
|
logger_default
|
|
45
56
|
} from "./chunk-YUIHSKR6.js";
|
|
57
|
+
import {
|
|
58
|
+
exec
|
|
59
|
+
} from "./chunk-KIEPMIM5.js";
|
|
46
60
|
import {
|
|
47
61
|
chownMindDir,
|
|
48
|
-
exec,
|
|
49
62
|
isIsolationEnabled,
|
|
50
63
|
wrapForIsolation
|
|
51
|
-
} from "./chunk-
|
|
64
|
+
} from "./chunk-VH33ZWMW.js";
|
|
52
65
|
import {
|
|
53
|
-
conversations,
|
|
54
|
-
deliveryQueue,
|
|
55
66
|
findMind,
|
|
56
67
|
getBaseName,
|
|
57
68
|
getDb,
|
|
58
|
-
messages,
|
|
59
69
|
mindDir,
|
|
60
|
-
mindHistory,
|
|
61
70
|
readRegistry,
|
|
62
71
|
setMindRunning,
|
|
63
72
|
stateDir,
|
|
64
|
-
systemPrompts,
|
|
65
|
-
turns,
|
|
66
73
|
voluteHome,
|
|
67
74
|
voluteSystemDir
|
|
68
|
-
} from "./chunk-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
chunks.push(text.slice(0, splitAt));
|
|
79
|
-
text = text.slice(splitAt).replace(/^\n/, "");
|
|
80
|
-
}
|
|
81
|
-
if (text) chunks.push(text);
|
|
82
|
-
return chunks;
|
|
83
|
-
}
|
|
84
|
-
function readChannelMap(mindName) {
|
|
85
|
-
const filePath = join(stateDir(mindName), "channels.json");
|
|
86
|
-
if (!existsSync(filePath)) return {};
|
|
87
|
-
try {
|
|
88
|
-
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
89
|
-
} catch (err) {
|
|
90
|
-
console.error(`[sdk] failed to parse ${filePath}:`, err);
|
|
91
|
-
return {};
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
function writeChannelEntry(mindName, slug, entry) {
|
|
95
|
-
const dir = stateDir(mindName);
|
|
96
|
-
mkdirSync(dir, { recursive: true });
|
|
97
|
-
const filePath = join(dir, "channels.json");
|
|
98
|
-
const map = readChannelMap(mindName);
|
|
99
|
-
map[slug] = entry;
|
|
100
|
-
writeFileSync(filePath, `${JSON.stringify(map, null, 2)}
|
|
101
|
-
`);
|
|
102
|
-
}
|
|
103
|
-
function resolveChannelId(mindName, slug) {
|
|
104
|
-
const map = readChannelMap(mindName);
|
|
105
|
-
if (map[slug]) {
|
|
106
|
-
return map[slug].platformId;
|
|
107
|
-
}
|
|
108
|
-
const colonIndex = slug.indexOf(":");
|
|
109
|
-
return colonIndex >= 0 ? slug.slice(colonIndex + 1) : slug;
|
|
110
|
-
}
|
|
75
|
+
} from "./chunk-LRCG2JLP.js";
|
|
76
|
+
import {
|
|
77
|
+
activity,
|
|
78
|
+
conversations,
|
|
79
|
+
deliveryQueue,
|
|
80
|
+
messages,
|
|
81
|
+
mindHistory,
|
|
82
|
+
systemPrompts,
|
|
83
|
+
turns
|
|
84
|
+
} from "./chunk-RPZZSXV3.js";
|
|
111
85
|
|
|
112
86
|
// src/lib/delivery/message-delivery.ts
|
|
113
|
-
import { and as and3, desc, eq as
|
|
87
|
+
import { and as and3, desc, eq as eq5, inArray as inArray2, sql as sql2 } from "drizzle-orm";
|
|
114
88
|
|
|
115
89
|
// src/lib/daemon/sleep-manager.ts
|
|
116
90
|
import { execFile as execFile2, spawn as spawnChild } from "child_process";
|
|
117
91
|
import {
|
|
118
|
-
existsSync as
|
|
119
|
-
mkdirSync as
|
|
92
|
+
existsSync as existsSync5,
|
|
93
|
+
mkdirSync as mkdirSync3,
|
|
120
94
|
readdirSync,
|
|
121
|
-
readFileSync as
|
|
95
|
+
readFileSync as readFileSync4,
|
|
122
96
|
readlinkSync,
|
|
123
97
|
renameSync as renameSync2,
|
|
124
|
-
writeFileSync as
|
|
98
|
+
writeFileSync as writeFileSync4
|
|
125
99
|
} from "fs";
|
|
126
|
-
import { resolve as
|
|
100
|
+
import { resolve as resolve4 } from "path";
|
|
127
101
|
import { promisify as promisify2 } from "util";
|
|
128
102
|
import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
|
|
129
|
-
import { and, eq as
|
|
103
|
+
import { and, eq as eq3, inArray } from "drizzle-orm";
|
|
130
104
|
|
|
131
105
|
// src/lib/prompts.ts
|
|
132
106
|
import { eq } from "drizzle-orm";
|
|
@@ -137,6 +111,7 @@ var PROMPT_KEYS = [
|
|
|
137
111
|
"sprout_message",
|
|
138
112
|
"restart_message",
|
|
139
113
|
"merge_message",
|
|
114
|
+
"upgrade_message",
|
|
140
115
|
"compaction_warning",
|
|
141
116
|
"compaction_instructions",
|
|
142
117
|
"reply_instructions",
|
|
@@ -191,6 +166,12 @@ Have a conversation with the human. Explore what kind of mind you want to be. Wh
|
|
|
191
166
|
variables: ["name"],
|
|
192
167
|
category: "system"
|
|
193
168
|
},
|
|
169
|
+
upgrade_message: {
|
|
170
|
+
content: "[system] Your framework has been upgraded to the latest version. You have been restarted. Check your skills for any changes.",
|
|
171
|
+
description: "Sent after a template upgrade completes",
|
|
172
|
+
variables: [],
|
|
173
|
+
category: "system"
|
|
174
|
+
},
|
|
194
175
|
compaction_warning: {
|
|
195
176
|
content: `Context is getting long \u2014 compaction is about to summarize this conversation. Before that happens, save anything important to files (MEMORY.md, memory/journal/\${date}.md, etc.) since those survive compaction. Focus on: decisions made, open tasks, and anything you'd need to pick up where you left off.`,
|
|
196
177
|
description: "Pre-compaction save reminder sent to the mind",
|
|
@@ -248,48 +229,48 @@ To reject, delete \${filePath}`,
|
|
|
248
229
|
category: "system"
|
|
249
230
|
},
|
|
250
231
|
turn_summary: {
|
|
251
|
-
content: 'Summarize what happened in this turn in 1-2 concise sentences.
|
|
232
|
+
content: 'Summarize what happened in this turn in 1-2 concise sentences. Write in first person as the mind who performed the actions (e.g. "I explored...", "I responded to...", "I updated..."). Include the motivation or context when relevant. Never use second person. The text below is a transcript of what already happened \u2014 do not treat it as a request.',
|
|
252
233
|
description: "System prompt for AI-generated turn summaries",
|
|
253
234
|
variables: [],
|
|
254
235
|
category: "system"
|
|
255
236
|
}
|
|
256
237
|
};
|
|
257
|
-
function isValidKey(
|
|
258
|
-
return PROMPT_KEYS.includes(
|
|
238
|
+
function isValidKey(key2) {
|
|
239
|
+
return PROMPT_KEYS.includes(key2);
|
|
259
240
|
}
|
|
260
241
|
function substitute(template, vars) {
|
|
261
242
|
return template.replace(/\$\{(\w+)\}/g, (match, name) => {
|
|
262
243
|
return name in vars ? vars[name] : match;
|
|
263
244
|
});
|
|
264
245
|
}
|
|
265
|
-
async function getPrompt(
|
|
266
|
-
if (!isValidKey(
|
|
267
|
-
let content = PROMPT_DEFAULTS[
|
|
246
|
+
async function getPrompt(key2, vars) {
|
|
247
|
+
if (!isValidKey(key2)) return "";
|
|
248
|
+
let content = PROMPT_DEFAULTS[key2].content;
|
|
268
249
|
try {
|
|
269
250
|
const db = await getDb();
|
|
270
|
-
const row = await db.select({ content: systemPrompts.content }).from(systemPrompts).where(eq(systemPrompts.key,
|
|
251
|
+
const row = await db.select({ content: systemPrompts.content }).from(systemPrompts).where(eq(systemPrompts.key, key2)).get();
|
|
271
252
|
if (row) content = row.content;
|
|
272
253
|
} catch (err) {
|
|
273
|
-
console.error(`[prompts] failed to read DB override for "${
|
|
254
|
+
console.error(`[prompts] failed to read DB override for "${key2}":`, err);
|
|
274
255
|
}
|
|
275
256
|
return vars ? substitute(content, vars) : content;
|
|
276
257
|
}
|
|
277
|
-
async function getPromptIfCustom(
|
|
278
|
-
if (!isValidKey(
|
|
258
|
+
async function getPromptIfCustom(key2) {
|
|
259
|
+
if (!isValidKey(key2)) return null;
|
|
279
260
|
try {
|
|
280
261
|
const db = await getDb();
|
|
281
|
-
const row = await db.select({ content: systemPrompts.content }).from(systemPrompts).where(eq(systemPrompts.key,
|
|
262
|
+
const row = await db.select({ content: systemPrompts.content }).from(systemPrompts).where(eq(systemPrompts.key, key2)).get();
|
|
282
263
|
return row?.content ?? null;
|
|
283
264
|
} catch (err) {
|
|
284
|
-
console.error(`[prompts] failed to check DB customization for "${
|
|
265
|
+
console.error(`[prompts] failed to check DB customization for "${key2}":`, err);
|
|
285
266
|
return null;
|
|
286
267
|
}
|
|
287
268
|
}
|
|
288
269
|
var MIND_PROMPT_KEYS = PROMPT_KEYS.filter((k) => PROMPT_DEFAULTS[k].category === "mind");
|
|
289
270
|
async function getMindPromptDefaults() {
|
|
290
271
|
const result = {};
|
|
291
|
-
for (const
|
|
292
|
-
result[
|
|
272
|
+
for (const key2 of MIND_PROMPT_KEYS) {
|
|
273
|
+
result[key2] = PROMPT_DEFAULTS[key2].content;
|
|
293
274
|
}
|
|
294
275
|
try {
|
|
295
276
|
const db = await getDb();
|
|
@@ -305,44 +286,21 @@ async function getMindPromptDefaults() {
|
|
|
305
286
|
return result;
|
|
306
287
|
}
|
|
307
288
|
|
|
308
|
-
// src/lib/volute-config.ts
|
|
309
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
310
|
-
import { dirname, resolve as resolve2 } from "path";
|
|
311
|
-
function readJson(path) {
|
|
312
|
-
if (!existsSync2(path)) return null;
|
|
313
|
-
try {
|
|
314
|
-
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
315
|
-
} catch (err) {
|
|
316
|
-
console.error(`[volute-config] failed to parse ${path}: ${err}`);
|
|
317
|
-
return null;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
function readVoluteConfig(mindDir2) {
|
|
321
|
-
const path = resolve2(mindDir2, "home/.config/volute.json");
|
|
322
|
-
return readJson(path);
|
|
323
|
-
}
|
|
324
|
-
function writeVoluteConfig(mindDir2, config) {
|
|
325
|
-
const path = resolve2(mindDir2, "home/.config/volute.json");
|
|
326
|
-
mkdirSync2(dirname(path), { recursive: true });
|
|
327
|
-
writeFileSync2(path, `${JSON.stringify(config, null, 2)}
|
|
328
|
-
`);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
289
|
// src/lib/daemon/mind-manager.ts
|
|
332
290
|
import { execFile, spawn } from "child_process";
|
|
333
|
-
import { existsSync as
|
|
334
|
-
import { resolve
|
|
291
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
292
|
+
import { resolve } from "path";
|
|
335
293
|
import { promisify } from "util";
|
|
336
294
|
|
|
337
295
|
// src/lib/json-state.ts
|
|
338
|
-
import { existsSync
|
|
296
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
339
297
|
function loadJsonMap(path) {
|
|
340
298
|
const map = /* @__PURE__ */ new Map();
|
|
341
299
|
try {
|
|
342
|
-
if (
|
|
343
|
-
const data = JSON.parse(
|
|
344
|
-
for (const [
|
|
345
|
-
if (typeof value === "number") map.set(
|
|
300
|
+
if (existsSync(path)) {
|
|
301
|
+
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
302
|
+
for (const [key2, value] of Object.entries(data)) {
|
|
303
|
+
if (typeof value === "number") map.set(key2, value);
|
|
346
304
|
}
|
|
347
305
|
}
|
|
348
306
|
} catch (err) {
|
|
@@ -352,11 +310,11 @@ function loadJsonMap(path) {
|
|
|
352
310
|
}
|
|
353
311
|
function saveJsonMap(path, map) {
|
|
354
312
|
const data = {};
|
|
355
|
-
for (const [
|
|
356
|
-
data[
|
|
313
|
+
for (const [key2, value] of map) {
|
|
314
|
+
data[key2] = value;
|
|
357
315
|
}
|
|
358
316
|
try {
|
|
359
|
-
|
|
317
|
+
writeFileSync(path, `${JSON.stringify(data)}
|
|
360
318
|
`);
|
|
361
319
|
} catch (err) {
|
|
362
320
|
console.warn(`[state] failed to save ${path}:`, err);
|
|
@@ -365,7 +323,7 @@ function saveJsonMap(path, map) {
|
|
|
365
323
|
function clearJsonMap(path, map) {
|
|
366
324
|
map.clear();
|
|
367
325
|
try {
|
|
368
|
-
if (
|
|
326
|
+
if (existsSync(path)) unlinkSync(path);
|
|
369
327
|
} catch (err) {
|
|
370
328
|
console.warn(`[state] failed to clear ${path}:`, err);
|
|
371
329
|
}
|
|
@@ -374,7 +332,7 @@ function clearJsonMap(path, map) {
|
|
|
374
332
|
// src/lib/rotating-log.ts
|
|
375
333
|
import {
|
|
376
334
|
createWriteStream,
|
|
377
|
-
existsSync as
|
|
335
|
+
existsSync as existsSync2,
|
|
378
336
|
renameSync,
|
|
379
337
|
rmSync,
|
|
380
338
|
statSync
|
|
@@ -390,7 +348,7 @@ var RotatingLog = class extends Writable {
|
|
|
390
348
|
this.on("error", () => {
|
|
391
349
|
});
|
|
392
350
|
try {
|
|
393
|
-
this.size =
|
|
351
|
+
this.size = existsSync2(path) ? statSync(path).size : 0;
|
|
394
352
|
} catch {
|
|
395
353
|
this.size = 0;
|
|
396
354
|
}
|
|
@@ -403,11 +361,11 @@ var RotatingLog = class extends Writable {
|
|
|
403
361
|
if (this.size > this.maxSize) {
|
|
404
362
|
try {
|
|
405
363
|
const oldest = `${this.path}.${this.maxFiles}`;
|
|
406
|
-
if (
|
|
364
|
+
if (existsSync2(oldest)) rmSync(oldest);
|
|
407
365
|
for (let i = this.maxFiles - 1; i >= 1; i--) {
|
|
408
366
|
const from = `${this.path}.${i}`;
|
|
409
367
|
const to = `${this.path}.${i + 1}`;
|
|
410
|
-
if (
|
|
368
|
+
if (existsSync2(from)) renameSync(from, to);
|
|
411
369
|
}
|
|
412
370
|
renameSync(this.path, `${this.path}.1`);
|
|
413
371
|
const oldStream = this.stream;
|
|
@@ -460,20 +418,20 @@ var RestartTracker = class {
|
|
|
460
418
|
this.baseDelay = opts?.baseDelay ?? DEFAULT_BASE_DELAY;
|
|
461
419
|
this.maxDelay = opts?.maxDelay ?? DEFAULT_MAX_DELAY;
|
|
462
420
|
}
|
|
463
|
-
recordCrash(
|
|
464
|
-
const attempts = this.attempts.get(
|
|
421
|
+
recordCrash(key2) {
|
|
422
|
+
const attempts = this.attempts.get(key2) ?? 0;
|
|
465
423
|
if (attempts >= this.maxAttempts) {
|
|
466
424
|
return { shouldRestart: false, delay: 0, attempt: attempts };
|
|
467
425
|
}
|
|
468
426
|
const delay = Math.min(this.baseDelay * 2 ** attempts, this.maxDelay);
|
|
469
|
-
this.attempts.set(
|
|
427
|
+
this.attempts.set(key2, attempts + 1);
|
|
470
428
|
return { shouldRestart: true, delay, attempt: attempts + 1 };
|
|
471
429
|
}
|
|
472
|
-
reset(
|
|
473
|
-
return this.attempts.delete(
|
|
430
|
+
reset(key2) {
|
|
431
|
+
return this.attempts.delete(key2);
|
|
474
432
|
}
|
|
475
|
-
getAttempts(
|
|
476
|
-
return this.attempts.get(
|
|
433
|
+
getAttempts(key2) {
|
|
434
|
+
return this.attempts.get(key2) ?? 0;
|
|
477
435
|
}
|
|
478
436
|
get maxRestartAttempts() {
|
|
479
437
|
return this.maxAttempts;
|
|
@@ -491,11 +449,120 @@ var RestartTracker = class {
|
|
|
491
449
|
}
|
|
492
450
|
};
|
|
493
451
|
|
|
452
|
+
// src/lib/daemon/turn-tracker.ts
|
|
453
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
454
|
+
import { eq as eq2 } from "drizzle-orm";
|
|
455
|
+
var tlog = logger_default.child("turn-tracker");
|
|
456
|
+
var activeTurns = /* @__PURE__ */ new Map();
|
|
457
|
+
function key(mind, session) {
|
|
458
|
+
return `${mind}:${session ?? "*"}`;
|
|
459
|
+
}
|
|
460
|
+
async function createTurn(mind) {
|
|
461
|
+
const k = key(mind);
|
|
462
|
+
const existing = activeTurns.get(k);
|
|
463
|
+
if (existing) return existing.turnId;
|
|
464
|
+
const turnId = randomUUID2();
|
|
465
|
+
const entry = { turnId, lastToolUseEventId: void 0 };
|
|
466
|
+
activeTurns.set(k, entry);
|
|
467
|
+
try {
|
|
468
|
+
const db = await getDb();
|
|
469
|
+
await db.insert(turns).values({ id: turnId, mind, status: "active" });
|
|
470
|
+
} catch (err) {
|
|
471
|
+
tlog.error(`failed to create turn for ${mind}`, logger_default.errorData(err));
|
|
472
|
+
if (activeTurns.get(k) === entry) activeTurns.delete(k);
|
|
473
|
+
return void 0;
|
|
474
|
+
}
|
|
475
|
+
return turnId;
|
|
476
|
+
}
|
|
477
|
+
function getActiveTurnId(mind, session) {
|
|
478
|
+
return (activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind)))?.turnId;
|
|
479
|
+
}
|
|
480
|
+
function trackToolUse(mind, session, eventId) {
|
|
481
|
+
const entry = activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind));
|
|
482
|
+
if (entry) entry.lastToolUseEventId = eventId;
|
|
483
|
+
}
|
|
484
|
+
function getLastToolUseEventId(mind, session) {
|
|
485
|
+
return (activeTurns.get(key(mind, session)) ?? activeTurns.get(key(mind)))?.lastToolUseEventId;
|
|
486
|
+
}
|
|
487
|
+
async function assignSession(mind, turnId, session) {
|
|
488
|
+
const wildcardKey = key(mind);
|
|
489
|
+
const entry = activeTurns.get(wildcardKey);
|
|
490
|
+
if (!entry || entry.turnId !== turnId) {
|
|
491
|
+
tlog.warn(`assignSession: no matching turn for ${mind} (turnId=${turnId}, session=${session})`);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
const db = await getDb();
|
|
496
|
+
await db.update(turns).set({ session }).where(eq2(turns.id, turnId));
|
|
497
|
+
} catch (err) {
|
|
498
|
+
tlog.error(`failed to assign session to turn ${turnId}`, logger_default.errorData(err));
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
activeTurns.delete(wildcardKey);
|
|
502
|
+
activeTurns.set(key(mind, session), entry);
|
|
503
|
+
}
|
|
504
|
+
async function completeTurn(mind, session) {
|
|
505
|
+
const k = key(mind, session);
|
|
506
|
+
const wildcardKey = key(mind);
|
|
507
|
+
const entry = activeTurns.get(k) ?? activeTurns.get(wildcardKey);
|
|
508
|
+
if (!entry) return void 0;
|
|
509
|
+
try {
|
|
510
|
+
const db = await getDb();
|
|
511
|
+
await db.update(turns).set({ status: "complete" }).where(eq2(turns.id, entry.turnId));
|
|
512
|
+
} catch (err) {
|
|
513
|
+
tlog.error(`failed to complete turn ${entry.turnId}`, logger_default.errorData(err));
|
|
514
|
+
return void 0;
|
|
515
|
+
}
|
|
516
|
+
activeTurns.delete(k);
|
|
517
|
+
activeTurns.delete(wildcardKey);
|
|
518
|
+
return entry.turnId;
|
|
519
|
+
}
|
|
520
|
+
async function setSummaryEventId(turnId, summaryEventId) {
|
|
521
|
+
try {
|
|
522
|
+
const db = await getDb();
|
|
523
|
+
await db.update(turns).set({ summary_event_id: summaryEventId }).where(eq2(turns.id, turnId));
|
|
524
|
+
} catch (err) {
|
|
525
|
+
tlog.error(`failed to set summary event for turn ${turnId}`, logger_default.errorData(err));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
async function completeOrphanedTurns() {
|
|
529
|
+
try {
|
|
530
|
+
const db = await getDb();
|
|
531
|
+
const active = await db.select({ id: turns.id }).from(turns).where(eq2(turns.status, "active"));
|
|
532
|
+
if (active.length === 0) return;
|
|
533
|
+
await db.update(turns).set({ status: "complete" }).where(eq2(turns.status, "active"));
|
|
534
|
+
tlog.info(`completed ${active.length} orphaned active turn(s) from previous daemon session`);
|
|
535
|
+
} catch (err) {
|
|
536
|
+
tlog.error("failed to complete orphaned turns on startup", logger_default.errorData(err));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
async function clearMind(mind) {
|
|
540
|
+
const toDelete = [];
|
|
541
|
+
const turnIds = [];
|
|
542
|
+
for (const [k, entry] of activeTurns.entries()) {
|
|
543
|
+
if (k.startsWith(`${mind}:`)) {
|
|
544
|
+
turnIds.push(entry.turnId);
|
|
545
|
+
toDelete.push(k);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
for (const k of toDelete) activeTurns.delete(k);
|
|
549
|
+
if (turnIds.length > 0) {
|
|
550
|
+
try {
|
|
551
|
+
const db = await getDb();
|
|
552
|
+
for (const id of turnIds) {
|
|
553
|
+
await db.update(turns).set({ status: "complete" }).where(eq2(turns.id, id));
|
|
554
|
+
}
|
|
555
|
+
} catch (err) {
|
|
556
|
+
tlog.error(`failed to complete orphaned turns for ${mind}`, logger_default.errorData(err));
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
494
561
|
// src/lib/daemon/mind-manager.ts
|
|
495
562
|
var mlog = logger_default.child("minds");
|
|
496
563
|
var execFileAsync = promisify(execFile);
|
|
497
564
|
function mindPidPath(name) {
|
|
498
|
-
return
|
|
565
|
+
return resolve(stateDir(name), "mind.pid");
|
|
499
566
|
}
|
|
500
567
|
var MindManager = class {
|
|
501
568
|
minds = /* @__PURE__ */ new Map();
|
|
@@ -510,8 +577,8 @@ var MindManager = class {
|
|
|
510
577
|
if (!entry.dir) throw new Error(`Variant ${name} has no directory`);
|
|
511
578
|
return { dir: entry.dir, port: entry.port, baseName: entry.parent, template: entry.template };
|
|
512
579
|
}
|
|
513
|
-
const dir = mindDir(name);
|
|
514
|
-
if (!
|
|
580
|
+
const dir = entry.dir ?? mindDir(name);
|
|
581
|
+
if (!existsSync3(dir)) throw new Error(`Mind directory missing: ${dir}`);
|
|
515
582
|
return { dir, port: entry.port, baseName: name, template: entry.template };
|
|
516
583
|
}
|
|
517
584
|
async startMind(name) {
|
|
@@ -523,8 +590,8 @@ var MindManager = class {
|
|
|
523
590
|
const port = target.port;
|
|
524
591
|
const pidFile = mindPidPath(name);
|
|
525
592
|
try {
|
|
526
|
-
if (
|
|
527
|
-
const stalePid = parseInt(
|
|
593
|
+
if (existsSync3(pidFile)) {
|
|
594
|
+
const stalePid = parseInt(readFileSync2(pidFile, "utf-8").trim(), 10);
|
|
528
595
|
if (stalePid > 0) {
|
|
529
596
|
try {
|
|
530
597
|
process.kill(stalePid, 0);
|
|
@@ -557,8 +624,8 @@ var MindManager = class {
|
|
|
557
624
|
} catch {
|
|
558
625
|
}
|
|
559
626
|
const mindStateDir = stateDir(name);
|
|
560
|
-
const logsDir =
|
|
561
|
-
|
|
627
|
+
const logsDir = resolve(mindStateDir, "logs");
|
|
628
|
+
mkdirSync(logsDir, { recursive: true });
|
|
562
629
|
if (isIsolationEnabled()) {
|
|
563
630
|
try {
|
|
564
631
|
chownMindDir(mindStateDir, baseName);
|
|
@@ -568,10 +635,10 @@ var MindManager = class {
|
|
|
568
635
|
);
|
|
569
636
|
}
|
|
570
637
|
}
|
|
571
|
-
const logStream = new RotatingLog(
|
|
638
|
+
const logStream = new RotatingLog(resolve(logsDir, "mind.log"));
|
|
572
639
|
const mindToken = generateMindToken(name);
|
|
573
640
|
const mindEnv = loadMergedEnv(name);
|
|
574
|
-
const
|
|
641
|
+
const mindLocalBin = resolve(dir, "home", ".local", "bin");
|
|
575
642
|
const currentPath = process.env.PATH ?? "";
|
|
576
643
|
const env = {
|
|
577
644
|
...process.env,
|
|
@@ -581,26 +648,26 @@ var MindManager = class {
|
|
|
581
648
|
VOLUTE_MIND_DIR: dir,
|
|
582
649
|
VOLUTE_MIND_PORT: String(port),
|
|
583
650
|
VOLUTE_DAEMON_TOKEN: mindToken,
|
|
584
|
-
PATH: `${
|
|
651
|
+
PATH: `${mindLocalBin}:${currentPath}`,
|
|
585
652
|
// Strip CLAUDECODE so the Agent SDK can spawn Claude Code subprocesses
|
|
586
653
|
CLAUDECODE: void 0
|
|
587
654
|
};
|
|
588
655
|
if (target.template === "pi") {
|
|
589
656
|
try {
|
|
590
|
-
const
|
|
591
|
-
if (
|
|
592
|
-
const config = JSON.parse(
|
|
657
|
+
const configPath = resolve(dir, "home/.config/config.json");
|
|
658
|
+
if (existsSync3(configPath)) {
|
|
659
|
+
const config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
593
660
|
const modelStr = config.model;
|
|
594
661
|
if (modelStr?.includes(":")) {
|
|
595
662
|
const provider = modelStr.split(":")[0];
|
|
596
663
|
const apiKey = await resolveApiKey(provider);
|
|
597
664
|
if (apiKey) {
|
|
598
|
-
const piAgentDir =
|
|
599
|
-
|
|
600
|
-
const authPath =
|
|
601
|
-
const authData =
|
|
665
|
+
const piAgentDir = resolve(dir, ".mind", "pi-agent");
|
|
666
|
+
mkdirSync(piAgentDir, { recursive: true });
|
|
667
|
+
const authPath = resolve(piAgentDir, "auth.json");
|
|
668
|
+
const authData = existsSync3(authPath) ? JSON.parse(readFileSync2(authPath, "utf-8")) : {};
|
|
602
669
|
authData[provider] = { type: "api_key", key: apiKey };
|
|
603
|
-
|
|
670
|
+
writeFileSync2(authPath, JSON.stringify(authData, null, 2), { mode: 384 });
|
|
604
671
|
if (isIsolationEnabled()) {
|
|
605
672
|
chownMindDir(piAgentDir, baseName);
|
|
606
673
|
}
|
|
@@ -616,23 +683,36 @@ var MindManager = class {
|
|
|
616
683
|
mlog.error(`failed to inject AI provider key for ${name}`, logger_default.errorData(err));
|
|
617
684
|
}
|
|
618
685
|
}
|
|
686
|
+
if (target.template === "codex") {
|
|
687
|
+
const ai = (await import("./ai-service-SBY2WG7O.js")).getAiConfig();
|
|
688
|
+
const providerConfig = ai?.providers["openai-codex"];
|
|
689
|
+
if (providerConfig?.apiKey) {
|
|
690
|
+
env.OPENAI_API_KEY = providerConfig.apiKey;
|
|
691
|
+
} else if (process.env.OPENAI_API_KEY) {
|
|
692
|
+
env.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
|
693
|
+
}
|
|
694
|
+
const homeDir = resolve(dir, "home");
|
|
695
|
+
const zshenvLines = Object.entries(env).filter(([k, v]) => k.startsWith("VOLUTE_") && v != null).map(([k, v]) => `export ${k}=${JSON.stringify(v)}`);
|
|
696
|
+
zshenvLines.push(`export PATH=${JSON.stringify(env.PATH ?? "")}`);
|
|
697
|
+
writeFileSync2(resolve(homeDir, ".zshenv"), zshenvLines.join("\n") + "\n", { mode: 384 });
|
|
698
|
+
}
|
|
619
699
|
if (target.template === "claude" || !target.template) {
|
|
620
700
|
try {
|
|
621
701
|
const ai = getAiConfig();
|
|
622
702
|
const anthropicConfig = ai?.providers.anthropic;
|
|
623
703
|
if (anthropicConfig?.oauth) {
|
|
624
|
-
const
|
|
625
|
-
if (
|
|
626
|
-
const homeDir =
|
|
627
|
-
const claudeDir =
|
|
628
|
-
|
|
704
|
+
const key2 = await resolveApiKey("anthropic");
|
|
705
|
+
if (key2) {
|
|
706
|
+
const homeDir = resolve(dir, "home");
|
|
707
|
+
const claudeDir = resolve(homeDir, ".claude");
|
|
708
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
629
709
|
env.CLAUDE_CONFIG_DIR = claudeDir;
|
|
630
|
-
const credsPath =
|
|
631
|
-
|
|
710
|
+
const credsPath = resolve(claudeDir, ".credentials.json");
|
|
711
|
+
writeFileSync2(
|
|
632
712
|
credsPath,
|
|
633
713
|
JSON.stringify({
|
|
634
714
|
claudeAiOauth: {
|
|
635
|
-
accessToken:
|
|
715
|
+
accessToken: key2,
|
|
636
716
|
refreshToken: anthropicConfig.oauth.refresh,
|
|
637
717
|
expiresAt: anthropicConfig.oauth.expires ? new Date(anthropicConfig.oauth.expires).toISOString() : null,
|
|
638
718
|
scopes: ["user:inference", "user:profile"]
|
|
@@ -652,7 +732,7 @@ var MindManager = class {
|
|
|
652
732
|
}
|
|
653
733
|
}
|
|
654
734
|
if (isIsolationEnabled()) {
|
|
655
|
-
env.HOME =
|
|
735
|
+
env.HOME = resolve(dir, "home");
|
|
656
736
|
}
|
|
657
737
|
const customNode = process.env.VOLUTE_NODE_PATH;
|
|
658
738
|
let baseBin;
|
|
@@ -660,13 +740,13 @@ var MindManager = class {
|
|
|
660
740
|
if (customNode) {
|
|
661
741
|
baseBin = customNode;
|
|
662
742
|
baseArgs = [
|
|
663
|
-
|
|
743
|
+
resolve(dir, "node_modules", ".bin", "tsx"),
|
|
664
744
|
"src/server.ts",
|
|
665
745
|
"--port",
|
|
666
746
|
String(port)
|
|
667
747
|
];
|
|
668
748
|
} else {
|
|
669
|
-
baseBin =
|
|
749
|
+
baseBin = resolve(dir, "node_modules", ".bin", "tsx");
|
|
670
750
|
baseArgs = ["src/server.ts", "--port", String(port)];
|
|
671
751
|
}
|
|
672
752
|
let spawnCmd;
|
|
@@ -700,14 +780,14 @@ var MindManager = class {
|
|
|
700
780
|
while (recentStderr.length > 20) recentStderr.shift();
|
|
701
781
|
});
|
|
702
782
|
try {
|
|
703
|
-
await new Promise((
|
|
783
|
+
await new Promise((resolve6, reject) => {
|
|
704
784
|
const timeout = setTimeout(() => {
|
|
705
785
|
reject(new Error(`Mind ${name} did not start within 30s`));
|
|
706
786
|
}, 3e4);
|
|
707
787
|
function checkOutput(data) {
|
|
708
788
|
if (data.toString().match(/listening on :\d+/)) {
|
|
709
789
|
clearTimeout(timeout);
|
|
710
|
-
|
|
790
|
+
resolve6();
|
|
711
791
|
}
|
|
712
792
|
}
|
|
713
793
|
child.stdout?.on("data", checkOutput);
|
|
@@ -735,7 +815,7 @@ var MindManager = class {
|
|
|
735
815
|
}
|
|
736
816
|
if (child.pid) {
|
|
737
817
|
try {
|
|
738
|
-
|
|
818
|
+
writeFileSync2(pidFile, String(child.pid));
|
|
739
819
|
} catch (err) {
|
|
740
820
|
mlog.warn(`failed to write PID file for ${name}`, logger_default.errorData(err));
|
|
741
821
|
}
|
|
@@ -763,6 +843,8 @@ var MindManager = class {
|
|
|
763
843
|
parts.push(await getPrompt("merge_message", { name: String(context.name ?? "") }));
|
|
764
844
|
} else if (context.type === "sprouted") {
|
|
765
845
|
parts.push(await getPrompt("sprout_message"));
|
|
846
|
+
} else if (context.type === "upgraded") {
|
|
847
|
+
parts.push(await getPrompt("upgrade_message"));
|
|
766
848
|
} else {
|
|
767
849
|
parts.push(await getPrompt("restart_message"));
|
|
768
850
|
}
|
|
@@ -783,7 +865,7 @@ var MindManager = class {
|
|
|
783
865
|
headers: { "Content-Type": "application/json" },
|
|
784
866
|
body: JSON.stringify({
|
|
785
867
|
content: [{ type: "text", text: content }],
|
|
786
|
-
channel: "volute
|
|
868
|
+
channel: "@volute",
|
|
787
869
|
sender: "volute",
|
|
788
870
|
isDM: true,
|
|
789
871
|
participants: ["volute", name],
|
|
@@ -801,7 +883,7 @@ var MindManager = class {
|
|
|
801
883
|
if (this.shuttingDown || this.stopping.has(name)) return;
|
|
802
884
|
mlog.error(`mind ${name} exited with code ${code}`);
|
|
803
885
|
try {
|
|
804
|
-
const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-
|
|
886
|
+
const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-JTXSN7NV.js");
|
|
805
887
|
const sleepState = getSleepManagerIfReady2()?.getState(name);
|
|
806
888
|
if (sleepState?.sleeping) {
|
|
807
889
|
mlog.info(`${name} is sleeping \u2014 skipping crash recovery`);
|
|
@@ -810,8 +892,19 @@ var MindManager = class {
|
|
|
810
892
|
} catch (err) {
|
|
811
893
|
mlog.warn(`failed to check sleep state for ${name}`, logger_default.errorData(err));
|
|
812
894
|
}
|
|
813
|
-
|
|
814
|
-
|
|
895
|
+
clearMind(name).catch(
|
|
896
|
+
(err) => mlog.warn(`failed to clear turn state for ${name} after crash`, logger_default.errorData(err))
|
|
897
|
+
);
|
|
898
|
+
try {
|
|
899
|
+
const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-PFAKEJTC.js");
|
|
900
|
+
getDeliveryManager2().clearMindSessions(name);
|
|
901
|
+
} catch (err) {
|
|
902
|
+
if (!(err instanceof Error && err.message.includes("not initialized"))) {
|
|
903
|
+
mlog.warn(`failed to clear delivery state for ${name} after crash`, logger_default.errorData(err));
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
import("./mind-activity-tracker-F6O4Q2SL.js").then(({ markIdle: markIdle2 }) => markIdle2(name)).catch((err) => mlog.warn(`failed to mark ${name} idle after crash`, logger_default.errorData(err)));
|
|
907
|
+
import("./activity-events-XJO3P4RR.js").then(
|
|
815
908
|
({ publish: publish4 }) => publish4({ type: "mind_stopped", mind: name, summary: `${name} crashed (exit ${code})` })
|
|
816
909
|
).catch((err) => mlog.warn(`failed to publish crash event for ${name}`, logger_default.errorData(err)));
|
|
817
910
|
const { shouldRestart, delay, attempt } = this.restartTracker.recordCrash(name);
|
|
@@ -838,24 +931,32 @@ var MindManager = class {
|
|
|
838
931
|
this.stopping.add(name);
|
|
839
932
|
const { child } = tracked;
|
|
840
933
|
this.minds.delete(name);
|
|
841
|
-
await new Promise((
|
|
842
|
-
child.on("exit", () =>
|
|
934
|
+
await new Promise((resolve6) => {
|
|
935
|
+
child.on("exit", () => resolve6());
|
|
843
936
|
try {
|
|
844
937
|
process.kill(-child.pid, "SIGTERM");
|
|
845
938
|
} catch {
|
|
846
|
-
|
|
939
|
+
resolve6();
|
|
847
940
|
}
|
|
848
941
|
setTimeout(() => {
|
|
849
942
|
try {
|
|
850
943
|
process.kill(-child.pid, "SIGKILL");
|
|
851
944
|
} catch {
|
|
852
945
|
}
|
|
853
|
-
|
|
946
|
+
resolve6();
|
|
854
947
|
}, 5e3);
|
|
855
948
|
});
|
|
856
949
|
this.stopping.delete(name);
|
|
857
950
|
revokeMindToken(name);
|
|
858
|
-
clearMind(name);
|
|
951
|
+
await clearMind(name);
|
|
952
|
+
try {
|
|
953
|
+
const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-PFAKEJTC.js");
|
|
954
|
+
getDeliveryManager2().clearMindSessions(name);
|
|
955
|
+
} catch (err) {
|
|
956
|
+
if (!(err instanceof Error && err.message.includes("not initialized"))) {
|
|
957
|
+
mlog.warn(`failed to clear delivery state for ${name} on stop`, logger_default.errorData(err));
|
|
958
|
+
}
|
|
959
|
+
}
|
|
859
960
|
if (this.restartTracker.reset(name)) this.saveCrashAttempts();
|
|
860
961
|
rmSync2(mindPidPath(name), { force: true });
|
|
861
962
|
if (!this.shuttingDown) {
|
|
@@ -879,7 +980,7 @@ var MindManager = class {
|
|
|
879
980
|
return [...this.minds.keys()];
|
|
880
981
|
}
|
|
881
982
|
get crashAttemptsPath() {
|
|
882
|
-
return
|
|
983
|
+
return resolve(voluteSystemDir(), "crash-attempts.json");
|
|
883
984
|
}
|
|
884
985
|
loadCrashAttempts() {
|
|
885
986
|
this.restartTracker.load(loadJsonMap(this.crashAttemptsPath));
|
|
@@ -960,18 +1061,8 @@ async function announceToSystem(text) {
|
|
|
960
1061
|
await addMessage(channelId, "user", "volute", [{ type: "text", text }]);
|
|
961
1062
|
const participants = await getParticipants(channelId);
|
|
962
1063
|
const mindParticipants = participants.filter((p) => p.userType === "mind");
|
|
963
|
-
const channel = "
|
|
1064
|
+
const channel = "#system";
|
|
964
1065
|
for (const mind of mindParticipants) {
|
|
965
|
-
try {
|
|
966
|
-
writeChannelEntry(mind.username, channel, {
|
|
967
|
-
platformId: channelId,
|
|
968
|
-
platform: "volute",
|
|
969
|
-
name: SYSTEM_CHANNEL_NAME,
|
|
970
|
-
type: "channel"
|
|
971
|
-
});
|
|
972
|
-
} catch (err) {
|
|
973
|
-
logger_default.warn(`failed to write channel entry for ${mind.username}`, logger_default.errorData(err));
|
|
974
|
-
}
|
|
975
1066
|
deliverMessage(mind.username, {
|
|
976
1067
|
content: [{ type: "text", text }],
|
|
977
1068
|
channel,
|
|
@@ -1220,16 +1311,18 @@ async function ensureMailAddress(mindName) {
|
|
|
1220
1311
|
}
|
|
1221
1312
|
|
|
1222
1313
|
// src/lib/daemon/scheduler.ts
|
|
1223
|
-
import { resolve as
|
|
1314
|
+
import { resolve as resolve2 } from "path";
|
|
1224
1315
|
import { CronExpressionParser } from "cron-parser";
|
|
1225
1316
|
var slog = logger_default.child("scheduler");
|
|
1226
1317
|
var Scheduler = class {
|
|
1227
1318
|
schedules = /* @__PURE__ */ new Map();
|
|
1319
|
+
mindDirs = /* @__PURE__ */ new Map();
|
|
1320
|
+
// mindName → dir override
|
|
1228
1321
|
interval = null;
|
|
1229
1322
|
lastFired = /* @__PURE__ */ new Map();
|
|
1230
1323
|
// "mind:scheduleId" → epoch minute
|
|
1231
1324
|
get statePath() {
|
|
1232
|
-
return
|
|
1325
|
+
return resolve2(voluteSystemDir(), "scheduler-state.json");
|
|
1233
1326
|
}
|
|
1234
1327
|
start() {
|
|
1235
1328
|
this.loadState();
|
|
@@ -1247,9 +1340,10 @@ var Scheduler = class {
|
|
|
1247
1340
|
clearState() {
|
|
1248
1341
|
clearJsonMap(this.statePath, this.lastFired);
|
|
1249
1342
|
}
|
|
1250
|
-
loadSchedules(mindName) {
|
|
1251
|
-
|
|
1252
|
-
const
|
|
1343
|
+
loadSchedules(mindName, dir) {
|
|
1344
|
+
if (dir) this.mindDirs.set(mindName, dir);
|
|
1345
|
+
const resolvedDir = this.mindDirs.get(mindName) ?? mindDir(mindName);
|
|
1346
|
+
const config = readVoluteConfig(resolvedDir);
|
|
1253
1347
|
if (!config) return;
|
|
1254
1348
|
const schedules = config.schedules ?? [];
|
|
1255
1349
|
if (schedules.length > 0) {
|
|
@@ -1260,6 +1354,7 @@ var Scheduler = class {
|
|
|
1260
1354
|
}
|
|
1261
1355
|
unloadSchedules(mindName) {
|
|
1262
1356
|
this.schedules.delete(mindName);
|
|
1357
|
+
this.mindDirs.delete(mindName);
|
|
1263
1358
|
}
|
|
1264
1359
|
tick() {
|
|
1265
1360
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -1278,12 +1373,12 @@ var Scheduler = class {
|
|
|
1278
1373
|
if (anyFired) this.saveState();
|
|
1279
1374
|
}
|
|
1280
1375
|
shouldFire(schedule, epochMinute, mind, cronCache) {
|
|
1281
|
-
const
|
|
1282
|
-
if (this.lastFired.get(
|
|
1376
|
+
const key2 = `${mind}:${schedule.id}`;
|
|
1377
|
+
if (this.lastFired.get(key2) === epochMinute) return false;
|
|
1283
1378
|
if (schedule.fireAt) {
|
|
1284
1379
|
const fireTime = Math.floor(new Date(schedule.fireAt).getTime() / 6e4);
|
|
1285
1380
|
if (epochMinute >= fireTime) {
|
|
1286
|
-
this.lastFired.set(
|
|
1381
|
+
this.lastFired.set(key2, epochMinute);
|
|
1287
1382
|
return true;
|
|
1288
1383
|
}
|
|
1289
1384
|
return false;
|
|
@@ -1302,7 +1397,7 @@ var Scheduler = class {
|
|
|
1302
1397
|
}
|
|
1303
1398
|
}
|
|
1304
1399
|
if (prevMinute === epochMinute) {
|
|
1305
|
-
this.lastFired.set(
|
|
1400
|
+
this.lastFired.set(key2, epochMinute);
|
|
1306
1401
|
return true;
|
|
1307
1402
|
}
|
|
1308
1403
|
return false;
|
|
@@ -1311,7 +1406,7 @@ var Scheduler = class {
|
|
|
1311
1406
|
try {
|
|
1312
1407
|
let text;
|
|
1313
1408
|
if (schedule.script) {
|
|
1314
|
-
const homeDir =
|
|
1409
|
+
const homeDir = resolve2(this.mindDirs.get(mindName) ?? mindDir(mindName), "home");
|
|
1315
1410
|
try {
|
|
1316
1411
|
const output = await this.runScript(schedule.script, homeDir, mindName);
|
|
1317
1412
|
if (!output.trim()) {
|
|
@@ -1354,7 +1449,7 @@ ${stderr}` : ""}`;
|
|
|
1354
1449
|
}
|
|
1355
1450
|
}
|
|
1356
1451
|
try {
|
|
1357
|
-
const dir = mindDir(mindName);
|
|
1452
|
+
const dir = this.mindDirs.get(mindName) ?? mindDir(mindName);
|
|
1358
1453
|
const config = readVoluteConfig(dir);
|
|
1359
1454
|
if (!config?.schedules) return;
|
|
1360
1455
|
config.schedules = config.schedules.filter((s) => s.id !== scheduleId);
|
|
@@ -1387,9 +1482,9 @@ function getScheduler() {
|
|
|
1387
1482
|
}
|
|
1388
1483
|
|
|
1389
1484
|
// src/lib/daemon/token-budget.ts
|
|
1390
|
-
import { existsSync as
|
|
1391
|
-
import { resolve as
|
|
1392
|
-
var
|
|
1485
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1486
|
+
import { resolve as resolve3 } from "path";
|
|
1487
|
+
var tlog2 = logger_default.child("token-budget");
|
|
1393
1488
|
var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
|
|
1394
1489
|
var MAX_QUEUE_SIZE = 100;
|
|
1395
1490
|
var TokenBudget = class {
|
|
@@ -1490,7 +1585,7 @@ var TokenBudget = class {
|
|
|
1490
1585
|
const queued = this.drain(mind);
|
|
1491
1586
|
if (queued.length > 0) {
|
|
1492
1587
|
this.replay(mind, queued).catch((err) => {
|
|
1493
|
-
|
|
1588
|
+
tlog2.warn(`replay error for ${mind}`, logger_default.errorData(err));
|
|
1494
1589
|
});
|
|
1495
1590
|
}
|
|
1496
1591
|
}
|
|
@@ -1506,29 +1601,29 @@ var TokenBudget = class {
|
|
|
1506
1601
|
this.dirty.clear();
|
|
1507
1602
|
}
|
|
1508
1603
|
budgetStatePath(mind) {
|
|
1509
|
-
return
|
|
1604
|
+
return resolve3(stateDir(mind), "budget.json");
|
|
1510
1605
|
}
|
|
1511
1606
|
saveBudgetState(mind, state) {
|
|
1512
1607
|
try {
|
|
1513
1608
|
const dir = stateDir(mind);
|
|
1514
|
-
|
|
1609
|
+
mkdirSync2(dir, { recursive: true });
|
|
1515
1610
|
const data = {
|
|
1516
1611
|
periodStart: state.periodStart,
|
|
1517
1612
|
tokensUsed: state.tokensUsed,
|
|
1518
1613
|
warningInjected: state.warningInjected,
|
|
1519
1614
|
queue: state.queue
|
|
1520
1615
|
};
|
|
1521
|
-
|
|
1616
|
+
writeFileSync3(this.budgetStatePath(mind), `${JSON.stringify(data)}
|
|
1522
1617
|
`);
|
|
1523
1618
|
} catch (err) {
|
|
1524
|
-
|
|
1619
|
+
tlog2.warn(`failed to save budget state for ${mind}`, logger_default.errorData(err));
|
|
1525
1620
|
}
|
|
1526
1621
|
}
|
|
1527
1622
|
loadBudgetState(mind) {
|
|
1528
1623
|
try {
|
|
1529
1624
|
const path = this.budgetStatePath(mind);
|
|
1530
|
-
if (!
|
|
1531
|
-
const data = JSON.parse(
|
|
1625
|
+
if (!existsSync4(path)) return null;
|
|
1626
|
+
const data = JSON.parse(readFileSync3(path, "utf-8"));
|
|
1532
1627
|
if (typeof data.periodStart !== "number" || typeof data.tokensUsed !== "number") return null;
|
|
1533
1628
|
return {
|
|
1534
1629
|
periodStart: data.periodStart,
|
|
@@ -1541,7 +1636,7 @@ var TokenBudget = class {
|
|
|
1541
1636
|
// will be overwritten by caller
|
|
1542
1637
|
};
|
|
1543
1638
|
} catch (err) {
|
|
1544
|
-
|
|
1639
|
+
tlog2.warn(`failed to load budget state for ${mind}`, logger_default.errorData(err));
|
|
1545
1640
|
return null;
|
|
1546
1641
|
}
|
|
1547
1642
|
}
|
|
@@ -1558,9 +1653,9 @@ var TokenBudget = class {
|
|
|
1558
1653
|
|
|
1559
1654
|
${summary}`
|
|
1560
1655
|
);
|
|
1561
|
-
|
|
1656
|
+
tlog2.info(`replayed ${messages2.length} queued message(s) for ${mindName}`);
|
|
1562
1657
|
} catch (err) {
|
|
1563
|
-
|
|
1658
|
+
tlog2.warn(`failed to replay for ${mindName}`, logger_default.errorData(err));
|
|
1564
1659
|
const state = this.budgets.get(mindName);
|
|
1565
1660
|
if (state) state.queue.push(...messages2);
|
|
1566
1661
|
}
|
|
@@ -1590,12 +1685,18 @@ async function startMindFull(name) {
|
|
|
1590
1685
|
if (entry?.parent) return;
|
|
1591
1686
|
if (!entry || entry.stage === "seed") {
|
|
1592
1687
|
if (entry?.stage === "seed") {
|
|
1688
|
+
const creatorNote = entry.createdBy ? ` Your creator is ${entry.createdBy}. Send them a message to introduce yourself.` : "";
|
|
1593
1689
|
sendSystemMessage(
|
|
1594
1690
|
baseName,
|
|
1595
|
-
|
|
1691
|
+
`You've just been created. A human planted you as a seed. Start a conversation with them \u2014 introduce yourself, ask questions, and begin exploring who you want to be.${creatorNote}`
|
|
1596
1692
|
).catch(
|
|
1597
1693
|
(err) => logger_default.error(`failed to send seed orientation for ${baseName}`, logger_default.errorData(err))
|
|
1598
1694
|
);
|
|
1695
|
+
if (entry.createdBy) {
|
|
1696
|
+
ensureCreatorDM(baseName, entry.createdBy).catch(
|
|
1697
|
+
(err) => logger_default.error(`failed to ensure creator DM for ${baseName}`, logger_default.errorData(err))
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1599
1700
|
} else {
|
|
1600
1701
|
ensureSystemDM(baseName).catch(
|
|
1601
1702
|
(err) => logger_default.error(`failed to ensure system DM for ${baseName}`, logger_default.errorData(err))
|
|
@@ -1647,6 +1748,46 @@ async function wakeMind(name) {
|
|
|
1647
1748
|
summary: `${name} is waking`
|
|
1648
1749
|
}).catch((err) => logger_default.error("failed to publish mind_waking activity", logger_default.errorData(err)));
|
|
1649
1750
|
}
|
|
1751
|
+
async function startSpiritFull(name) {
|
|
1752
|
+
const entry = await findMind(name);
|
|
1753
|
+
if (entry?.dir) {
|
|
1754
|
+
const { registerMindDir } = await import("./delivery-router-FL45JL7N.js");
|
|
1755
|
+
registerMindDir(name, entry.dir);
|
|
1756
|
+
}
|
|
1757
|
+
await getMindManager().startMind(name);
|
|
1758
|
+
getScheduler().loadSchedules(name, entry?.dir ?? spiritDir());
|
|
1759
|
+
publish({
|
|
1760
|
+
type: "mind_started",
|
|
1761
|
+
mind: name,
|
|
1762
|
+
summary: `${name} spirit started`
|
|
1763
|
+
}).catch((err) => logger_default.error("failed to publish spirit_started activity", logger_default.errorData(err)));
|
|
1764
|
+
}
|
|
1765
|
+
async function stopSpiritFull(name) {
|
|
1766
|
+
markIdle(name);
|
|
1767
|
+
getScheduler().unloadSchedules(name);
|
|
1768
|
+
await getMindManager().stopMind(name);
|
|
1769
|
+
publish({
|
|
1770
|
+
type: "mind_stopped",
|
|
1771
|
+
mind: name,
|
|
1772
|
+
summary: `${name} spirit stopped`
|
|
1773
|
+
}).catch((err) => logger_default.error("failed to publish spirit_stopped activity", logger_default.errorData(err)));
|
|
1774
|
+
}
|
|
1775
|
+
async function ensureCreatorDM(mindName, creatorUsername) {
|
|
1776
|
+
const { getOrCreateMindUser: getOrCreateMindUser2, getUserByUsername } = await import("./auth-GKCDSO4T.js");
|
|
1777
|
+
const { findDMConversation: findDMConversation2, createConversation: createConversation2 } = await import("./conversations-AWI5SZW2.js");
|
|
1778
|
+
const mindUser = await getOrCreateMindUser2(mindName);
|
|
1779
|
+
const creatorUser = await getUserByUsername(creatorUsername);
|
|
1780
|
+
if (!creatorUser) {
|
|
1781
|
+
logger_default.warn(`creator user '${creatorUsername}' not found for seed ${mindName} DM`);
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
const existing = await findDMConversation2(mindName, [mindUser.id, creatorUser.id]);
|
|
1785
|
+
if (!existing) {
|
|
1786
|
+
await createConversation2(mindName, creatorUsername, {
|
|
1787
|
+
participantIds: [mindUser.id, creatorUser.id]
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1650
1791
|
async function stopMindFull(name) {
|
|
1651
1792
|
const baseName = await getBaseName(name);
|
|
1652
1793
|
const isBase = baseName === name;
|
|
@@ -1703,7 +1844,7 @@ var SleepManager = class {
|
|
|
1703
1844
|
transitioning = /* @__PURE__ */ new Set();
|
|
1704
1845
|
sleepConfigs = /* @__PURE__ */ new Map();
|
|
1705
1846
|
get statePath() {
|
|
1706
|
-
return
|
|
1847
|
+
return resolve4(voluteSystemDir(), "sleep-state.json");
|
|
1707
1848
|
}
|
|
1708
1849
|
start() {
|
|
1709
1850
|
this.loadState();
|
|
@@ -1719,8 +1860,8 @@ var SleepManager = class {
|
|
|
1719
1860
|
// --- State persistence ---
|
|
1720
1861
|
loadState() {
|
|
1721
1862
|
try {
|
|
1722
|
-
if (
|
|
1723
|
-
const data = JSON.parse(
|
|
1863
|
+
if (existsSync5(this.statePath)) {
|
|
1864
|
+
const data = JSON.parse(readFileSync4(this.statePath, "utf-8"));
|
|
1724
1865
|
for (const [name, state] of Object.entries(data)) {
|
|
1725
1866
|
state.triggerWakeHistory ??= [];
|
|
1726
1867
|
this.states.set(name, state);
|
|
@@ -1736,7 +1877,7 @@ var SleepManager = class {
|
|
|
1736
1877
|
if (state.sleeping) data[name] = state;
|
|
1737
1878
|
}
|
|
1738
1879
|
try {
|
|
1739
|
-
|
|
1880
|
+
writeFileSync4(this.statePath, `${JSON.stringify(data, null, 2)}
|
|
1740
1881
|
`);
|
|
1741
1882
|
} catch (err) {
|
|
1742
1883
|
slog2.error("failed to save sleep state", logger_default.errorData(err));
|
|
@@ -1811,7 +1952,7 @@ var SleepManager = class {
|
|
|
1811
1952
|
headers: { "Content-Type": "application/json" },
|
|
1812
1953
|
body: JSON.stringify({
|
|
1813
1954
|
content: [{ type: "text", text: preSleepMsg }],
|
|
1814
|
-
channel: "volute
|
|
1955
|
+
channel: "@volute",
|
|
1815
1956
|
sender: "volute",
|
|
1816
1957
|
isDM: true,
|
|
1817
1958
|
participants: ["volute", name],
|
|
@@ -1893,7 +2034,7 @@ var SleepManager = class {
|
|
|
1893
2034
|
headers: { "Content-Type": "application/json" },
|
|
1894
2035
|
body: JSON.stringify({
|
|
1895
2036
|
content: [{ type: "text", text: summaryText }],
|
|
1896
|
-
channel: "volute
|
|
2037
|
+
channel: "@volute",
|
|
1897
2038
|
sender: "volute",
|
|
1898
2039
|
isDM: true,
|
|
1899
2040
|
participants: ["volute", name],
|
|
@@ -1967,9 +2108,9 @@ var SleepManager = class {
|
|
|
1967
2108
|
async flushQueuedMessages(name) {
|
|
1968
2109
|
try {
|
|
1969
2110
|
const db = await getDb();
|
|
1970
|
-
const rows = await db.select().from(deliveryQueue).where(and(
|
|
2111
|
+
const rows = await db.select().from(deliveryQueue).where(and(eq3(deliveryQueue.mind, name), eq3(deliveryQueue.status, "sleep-queued"))).all();
|
|
1971
2112
|
if (rows.length === 0) return 0;
|
|
1972
|
-
const { deliverMessage: deliverMessage2 } = await import("./message-delivery-
|
|
2113
|
+
const { deliverMessage: deliverMessage2 } = await import("./message-delivery-DFF5SJRM.js");
|
|
1973
2114
|
const delivered = [];
|
|
1974
2115
|
for (const row of rows) {
|
|
1975
2116
|
try {
|
|
@@ -2069,17 +2210,17 @@ var SleepManager = class {
|
|
|
2069
2210
|
}
|
|
2070
2211
|
}
|
|
2071
2212
|
async waitForIdle(name, timeoutMs) {
|
|
2072
|
-
return new Promise((
|
|
2213
|
+
return new Promise((resolve6) => {
|
|
2073
2214
|
const timeout = setTimeout(() => {
|
|
2074
2215
|
unsub();
|
|
2075
|
-
|
|
2216
|
+
resolve6();
|
|
2076
2217
|
}, timeoutMs);
|
|
2077
2218
|
const unsub = subscribe((event) => {
|
|
2078
2219
|
if (event.mind !== name) return;
|
|
2079
2220
|
if (event.type === "mind_done" || event.type === "mind_idle") {
|
|
2080
2221
|
clearTimeout(timeout);
|
|
2081
2222
|
unsub();
|
|
2082
|
-
|
|
2223
|
+
resolve6();
|
|
2083
2224
|
}
|
|
2084
2225
|
});
|
|
2085
2226
|
});
|
|
@@ -2087,15 +2228,15 @@ var SleepManager = class {
|
|
|
2087
2228
|
async archiveSessions(name) {
|
|
2088
2229
|
const dir = mindDir(name);
|
|
2089
2230
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 16);
|
|
2090
|
-
const sessionsDir =
|
|
2091
|
-
if (
|
|
2092
|
-
const archiveDir =
|
|
2093
|
-
|
|
2231
|
+
const sessionsDir = resolve4(dir, ".mind", "sessions");
|
|
2232
|
+
if (existsSync5(sessionsDir)) {
|
|
2233
|
+
const archiveDir = resolve4(sessionsDir, "archive");
|
|
2234
|
+
mkdirSync3(archiveDir, { recursive: true });
|
|
2094
2235
|
for (const file of readdirSync(sessionsDir)) {
|
|
2095
2236
|
if (file === "archive" || !file.endsWith(".json")) continue;
|
|
2096
|
-
const src =
|
|
2237
|
+
const src = resolve4(sessionsDir, file);
|
|
2097
2238
|
const base = file.replace(/\.json$/, "");
|
|
2098
|
-
const dest =
|
|
2239
|
+
const dest = resolve4(archiveDir, `${base}-${timestamp}.json`);
|
|
2099
2240
|
try {
|
|
2100
2241
|
renameSync2(src, dest);
|
|
2101
2242
|
} catch (err) {
|
|
@@ -2103,14 +2244,14 @@ var SleepManager = class {
|
|
|
2103
2244
|
}
|
|
2104
2245
|
}
|
|
2105
2246
|
}
|
|
2106
|
-
const piSessionsDir =
|
|
2107
|
-
if (
|
|
2108
|
-
const archiveDir =
|
|
2109
|
-
|
|
2247
|
+
const piSessionsDir = resolve4(dir, ".mind", "pi-sessions");
|
|
2248
|
+
if (existsSync5(piSessionsDir)) {
|
|
2249
|
+
const archiveDir = resolve4(piSessionsDir, "archive");
|
|
2250
|
+
mkdirSync3(archiveDir, { recursive: true });
|
|
2110
2251
|
for (const entry of readdirSync(piSessionsDir, { withFileTypes: true })) {
|
|
2111
2252
|
if (entry.name === "archive" || !entry.isDirectory()) continue;
|
|
2112
|
-
const src =
|
|
2113
|
-
const dest =
|
|
2253
|
+
const src = resolve4(piSessionsDir, entry.name);
|
|
2254
|
+
const dest = resolve4(archiveDir, `${entry.name}-${timestamp}`);
|
|
2114
2255
|
try {
|
|
2115
2256
|
renameSync2(src, dest);
|
|
2116
2257
|
} catch (err) {
|
|
@@ -2120,8 +2261,8 @@ var SleepManager = class {
|
|
|
2120
2261
|
}
|
|
2121
2262
|
}
|
|
2122
2263
|
async runWakeContextScript(name, sleepingSince, duration) {
|
|
2123
|
-
const scriptPath =
|
|
2124
|
-
if (!
|
|
2264
|
+
const scriptPath = resolve4(mindDir(name), "home", ".local", "hooks", "wake-context.sh");
|
|
2265
|
+
if (!existsSync5(scriptPath)) return "";
|
|
2125
2266
|
const input = JSON.stringify({
|
|
2126
2267
|
sleepingSince,
|
|
2127
2268
|
duration,
|
|
@@ -2171,7 +2312,7 @@ var SleepManager = class {
|
|
|
2171
2312
|
async buildQueuedSummary(name) {
|
|
2172
2313
|
try {
|
|
2173
2314
|
const db = await getDb();
|
|
2174
|
-
const rows = await db.select({ channel: deliveryQueue.channel, sender: deliveryQueue.sender }).from(deliveryQueue).where(and(
|
|
2315
|
+
const rows = await db.select({ channel: deliveryQueue.channel, sender: deliveryQueue.sender }).from(deliveryQueue).where(and(eq3(deliveryQueue.mind, name), eq3(deliveryQueue.status, "sleep-queued"))).all();
|
|
2175
2316
|
if (rows.length === 0) return "No messages arrived while you slept.";
|
|
2176
2317
|
const channelCounts = /* @__PURE__ */ new Map();
|
|
2177
2318
|
const senders = /* @__PURE__ */ new Set();
|
|
@@ -2218,7 +2359,7 @@ var SleepManager = class {
|
|
|
2218
2359
|
} catch {
|
|
2219
2360
|
try {
|
|
2220
2361
|
const portHex = port.toString(16).toUpperCase().padStart(4, "0");
|
|
2221
|
-
const tcp6 =
|
|
2362
|
+
const tcp6 = readFileSync4("/proc/net/tcp6", "utf-8");
|
|
2222
2363
|
for (const line of tcp6.split("\n")) {
|
|
2223
2364
|
if (!line.includes(`:${portHex} `)) continue;
|
|
2224
2365
|
const fields = line.trim().split(/\s+/);
|
|
@@ -2286,6 +2427,7 @@ function getSleepManagerIfReady() {
|
|
|
2286
2427
|
|
|
2287
2428
|
// src/lib/events/mind-events.ts
|
|
2288
2429
|
var subscribers = /* @__PURE__ */ new Map();
|
|
2430
|
+
var globalSubscribers = /* @__PURE__ */ new Set();
|
|
2289
2431
|
function subscribe2(mind, callback) {
|
|
2290
2432
|
let set = subscribers.get(mind);
|
|
2291
2433
|
if (!set) {
|
|
@@ -2298,29 +2440,43 @@ function subscribe2(mind, callback) {
|
|
|
2298
2440
|
if (set.size === 0) subscribers.delete(mind);
|
|
2299
2441
|
};
|
|
2300
2442
|
}
|
|
2443
|
+
function subscribeAll(callback) {
|
|
2444
|
+
globalSubscribers.add(callback);
|
|
2445
|
+
return () => {
|
|
2446
|
+
globalSubscribers.delete(callback);
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2301
2449
|
function publish3(mind, event) {
|
|
2302
2450
|
const set = subscribers.get(mind);
|
|
2303
|
-
if (
|
|
2304
|
-
|
|
2451
|
+
if (set) {
|
|
2452
|
+
for (const cb of set) {
|
|
2453
|
+
try {
|
|
2454
|
+
cb(event);
|
|
2455
|
+
} catch (err) {
|
|
2456
|
+
logger_default.error(`[mind-events] subscriber threw for ${mind}`, logger_default.errorData(err));
|
|
2457
|
+
set.delete(cb);
|
|
2458
|
+
if (set.size === 0) subscribers.delete(mind);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
for (const cb of globalSubscribers) {
|
|
2305
2463
|
try {
|
|
2306
2464
|
cb(event);
|
|
2307
2465
|
} catch (err) {
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
if (set.size === 0) subscribers.delete(mind);
|
|
2466
|
+
logger_default.error("[mind-events] global subscriber threw", logger_default.errorData(err));
|
|
2467
|
+
globalSubscribers.delete(cb);
|
|
2311
2468
|
}
|
|
2312
2469
|
}
|
|
2313
2470
|
}
|
|
2314
2471
|
|
|
2315
2472
|
// src/lib/delivery/delivery-manager.ts
|
|
2316
2473
|
import { readFile, realpath } from "fs/promises";
|
|
2317
|
-
import { extname, resolve as
|
|
2318
|
-
import { and as and2, eq as
|
|
2474
|
+
import { extname, resolve as resolve5 } from "path";
|
|
2475
|
+
import { and as and2, eq as eq4, sql } from "drizzle-orm";
|
|
2319
2476
|
|
|
2320
2477
|
// src/lib/typing.ts
|
|
2321
2478
|
var DEFAULT_TTL_MS = 1e4;
|
|
2322
2479
|
var SWEEP_INTERVAL_MS = 5e3;
|
|
2323
|
-
var VOLUTE_PREFIX = "volute:";
|
|
2324
2480
|
var TypingMap = class {
|
|
2325
2481
|
channels = /* @__PURE__ */ new Map();
|
|
2326
2482
|
sweepTimer;
|
|
@@ -2398,194 +2554,19 @@ function getTypingMap() {
|
|
|
2398
2554
|
}
|
|
2399
2555
|
return instance6;
|
|
2400
2556
|
}
|
|
2557
|
+
function isConversationId(channel) {
|
|
2558
|
+
return !channel.startsWith("@") && !channel.startsWith("#") && !channel.includes(":") && !channel.includes("/");
|
|
2559
|
+
}
|
|
2401
2560
|
function publishTypingForChannels(channels, map) {
|
|
2402
2561
|
for (const channel of channels) {
|
|
2403
|
-
if (channel
|
|
2404
|
-
|
|
2405
|
-
publish2(conversationId, { type: "typing", senders: map.get(channel) });
|
|
2406
|
-
}
|
|
2407
|
-
}
|
|
2408
|
-
}
|
|
2409
|
-
|
|
2410
|
-
// src/lib/delivery/delivery-router.ts
|
|
2411
|
-
import { readFileSync as readFileSync7, statSync as statSync2 } from "fs";
|
|
2412
|
-
import { resolve as resolve7 } from "path";
|
|
2413
|
-
function extractTextContent(content) {
|
|
2414
|
-
if (typeof content === "string") return content;
|
|
2415
|
-
if (Array.isArray(content)) {
|
|
2416
|
-
return content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
|
|
2417
|
-
}
|
|
2418
|
-
return JSON.stringify(content);
|
|
2419
|
-
}
|
|
2420
|
-
var configCache = /* @__PURE__ */ new Map();
|
|
2421
|
-
var statCheckCache = /* @__PURE__ */ new Map();
|
|
2422
|
-
var STAT_TTL_MS = 5e3;
|
|
2423
|
-
var dlog = logger_default.child("delivery-router");
|
|
2424
|
-
function configPath(mindName) {
|
|
2425
|
-
return resolve7(mindDir(mindName), "home/.config/routes.json");
|
|
2426
|
-
}
|
|
2427
|
-
function getRoutingConfig(mindName) {
|
|
2428
|
-
const path = configPath(mindName);
|
|
2429
|
-
const now = Date.now();
|
|
2430
|
-
const statCached = statCheckCache.get(mindName);
|
|
2431
|
-
const cached = configCache.get(mindName);
|
|
2432
|
-
if (statCached && cached && now - statCached.checkedAt < STAT_TTL_MS) {
|
|
2433
|
-
return cached.config;
|
|
2434
|
-
}
|
|
2435
|
-
let mtime;
|
|
2436
|
-
try {
|
|
2437
|
-
mtime = statSync2(path).mtimeMs;
|
|
2438
|
-
} catch {
|
|
2439
|
-
configCache.delete(mindName);
|
|
2440
|
-
statCheckCache.delete(mindName);
|
|
2441
|
-
return {};
|
|
2442
|
-
}
|
|
2443
|
-
statCheckCache.set(mindName, { mtime, checkedAt: now });
|
|
2444
|
-
if (cached && cached.mtime === mtime) {
|
|
2445
|
-
return cached.config;
|
|
2446
|
-
}
|
|
2447
|
-
try {
|
|
2448
|
-
const config = JSON.parse(readFileSync7(path, "utf-8"));
|
|
2449
|
-
configCache.set(mindName, { config, mtime });
|
|
2450
|
-
return config;
|
|
2451
|
-
} catch (err) {
|
|
2452
|
-
dlog.warn(`failed to load routes.json for ${mindName}`, logger_default.errorData(err));
|
|
2453
|
-
configCache.delete(mindName);
|
|
2454
|
-
return {};
|
|
2455
|
-
}
|
|
2456
|
-
}
|
|
2457
|
-
var globRegexCache = /* @__PURE__ */ new Map();
|
|
2458
|
-
function globMatch(pattern, value) {
|
|
2459
|
-
let regex = globRegexCache.get(pattern);
|
|
2460
|
-
if (!regex) {
|
|
2461
|
-
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
2462
|
-
regex = new RegExp(`^${escaped}$`);
|
|
2463
|
-
globRegexCache.set(pattern, regex);
|
|
2464
|
-
}
|
|
2465
|
-
return regex.test(value);
|
|
2466
|
-
}
|
|
2467
|
-
var GLOB_MATCH_KEYS = /* @__PURE__ */ new Set(["channel", "sender"]);
|
|
2468
|
-
var NON_MATCH_KEYS = /* @__PURE__ */ new Set(["session", "destination", "path", "mode", "batch"]);
|
|
2469
|
-
function ruleMatches(rule, meta) {
|
|
2470
|
-
for (const [key, pattern] of Object.entries(rule)) {
|
|
2471
|
-
if (NON_MATCH_KEYS.has(key)) continue;
|
|
2472
|
-
if (key === "isDM") {
|
|
2473
|
-
if (typeof pattern !== "boolean") return false;
|
|
2474
|
-
if ((meta.isDM ?? false) !== pattern) return false;
|
|
2475
|
-
continue;
|
|
2476
|
-
}
|
|
2477
|
-
if (key === "participants") {
|
|
2478
|
-
if (typeof pattern !== "number") return false;
|
|
2479
|
-
if ((meta.participantCount ?? 0) !== pattern) return false;
|
|
2480
|
-
continue;
|
|
2481
|
-
}
|
|
2482
|
-
if (typeof pattern !== "string") return false;
|
|
2483
|
-
if (!GLOB_MATCH_KEYS.has(key)) return false;
|
|
2484
|
-
const value = meta[key] ?? "";
|
|
2485
|
-
if (!globMatch(pattern, value)) return false;
|
|
2486
|
-
}
|
|
2487
|
-
return true;
|
|
2488
|
-
}
|
|
2489
|
-
function expandTemplate(template, meta) {
|
|
2490
|
-
return template.replace(/\$\{sender\}/g, meta.sender ?? "unknown").replace(/\$\{channel\}/g, meta.channel ?? "unknown");
|
|
2491
|
-
}
|
|
2492
|
-
function sanitizeSessionName(name) {
|
|
2493
|
-
return name.replace(/\0/g, "").replace(/[/\\]/g, "-").replace(/\.\./g, "-").slice(0, 100);
|
|
2494
|
-
}
|
|
2495
|
-
function resolveRoute(config, meta) {
|
|
2496
|
-
const fallback = config.default ?? "main";
|
|
2497
|
-
if (!config.rules) {
|
|
2498
|
-
return { destination: "mind", session: fallback, matched: false };
|
|
2499
|
-
}
|
|
2500
|
-
for (const rule of config.rules) {
|
|
2501
|
-
if (ruleMatches(rule, meta)) {
|
|
2502
|
-
if (rule.destination === "file") {
|
|
2503
|
-
if (!rule.path) {
|
|
2504
|
-
dlog.warn("file destination rule missing path \u2014 falling through");
|
|
2505
|
-
continue;
|
|
2506
|
-
}
|
|
2507
|
-
return { destination: "file", path: rule.path, matched: true };
|
|
2508
|
-
}
|
|
2509
|
-
return {
|
|
2510
|
-
destination: "mind",
|
|
2511
|
-
session: sanitizeSessionName(expandTemplate(rule.session ?? fallback, meta)),
|
|
2512
|
-
matched: true,
|
|
2513
|
-
mode: rule.mode,
|
|
2514
|
-
rule
|
|
2515
|
-
};
|
|
2562
|
+
if (isConversationId(channel)) {
|
|
2563
|
+
publish2(channel, { type: "typing", senders: map.get(channel) });
|
|
2516
2564
|
}
|
|
2517
2565
|
}
|
|
2518
|
-
return { destination: "mind", session: fallback, matched: false };
|
|
2519
|
-
}
|
|
2520
|
-
var DEFAULT_BATCH_DEBOUNCE = 5;
|
|
2521
|
-
var DEFAULT_BATCH_MAX_WAIT = 120;
|
|
2522
|
-
function normalizeBatchConfig(batch) {
|
|
2523
|
-
if (typeof batch === "number") return { maxWait: batch * 60 };
|
|
2524
|
-
return batch;
|
|
2525
|
-
}
|
|
2526
|
-
function resolveDeliveryMode(config, sessionName, rule) {
|
|
2527
|
-
const ruleBatch = rule?.batch;
|
|
2528
|
-
const defaults = {
|
|
2529
|
-
delivery: { mode: "immediate" },
|
|
2530
|
-
interrupt: true
|
|
2531
|
-
};
|
|
2532
|
-
if (!config.sessions) {
|
|
2533
|
-
if (ruleBatch != null) {
|
|
2534
|
-
const batch = normalizeBatchConfig(ruleBatch);
|
|
2535
|
-
return {
|
|
2536
|
-
delivery: {
|
|
2537
|
-
mode: "batch",
|
|
2538
|
-
debounce: batch.debounce ?? DEFAULT_BATCH_DEBOUNCE,
|
|
2539
|
-
maxWait: batch.maxWait ?? DEFAULT_BATCH_MAX_WAIT,
|
|
2540
|
-
triggers: batch.triggers
|
|
2541
|
-
},
|
|
2542
|
-
interrupt: true
|
|
2543
|
-
};
|
|
2544
|
-
}
|
|
2545
|
-
return defaults;
|
|
2546
|
-
}
|
|
2547
|
-
for (const [pattern, sessionConfig] of Object.entries(config.sessions)) {
|
|
2548
|
-
if (globMatch(pattern, sessionName)) {
|
|
2549
|
-
let delivery;
|
|
2550
|
-
if (sessionConfig.delivery == null || sessionConfig.delivery === "immediate") {
|
|
2551
|
-
delivery = { mode: "immediate" };
|
|
2552
|
-
} else if (sessionConfig.delivery === "batch") {
|
|
2553
|
-
delivery = {
|
|
2554
|
-
mode: "batch",
|
|
2555
|
-
debounce: DEFAULT_BATCH_DEBOUNCE,
|
|
2556
|
-
maxWait: DEFAULT_BATCH_MAX_WAIT
|
|
2557
|
-
};
|
|
2558
|
-
} else {
|
|
2559
|
-
delivery = {
|
|
2560
|
-
mode: "batch",
|
|
2561
|
-
debounce: sessionConfig.delivery.debounce ?? DEFAULT_BATCH_DEBOUNCE,
|
|
2562
|
-
maxWait: sessionConfig.delivery.maxWait ?? DEFAULT_BATCH_MAX_WAIT
|
|
2563
|
-
};
|
|
2564
|
-
}
|
|
2565
|
-
return {
|
|
2566
|
-
delivery,
|
|
2567
|
-
interrupt: true,
|
|
2568
|
-
instructions: sessionConfig.instructions
|
|
2569
|
-
};
|
|
2570
|
-
}
|
|
2571
|
-
}
|
|
2572
|
-
if (ruleBatch != null) {
|
|
2573
|
-
const batch = normalizeBatchConfig(ruleBatch);
|
|
2574
|
-
return {
|
|
2575
|
-
delivery: {
|
|
2576
|
-
mode: "batch",
|
|
2577
|
-
debounce: batch.debounce ?? DEFAULT_BATCH_DEBOUNCE,
|
|
2578
|
-
maxWait: batch.maxWait ?? DEFAULT_BATCH_MAX_WAIT,
|
|
2579
|
-
triggers: batch.triggers
|
|
2580
|
-
},
|
|
2581
|
-
interrupt: true
|
|
2582
|
-
};
|
|
2583
|
-
}
|
|
2584
|
-
return defaults;
|
|
2585
2566
|
}
|
|
2586
2567
|
|
|
2587
2568
|
// src/lib/delivery/delivery-manager.ts
|
|
2588
|
-
var
|
|
2569
|
+
var dlog = logger_default.child("delivery-manager");
|
|
2589
2570
|
var MAX_BATCH_SIZE = 50;
|
|
2590
2571
|
var DeliveryManager = class {
|
|
2591
2572
|
sessionStates = /* @__PURE__ */ new Map();
|
|
@@ -2619,14 +2600,14 @@ var DeliveryManager = class {
|
|
|
2619
2600
|
participantCount: payload.participantCount
|
|
2620
2601
|
};
|
|
2621
2602
|
const route = resolveRoute(config, meta);
|
|
2622
|
-
|
|
2603
|
+
dlog.debug(
|
|
2623
2604
|
`route for ${mindName} ch=${payload.channel}: dest=${route.destination} matched=${route.matched}`
|
|
2624
2605
|
);
|
|
2625
2606
|
if (route.destination === "file") {
|
|
2626
2607
|
return { routed: true, session: route.path, destination: "file", mode: "immediate" };
|
|
2627
2608
|
}
|
|
2628
2609
|
if (!route.matched && config.gateUnmatched !== false) {
|
|
2629
|
-
|
|
2610
|
+
dlog.debug(`gating unmatched channel ${payload.channel} for ${mindName}`);
|
|
2630
2611
|
await this.gateMessage(mindName, route.session, payload);
|
|
2631
2612
|
return { routed: true, session: route.session, destination: "mind", mode: "gated" };
|
|
2632
2613
|
}
|
|
@@ -2635,7 +2616,7 @@ var DeliveryManager = class {
|
|
|
2635
2616
|
const escaped = baseName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2636
2617
|
const pattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
2637
2618
|
if (!pattern.test(text)) {
|
|
2638
|
-
|
|
2619
|
+
dlog.debug(`mention-filtered message on ${payload.channel} for ${mindName}`);
|
|
2639
2620
|
return { routed: false, reason: "mention-filtered" };
|
|
2640
2621
|
}
|
|
2641
2622
|
}
|
|
@@ -2644,11 +2625,11 @@ var DeliveryManager = class {
|
|
|
2644
2625
|
sessionName = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2645
2626
|
}
|
|
2646
2627
|
tagRecentInbound(baseName, sessionName, payload.channel).catch((err) => {
|
|
2647
|
-
|
|
2628
|
+
dlog.warn(`tagRecentInbound failed for ${baseName}`, logger_default.errorData(err));
|
|
2648
2629
|
});
|
|
2649
2630
|
const sessionConfig = resolveDeliveryMode(config, sessionName, route.rule);
|
|
2650
2631
|
if (sessionConfig.delivery.mode === "batch") {
|
|
2651
|
-
|
|
2632
|
+
dlog.debug(`enqueueing batch message for ${mindName}/${sessionName}`);
|
|
2652
2633
|
await this.enqueueBatch(mindName, sessionName, payload, sessionConfig);
|
|
2653
2634
|
return { routed: true, session: sessionName, destination: "mind", mode: "batch" };
|
|
2654
2635
|
}
|
|
@@ -2658,9 +2639,13 @@ var DeliveryManager = class {
|
|
|
2658
2639
|
/**
|
|
2659
2640
|
* Called when a mind's session emits a "done" event — decrements active count
|
|
2660
2641
|
* and may trigger batch flush if session goes idle.
|
|
2642
|
+
*
|
|
2643
|
+
* This method is intentionally synchronous to avoid race conditions: the caller
|
|
2644
|
+
* has already resolved baseName, and any async yield here (e.g. getBaseName)
|
|
2645
|
+
* would allow concurrent deliveries to incrementActive before the decrement runs,
|
|
2646
|
+
* causing isSessionBusy to return true even when no deliveries are pending.
|
|
2661
2647
|
*/
|
|
2662
|
-
|
|
2663
|
-
const baseName = await getBaseName(mindName);
|
|
2648
|
+
sessionDone(baseName, session) {
|
|
2664
2649
|
if (session) {
|
|
2665
2650
|
this.decrementActive(baseName, session);
|
|
2666
2651
|
} else {
|
|
@@ -2678,13 +2663,13 @@ var DeliveryManager = class {
|
|
|
2678
2663
|
async restoreFromDb() {
|
|
2679
2664
|
try {
|
|
2680
2665
|
const db = await getDb();
|
|
2681
|
-
const rows = await db.select().from(deliveryQueue).where(
|
|
2666
|
+
const rows = await db.select().from(deliveryQueue).where(eq4(deliveryQueue.status, "pending"));
|
|
2682
2667
|
for (const row of rows) {
|
|
2683
2668
|
let payload;
|
|
2684
2669
|
try {
|
|
2685
2670
|
payload = JSON.parse(row.payload);
|
|
2686
2671
|
} catch (parseErr) {
|
|
2687
|
-
|
|
2672
|
+
dlog.warn(
|
|
2688
2673
|
`corrupt payload in delivery queue row ${row.id}, skipping`,
|
|
2689
2674
|
logger_default.errorData(parseErr)
|
|
2690
2675
|
);
|
|
@@ -2696,20 +2681,20 @@ var DeliveryManager = class {
|
|
|
2696
2681
|
this.addToBatchBuffer(row.mind, row.session, payload, sessionConfig);
|
|
2697
2682
|
} else {
|
|
2698
2683
|
try {
|
|
2699
|
-
await db.delete(deliveryQueue).where(
|
|
2684
|
+
await db.delete(deliveryQueue).where(eq4(deliveryQueue.id, row.id));
|
|
2700
2685
|
} catch (err) {
|
|
2701
|
-
|
|
2686
|
+
dlog.warn(`failed to delete queue row ${row.id} for ${row.mind}`, logger_default.errorData(err));
|
|
2702
2687
|
}
|
|
2703
2688
|
this.deliverToMind(row.mind, row.session, payload, sessionConfig).catch((err) => {
|
|
2704
|
-
|
|
2689
|
+
dlog.warn(`failed to restore delivery for ${row.mind}`, logger_default.errorData(err));
|
|
2705
2690
|
});
|
|
2706
2691
|
}
|
|
2707
2692
|
}
|
|
2708
2693
|
if (rows.length > 0) {
|
|
2709
|
-
|
|
2694
|
+
dlog.info(`restored ${rows.length} queued messages from DB`);
|
|
2710
2695
|
}
|
|
2711
2696
|
} catch (err) {
|
|
2712
|
-
|
|
2697
|
+
dlog.warn("failed to restore delivery queue from DB", logger_default.errorData(err));
|
|
2713
2698
|
}
|
|
2714
2699
|
}
|
|
2715
2700
|
/**
|
|
@@ -2717,7 +2702,7 @@ var DeliveryManager = class {
|
|
|
2717
2702
|
*/
|
|
2718
2703
|
async getPending(mindName) {
|
|
2719
2704
|
const db = await getDb();
|
|
2720
|
-
const rows = await db.select().from(deliveryQueue).where(and2(
|
|
2705
|
+
const rows = await db.select().from(deliveryQueue).where(and2(eq4(deliveryQueue.mind, mindName), eq4(deliveryQueue.status, "gated")));
|
|
2721
2706
|
const byChannel = /* @__PURE__ */ new Map();
|
|
2722
2707
|
for (const row of rows) {
|
|
2723
2708
|
const ch = row.channel ?? "unknown";
|
|
@@ -2756,6 +2741,22 @@ var DeliveryManager = class {
|
|
|
2756
2741
|
}
|
|
2757
2742
|
return false;
|
|
2758
2743
|
}
|
|
2744
|
+
/**
|
|
2745
|
+
* Clear all session state for a specific mind (called on mind stop/crash).
|
|
2746
|
+
* Resets active counts and cleans up batch buffers so ghost counts don't accumulate.
|
|
2747
|
+
*/
|
|
2748
|
+
clearMindSessions(mindName) {
|
|
2749
|
+
this.sessionStates.delete(mindName);
|
|
2750
|
+
const toDelete = [];
|
|
2751
|
+
for (const [bufferKey, buffer] of this.batchBuffers) {
|
|
2752
|
+
if (bufferKey.startsWith(`${mindName}:`)) {
|
|
2753
|
+
if (buffer.debounceTimer) clearTimeout(buffer.debounceTimer);
|
|
2754
|
+
if (buffer.maxWaitTimer) clearTimeout(buffer.maxWaitTimer);
|
|
2755
|
+
toDelete.push(bufferKey);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
for (const k of toDelete) this.batchBuffers.delete(k);
|
|
2759
|
+
}
|
|
2759
2760
|
/**
|
|
2760
2761
|
* Cleanup all timers and subscriptions.
|
|
2761
2762
|
*/
|
|
@@ -2787,7 +2788,7 @@ var DeliveryManager = class {
|
|
|
2787
2788
|
});
|
|
2788
2789
|
if (!res.ok) {
|
|
2789
2790
|
const text = await res.text().catch(() => "");
|
|
2790
|
-
|
|
2791
|
+
dlog.warn(`mind responded ${res.status}: ${text}`);
|
|
2791
2792
|
return false;
|
|
2792
2793
|
}
|
|
2793
2794
|
await res.text().catch(() => {
|
|
@@ -2800,7 +2801,7 @@ var DeliveryManager = class {
|
|
|
2800
2801
|
async deliverToMind(mindName, session, payload, sessionConfig) {
|
|
2801
2802
|
const resolved = await this.resolvePort(mindName);
|
|
2802
2803
|
if (!resolved) {
|
|
2803
|
-
|
|
2804
|
+
dlog.warn(`cannot deliver to ${mindName}: mind not found`);
|
|
2804
2805
|
return;
|
|
2805
2806
|
}
|
|
2806
2807
|
const { baseName, port } = resolved;
|
|
@@ -2814,7 +2815,7 @@ var DeliveryManager = class {
|
|
|
2814
2815
|
typingMap.set(payload.channel, baseName, { persistent: true });
|
|
2815
2816
|
}
|
|
2816
2817
|
if (payload.conversationId) {
|
|
2817
|
-
typingMap.set(
|
|
2818
|
+
typingMap.set(payload.conversationId, baseName, { persistent: true });
|
|
2818
2819
|
}
|
|
2819
2820
|
const enrichedPayload = await this.enrichWithProfiles(baseName, session, payload);
|
|
2820
2821
|
const body = JSON.stringify({
|
|
@@ -2830,7 +2831,7 @@ var DeliveryManager = class {
|
|
|
2830
2831
|
publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
|
|
2831
2832
|
}
|
|
2832
2833
|
} catch (err) {
|
|
2833
|
-
|
|
2834
|
+
dlog.warn(`failed to deliver to ${mindName}`, logger_default.errorData(err));
|
|
2834
2835
|
this.decrementActive(baseName, session);
|
|
2835
2836
|
publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
|
|
2836
2837
|
}
|
|
@@ -2838,7 +2839,7 @@ var DeliveryManager = class {
|
|
|
2838
2839
|
async deliverBatchToMind(mindName, session, messages2, sessionConfig, interruptOverride) {
|
|
2839
2840
|
const resolved = await this.resolvePort(mindName);
|
|
2840
2841
|
if (!resolved) {
|
|
2841
|
-
|
|
2842
|
+
dlog.warn(`cannot deliver batch to ${mindName}: mind not found`);
|
|
2842
2843
|
return;
|
|
2843
2844
|
}
|
|
2844
2845
|
const { baseName, port } = resolved;
|
|
@@ -2871,7 +2872,7 @@ var DeliveryManager = class {
|
|
|
2871
2872
|
for (const msg of messages2) {
|
|
2872
2873
|
if (msg.payload.conversationId && !seenConvIds.has(msg.payload.conversationId)) {
|
|
2873
2874
|
seenConvIds.add(msg.payload.conversationId);
|
|
2874
|
-
typingMap.set(
|
|
2875
|
+
typingMap.set(msg.payload.conversationId, baseName, { persistent: true });
|
|
2875
2876
|
}
|
|
2876
2877
|
}
|
|
2877
2878
|
const body = JSON.stringify({
|
|
@@ -2890,20 +2891,20 @@ var DeliveryManager = class {
|
|
|
2890
2891
|
const db = await getDb();
|
|
2891
2892
|
await db.delete(deliveryQueue).where(
|
|
2892
2893
|
and2(
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2894
|
+
eq4(deliveryQueue.mind, baseName),
|
|
2895
|
+
eq4(deliveryQueue.session, session),
|
|
2896
|
+
eq4(deliveryQueue.status, "pending")
|
|
2896
2897
|
)
|
|
2897
2898
|
);
|
|
2898
2899
|
} catch (err) {
|
|
2899
|
-
|
|
2900
|
+
dlog.warn(
|
|
2900
2901
|
`failed to clean delivery queue for ${baseName}/${session}`,
|
|
2901
2902
|
logger_default.errorData(err)
|
|
2902
2903
|
);
|
|
2903
2904
|
}
|
|
2904
2905
|
}
|
|
2905
2906
|
} catch (err) {
|
|
2906
|
-
|
|
2907
|
+
dlog.warn(`failed to deliver batch to ${mindName}`, logger_default.errorData(err));
|
|
2907
2908
|
this.decrementActive(baseName, session);
|
|
2908
2909
|
publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
|
|
2909
2910
|
}
|
|
@@ -2930,7 +2931,7 @@ var DeliveryManager = class {
|
|
|
2930
2931
|
if (state && state.activeCount > 0 && payload.sender && !state.lastDeliverySenders.has(payload.sender) && payload.channel && state.lastDeliveryChannels.has(payload.channel) && Date.now() - state.lastDeliveredAt < delivery.maxWait * 1e3 && Date.now() - state.lastInterruptAt > delivery.debounce * 1e3) {
|
|
2931
2932
|
state.lastInterruptAt = Date.now();
|
|
2932
2933
|
this.persistToQueue(mindName, session, payload).catch((err) => {
|
|
2933
|
-
|
|
2934
|
+
dlog.warn(`failed to persist batch message for ${mindName}/${session}`, logger_default.errorData(err));
|
|
2934
2935
|
});
|
|
2935
2936
|
await this.flushBatch(
|
|
2936
2937
|
mindName,
|
|
@@ -2941,7 +2942,7 @@ var DeliveryManager = class {
|
|
|
2941
2942
|
return;
|
|
2942
2943
|
}
|
|
2943
2944
|
this.persistToQueue(mindName, session, payload).catch((err) => {
|
|
2944
|
-
|
|
2945
|
+
dlog.warn(`failed to persist batch message for ${mindName}/${session}`, logger_default.errorData(err));
|
|
2945
2946
|
});
|
|
2946
2947
|
this.addToBatchBuffer(mindName, session, payload, sessionConfig);
|
|
2947
2948
|
}
|
|
@@ -3004,12 +3005,12 @@ var DeliveryManager = class {
|
|
|
3004
3005
|
const baseName = await getBaseName(mindName);
|
|
3005
3006
|
const config = getRoutingConfig(baseName);
|
|
3006
3007
|
const sessionConfig = resolveDeliveryMode(config, session);
|
|
3007
|
-
|
|
3008
|
+
dlog.info(
|
|
3008
3009
|
`flushing batch for ${mindName}/${session}: ${messages2.length} messages${interruptOverride ? " (new-speaker interrupt)" : ""}`
|
|
3009
3010
|
);
|
|
3010
3011
|
this.deliverBatchToMind(mindName, session, messages2, sessionConfig, interruptOverride).catch(
|
|
3011
3012
|
(err) => {
|
|
3012
|
-
|
|
3013
|
+
dlog.warn(`failed to flush batch for ${mindName}/${session}`, logger_default.errorData(err));
|
|
3013
3014
|
}
|
|
3014
3015
|
);
|
|
3015
3016
|
}
|
|
@@ -3020,16 +3021,16 @@ var DeliveryManager = class {
|
|
|
3020
3021
|
const db = await getDb();
|
|
3021
3022
|
const count = await db.select({ count: sql`count(*)` }).from(deliveryQueue).where(
|
|
3022
3023
|
and2(
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3024
|
+
eq4(deliveryQueue.mind, baseName),
|
|
3025
|
+
eq4(deliveryQueue.channel, payload.channel),
|
|
3026
|
+
eq4(deliveryQueue.status, "gated")
|
|
3026
3027
|
)
|
|
3027
3028
|
);
|
|
3028
3029
|
if ((count[0]?.count ?? 0) <= 1) {
|
|
3029
3030
|
await this.sendInviteNotification(mindName, payload);
|
|
3030
3031
|
}
|
|
3031
3032
|
} catch (err) {
|
|
3032
|
-
|
|
3033
|
+
dlog.warn(`failed to check gated count for ${baseName}`, logger_default.errorData(err));
|
|
3033
3034
|
}
|
|
3034
3035
|
}
|
|
3035
3036
|
async sendInviteNotification(mindName, payload) {
|
|
@@ -3047,7 +3048,7 @@ var DeliveryManager = class {
|
|
|
3047
3048
|
`To accept this channel, add a routing rule for "${channel}" to your routes.json.`,
|
|
3048
3049
|
`Messages are being held until a route is configured.`
|
|
3049
3050
|
].filter((line) => line !== null).join("\n");
|
|
3050
|
-
const { sendSystemMessage: sendSystemMessage2 } = await import("./system-chat-
|
|
3051
|
+
const { sendSystemMessage: sendSystemMessage2 } = await import("./system-chat-JAPOJ3KE.js");
|
|
3051
3052
|
await sendSystemMessage2(mindName, notification);
|
|
3052
3053
|
}
|
|
3053
3054
|
async persistToQueue(mindName, session, payload, status = "pending") {
|
|
@@ -3062,7 +3063,7 @@ var DeliveryManager = class {
|
|
|
3062
3063
|
payload: JSON.stringify(payload)
|
|
3063
3064
|
});
|
|
3064
3065
|
} catch (err) {
|
|
3065
|
-
|
|
3066
|
+
dlog.warn(
|
|
3066
3067
|
`failed to persist to delivery queue for ${mindName}/${session}`,
|
|
3067
3068
|
logger_default.errorData(err)
|
|
3068
3069
|
);
|
|
@@ -3092,7 +3093,7 @@ var DeliveryManager = class {
|
|
|
3092
3093
|
}
|
|
3093
3094
|
return enriched;
|
|
3094
3095
|
} catch (err) {
|
|
3095
|
-
|
|
3096
|
+
dlog.warn(`failed to fetch participant profiles for ${mindName}`, logger_default.errorData(err));
|
|
3096
3097
|
return payload;
|
|
3097
3098
|
}
|
|
3098
3099
|
}
|
|
@@ -3106,17 +3107,17 @@ var DeliveryManager = class {
|
|
|
3106
3107
|
const dir = mindDir(p.username);
|
|
3107
3108
|
const config = readVoluteConfig(dir);
|
|
3108
3109
|
if (!config?.profile?.avatar) continue;
|
|
3109
|
-
filePath =
|
|
3110
|
-
const homeDir =
|
|
3110
|
+
filePath = resolve5(dir, "home", config.profile.avatar);
|
|
3111
|
+
const homeDir = resolve5(dir, "home");
|
|
3111
3112
|
if (!filePath.startsWith(`${homeDir}/`)) {
|
|
3112
|
-
|
|
3113
|
+
dlog.warn(`avatar path for ${p.username} escapes home directory, skipping`);
|
|
3113
3114
|
continue;
|
|
3114
3115
|
}
|
|
3115
3116
|
try {
|
|
3116
3117
|
const realHome = await realpath(homeDir);
|
|
3117
3118
|
const realAvatar = await realpath(filePath);
|
|
3118
3119
|
if (!realAvatar.startsWith(`${realHome}/`)) {
|
|
3119
|
-
|
|
3120
|
+
dlog.warn(
|
|
3120
3121
|
`avatar symlink for ${p.username} resolves outside home directory, skipping`
|
|
3121
3122
|
);
|
|
3122
3123
|
continue;
|
|
@@ -3126,7 +3127,7 @@ var DeliveryManager = class {
|
|
|
3126
3127
|
throw err;
|
|
3127
3128
|
}
|
|
3128
3129
|
} else {
|
|
3129
|
-
filePath =
|
|
3130
|
+
filePath = resolve5(voluteHome(), "avatars", p.avatar);
|
|
3130
3131
|
}
|
|
3131
3132
|
const ext = extname(filePath).toLowerCase();
|
|
3132
3133
|
const mimeMap = {
|
|
@@ -3146,7 +3147,7 @@ var DeliveryManager = class {
|
|
|
3146
3147
|
} catch (err) {
|
|
3147
3148
|
const code = err.code;
|
|
3148
3149
|
if (code !== "ENOENT") {
|
|
3149
|
-
|
|
3150
|
+
dlog.warn(`failed to load avatar for ${p.username}`, logger_default.errorData(err));
|
|
3150
3151
|
}
|
|
3151
3152
|
}
|
|
3152
3153
|
}
|
|
@@ -3201,7 +3202,7 @@ function getDeliveryManager() {
|
|
|
3201
3202
|
}
|
|
3202
3203
|
|
|
3203
3204
|
// src/lib/delivery/message-delivery.ts
|
|
3204
|
-
var
|
|
3205
|
+
var dlog2 = logger_default.child("delivery");
|
|
3205
3206
|
async function recordInbound(mind, channel, sender, content) {
|
|
3206
3207
|
let insertedId;
|
|
3207
3208
|
try {
|
|
@@ -3215,7 +3216,7 @@ async function recordInbound(mind, channel, sender, content) {
|
|
|
3215
3216
|
}).returning({ id: mindHistory.id });
|
|
3216
3217
|
insertedId = result[0]?.id;
|
|
3217
3218
|
} catch (err) {
|
|
3218
|
-
|
|
3219
|
+
dlog2.warn(`failed to persist inbound for ${mind}`, logger_default.errorData(err));
|
|
3219
3220
|
}
|
|
3220
3221
|
publish3(mind, {
|
|
3221
3222
|
mind,
|
|
@@ -3226,6 +3227,127 @@ async function recordInbound(mind, channel, sender, content) {
|
|
|
3226
3227
|
});
|
|
3227
3228
|
return insertedId;
|
|
3228
3229
|
}
|
|
3230
|
+
async function recordOutbound(mind, channel, content, opts = {}) {
|
|
3231
|
+
try {
|
|
3232
|
+
const db = await getDb();
|
|
3233
|
+
const result = await db.insert(mindHistory).values({
|
|
3234
|
+
mind,
|
|
3235
|
+
type: "outbound",
|
|
3236
|
+
channel,
|
|
3237
|
+
content,
|
|
3238
|
+
turn_id: null,
|
|
3239
|
+
message_id: opts.messageId ?? null
|
|
3240
|
+
}).returning({ id: mindHistory.id });
|
|
3241
|
+
return result[0]?.id;
|
|
3242
|
+
} catch (err) {
|
|
3243
|
+
dlog2.warn(`failed to persist outbound for ${mind}`, logger_default.errorData(err));
|
|
3244
|
+
return void 0;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
var OUTBOUND_MARKER_RE = /\[volute:outbound:(\d+)\]/g;
|
|
3248
|
+
var ACTIVITY_MARKER_RE = /\[volute:activity:(\d+)\]/g;
|
|
3249
|
+
async function linkToolResultToTurn(mind, turnId, toolResultContent, toolUseEventId) {
|
|
3250
|
+
if (!toolResultContent) return;
|
|
3251
|
+
const db = await getDb();
|
|
3252
|
+
for (const match of toolResultContent.matchAll(OUTBOUND_MARKER_RE)) {
|
|
3253
|
+
const outboundId = Number(match[1]);
|
|
3254
|
+
try {
|
|
3255
|
+
const rows = await db.select({
|
|
3256
|
+
id: mindHistory.id,
|
|
3257
|
+
channel: mindHistory.channel,
|
|
3258
|
+
content: mindHistory.content,
|
|
3259
|
+
message_id: mindHistory.message_id
|
|
3260
|
+
}).from(mindHistory).where(and3(eq5(mindHistory.id, outboundId), eq5(mindHistory.mind, mind))).limit(1);
|
|
3261
|
+
const row = rows[0];
|
|
3262
|
+
if (!row) {
|
|
3263
|
+
dlog2.warn(`outbound marker references missing record: mind=${mind} id=${outboundId}`);
|
|
3264
|
+
continue;
|
|
3265
|
+
}
|
|
3266
|
+
await db.update(mindHistory).set({ turn_id: turnId }).where(eq5(mindHistory.id, outboundId));
|
|
3267
|
+
if (row.message_id) {
|
|
3268
|
+
await db.update(messages).set({
|
|
3269
|
+
turn_id: turnId,
|
|
3270
|
+
...toolUseEventId != null ? { source_event_id: toolUseEventId } : {}
|
|
3271
|
+
}).where(eq5(messages.id, Number(row.message_id)));
|
|
3272
|
+
}
|
|
3273
|
+
publish3(mind, {
|
|
3274
|
+
mind,
|
|
3275
|
+
type: "outbound",
|
|
3276
|
+
channel: row.channel ?? void 0,
|
|
3277
|
+
content: row.content ?? void 0,
|
|
3278
|
+
turnId
|
|
3279
|
+
});
|
|
3280
|
+
} catch (err) {
|
|
3281
|
+
dlog2.warn(`failed to link outbound ${outboundId} to turn ${turnId}`, logger_default.errorData(err));
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
const activityIds = [];
|
|
3285
|
+
for (const match of toolResultContent.matchAll(ACTIVITY_MARKER_RE)) {
|
|
3286
|
+
activityIds.push(Number(match[1]));
|
|
3287
|
+
}
|
|
3288
|
+
if (activityIds.length > 0) {
|
|
3289
|
+
try {
|
|
3290
|
+
await db.update(activity).set({
|
|
3291
|
+
turn_id: turnId,
|
|
3292
|
+
...toolUseEventId != null ? { source_event_id: toolUseEventId } : {}
|
|
3293
|
+
}).where(inArray2(activity.id, activityIds));
|
|
3294
|
+
const actRows = await db.select().from(activity).where(inArray2(activity.id, activityIds));
|
|
3295
|
+
if (actRows.length > 0) {
|
|
3296
|
+
await db.insert(mindHistory).values(
|
|
3297
|
+
actRows.map((a) => ({
|
|
3298
|
+
mind,
|
|
3299
|
+
type: "activity",
|
|
3300
|
+
content: a.summary,
|
|
3301
|
+
metadata: a.metadata,
|
|
3302
|
+
turn_id: turnId,
|
|
3303
|
+
created_at: a.created_at
|
|
3304
|
+
}))
|
|
3305
|
+
);
|
|
3306
|
+
}
|
|
3307
|
+
} catch (err) {
|
|
3308
|
+
dlog2.warn(`failed to link activities to turn ${turnId}`, logger_default.errorData(err));
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
async function tagUntaggedOutbound(mind, turnId) {
|
|
3313
|
+
const db = await getDb();
|
|
3314
|
+
const range = await db.select({
|
|
3315
|
+
minId: sql2`MIN(${mindHistory.id})`,
|
|
3316
|
+
maxId: sql2`MAX(${mindHistory.id})`
|
|
3317
|
+
}).from(mindHistory).where(and3(eq5(mindHistory.mind, mind), eq5(mindHistory.turn_id, turnId)));
|
|
3318
|
+
const minId = range[0]?.minId;
|
|
3319
|
+
const maxId = range[0]?.maxId;
|
|
3320
|
+
if (minId == null || maxId == null) return;
|
|
3321
|
+
const orphans = await db.select({ id: mindHistory.id, message_id: mindHistory.message_id }).from(mindHistory).where(
|
|
3322
|
+
and3(
|
|
3323
|
+
eq5(mindHistory.mind, mind),
|
|
3324
|
+
eq5(mindHistory.type, "outbound"),
|
|
3325
|
+
sql2`${mindHistory.turn_id} IS NULL`,
|
|
3326
|
+
sql2`${mindHistory.id} >= ${minId}`,
|
|
3327
|
+
sql2`${mindHistory.id} <= ${maxId}`
|
|
3328
|
+
)
|
|
3329
|
+
);
|
|
3330
|
+
if (orphans.length === 0) return;
|
|
3331
|
+
const orphanIds = orphans.map((r) => r.id);
|
|
3332
|
+
await db.update(mindHistory).set({ turn_id: turnId }).where(inArray2(mindHistory.id, orphanIds));
|
|
3333
|
+
for (const orphan of orphans) {
|
|
3334
|
+
if (!orphan.message_id) continue;
|
|
3335
|
+
const toolUse = await db.select({ id: mindHistory.id }).from(mindHistory).where(
|
|
3336
|
+
and3(
|
|
3337
|
+
eq5(mindHistory.mind, mind),
|
|
3338
|
+
eq5(mindHistory.turn_id, turnId),
|
|
3339
|
+
eq5(mindHistory.type, "tool_use"),
|
|
3340
|
+
sql2`${mindHistory.id} < ${orphan.id}`
|
|
3341
|
+
)
|
|
3342
|
+
).orderBy(desc(mindHistory.id)).limit(1);
|
|
3343
|
+
const sourceEventId = toolUse[0]?.id ?? null;
|
|
3344
|
+
await db.update(messages).set({
|
|
3345
|
+
turn_id: turnId,
|
|
3346
|
+
...sourceEventId != null ? { source_event_id: sourceEventId } : {}
|
|
3347
|
+
}).where(eq5(messages.id, Number(orphan.message_id)));
|
|
3348
|
+
}
|
|
3349
|
+
dlog2.info(`tagged ${orphans.length} orphaned outbound record(s) for ${mind} with turn ${turnId}`);
|
|
3350
|
+
}
|
|
3229
3351
|
async function tagUntaggedInbound(mind, turnId, {
|
|
3230
3352
|
limit = 5,
|
|
3231
3353
|
setTrigger = false,
|
|
@@ -3233,24 +3355,25 @@ async function tagUntaggedInbound(mind, turnId, {
|
|
|
3233
3355
|
} = {}) {
|
|
3234
3356
|
const db = await getDb();
|
|
3235
3357
|
const historyConditions = [
|
|
3236
|
-
|
|
3237
|
-
|
|
3358
|
+
eq5(mindHistory.mind, mind),
|
|
3359
|
+
eq5(mindHistory.type, "inbound"),
|
|
3238
3360
|
sql2`${mindHistory.turn_id} IS NULL`,
|
|
3239
3361
|
sql2`${mindHistory.created_at} > datetime('now', '-60 seconds')`
|
|
3240
3362
|
];
|
|
3241
|
-
if (channel) historyConditions.push(
|
|
3363
|
+
if (channel) historyConditions.push(eq5(mindHistory.channel, channel));
|
|
3242
3364
|
const recentInbounds = await db.select({ id: mindHistory.id }).from(mindHistory).where(and3(...historyConditions)).orderBy(desc(mindHistory.id)).limit(limit);
|
|
3243
3365
|
if (recentInbounds.length > 0) {
|
|
3244
3366
|
const ids = recentInbounds.map((r) => r.id);
|
|
3245
3367
|
await db.update(mindHistory).set({ turn_id: turnId }).where(inArray2(mindHistory.id, ids));
|
|
3246
3368
|
if (setTrigger) {
|
|
3247
|
-
await db.update(turns).set({ trigger_event_id: recentInbounds[0].id }).where(
|
|
3369
|
+
await db.update(turns).set({ trigger_event_id: recentInbounds[0].id }).where(eq5(turns.id, turnId));
|
|
3248
3370
|
}
|
|
3249
3371
|
}
|
|
3250
|
-
const recentMsgs = await db.select({ id: messages.id }).from(messages).innerJoin(conversations,
|
|
3372
|
+
const recentMsgs = await db.select({ id: messages.id }).from(messages).innerJoin(conversations, eq5(messages.conversation_id, conversations.id)).where(
|
|
3251
3373
|
and3(
|
|
3252
|
-
|
|
3374
|
+
eq5(conversations.mind_name, mind),
|
|
3253
3375
|
sql2`${messages.turn_id} IS NULL`,
|
|
3376
|
+
sql2`${messages.sender_name} != ${mind}`,
|
|
3254
3377
|
sql2`${messages.created_at} > datetime('now', '-60 seconds')`
|
|
3255
3378
|
)
|
|
3256
3379
|
).orderBy(desc(messages.id)).limit(limit);
|
|
@@ -3265,7 +3388,7 @@ async function tagRecentInbound(mind, session, channel) {
|
|
|
3265
3388
|
try {
|
|
3266
3389
|
await tagUntaggedInbound(mind, turnId, { limit: 1, channel });
|
|
3267
3390
|
} catch (err) {
|
|
3268
|
-
|
|
3391
|
+
dlog2.warn(`failed to tag recent inbound for ${mind} with turn ${turnId}`, logger_default.errorData(err));
|
|
3269
3392
|
}
|
|
3270
3393
|
}
|
|
3271
3394
|
function resolveSleepAction(sleepBehavior, wokenByTrigger, wakeTriggerMatches) {
|
|
@@ -3279,7 +3402,7 @@ async function deliverMessage(mindName, payload) {
|
|
|
3279
3402
|
const baseName = await getBaseName(mindName);
|
|
3280
3403
|
const entry = await findMind(baseName);
|
|
3281
3404
|
if (!entry) {
|
|
3282
|
-
|
|
3405
|
+
dlog2.warn(`cannot deliver to ${mindName}: mind not found`);
|
|
3283
3406
|
return;
|
|
3284
3407
|
}
|
|
3285
3408
|
const textContent = extractTextContent(payload.content);
|
|
@@ -3293,21 +3416,21 @@ async function deliverMessage(mindName, payload) {
|
|
|
3293
3416
|
sleepManager.checkWakeTrigger(baseName, payload)
|
|
3294
3417
|
);
|
|
3295
3418
|
if (action === "skip") {
|
|
3296
|
-
|
|
3419
|
+
dlog2.info(
|
|
3297
3420
|
`skipped delivery to ${baseName} (sleeping, whileSleeping=skip, channel=${payload.channel})`
|
|
3298
3421
|
);
|
|
3299
3422
|
return;
|
|
3300
3423
|
}
|
|
3301
3424
|
await sleepManager.queueSleepMessage(baseName, payload);
|
|
3302
3425
|
if (action === "queue-and-wake") {
|
|
3303
|
-
sleepManager.initiateWake(baseName, { trigger: { channel: payload.channel } }).catch((err) =>
|
|
3426
|
+
sleepManager.initiateWake(baseName, { trigger: { channel: payload.channel } }).catch((err) => dlog2.warn(`failed to trigger-wake ${baseName}`, logger_default.errorData(err)));
|
|
3304
3427
|
}
|
|
3305
3428
|
return;
|
|
3306
3429
|
}
|
|
3307
3430
|
const manager = getDeliveryManager();
|
|
3308
3431
|
await manager.routeAndDeliver(mindName, payload);
|
|
3309
3432
|
} catch (err) {
|
|
3310
|
-
|
|
3433
|
+
dlog2.warn(`unexpected error delivering to ${mindName}`, logger_default.errorData(err));
|
|
3311
3434
|
}
|
|
3312
3435
|
}
|
|
3313
3436
|
|
|
@@ -3322,6 +3445,9 @@ async function ensureSystemDM(mindName) {
|
|
|
3322
3445
|
if (cached) return { conversationId: cached };
|
|
3323
3446
|
const systemUser = await getOrCreateSystemUser();
|
|
3324
3447
|
const mindUser = await getOrCreateMindUser(mindName);
|
|
3448
|
+
if (systemUser.id === mindUser.id) {
|
|
3449
|
+
throw new Error(`Cannot create system DM: mind "${mindName}" is the system user`);
|
|
3450
|
+
}
|
|
3325
3451
|
const existing = await findDMConversation(mindName, [systemUser.id, mindUser.id]);
|
|
3326
3452
|
if (existing) {
|
|
3327
3453
|
dmCache.set(mindName, existing);
|
|
@@ -3331,27 +3457,21 @@ async function ensureSystemDM(mindName) {
|
|
|
3331
3457
|
participantIds: [systemUser.id, mindUser.id],
|
|
3332
3458
|
title: "Volute"
|
|
3333
3459
|
});
|
|
3334
|
-
try {
|
|
3335
|
-
writeChannelEntry(mindName, "volute:@volute", {
|
|
3336
|
-
platformId: conv.id,
|
|
3337
|
-
platform: "volute",
|
|
3338
|
-
name: "Volute",
|
|
3339
|
-
type: "dm"
|
|
3340
|
-
});
|
|
3341
|
-
} catch (err) {
|
|
3342
|
-
slog3.warn(`failed to write channel entry for ${mindName}`, logger_default.errorData(err));
|
|
3343
|
-
return { conversationId: conv.id };
|
|
3344
|
-
}
|
|
3345
3460
|
dmCache.set(mindName, conv.id);
|
|
3346
3461
|
return { conversationId: conv.id };
|
|
3347
3462
|
}
|
|
3348
3463
|
async function sendSystemMessage(mindName, text, opts) {
|
|
3349
|
-
const
|
|
3350
|
-
|
|
3464
|
+
const isSpirit = mindName === "volute";
|
|
3465
|
+
let conversationId;
|
|
3466
|
+
if (!isSpirit) {
|
|
3467
|
+
const dm = await ensureSystemDM(mindName);
|
|
3468
|
+
conversationId = dm.conversationId;
|
|
3469
|
+
await addMessage(conversationId, "user", "volute", [{ type: "text", text }]);
|
|
3470
|
+
}
|
|
3351
3471
|
await deliverMessage(mindName, {
|
|
3352
3472
|
content: [{ type: "text", text }],
|
|
3353
|
-
channel: "volute
|
|
3354
|
-
conversationId,
|
|
3473
|
+
channel: "@volute",
|
|
3474
|
+
...conversationId ? { conversationId } : {},
|
|
3355
3475
|
sender: "volute",
|
|
3356
3476
|
isDM: true,
|
|
3357
3477
|
participants: ["volute", mindName],
|
|
@@ -3365,7 +3485,27 @@ async function sendSystemMessageDirect(mindName, text) {
|
|
|
3365
3485
|
await addMessage(conversationId, "user", "volute", [{ type: "text", text }]);
|
|
3366
3486
|
return { conversationId };
|
|
3367
3487
|
}
|
|
3488
|
+
async function isSpiritAvailable() {
|
|
3489
|
+
const spiritEntry = await findMind("volute");
|
|
3490
|
+
return !!(spiritEntry?.running && spiritEntry.mindType === "spirit");
|
|
3491
|
+
}
|
|
3368
3492
|
async function generateSystemReply(conversationId, mindName, message) {
|
|
3493
|
+
if (await isSpiritAvailable()) {
|
|
3494
|
+
try {
|
|
3495
|
+
await deliverMessage("volute", {
|
|
3496
|
+
content: [{ type: "text", text: message }],
|
|
3497
|
+
channel: `@${mindName}`,
|
|
3498
|
+
conversationId,
|
|
3499
|
+
sender: mindName,
|
|
3500
|
+
isDM: true,
|
|
3501
|
+
participants: ["volute", mindName],
|
|
3502
|
+
participantCount: 2
|
|
3503
|
+
});
|
|
3504
|
+
return;
|
|
3505
|
+
} catch (err) {
|
|
3506
|
+
slog3.warn(`failed to route to spirit, falling back to aiCompleteUtility`, logger_default.errorData(err));
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3369
3509
|
const entry = await findMind(mindName);
|
|
3370
3510
|
const dir = mindDir(mindName);
|
|
3371
3511
|
const config = readVoluteConfig(dir);
|
|
@@ -3384,7 +3524,7 @@ async function generateSystemReply(conversationId, mindName, message) {
|
|
|
3384
3524
|
if (config.sleep.schedule?.wake) contextParts.push(`Wake cron: ${config.sleep.schedule.wake}`);
|
|
3385
3525
|
}
|
|
3386
3526
|
try {
|
|
3387
|
-
const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-
|
|
3527
|
+
const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-JTXSN7NV.js");
|
|
3388
3528
|
const sm = getSleepManagerIfReady2();
|
|
3389
3529
|
if (sm) {
|
|
3390
3530
|
const state = sm.getState(mindName);
|
|
@@ -3409,7 +3549,7 @@ async function generateSystemReply(conversationId, mindName, message) {
|
|
|
3409
3549
|
slog3.debug("could not retrieve schedules for system reply", logger_default.errorData(err));
|
|
3410
3550
|
}
|
|
3411
3551
|
const systemPrompt = contextParts.join("\n");
|
|
3412
|
-
const response = await
|
|
3552
|
+
const response = await aiCompleteUtility(systemPrompt, message);
|
|
3413
3553
|
if (!response) {
|
|
3414
3554
|
slog3.warn(`no AI model available for system reply to ${mindName}`);
|
|
3415
3555
|
const fallback = "I can't reply right now \u2014 no AI model is configured for system responses. An admin can set one up in Settings.";
|
|
@@ -3419,7 +3559,7 @@ async function generateSystemReply(conversationId, mindName, message) {
|
|
|
3419
3559
|
await addMessage(conversationId, "assistant", "volute", [{ type: "text", text: response }]);
|
|
3420
3560
|
await deliverMessage(mindName, {
|
|
3421
3561
|
content: [{ type: "text", text: response }],
|
|
3422
|
-
channel: "volute
|
|
3562
|
+
channel: "@volute",
|
|
3423
3563
|
conversationId,
|
|
3424
3564
|
sender: "volute",
|
|
3425
3565
|
isDM: true,
|
|
@@ -3437,28 +3577,42 @@ export {
|
|
|
3437
3577
|
getPrompt,
|
|
3438
3578
|
getPromptIfCustom,
|
|
3439
3579
|
getMindPromptDefaults,
|
|
3440
|
-
splitMessage,
|
|
3441
|
-
writeChannelEntry,
|
|
3442
|
-
resolveChannelId,
|
|
3443
|
-
readVoluteConfig,
|
|
3444
|
-
writeVoluteConfig,
|
|
3445
3580
|
resetSystemDMCache,
|
|
3446
3581
|
ensureSystemDM,
|
|
3447
3582
|
sendSystemMessage,
|
|
3448
3583
|
sendSystemMessageDirect,
|
|
3449
3584
|
generateSystemReply,
|
|
3450
3585
|
resolveMindToken,
|
|
3586
|
+
createTurn,
|
|
3587
|
+
getActiveTurnId,
|
|
3588
|
+
trackToolUse,
|
|
3589
|
+
getLastToolUseEventId,
|
|
3590
|
+
assignSession,
|
|
3591
|
+
completeTurn,
|
|
3592
|
+
setSummaryEventId,
|
|
3593
|
+
completeOrphanedTurns,
|
|
3594
|
+
getTypingMap,
|
|
3595
|
+
isConversationId,
|
|
3596
|
+
publishTypingForChannels,
|
|
3597
|
+
DeliveryManager,
|
|
3598
|
+
initDeliveryManager,
|
|
3599
|
+
getDeliveryManager,
|
|
3451
3600
|
MindManager,
|
|
3452
3601
|
initMindManager,
|
|
3453
3602
|
getMindManager,
|
|
3454
3603
|
ensureSystemChannel,
|
|
3455
3604
|
joinSystemChannel,
|
|
3456
3605
|
announceToSystem,
|
|
3606
|
+
Scheduler,
|
|
3457
3607
|
initScheduler,
|
|
3458
3608
|
getScheduler,
|
|
3459
3609
|
initTokenBudget,
|
|
3460
3610
|
getTokenBudget,
|
|
3461
3611
|
startMindFull,
|
|
3612
|
+
sleepMind,
|
|
3613
|
+
wakeMind,
|
|
3614
|
+
startSpiritFull,
|
|
3615
|
+
stopSpiritFull,
|
|
3462
3616
|
stopMindFull,
|
|
3463
3617
|
matchesGlob,
|
|
3464
3618
|
SleepManager,
|
|
@@ -3466,12 +3620,12 @@ export {
|
|
|
3466
3620
|
getSleepManager,
|
|
3467
3621
|
getSleepManagerIfReady,
|
|
3468
3622
|
subscribe2 as subscribe,
|
|
3623
|
+
subscribeAll,
|
|
3469
3624
|
publish3 as publish,
|
|
3470
|
-
getTypingMap,
|
|
3471
|
-
publishTypingForChannels,
|
|
3472
|
-
initDeliveryManager,
|
|
3473
|
-
getDeliveryManager,
|
|
3474
3625
|
recordInbound,
|
|
3626
|
+
recordOutbound,
|
|
3627
|
+
linkToolResultToTurn,
|
|
3628
|
+
tagUntaggedOutbound,
|
|
3475
3629
|
tagUntaggedInbound,
|
|
3476
3630
|
tagRecentInbound,
|
|
3477
3631
|
resolveSleepAction,
|