volute 0.45.2 → 0.47.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 +4 -4
- package/dist/{activity-events-BUR2RYTJ.js → activity-events-MIOK3IOU.js} +3 -3
- package/dist/{ai-service-UODCCE4O.js → ai-service-CVZEYKTK.js} +4 -4
- package/dist/{archive-WS2L2VIQ.js → archive-6Z4GFQPH.js} +3 -3
- package/dist/{auth-FLAEXXZK.js → auth-A4VGHADI.js} +4 -4
- package/dist/avatar-image-7S3WA77A.js +22 -0
- package/dist/{backup-TST5C2YN.js → backup-IIAFOW2L.js} +13 -13
- package/dist/channels-ITE4YC57.js +89 -0
- package/dist/{chat-76MRWWQX.js → chat-AJLJCQ3M.js} +12 -8
- package/dist/{chunk-ZV7MFISX.js → chunk-2MJ7EOMT.js} +20 -2
- package/dist/{chunk-R77O2WMK.js → chunk-2OIYNHWO.js} +1 -1
- package/dist/{chunk-QCN24XEN.js → chunk-3YUBFTDN.js} +1 -1
- package/dist/{chunk-NYC5U6K7.js → chunk-4I72V3E5.js} +10 -10
- package/dist/{chunk-QNQMSWZE.js → chunk-5IERLDUS.js} +3 -3
- package/dist/{chunk-FXORA5JG.js → chunk-5NN2VHJV.js} +3 -3
- package/dist/chunk-7KRIHYLR.js +124 -0
- package/dist/{chunk-RRNXJOKB.js → chunk-BGYHLR54.js} +2 -2
- package/dist/{chunk-QMJCNZ6C.js → chunk-C3KLHJDB.js} +1 -1
- package/dist/{chunk-TQJ6ZYVY.js → chunk-CBOIHNMR.js} +2 -2
- package/dist/{chunk-H2X54EHC.js → chunk-DGGZVZTM.js} +1 -1
- package/dist/{chunk-4JOQZPCE.js → chunk-EIQDZC43.js} +1 -1
- package/dist/{chunk-YT7PEESJ.js → chunk-F55GRIEK.js} +464 -221
- package/dist/{chunk-OP4JG2BS.js → chunk-HPGN6XRT.js} +8 -4
- package/dist/{chunk-7ZUB63PS.js → chunk-IWJSNMSY.js} +5 -5
- package/dist/{chunk-ZDZISKCL.js → chunk-IYG4FBMP.js} +1 -1
- package/dist/chunk-JCMGDCGX.js +237 -0
- package/dist/{clock-K43IMROX.js → chunk-JSVHH32K.js} +26 -21
- package/dist/{chunk-UYLGEDVD.js → chunk-K23P5Q5N.js} +340 -339
- package/dist/{chunk-7C44R625.js → chunk-LTLFOSA4.js} +49 -3
- package/dist/{chunk-IHKGWTGT.js → chunk-LWDTP6DH.js} +1 -1
- package/dist/{chunk-JZ62SYWR.js → chunk-M3XDVS5Q.js} +1 -1
- package/dist/{chunk-FR6S2KNN.js → chunk-M6TTZRGH.js} +9 -9
- package/dist/{chunk-EJVPTVTI.js → chunk-MTRMQFCQ.js} +1 -1
- package/dist/{chunk-T6HYVHPG.js → chunk-ODDGGLM6.js} +3 -3
- package/dist/{chunk-QLWG3C2Z.js → chunk-RRZ4RQQU.js} +2 -2
- package/dist/{chunk-CDPBTTZO.js → chunk-RUFLGTOT.js} +2 -2
- package/dist/{chunk-F7H6ATM4.js → chunk-SKNXMUIU.js} +1 -1
- package/dist/{chunk-RXEEYDIB.js → chunk-SZJCJVXV.js} +1 -1
- package/dist/{chunk-DRJ24GXK.js → chunk-VMCZBXF2.js} +52 -8
- package/dist/{chunk-ALKYQULU.js → chunk-WW6C3PLV.js} +1 -1
- package/dist/{chunk-A7WQZKOU.js → chunk-XHN6XBWV.js} +2 -2
- package/dist/{chunk-6EMWJZBK.js → chunk-YKA7DCVW.js} +1 -1
- package/dist/cli.js +24 -24
- package/dist/clock-E6N5Y3YW.js +16 -0
- package/dist/{cloud-sync-3OJOPZRT.js → cloud-sync-LQTVDTQ3.js} +19 -17
- package/dist/{conversations-JZTD6KLR.js → conversations-C7IKYSHD.js} +4 -4
- package/dist/{create-IOV4GCRX.js → create-PQQXKTJK.js} +1 -1
- package/dist/{daemon-restart-TIHKVVVA.js → daemon-restart-5F3YTLZK.js} +7 -7
- package/dist/daemon.js +418 -273
- package/dist/{db-MF2JNRUW.js → db-FO6LXYCW.js} +2 -2
- package/dist/{delete-BFMIGAIB.js → delete-HPLC7ETU.js} +1 -1
- package/dist/delivery-manager-TN5KK2AP.js +34 -0
- package/dist/{delivery-router-N4A7ZC5T.js → delivery-router-3QFG2ZEX.js} +3 -3
- package/dist/{down-U2CZ23M4.js → down-ZI4S6265.js} +5 -5
- package/dist/echo-text-SEQCLGAT.js +34 -0
- package/dist/{exec-G3ZRST4Z.js → exec-QIFYZG4C.js} +3 -3
- package/dist/{export-GGIVO76O.js → export-4GVGNW54.js} +6 -6
- package/dist/{extensions-JONTW2HF.js → extensions-PIMIQ7J3.js} +21 -17
- package/dist/{files-4LN3EN7Q.js → files-FEFZKMNM.js} +6 -6
- package/dist/{import-42YNQBF4.js → import-7MO3YTRE.js} +6 -6
- package/dist/{isolation-YMGZZ4E5.js → isolation-MCT4ZYLT.js} +3 -3
- package/dist/{login-M6OJUPQ2.js → login-OF4WXDAN.js} +5 -5
- package/dist/{logout-WQBWO77G.js → logout-ERE2BGA3.js} +5 -5
- package/dist/message-delivery-ZE3YGJDD.js +38 -0
- package/dist/{mind-DWB6XYCH.js → mind-POOHFZHP.js} +18 -18
- package/dist/{mind-activity-tracker-POBLU3LC.js → mind-activity-tracker-YVZPC4A5.js} +4 -4
- package/dist/{mind-list-4D7J4CHD.js → mind-list-KEGRGQEQ.js} +5 -4
- package/dist/mind-service-JFY7UG5E.js +40 -0
- package/dist/mind-sleep-IN3WPA76.js +83 -0
- package/dist/{mind-status-UO34ASXZ.js → mind-status-SMUWJKGV.js} +20 -3
- package/dist/{package-A7KXT67Q.js → package-WPEE2HVJ.js} +3 -2
- package/dist/{registry-OUZBZ3Q4.js → registry-UMMDRPRM.js} +2 -2
- package/dist/{sandbox-ZJ7QA6Z6.js → sandbox-G4X4K3UN.js} +4 -4
- package/dist/scheduler-HQSDYJFY.js +32 -0
- package/dist/{schema-PVNG3PQM.js → schema-VEVO7HG6.js} +3 -1
- package/dist/{seed-HCP5NYKH.js → seed-VANPU43L.js} +1 -1
- package/dist/{seed-cmd-ILOGNIFB.js → seed-cmd-SYVDZKM4.js} +2 -2
- package/dist/{seed-create-OURS2LS4.js → seed-create-3FGZPS6C.js} +5 -5
- package/dist/{seed-sprout-ELDFDAN6.js → seed-sprout-H7QLFPFM.js} +13 -11
- package/dist/{send-AAIOPJAV.js → send-HYDFGC6G.js} +13 -7
- package/dist/{service-UOCLXDWR.js → service-SYJ5G6LP.js} +4 -4
- package/dist/{service-install-4HHFDDWY.js → service-install-FQ43Z4DF.js} +5 -5
- package/dist/{setup-Y2EJI3GG.js → setup-KFYVWTXG.js} +3 -3
- package/dist/{setup-DQM5VPY4.js → setup-QJHGMGSN.js} +7 -7
- package/dist/skills/orientation/SKILL.md +1 -1
- package/dist/skills/volute-mind/SKILL.md +8 -8
- package/dist/skills/volute-mind/references/sleep.md +8 -1
- package/dist/{skills-BKINFL7W.js → skills-L2CMTW7A.js} +5 -5
- package/dist/sleep-manager-3NA7ZJP3.js +36 -0
- package/dist/{spirit-DBTIW7P4.js → spirit-WXN4IDBF.js} +8 -8
- package/dist/{sprout-W3KC7UGK.js → sprout-LTL5P36T.js} +1 -1
- package/dist/{status-SWRVGXBI.js → status-V4RJMHJ4.js} +19 -9
- package/dist/system-chat-N2F7HVY3.js +38 -0
- package/dist/{systems-VNB5WKSS.js → systems-B5YJKMW4.js} +6 -6
- package/dist/{tailscale-3XOKOW7T.js → tailscale-MCLY5GYE.js} +2 -2
- package/dist/{template-hash-446HHH5V.js → template-hash-GSOUARW4.js} +2 -2
- package/dist/template-staleness-EMQORSGG.js +15 -0
- package/dist/{turn-tracker-2W6Q2NSR.js → turn-tracker-JZRZDFWV.js} +6 -5
- package/dist/{up-FTK7CTO3.js → up-J2PBPOZL.js} +6 -6
- package/dist/{update-SWAYZRWJ.js → update-ACV4SAY3.js} +5 -5
- package/dist/{update-check-PQ2MZ47K.js → update-check-266R4ZSY.js} +3 -3
- package/dist/{version-notify-3XMAIQTE.js → version-notify-XRGTEEDK.js} +42 -23
- package/dist/web-assets/assets/index-DtctoDXY.css +1 -0
- package/dist/web-assets/assets/index-Wpyb2CML.js +86 -0
- package/dist/web-assets/index.html +2 -2
- package/drizzle/0011_drop_turn_summary_event_id.sql +3 -0
- package/drizzle/0012_channel_gates.sql +17 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +3 -2
- package/templates/_base/home/VOLUTE.md +6 -2
- package/templates/_base/src/lib/context-breakdown.ts +57 -67
- package/templates/_base/src/lib/startup.ts +43 -2
- package/templates/_base/src/lib/volute-server.ts +2 -2
- package/templates/claude/src/agent.ts +105 -43
- package/templates/claude/src/lib/session-reaper.ts +63 -0
- package/templates/claude/src/server.ts +10 -1
- package/templates/codex/src/agent.ts +62 -40
- package/templates/pi/src/agent.ts +55 -38
- package/dist/avatar-image-6KY6UU4D.js +0 -16
- package/dist/delivery-manager-VDVKVA46.js +0 -32
- package/dist/echo-text-6JJOPUD4.js +0 -32
- package/dist/message-delivery-3C65ZI6L.js +0 -34
- package/dist/mind-service-3F4I46CI.js +0 -36
- package/dist/mind-sleep-YMBR3I35.js +0 -47
- package/dist/scheduler-P6DXLLXI.js +0 -30
- package/dist/sleep-manager-HCUH4PM4.js +0 -34
- package/dist/system-chat-XOALE2RD.js +0 -36
- package/dist/web-assets/assets/index-B1JBQv2m.css +0 -1
- package/dist/web-assets/assets/index-Bc2m8gRC.js +0 -86
- package/dist/{accept-N5XULCXK.js → accept-YAFP5JFJ.js} +3 -3
- package/dist/{bridge-2PO3AI5D.js → bridge-TIZGSXOD.js} +3 -3
- package/dist/{create-DF3KISTW.js → create-A7PDHIIL.js} +3 -3
- package/dist/{env-VZ4MLFQW.js → env-HRL2PWAT.js} +5 -5
- package/dist/{extension-SEID47SR.js → extension-QBY4C44L.js} +3 -3
- package/dist/{list-OXIAY3W7.js → list-LQO2DCIK.js} +3 -3
- package/dist/{login-MY6ETFYB.js → login-6TLMI2TQ.js} +3 -3
- package/dist/{logout-NCG3TMDL.js → logout-BK53HXNY.js} +3 -3
- package/dist/{mind-history-VRO5A76U.js → mind-history-47DN6ILW.js} +4 -4
- package/dist/{mind-wake-LCLGTXA2.js → mind-wake-R54BPESY.js} +3 -3
- package/dist/{read-QVAXMTPV.js → read-PWAJOQIQ.js} +3 -3
- package/dist/{register-Y2H4PY5R.js → register-7SZJFHU6.js} +3 -3
- package/dist/{reject-EV35X64Y.js → reject-6VH7N2A4.js} +3 -3
- package/dist/{restart-FJQ662XJ.js → restart-H5WJDMA7.js} +4 -4
- package/dist/{skill-FKFHIJRR.js → skill-GUKG2YVH.js} +5 -5
- package/dist/{start-5JE3TV66.js → start-X46I3GXB.js} +4 -4
- package/dist/{stop-AAMTG3TU.js → stop-HJQV4YSM.js} +4 -4
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
loadMergedEnv
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-IYG4FBMP.js";
|
|
5
5
|
import {
|
|
6
6
|
spiritDir
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-M6TTZRGH.js";
|
|
8
|
+
import {
|
|
9
|
+
clearMind,
|
|
10
|
+
linkInboundToActiveTurn,
|
|
11
|
+
publish as publish3,
|
|
12
|
+
summarizeOrphanedTurns
|
|
13
|
+
} from "./chunk-K23P5Q5N.js";
|
|
14
|
+
import {
|
|
15
|
+
getPrompt
|
|
16
|
+
} from "./chunk-JCMGDCGX.js";
|
|
8
17
|
import {
|
|
9
18
|
hashSkillDir,
|
|
10
19
|
importSkillFromDir,
|
|
11
20
|
removeSharedSkill,
|
|
12
21
|
sharedSkillsDir
|
|
13
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-IWJSNMSY.js";
|
|
14
23
|
import {
|
|
15
24
|
addMessage,
|
|
16
25
|
createChannel,
|
|
@@ -20,61 +29,57 @@ import {
|
|
|
20
29
|
getParticipants,
|
|
21
30
|
joinChannel,
|
|
22
31
|
publish as publish2
|
|
23
|
-
} from "./chunk-
|
|
24
|
-
import {
|
|
25
|
-
readVoluteConfig,
|
|
26
|
-
resolveWakeTriggers,
|
|
27
|
-
writeVoluteConfig
|
|
28
|
-
} from "./chunk-PFZBNKUH.js";
|
|
32
|
+
} from "./chunk-HPGN6XRT.js";
|
|
29
33
|
import {
|
|
30
34
|
isSandboxEnabled,
|
|
31
35
|
wrapForSandbox
|
|
32
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-M3XDVS5Q.js";
|
|
33
37
|
import {
|
|
34
38
|
markIdle,
|
|
35
39
|
onMindEvent
|
|
36
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-MTRMQFCQ.js";
|
|
41
|
+
import {
|
|
42
|
+
renderAvatarBlock
|
|
43
|
+
} from "./chunk-VMCZBXF2.js";
|
|
44
|
+
import {
|
|
45
|
+
readVoluteConfig,
|
|
46
|
+
resolveWakeTriggers,
|
|
47
|
+
writeVoluteConfig
|
|
48
|
+
} from "./chunk-PFZBNKUH.js";
|
|
37
49
|
import {
|
|
38
50
|
extractTextContent,
|
|
39
51
|
getRoutingConfig,
|
|
40
52
|
resolveDeliveryMode,
|
|
41
53
|
resolveRoute,
|
|
42
54
|
setRoutesChangeListener
|
|
43
|
-
} from "./chunk-
|
|
44
|
-
import {
|
|
45
|
-
clearMind,
|
|
46
|
-
getPrompt,
|
|
47
|
-
linkInboundToActiveTurn,
|
|
48
|
-
publish as publish3,
|
|
49
|
-
summarizeOrphanedTurns
|
|
50
|
-
} from "./chunk-UYLGEDVD.js";
|
|
55
|
+
} from "./chunk-EIQDZC43.js";
|
|
51
56
|
import {
|
|
52
57
|
chownMindDir,
|
|
53
58
|
exec,
|
|
54
59
|
isIsolationEnabled,
|
|
55
60
|
mindUserName,
|
|
56
61
|
wrapForIsolation
|
|
57
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-DGGZVZTM.js";
|
|
58
63
|
import {
|
|
59
64
|
aiCompleteUtility,
|
|
60
65
|
getAiConfig,
|
|
61
66
|
resolveApiKey
|
|
62
|
-
} from "./chunk-
|
|
67
|
+
} from "./chunk-LWDTP6DH.js";
|
|
63
68
|
import {
|
|
64
69
|
readGlobalConfig,
|
|
65
70
|
writeGlobalConfig
|
|
66
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-SKNXMUIU.js";
|
|
67
72
|
import {
|
|
68
73
|
getOrCreateMindUser,
|
|
69
74
|
getOrCreateSystemUser,
|
|
70
75
|
getUser,
|
|
71
76
|
getUserByUsername,
|
|
72
77
|
syncMindProfile
|
|
73
|
-
} from "./chunk-
|
|
78
|
+
} from "./chunk-ODDGGLM6.js";
|
|
74
79
|
import {
|
|
75
80
|
publish,
|
|
76
81
|
subscribe
|
|
77
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-LTLFOSA4.js";
|
|
78
83
|
import {
|
|
79
84
|
logger_default
|
|
80
85
|
} from "./chunk-T2TP6ZC6.js";
|
|
@@ -90,18 +95,19 @@ import {
|
|
|
90
95
|
stateDir,
|
|
91
96
|
voluteHome,
|
|
92
97
|
voluteSystemDir
|
|
93
|
-
} from "./chunk-
|
|
98
|
+
} from "./chunk-WW6C3PLV.js";
|
|
94
99
|
import {
|
|
95
100
|
activity,
|
|
101
|
+
channelGates,
|
|
96
102
|
deliveryQueue,
|
|
97
103
|
messages,
|
|
98
104
|
mindHistory,
|
|
99
105
|
mindNotices,
|
|
100
106
|
sessions
|
|
101
|
-
} from "./chunk-
|
|
107
|
+
} from "./chunk-2MJ7EOMT.js";
|
|
102
108
|
|
|
103
109
|
// packages/daemon/src/lib/delivery/message-delivery.ts
|
|
104
|
-
import { and as and5, eq as eq6, inArray as
|
|
110
|
+
import { and as and5, eq as eq6, inArray as inArray3 } from "drizzle-orm";
|
|
105
111
|
|
|
106
112
|
// packages/daemon/src/lib/daemon/sleep-manager.ts
|
|
107
113
|
import { execFile as execFile2, spawn as spawnChild } from "child_process";
|
|
@@ -117,7 +123,7 @@ import {
|
|
|
117
123
|
import { resolve as resolve7 } from "path";
|
|
118
124
|
import { promisify as promisify2 } from "util";
|
|
119
125
|
import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
|
|
120
|
-
import { and as and2, eq as eq3 } from "drizzle-orm";
|
|
126
|
+
import { and as and2, eq as eq3, inArray } from "drizzle-orm";
|
|
121
127
|
|
|
122
128
|
// packages/daemon/src/lib/daemon/mind-manager.ts
|
|
123
129
|
import { execFile, spawn } from "child_process";
|
|
@@ -769,21 +775,8 @@ var MindManager = class {
|
|
|
769
775
|
if (isIsolationEnabled()) {
|
|
770
776
|
env.HOME = resolve2(dir, "home");
|
|
771
777
|
}
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
let baseArgs;
|
|
775
|
-
if (customNode) {
|
|
776
|
-
baseBin = customNode;
|
|
777
|
-
baseArgs = [
|
|
778
|
-
resolve2(dir, "node_modules", ".bin", "tsx"),
|
|
779
|
-
"src/server.ts",
|
|
780
|
-
"--port",
|
|
781
|
-
String(port)
|
|
782
|
-
];
|
|
783
|
-
} else {
|
|
784
|
-
baseBin = resolve2(dir, "node_modules", ".bin", "tsx");
|
|
785
|
-
baseArgs = ["src/server.ts", "--port", String(port)];
|
|
786
|
-
}
|
|
778
|
+
const baseBin = process.env.VOLUTE_NODE_PATH ?? process.execPath;
|
|
779
|
+
const baseArgs = ["--import", "tsx", "src/server.ts", "--port", String(port)];
|
|
787
780
|
let spawnCmd;
|
|
788
781
|
let spawnArgs;
|
|
789
782
|
if (isIsolationEnabled()) {
|
|
@@ -945,7 +938,7 @@ var MindManager = class {
|
|
|
945
938
|
if (this.shuttingDown || this.stopping.has(name)) return;
|
|
946
939
|
mlog.error(`mind ${name} exited with code ${code}`);
|
|
947
940
|
try {
|
|
948
|
-
const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-
|
|
941
|
+
const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-3NA7ZJP3.js");
|
|
949
942
|
const sleepMgr = getSleepManagerIfReady2();
|
|
950
943
|
const sleepState = sleepMgr?.getState(name);
|
|
951
944
|
if (sleepState?.sleeping) {
|
|
@@ -975,15 +968,15 @@ var MindManager = class {
|
|
|
975
968
|
(err) => mlog.warn(`failed to clear turn state for ${name} after crash`, logger_default.errorData(err))
|
|
976
969
|
);
|
|
977
970
|
try {
|
|
978
|
-
const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-
|
|
971
|
+
const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-TN5KK2AP.js");
|
|
979
972
|
getDeliveryManager2().clearMindSessions(name);
|
|
980
973
|
} catch (err) {
|
|
981
974
|
if (!(err instanceof Error && err.message.includes("not initialized"))) {
|
|
982
975
|
mlog.warn(`failed to clear delivery state for ${name} after crash`, logger_default.errorData(err));
|
|
983
976
|
}
|
|
984
977
|
}
|
|
985
|
-
import("./mind-activity-tracker-
|
|
986
|
-
import("./activity-events-
|
|
978
|
+
import("./mind-activity-tracker-YVZPC4A5.js").then(({ markIdle: markIdle2 }) => markIdle2(name)).catch((err) => mlog.warn(`failed to mark ${name} idle after crash`, logger_default.errorData(err)));
|
|
979
|
+
import("./activity-events-MIOK3IOU.js").then(
|
|
987
980
|
({ publish: publish4 }) => publish4({ type: "mind_stopped", mind: name, summary: `${name} crashed (exit ${code})` })
|
|
988
981
|
).catch((err) => mlog.warn(`failed to publish crash event for ${name}`, logger_default.errorData(err)));
|
|
989
982
|
const { shouldRestart, delay, attempt } = this.restartTracker.recordCrash(name);
|
|
@@ -1041,7 +1034,7 @@ var MindManager = class {
|
|
|
1041
1034
|
mlog.warn(`failed to clear turn state for ${name} on stop`, logger_default.errorData(err));
|
|
1042
1035
|
}
|
|
1043
1036
|
try {
|
|
1044
|
-
const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-
|
|
1037
|
+
const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-TN5KK2AP.js");
|
|
1045
1038
|
getDeliveryManager2().clearMindSessions(name);
|
|
1046
1039
|
} catch (err) {
|
|
1047
1040
|
if (!(err instanceof Error && err.message.includes("not initialized"))) {
|
|
@@ -1049,7 +1042,7 @@ var MindManager = class {
|
|
|
1049
1042
|
}
|
|
1050
1043
|
}
|
|
1051
1044
|
try {
|
|
1052
|
-
const { clearEchoTextCache } = await import("./echo-text-
|
|
1045
|
+
const { clearEchoTextCache } = await import("./echo-text-SEQCLGAT.js");
|
|
1053
1046
|
clearEchoTextCache(name);
|
|
1054
1047
|
} catch (err) {
|
|
1055
1048
|
mlog.debug(`failed to clear echo-text cache for ${name}`, logger_default.errorData(err));
|
|
@@ -1226,6 +1219,10 @@ async function getSessionUserId(sessionId) {
|
|
|
1226
1219
|
return row.userId;
|
|
1227
1220
|
}
|
|
1228
1221
|
async function cleanExpiredSessions() {
|
|
1222
|
+
const now = Date.now();
|
|
1223
|
+
for (const [id, cached] of sessionCache) {
|
|
1224
|
+
if (cached.expires <= now) sessionCache.delete(id);
|
|
1225
|
+
}
|
|
1229
1226
|
const db = await getDb();
|
|
1230
1227
|
const cutoff = Date.now() - SESSION_MAX_AGE;
|
|
1231
1228
|
await db.delete(sessions).where(lt(sessions.createdAt, cutoff));
|
|
@@ -1369,10 +1366,54 @@ function deleteSystemsConfig() {
|
|
|
1369
1366
|
}
|
|
1370
1367
|
}
|
|
1371
1368
|
|
|
1369
|
+
// packages/daemon/src/lib/util/sanitize-svg.ts
|
|
1370
|
+
import DOMPurify from "isomorphic-dompurify";
|
|
1371
|
+
var ALLOWED_TAGS = ["svg", "path", "circle", "rect", "line", "polyline", "polygon", "g"];
|
|
1372
|
+
var ALLOWED_ATTR = [
|
|
1373
|
+
"viewBox",
|
|
1374
|
+
"fill",
|
|
1375
|
+
"fill-opacity",
|
|
1376
|
+
"fill-rule",
|
|
1377
|
+
"stroke",
|
|
1378
|
+
"stroke-width",
|
|
1379
|
+
"stroke-linecap",
|
|
1380
|
+
"stroke-linejoin",
|
|
1381
|
+
"stroke-dasharray",
|
|
1382
|
+
"stroke-opacity",
|
|
1383
|
+
"opacity",
|
|
1384
|
+
"d",
|
|
1385
|
+
"cx",
|
|
1386
|
+
"cy",
|
|
1387
|
+
"r",
|
|
1388
|
+
"x",
|
|
1389
|
+
"y",
|
|
1390
|
+
"width",
|
|
1391
|
+
"height",
|
|
1392
|
+
"points",
|
|
1393
|
+
"transform"
|
|
1394
|
+
];
|
|
1395
|
+
function sanitizeSvgIcon(html) {
|
|
1396
|
+
return DOMPurify.sanitize(html, {
|
|
1397
|
+
ALLOWED_TAGS,
|
|
1398
|
+
ALLOWED_ATTR,
|
|
1399
|
+
FORBID_TAGS: ["script", "foreignObject"],
|
|
1400
|
+
FORBID_ATTR: ["href", "xlink:href"]
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1372
1404
|
// packages/daemon/src/lib/extensions.ts
|
|
1373
1405
|
var VALID_EXTENSION_ID = /^[a-z0-9][a-z0-9_-]*$/;
|
|
1374
1406
|
var loaded = [];
|
|
1375
1407
|
var discovered = [];
|
|
1408
|
+
function enrichActivityMetadata(manifest, metadata) {
|
|
1409
|
+
const enriched = {
|
|
1410
|
+
...metadata,
|
|
1411
|
+
...manifest.icon && !metadata?.icon ? { icon: manifest.icon } : {},
|
|
1412
|
+
...manifest.color && !metadata?.color ? { color: manifest.color } : {}
|
|
1413
|
+
};
|
|
1414
|
+
if (typeof enriched.icon === "string") enriched.icon = sanitizeSvgIcon(enriched.icon);
|
|
1415
|
+
return enriched;
|
|
1416
|
+
}
|
|
1376
1417
|
function toCommandInfo(cmd) {
|
|
1377
1418
|
const { handler: _, ...info } = cmd;
|
|
1378
1419
|
return info;
|
|
@@ -1476,14 +1517,8 @@ async function buildContext(manifest, dataDir, authMw) {
|
|
|
1476
1517
|
getUser: async (id) => getUser(id),
|
|
1477
1518
|
getUserByUsername: async (username) => getUserByUsername(username),
|
|
1478
1519
|
publishActivity: (event) => {
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
metadata: {
|
|
1482
|
-
...event.metadata,
|
|
1483
|
-
...manifest.icon && !event.metadata?.icon ? { icon: manifest.icon } : {},
|
|
1484
|
-
...manifest.color && !event.metadata?.color ? { color: manifest.color } : {}
|
|
1485
|
-
}
|
|
1486
|
-
};
|
|
1520
|
+
const metadata = enrichActivityMetadata(manifest, event.metadata);
|
|
1521
|
+
const enriched = { ...event, metadata };
|
|
1487
1522
|
publish(enriched).catch(
|
|
1488
1523
|
(err) => logger_default.error(`extension ${manifest.id}: failed to publish activity`, logger_default.errorData(err))
|
|
1489
1524
|
);
|
|
@@ -1561,14 +1596,8 @@ async function loadExtension(manifest, app, authMw) {
|
|
|
1561
1596
|
const result = await cmd.handler(parsed, {
|
|
1562
1597
|
...context,
|
|
1563
1598
|
publishActivity: (rawEvent) => {
|
|
1564
|
-
const
|
|
1565
|
-
|
|
1566
|
-
metadata: {
|
|
1567
|
-
...rawEvent.metadata,
|
|
1568
|
-
...manifest.icon && !rawEvent.metadata?.icon ? { icon: manifest.icon } : {},
|
|
1569
|
-
...manifest.color && !rawEvent.metadata?.color ? { color: manifest.color } : {}
|
|
1570
|
-
}
|
|
1571
|
-
};
|
|
1599
|
+
const metadata = enrichActivityMetadata(manifest, rawEvent.metadata);
|
|
1600
|
+
const event = { ...rawEvent, metadata };
|
|
1572
1601
|
activityPromises.push(
|
|
1573
1602
|
publish(event).catch((err) => {
|
|
1574
1603
|
logger_default.error(
|
|
@@ -1608,7 +1637,7 @@ async function loadExtension(manifest, app, authMw) {
|
|
|
1608
1637
|
}
|
|
1609
1638
|
if (resolvedAssetsDir && existsSync6(resolvedAssetsDir)) {
|
|
1610
1639
|
const assetsDir = resolvedAssetsDir;
|
|
1611
|
-
const { readFile
|
|
1640
|
+
const { readFile, stat: fsStat } = await import("fs/promises");
|
|
1612
1641
|
const { extname: ext } = await import("path");
|
|
1613
1642
|
const mimeTypes = {
|
|
1614
1643
|
".html": "text/html",
|
|
@@ -1633,11 +1662,11 @@ async function loadExtension(manifest, app, authMw) {
|
|
|
1633
1662
|
const s = await fsStat(filePath).catch(() => null);
|
|
1634
1663
|
if (s?.isFile()) {
|
|
1635
1664
|
const mime = mimeTypes[ext(filePath)] || "application/octet-stream";
|
|
1636
|
-
const body = await
|
|
1665
|
+
const body = await readFile(filePath);
|
|
1637
1666
|
return c.body(body, 200, { "Content-Type": mime });
|
|
1638
1667
|
}
|
|
1639
1668
|
if (existsSync6(indexPath)) {
|
|
1640
|
-
const body = await
|
|
1669
|
+
const body = await readFile(indexPath, "utf-8");
|
|
1641
1670
|
return c.html(body);
|
|
1642
1671
|
}
|
|
1643
1672
|
return c.text("Not found", 404);
|
|
@@ -1955,7 +1984,7 @@ async function installNpmExtension(pkg) {
|
|
|
1955
1984
|
throw new Error(`Extension "${pkg}" is already installed`);
|
|
1956
1985
|
}
|
|
1957
1986
|
const dir = ensureExtensionsNpmDir();
|
|
1958
|
-
const { exec: exec2 } = await import("./exec-
|
|
1987
|
+
const { exec: exec2 } = await import("./exec-QIFYZG4C.js");
|
|
1959
1988
|
try {
|
|
1960
1989
|
await exec2("npm", ["install", "--ignore-scripts", pkg], { cwd: dir });
|
|
1961
1990
|
} catch (err) {
|
|
@@ -1976,7 +2005,7 @@ async function uninstallNpmExtension(pkg) {
|
|
|
1976
2005
|
packages.splice(idx, 1);
|
|
1977
2006
|
writeExtensionsConfig(packages);
|
|
1978
2007
|
try {
|
|
1979
|
-
const { exec: exec2 } = await import("./exec-
|
|
2008
|
+
const { exec: exec2 } = await import("./exec-QIFYZG4C.js");
|
|
1980
2009
|
await exec2("npm", ["uninstall", "--ignore-scripts", pkg], { cwd: extensionsNpmDir() });
|
|
1981
2010
|
} catch (err) {
|
|
1982
2011
|
logger_default.warn(
|
|
@@ -2306,6 +2335,7 @@ async function ensureMailAddress(mindName) {
|
|
|
2306
2335
|
import { resolve as resolve5 } from "path";
|
|
2307
2336
|
import { CronExpressionParser } from "cron-parser";
|
|
2308
2337
|
var slog2 = logger_default.child("scheduler");
|
|
2338
|
+
var CATCHUP_STALE_MINUTES = 10;
|
|
2309
2339
|
var Scheduler = class {
|
|
2310
2340
|
schedules = /* @__PURE__ */ new Map();
|
|
2311
2341
|
mindDirs = /* @__PURE__ */ new Map();
|
|
@@ -2345,6 +2375,26 @@ var Scheduler = class {
|
|
|
2345
2375
|
} else {
|
|
2346
2376
|
this.schedules.delete(mindName);
|
|
2347
2377
|
}
|
|
2378
|
+
const epochMinute = Math.floor(Date.now() / 6e4);
|
|
2379
|
+
const validKeys = new Set(schedules.map((s) => `${mindName}:${s.id}`));
|
|
2380
|
+
let changed = false;
|
|
2381
|
+
for (const key of this.lastFired.keys()) {
|
|
2382
|
+
if (key.startsWith(`${mindName}:`) && !validKeys.has(key)) {
|
|
2383
|
+
this.lastFired.delete(key);
|
|
2384
|
+
changed = true;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
for (const key of validKeys) {
|
|
2388
|
+
if (!this.lastFired.has(key)) {
|
|
2389
|
+
this.lastFired.set(key, epochMinute);
|
|
2390
|
+
changed = true;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
if (changed) {
|
|
2394
|
+
this.saveState().catch(
|
|
2395
|
+
(err) => slog2.warn(`failed to persist scheduler state for ${mindName}`, logger_default.errorData(err))
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2348
2398
|
}
|
|
2349
2399
|
unloadSchedules(mindName) {
|
|
2350
2400
|
this.schedules.delete(mindName);
|
|
@@ -2390,11 +2440,14 @@ var Scheduler = class {
|
|
|
2390
2440
|
return false;
|
|
2391
2441
|
}
|
|
2392
2442
|
}
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2443
|
+
const lastFired = this.lastFired.get(key) ?? epochMinute;
|
|
2444
|
+
if (prevMinute <= lastFired) return false;
|
|
2445
|
+
this.lastFired.set(key, prevMinute);
|
|
2446
|
+
if (epochMinute - prevMinute > CATCHUP_STALE_MINUTES) {
|
|
2447
|
+
slog2.info(`skipping stale catch-up fire for ${key} (${epochMinute - prevMinute}min late)`);
|
|
2448
|
+
return false;
|
|
2396
2449
|
}
|
|
2397
|
-
return
|
|
2450
|
+
return true;
|
|
2398
2451
|
}
|
|
2399
2452
|
async fire(mindName, schedule) {
|
|
2400
2453
|
try {
|
|
@@ -2739,11 +2792,7 @@ async function startMindFull(name) {
|
|
|
2739
2792
|
if (entry?.parent) return;
|
|
2740
2793
|
if (!entry || entry.stage === "seed") {
|
|
2741
2794
|
if (entry?.stage === "seed") {
|
|
2742
|
-
|
|
2743
|
-
sendSystemMessage(
|
|
2744
|
-
baseName,
|
|
2745
|
-
`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}`
|
|
2746
|
-
).catch(
|
|
2795
|
+
sendSeedOrientation(baseName, entry.createdBy).catch(
|
|
2747
2796
|
(err) => logger_default.error(`failed to send seed orientation for ${baseName}`, logger_default.errorData(err))
|
|
2748
2797
|
);
|
|
2749
2798
|
if (entry.createdBy) {
|
|
@@ -2805,7 +2854,7 @@ async function wakeMind(name) {
|
|
|
2805
2854
|
async function startSpiritFull(name) {
|
|
2806
2855
|
const entry = await findMind(name);
|
|
2807
2856
|
if (entry?.dir) {
|
|
2808
|
-
const { registerMindDir } = await import("./delivery-router-
|
|
2857
|
+
const { registerMindDir } = await import("./delivery-router-3QFG2ZEX.js");
|
|
2809
2858
|
registerMindDir(name, entry.dir);
|
|
2810
2859
|
}
|
|
2811
2860
|
await getMindManager().startMind(name);
|
|
@@ -2826,9 +2875,23 @@ async function stopSpiritFull(name) {
|
|
|
2826
2875
|
summary: `${name} spirit stopped`
|
|
2827
2876
|
}).catch((err) => logger_default.error("failed to publish spirit_stopped activity", logger_default.errorData(err)));
|
|
2828
2877
|
}
|
|
2878
|
+
function buildSeedOrientation(createdBy, creatorType) {
|
|
2879
|
+
const intro = "You've just been created as a seed. Start a conversation \u2014 introduce yourself, ask questions, and begin exploring who you want to be.";
|
|
2880
|
+
if (!createdBy) return intro;
|
|
2881
|
+
const who = creatorType === "mind" ? `another mind, ${createdBy}` : creatorType === "system" ? `the spirit of this system (${createdBy})` : createdBy;
|
|
2882
|
+
return `${intro} Your creator is ${who}. Send them a message to introduce yourself.`;
|
|
2883
|
+
}
|
|
2884
|
+
async function sendSeedOrientation(mindName, createdBy) {
|
|
2885
|
+
let creatorType;
|
|
2886
|
+
if (createdBy) {
|
|
2887
|
+
const { getUserByUsername: getUserByUsername2 } = await import("./auth-A4VGHADI.js");
|
|
2888
|
+
creatorType = (await getUserByUsername2(createdBy))?.user_type;
|
|
2889
|
+
}
|
|
2890
|
+
await sendSystemMessage(mindName, buildSeedOrientation(createdBy, creatorType));
|
|
2891
|
+
}
|
|
2829
2892
|
async function ensureCreatorDM(mindName, creatorUsername) {
|
|
2830
|
-
const { getOrCreateMindUser: getOrCreateMindUser2, getUserByUsername: getUserByUsername2 } = await import("./auth-
|
|
2831
|
-
const { findDMConversation: findDMConversation2, createConversation: createConversation2 } = await import("./conversations-
|
|
2893
|
+
const { getOrCreateMindUser: getOrCreateMindUser2, getUserByUsername: getUserByUsername2 } = await import("./auth-A4VGHADI.js");
|
|
2894
|
+
const { findDMConversation: findDMConversation2, createConversation: createConversation2 } = await import("./conversations-C7IKYSHD.js");
|
|
2832
2895
|
const mindUser = await getOrCreateMindUser2(mindName);
|
|
2833
2896
|
const creatorUser = await getUserByUsername2(creatorUsername);
|
|
2834
2897
|
if (!creatorUser) {
|
|
@@ -2873,7 +2936,8 @@ function defaultState() {
|
|
|
2873
2936
|
queuedMessageCount: 0,
|
|
2874
2937
|
triggerWakeHistory: [],
|
|
2875
2938
|
wakeFailures: 0,
|
|
2876
|
-
nextWakeAttemptAt: null
|
|
2939
|
+
nextWakeAttemptAt: null,
|
|
2940
|
+
lastWakeAt: null
|
|
2877
2941
|
};
|
|
2878
2942
|
}
|
|
2879
2943
|
function formatCurrentDate() {
|
|
@@ -2945,6 +3009,7 @@ var SleepManager = class {
|
|
|
2945
3009
|
state2.triggerWakeHistory ??= [];
|
|
2946
3010
|
state2.wakeFailures ??= 0;
|
|
2947
3011
|
state2.nextWakeAttemptAt ??= null;
|
|
3012
|
+
state2.lastWakeAt ??= null;
|
|
2948
3013
|
this.states.set(name, state2);
|
|
2949
3014
|
}
|
|
2950
3015
|
}
|
|
@@ -2955,7 +3020,7 @@ var SleepManager = class {
|
|
|
2955
3020
|
saveState() {
|
|
2956
3021
|
const data = {};
|
|
2957
3022
|
for (const [name, state2] of this.states) {
|
|
2958
|
-
if (state2.sleeping) data[name] = state2;
|
|
3023
|
+
if (state2.sleeping || state2.lastWakeAt) data[name] = state2;
|
|
2959
3024
|
}
|
|
2960
3025
|
try {
|
|
2961
3026
|
writeFileSync6(this.statePath, `${JSON.stringify(data, null, 2)}
|
|
@@ -3005,8 +3070,9 @@ var SleepManager = class {
|
|
|
3005
3070
|
}
|
|
3006
3071
|
await this.archiveSessions(name);
|
|
3007
3072
|
state2.sleeping = true;
|
|
3008
|
-
|
|
3009
|
-
|
|
3073
|
+
if (!state2.voluntaryWakeAt) {
|
|
3074
|
+
state2.scheduledWakeAt = this.getNextWakeTime(this.getSleepConfig(name));
|
|
3075
|
+
}
|
|
3010
3076
|
this.saveState();
|
|
3011
3077
|
slog3.info(`${name} returned to sleep after crashing during trigger wake`);
|
|
3012
3078
|
} catch (err) {
|
|
@@ -3162,6 +3228,7 @@ var SleepManager = class {
|
|
|
3162
3228
|
} catch (err) {
|
|
3163
3229
|
slog3.warn(`failed to deliver wake summary to ${name}`, logger_default.errorData(err));
|
|
3164
3230
|
}
|
|
3231
|
+
await this.waitForIdle(name, 12e4);
|
|
3165
3232
|
}
|
|
3166
3233
|
const flushed = await this.flushQueuedMessages(name);
|
|
3167
3234
|
if (flushed > 0) {
|
|
@@ -3219,14 +3286,18 @@ var SleepManager = class {
|
|
|
3219
3286
|
}
|
|
3220
3287
|
}
|
|
3221
3288
|
/**
|
|
3222
|
-
* Flush all queued sleep messages for a mind
|
|
3289
|
+
* Flush all queued sleep messages for a mind on wake. Rows are grouped by channel
|
|
3290
|
+
* and each channel group is delivered as ONE pre-batched turn (#382) — a night's
|
|
3291
|
+
* backlog becomes one stack per channel, not N cold-start doorbells. Delivery is
|
|
3292
|
+
* all-or-nothing per channel: a failed group's rows stay queued for the next wake,
|
|
3293
|
+
* while other channels still flush.
|
|
3223
3294
|
*/
|
|
3224
3295
|
async flushQueuedMessages(name) {
|
|
3225
3296
|
try {
|
|
3226
3297
|
const db = await getDb();
|
|
3227
3298
|
const rows = await db.select().from(deliveryQueue).where(and2(eq3(deliveryQueue.mind, name), eq3(deliveryQueue.status, "sleep-queued"))).all();
|
|
3228
3299
|
if (rows.length === 0) return 0;
|
|
3229
|
-
|
|
3300
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3230
3301
|
for (const row of rows) {
|
|
3231
3302
|
let payload;
|
|
3232
3303
|
try {
|
|
@@ -3239,13 +3310,29 @@ var SleepManager = class {
|
|
|
3239
3310
|
await db.delete(deliveryQueue).where(eq3(deliveryQueue.id, row.id));
|
|
3240
3311
|
continue;
|
|
3241
3312
|
}
|
|
3242
|
-
const
|
|
3313
|
+
const channel = payload.channel ?? "unknown";
|
|
3314
|
+
if (!groups.has(channel)) groups.set(channel, []);
|
|
3315
|
+
groups.get(channel)?.push({ id: row.id, payload });
|
|
3316
|
+
}
|
|
3317
|
+
let deliveredCount = 0;
|
|
3318
|
+
for (const [channel, items] of groups) {
|
|
3319
|
+
const delivered = await this.deliverQueuedBatch(
|
|
3320
|
+
name,
|
|
3321
|
+
items.map((i) => i.payload)
|
|
3322
|
+
);
|
|
3243
3323
|
if (!delivered) {
|
|
3244
|
-
slog3.warn(
|
|
3245
|
-
|
|
3324
|
+
slog3.warn(
|
|
3325
|
+
`failed to flush ${items.length} queued message(s) on ${channel} for ${name}; leaving them queued`
|
|
3326
|
+
);
|
|
3327
|
+
continue;
|
|
3246
3328
|
}
|
|
3247
|
-
await db.delete(deliveryQueue).where(
|
|
3248
|
-
|
|
3329
|
+
await db.delete(deliveryQueue).where(
|
|
3330
|
+
inArray(
|
|
3331
|
+
deliveryQueue.id,
|
|
3332
|
+
items.map((i) => i.id)
|
|
3333
|
+
)
|
|
3334
|
+
);
|
|
3335
|
+
deliveredCount += items.length;
|
|
3249
3336
|
}
|
|
3250
3337
|
const state2 = this.states.get(name);
|
|
3251
3338
|
if (state2) {
|
|
@@ -3258,14 +3345,13 @@ var SleepManager = class {
|
|
|
3258
3345
|
}
|
|
3259
3346
|
}
|
|
3260
3347
|
/**
|
|
3261
|
-
* Deliver one
|
|
3262
|
-
*
|
|
3263
|
-
*
|
|
3264
|
-
* Extracted as a seam so the flush loop can be unit-tested without the real delivery stack.
|
|
3348
|
+
* Deliver one channel's worth of queued messages as a single pre-batched turn on
|
|
3349
|
+
* the wake path. Extracted as a seam so the flush loop can be unit-tested without
|
|
3350
|
+
* the real delivery stack. Returns whether the batch was handed off to the mind.
|
|
3265
3351
|
*/
|
|
3266
|
-
async
|
|
3267
|
-
const {
|
|
3268
|
-
return
|
|
3352
|
+
async deliverQueuedBatch(name, payloads) {
|
|
3353
|
+
const { deliverBatch: deliverBatch2 } = await import("./message-delivery-ZE3YGJDD.js");
|
|
3354
|
+
return deliverBatch2(name, payloads);
|
|
3269
3355
|
}
|
|
3270
3356
|
// --- Wake failure handling (#419) ---
|
|
3271
3357
|
/**
|
|
@@ -3307,7 +3393,8 @@ var SleepManager = class {
|
|
|
3307
3393
|
*/
|
|
3308
3394
|
async markWakeErrored(name, err) {
|
|
3309
3395
|
const detail = err instanceof Error ? err.message : String(err);
|
|
3310
|
-
this.
|
|
3396
|
+
this.states.delete(name);
|
|
3397
|
+
this.saveState();
|
|
3311
3398
|
slog3.error(
|
|
3312
3399
|
`${name} failed to wake ${MAX_WAKE_ATTEMPTS} times in a row; giving up and marking errored`,
|
|
3313
3400
|
logger_default.errorData(err)
|
|
@@ -3336,19 +3423,22 @@ var SleepManager = class {
|
|
|
3336
3423
|
const state2 = {
|
|
3337
3424
|
sleeping: true,
|
|
3338
3425
|
sleepingSince: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3339
|
-
|
|
3426
|
+
// An explicit voluntary wake-at is authoritative for the night — null out
|
|
3427
|
+
// the cron wake so an earlier scheduled wake can't silently override it.
|
|
3428
|
+
scheduledWakeAt: opts?.voluntaryWakeAt ? null : this.getNextWakeTime(sleepConfig),
|
|
3340
3429
|
wokenByTrigger: false,
|
|
3341
3430
|
voluntaryWakeAt: opts?.voluntaryWakeAt ?? null,
|
|
3342
3431
|
queuedMessageCount: this.states.get(name)?.queuedMessageCount ?? 0,
|
|
3343
3432
|
triggerWakeHistory: [],
|
|
3344
3433
|
wakeFailures: 0,
|
|
3345
|
-
nextWakeAttemptAt: null
|
|
3434
|
+
nextWakeAttemptAt: null,
|
|
3435
|
+
lastWakeAt: this.states.get(name)?.lastWakeAt ?? null
|
|
3346
3436
|
};
|
|
3347
3437
|
this.states.set(name, state2);
|
|
3348
3438
|
this.saveState();
|
|
3349
3439
|
}
|
|
3350
3440
|
markAwake(name) {
|
|
3351
|
-
this.states.
|
|
3441
|
+
this.states.set(name, { ...defaultState(), lastWakeAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3352
3442
|
this.saveState();
|
|
3353
3443
|
}
|
|
3354
3444
|
getNextWakeTime(config) {
|
|
@@ -3363,55 +3453,78 @@ var SleepManager = class {
|
|
|
3363
3453
|
}
|
|
3364
3454
|
async tick() {
|
|
3365
3455
|
const now = /* @__PURE__ */ new Date();
|
|
3366
|
-
const epochMinute = Math.floor(now.getTime() / 6e4);
|
|
3367
3456
|
const mindNames = /* @__PURE__ */ new Set([...this.sleepConfigs.keys(), ...this.states.keys()]);
|
|
3368
3457
|
for (const name of mindNames) {
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
if (state2?.sleeping && state2.scheduledWakeAt) {
|
|
3389
|
-
const wakeAt = new Date(state2.scheduledWakeAt);
|
|
3390
|
-
if (now >= wakeAt) {
|
|
3391
|
-
this.initiateWake(name).catch(
|
|
3392
|
-
(err) => slog3.error(`failed scheduled wake for ${name}`, logger_default.errorData(err))
|
|
3393
|
-
);
|
|
3394
|
-
continue;
|
|
3395
|
-
}
|
|
3458
|
+
await this.evaluateMind(name, now);
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
/**
|
|
3462
|
+
* Per-mind tick evaluation. The wake checks depend only on the persisted sleep
|
|
3463
|
+
* state — never on a currently-enabled sleep config — so a mind that was put to
|
|
3464
|
+
* sleep with a wake time still wakes even if the schedule is later disabled or
|
|
3465
|
+
* removed (or never existed, e.g. a bare `clock sleep --wake-at`). Only the
|
|
3466
|
+
* sleep-onset check requires a running mind with an enabled schedule.
|
|
3467
|
+
*/
|
|
3468
|
+
async evaluateMind(name, now) {
|
|
3469
|
+
const state2 = this.states.get(name);
|
|
3470
|
+
if (state2?.sleeping) {
|
|
3471
|
+
if (state2.nextWakeAttemptAt && now < new Date(state2.nextWakeAttemptAt)) return;
|
|
3472
|
+
if (state2.voluntaryWakeAt && now >= new Date(state2.voluntaryWakeAt)) {
|
|
3473
|
+
this.initiateWake(name).catch(
|
|
3474
|
+
(err) => slog3.error(`failed voluntary wake for ${name}`, logger_default.errorData(err))
|
|
3475
|
+
);
|
|
3476
|
+
return;
|
|
3396
3477
|
}
|
|
3397
|
-
if (
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
}
|
|
3478
|
+
if (state2.scheduledWakeAt && now >= new Date(state2.scheduledWakeAt)) {
|
|
3479
|
+
this.initiateWake(name).catch(
|
|
3480
|
+
(err) => slog3.error(`failed scheduled wake for ${name}`, logger_default.errorData(err))
|
|
3481
|
+
);
|
|
3482
|
+
return;
|
|
3403
3483
|
}
|
|
3484
|
+
return;
|
|
3485
|
+
}
|
|
3486
|
+
const mind = await findMind(name);
|
|
3487
|
+
if (!mind?.running) return;
|
|
3488
|
+
const config = this.getSleepConfig(name);
|
|
3489
|
+
if (!config?.enabled || !config.schedule) return;
|
|
3490
|
+
if (this.shouldSleepNow(config.schedule, state2?.lastWakeAt ?? null, now)) {
|
|
3491
|
+
this.initiateSleep(name).catch(
|
|
3492
|
+
(err) => slog3.error(`failed to initiate sleep for ${name}`, logger_default.errorData(err))
|
|
3493
|
+
);
|
|
3404
3494
|
}
|
|
3405
3495
|
}
|
|
3406
|
-
|
|
3496
|
+
/**
|
|
3497
|
+
* Level-triggered sleep-onset decision: sleep whenever we're inside the nightly
|
|
3498
|
+
* window, not only on the exact sleep minute — so a daemon restart or a stalled
|
|
3499
|
+
* tick across the sleep minute still puts the mind to bed on the next tick.
|
|
3500
|
+
*
|
|
3501
|
+
* Manual-wake exemption: a full wake inside the current window (human
|
|
3502
|
+
* `clock wake`, or a scheduled wake) suppresses re-sleep until the next window
|
|
3503
|
+
* begins, so `volute clock wake` at 3am sticks instead of being undone a minute
|
|
3504
|
+
* later.
|
|
3505
|
+
*/
|
|
3506
|
+
shouldSleepNow(schedule, lastWakeAt, now) {
|
|
3507
|
+
const { inWindow, windowStart } = this.inSleepWindow(schedule, now);
|
|
3508
|
+
if (!inWindow || !windowStart) return false;
|
|
3509
|
+
if (lastWakeAt && new Date(lastWakeAt) >= windowStart) return false;
|
|
3510
|
+
return true;
|
|
3511
|
+
}
|
|
3512
|
+
/**
|
|
3513
|
+
* Whether `now` falls inside the mind's nightly sleep window: the most recent
|
|
3514
|
+
* sleep-cron fire is more recent than the most recent wake-cron fire. Returns
|
|
3515
|
+
* the window start (last sleep fire) for the manual-wake exemption check.
|
|
3516
|
+
*/
|
|
3517
|
+
inSleepWindow(schedule, now) {
|
|
3407
3518
|
try {
|
|
3408
|
-
const
|
|
3409
|
-
const
|
|
3410
|
-
|
|
3411
|
-
return prevMinute === epochMinute;
|
|
3519
|
+
const prevSleep = CronExpressionParser2.parse(schedule.sleep, { currentDate: now }).prev().toDate();
|
|
3520
|
+
const prevWake = CronExpressionParser2.parse(schedule.wake, { currentDate: now }).prev().toDate();
|
|
3521
|
+
return { inWindow: prevSleep > prevWake, windowStart: prevSleep };
|
|
3412
3522
|
} catch (err) {
|
|
3413
|
-
slog3.warn(
|
|
3414
|
-
|
|
3523
|
+
slog3.warn(
|
|
3524
|
+
`invalid sleep/wake cron "${schedule.sleep}"/"${schedule.wake}"`,
|
|
3525
|
+
logger_default.errorData(err)
|
|
3526
|
+
);
|
|
3527
|
+
return { inWindow: false, windowStart: null };
|
|
3415
3528
|
}
|
|
3416
3529
|
}
|
|
3417
3530
|
async waitForIdle(name, timeoutMs) {
|
|
@@ -3603,9 +3716,10 @@ var SleepManager = class {
|
|
|
3603
3716
|
this.transitioning.add(event.mind);
|
|
3604
3717
|
sleepMind(event.mind).then(() => this.archiveSessions(event.mind)).then(() => {
|
|
3605
3718
|
state2.sleeping = true;
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3719
|
+
if (!state2.voluntaryWakeAt) {
|
|
3720
|
+
const sleepConfig = this.getSleepConfig(event.mind);
|
|
3721
|
+
state2.scheduledWakeAt = this.getNextWakeTime(sleepConfig);
|
|
3722
|
+
}
|
|
3609
3723
|
this.saveState();
|
|
3610
3724
|
slog3.info(`${event.mind} returned to sleep`);
|
|
3611
3725
|
}).catch((err) => {
|
|
@@ -3631,9 +3745,9 @@ function getSleepManagerIfReady() {
|
|
|
3631
3745
|
}
|
|
3632
3746
|
|
|
3633
3747
|
// packages/daemon/src/lib/delivery/delivery-manager.ts
|
|
3634
|
-
import {
|
|
3635
|
-
import {
|
|
3636
|
-
import { and as and4, eq as eq5, inArray, sql as sql2 } from "drizzle-orm";
|
|
3748
|
+
import { realpath } from "fs/promises";
|
|
3749
|
+
import { resolve as resolve8 } from "path";
|
|
3750
|
+
import { and as and4, eq as eq5, inArray as inArray2, sql as sql2 } from "drizzle-orm";
|
|
3637
3751
|
|
|
3638
3752
|
// packages/daemon/src/lib/chat/typing.ts
|
|
3639
3753
|
var DEFAULT_TTL_MS = 1e4;
|
|
@@ -3772,13 +3886,11 @@ async function checkStaleSend(mindBase, conversationId, excludeSenders) {
|
|
|
3772
3886
|
unseen: unseen.map((r) => ({ sender: r.sender, text: extractText(r.content) }))
|
|
3773
3887
|
};
|
|
3774
3888
|
}
|
|
3889
|
+
function clearMind2(mindBase) {
|
|
3890
|
+
state.delete(mindBase);
|
|
3891
|
+
}
|
|
3775
3892
|
function resetTurn(mindBase) {
|
|
3776
|
-
|
|
3777
|
-
if (!conv) return;
|
|
3778
|
-
for (const entry of conv.values()) {
|
|
3779
|
-
entry.turnOpen = false;
|
|
3780
|
-
entry.heldThisTurn = false;
|
|
3781
|
-
}
|
|
3893
|
+
clearMind2(mindBase);
|
|
3782
3894
|
}
|
|
3783
3895
|
function formatHoldNotice(channelLabel, unseen) {
|
|
3784
3896
|
const lines = unseen.map((m) => {
|
|
@@ -3800,6 +3912,8 @@ var REDRIVE_INTERVAL_MS = 15e3;
|
|
|
3800
3912
|
var RETRY_BASE_MS = 5e3;
|
|
3801
3913
|
var RETRY_MAX_MS = 5 * 6e4;
|
|
3802
3914
|
var REDRIVE_BATCH_LIMIT = 200;
|
|
3915
|
+
var GATED_RELEASE_LIMIT_PER_CHANNEL = 10;
|
|
3916
|
+
var GATED_NOTIFY_EVERY = 10;
|
|
3803
3917
|
var mentionRegexCache = /* @__PURE__ */ new Map();
|
|
3804
3918
|
var avatarBlocksCache = /* @__PURE__ */ new Map();
|
|
3805
3919
|
var AVATAR_CACHE_TTL = 5 * 60 * 1e3;
|
|
@@ -3820,6 +3934,11 @@ var DeliveryManager = class {
|
|
|
3820
3934
|
redriveTimer = null;
|
|
3821
3935
|
/** Predicate for whether a mind is up; overridable in tests. */
|
|
3822
3936
|
isMindRunning = (name) => tryGetMindManager()?.isRunning(name) ?? false;
|
|
3937
|
+
/** Sends a system message to a mind (invites, release summaries); overridable in tests. */
|
|
3938
|
+
notify = async (mindName, text) => {
|
|
3939
|
+
const { sendSystemMessage: sendSystemMessage2 } = await import("./system-chat-N2F7HVY3.js");
|
|
3940
|
+
await sendSystemMessage2(mindName, text);
|
|
3941
|
+
};
|
|
3823
3942
|
constructor() {
|
|
3824
3943
|
setRoutesChangeListener((mind) => {
|
|
3825
3944
|
this.releaseGated(mind).catch(
|
|
@@ -3831,6 +3950,10 @@ var DeliveryManager = class {
|
|
|
3831
3950
|
setRunningCheck(fn) {
|
|
3832
3951
|
this.isMindRunning = fn;
|
|
3833
3952
|
}
|
|
3953
|
+
/** Test seam: capture/override system notifications sent to minds. */
|
|
3954
|
+
setNotifier(fn) {
|
|
3955
|
+
this.notify = fn;
|
|
3956
|
+
}
|
|
3834
3957
|
// --- Public API ---
|
|
3835
3958
|
/**
|
|
3836
3959
|
* Route and deliver a message to a mind. This is the main entry point.
|
|
@@ -3993,9 +4116,15 @@ var DeliveryManager = class {
|
|
|
3993
4116
|
if (redriven > 0) dlog.info(`redrove ${redriven} pending delivery queue rows`);
|
|
3994
4117
|
}
|
|
3995
4118
|
/**
|
|
3996
|
-
* Re-evaluate a mind's `gated` rows against its current routes.json
|
|
3997
|
-
*
|
|
3998
|
-
*
|
|
4119
|
+
* Re-evaluate a mind's `gated` rows against its current routes.json when the routing
|
|
4120
|
+
* config changes. For each channel that now matches a `mind` route:
|
|
4121
|
+
* - the newest {@link GATED_RELEASE_LIMIT_PER_CHANNEL} rows are promoted to `pending`
|
|
4122
|
+
* and re-stamped with the freshly-resolved session (NOT the gate-time fallback), so
|
|
4123
|
+
* they land in the correct session instead of `main` (#537 bug 1);
|
|
4124
|
+
* - any older rows are `archived` (inert) so a long backlog can't flood the mind
|
|
4125
|
+
* in one sweep (#537 bug 2), and the mind gets one summary rather than a flood.
|
|
4126
|
+
* Rows resolving to a `file` destination are archived (the delivery manager doesn't
|
|
4127
|
+
* deliver file routes). Declined channels are skipped entirely — they stay gated.
|
|
3999
4128
|
*/
|
|
4000
4129
|
async releaseGated(mindName) {
|
|
4001
4130
|
const baseName = await getBaseName(mindName);
|
|
@@ -4008,7 +4137,8 @@ var DeliveryManager = class {
|
|
|
4008
4137
|
dlog.warn(`failed to read gated rows for ${baseName}`, logger_default.errorData(err));
|
|
4009
4138
|
return;
|
|
4010
4139
|
}
|
|
4011
|
-
const
|
|
4140
|
+
const byChannel = /* @__PURE__ */ new Map();
|
|
4141
|
+
const archiveIds = [];
|
|
4012
4142
|
for (const row of rows) {
|
|
4013
4143
|
let payload;
|
|
4014
4144
|
try {
|
|
@@ -4023,21 +4153,121 @@ var DeliveryManager = class {
|
|
|
4023
4153
|
participantCount: payload.participantCount
|
|
4024
4154
|
};
|
|
4025
4155
|
const route = resolveRoute(config, meta);
|
|
4026
|
-
if (route.matched
|
|
4027
|
-
|
|
4156
|
+
if (!route.matched) continue;
|
|
4157
|
+
if (route.destination === "file") {
|
|
4158
|
+
archiveIds.push(row.id);
|
|
4159
|
+
continue;
|
|
4160
|
+
}
|
|
4161
|
+
let session = route.session;
|
|
4162
|
+
if (session === "$new") {
|
|
4163
|
+
session = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
4164
|
+
}
|
|
4165
|
+
const channel = row.channel ?? payload.channel ?? "unknown";
|
|
4166
|
+
const list = byChannel.get(channel) ?? [];
|
|
4167
|
+
list.push({ id: row.id, session });
|
|
4168
|
+
byChannel.set(channel, list);
|
|
4169
|
+
}
|
|
4170
|
+
const promote = [];
|
|
4171
|
+
const truncationNotes = [];
|
|
4172
|
+
for (const [channel, items] of byChannel) {
|
|
4173
|
+
if (await this.isChannelDeclined(baseName, channel)) continue;
|
|
4174
|
+
items.sort((a, b) => b.id - a.id);
|
|
4175
|
+
const keep = items.slice(0, GATED_RELEASE_LIMIT_PER_CHANNEL);
|
|
4176
|
+
const drop = items.slice(GATED_RELEASE_LIMIT_PER_CHANNEL);
|
|
4177
|
+
promote.push(...keep);
|
|
4178
|
+
for (const d of drop) archiveIds.push(d.id);
|
|
4179
|
+
if (drop.length > 0) {
|
|
4180
|
+
truncationNotes.push(
|
|
4181
|
+
`${channel}: released the ${keep.length} most recent message(s); ${drop.length} earlier message(s) were held while unrouted and remain in the channel history (volute chat read ${channel}).`
|
|
4182
|
+
);
|
|
4183
|
+
dlog.info(
|
|
4184
|
+
`truncated gated release for ${baseName} on ${channel}: kept ${keep.length}, archived ${drop.length}`
|
|
4185
|
+
);
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
if (archiveIds.length > 0) {
|
|
4189
|
+
try {
|
|
4190
|
+
const db = await getDb();
|
|
4191
|
+
for (let i = 0; i < archiveIds.length; i += 500) {
|
|
4192
|
+
await db.update(deliveryQueue).set({ status: "archived" }).where(inArray2(deliveryQueue.id, archiveIds.slice(i, i + 500)));
|
|
4193
|
+
}
|
|
4194
|
+
} catch (err) {
|
|
4195
|
+
dlog.warn(`failed to archive gated rows for ${baseName}`, logger_default.errorData(err));
|
|
4028
4196
|
}
|
|
4029
4197
|
}
|
|
4030
|
-
if (promote.length === 0)
|
|
4198
|
+
if (promote.length === 0) {
|
|
4199
|
+
if (truncationNotes.length > 0) await this.sendReleaseSummary(mindName, truncationNotes);
|
|
4200
|
+
return;
|
|
4201
|
+
}
|
|
4031
4202
|
try {
|
|
4032
4203
|
const db = await getDb();
|
|
4033
|
-
|
|
4204
|
+
for (const p of promote) {
|
|
4205
|
+
await db.update(deliveryQueue).set({ status: "pending", session: p.session, attempts: 0, next_attempt_at: null }).where(eq5(deliveryQueue.id, p.id));
|
|
4206
|
+
}
|
|
4034
4207
|
dlog.info(`released ${promote.length} gated message(s) for ${baseName} after route change`);
|
|
4035
4208
|
} catch (err) {
|
|
4036
4209
|
dlog.warn(`failed to promote gated rows for ${baseName}`, logger_default.errorData(err));
|
|
4037
4210
|
return;
|
|
4038
4211
|
}
|
|
4212
|
+
if (truncationNotes.length > 0) await this.sendReleaseSummary(mindName, truncationNotes);
|
|
4039
4213
|
await this.redrive();
|
|
4040
4214
|
}
|
|
4215
|
+
/**
|
|
4216
|
+
* Whether the mind has explicitly declined a channel. A declined channel keeps
|
|
4217
|
+
* persisting history but never notifies and is never released. #537
|
|
4218
|
+
*/
|
|
4219
|
+
async isChannelDeclined(baseName, channel) {
|
|
4220
|
+
if (!channel) return false;
|
|
4221
|
+
try {
|
|
4222
|
+
const db = await getDb();
|
|
4223
|
+
const rows = await db.select({ state: channelGates.state }).from(channelGates).where(and4(eq5(channelGates.mind, baseName), eq5(channelGates.channel, channel)));
|
|
4224
|
+
return rows[0]?.state === "declined";
|
|
4225
|
+
} catch (err) {
|
|
4226
|
+
dlog.warn(`failed to read gate state for ${baseName}/${channel}`, logger_default.errorData(err));
|
|
4227
|
+
return false;
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
/**
|
|
4231
|
+
* Record that a mind has declined an unrouted channel: future messages are still
|
|
4232
|
+
* persisted (history is preserved) but never notify, and any currently-gated rows are
|
|
4233
|
+
* archived so they're inert. Returns the number of held messages archived. #537
|
|
4234
|
+
*/
|
|
4235
|
+
async declineChannel(mindName, channel) {
|
|
4236
|
+
const baseName = await getBaseName(mindName);
|
|
4237
|
+
const db = await getDb();
|
|
4238
|
+
await db.insert(channelGates).values({ mind: baseName, channel, state: "declined" }).onConflictDoUpdate({
|
|
4239
|
+
target: [channelGates.mind, channelGates.channel],
|
|
4240
|
+
set: { state: "declined", updated_at: sql2`(datetime('now'))` }
|
|
4241
|
+
});
|
|
4242
|
+
const archived = await db.update(deliveryQueue).set({ status: "archived" }).where(
|
|
4243
|
+
and4(
|
|
4244
|
+
eq5(deliveryQueue.mind, baseName),
|
|
4245
|
+
eq5(deliveryQueue.channel, channel),
|
|
4246
|
+
eq5(deliveryQueue.status, "gated")
|
|
4247
|
+
)
|
|
4248
|
+
).returning({ id: deliveryQueue.id });
|
|
4249
|
+
dlog.info(
|
|
4250
|
+
`declined channel ${channel} for ${baseName}; archived ${archived.length} held row(s)`
|
|
4251
|
+
);
|
|
4252
|
+
return archived.length;
|
|
4253
|
+
}
|
|
4254
|
+
/**
|
|
4255
|
+
* Send the mind a single summary when a routing change released a truncated backlog,
|
|
4256
|
+
* rather than a flood of individual messages. #537
|
|
4257
|
+
*/
|
|
4258
|
+
async sendReleaseSummary(mindName, notes) {
|
|
4259
|
+
const body = [
|
|
4260
|
+
`[Channel backlog released]`,
|
|
4261
|
+
`A routing change matched channel(s) that had held messages while unrouted. To avoid flooding you, only the ${GATED_RELEASE_LIMIT_PER_CHANNEL} most recent per channel were delivered:`,
|
|
4262
|
+
"",
|
|
4263
|
+
...notes.map((n) => `- ${n}`)
|
|
4264
|
+
].join("\n");
|
|
4265
|
+
try {
|
|
4266
|
+
await this.notify(mindName, body);
|
|
4267
|
+
} catch (err) {
|
|
4268
|
+
dlog.warn(`failed to send release summary for ${mindName}`, logger_default.errorData(err));
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4041
4271
|
/**
|
|
4042
4272
|
* Get pending (gated) messages for a mind.
|
|
4043
4273
|
*/
|
|
@@ -4088,6 +4318,7 @@ var DeliveryManager = class {
|
|
|
4088
4318
|
*/
|
|
4089
4319
|
clearMindSessions(mindName) {
|
|
4090
4320
|
this.sessionStates.delete(mindName);
|
|
4321
|
+
clearMind2(mindName);
|
|
4091
4322
|
const toDelete = [];
|
|
4092
4323
|
for (const [bufferKey, buffer] of this.batchBuffers) {
|
|
4093
4324
|
if (bufferKey.startsWith(`${mindName}:`)) {
|
|
@@ -4195,7 +4426,7 @@ var DeliveryManager = class {
|
|
|
4195
4426
|
if (valid.length === 0) return;
|
|
4196
4427
|
try {
|
|
4197
4428
|
const db = await getDb();
|
|
4198
|
-
await db.delete(deliveryQueue).where(
|
|
4429
|
+
await db.delete(deliveryQueue).where(inArray2(deliveryQueue.id, valid));
|
|
4199
4430
|
} catch (err) {
|
|
4200
4431
|
dlog.warn("failed to delete delivered delivery queue rows", logger_default.errorData(err));
|
|
4201
4432
|
}
|
|
@@ -4206,7 +4437,7 @@ var DeliveryManager = class {
|
|
|
4206
4437
|
if (valid.length === 0) return;
|
|
4207
4438
|
try {
|
|
4208
4439
|
const db = await getDb();
|
|
4209
|
-
const rows = await db.select({ id: deliveryQueue.id, attempts: deliveryQueue.attempts }).from(deliveryQueue).where(
|
|
4440
|
+
const rows = await db.select({ id: deliveryQueue.id, attempts: deliveryQueue.attempts }).from(deliveryQueue).where(inArray2(deliveryQueue.id, valid));
|
|
4210
4441
|
for (const row of rows) {
|
|
4211
4442
|
const attempts = row.attempts + 1;
|
|
4212
4443
|
const backoffSec = Math.round(
|
|
@@ -4436,41 +4667,44 @@ var DeliveryManager = class {
|
|
|
4436
4667
|
}
|
|
4437
4668
|
async gateMessage(mindName, session, payload) {
|
|
4438
4669
|
const baseName = await getBaseName(mindName);
|
|
4439
|
-
await this.
|
|
4670
|
+
const declined = await this.isChannelDeclined(baseName, payload.channel);
|
|
4671
|
+
await this.persistToQueue(mindName, session, payload, declined ? "archived" : "gated");
|
|
4672
|
+
if (declined) return;
|
|
4440
4673
|
try {
|
|
4441
4674
|
const db = await getDb();
|
|
4442
|
-
const
|
|
4675
|
+
const rows = await db.select({ count: sql2`count(*)` }).from(deliveryQueue).where(
|
|
4443
4676
|
and4(
|
|
4444
4677
|
eq5(deliveryQueue.mind, baseName),
|
|
4445
4678
|
eq5(deliveryQueue.channel, payload.channel),
|
|
4446
|
-
|
|
4679
|
+
inArray2(deliveryQueue.status, ["gated", "archived"])
|
|
4447
4680
|
)
|
|
4448
4681
|
);
|
|
4449
|
-
|
|
4450
|
-
|
|
4682
|
+
const count = rows[0]?.count ?? 0;
|
|
4683
|
+
if (count === 1 || count % GATED_NOTIFY_EVERY === 0) {
|
|
4684
|
+
await this.sendInviteNotification(mindName, payload, count);
|
|
4451
4685
|
}
|
|
4452
4686
|
} catch (err) {
|
|
4453
4687
|
dlog.warn(`failed to check gated count for ${baseName}`, logger_default.errorData(err));
|
|
4454
4688
|
}
|
|
4455
4689
|
}
|
|
4456
|
-
async sendInviteNotification(mindName, payload) {
|
|
4690
|
+
async sendInviteNotification(mindName, payload, gatedCount = 1) {
|
|
4457
4691
|
const text = extractTextContent(payload.content);
|
|
4458
4692
|
const preview = text.length > 200 ? `${text.slice(0, 200)}...` : text;
|
|
4459
4693
|
const channel = payload.channel ?? "unknown";
|
|
4694
|
+
const heldLine = gatedCount > 1 ? `${gatedCount} messages from this channel are being held, unrouted \u2014 you've not routed it yet.` : `Someone new is reaching out \u2014 you don't have a route for this channel yet.`;
|
|
4460
4695
|
const notification = [
|
|
4461
4696
|
`[New channel: ${channel}]`,
|
|
4462
|
-
|
|
4697
|
+
heldLine,
|
|
4463
4698
|
`Sender: ${payload.sender ?? "unknown"}`,
|
|
4464
4699
|
payload.platform ? `Platform: ${payload.platform}` : null,
|
|
4465
4700
|
payload.participantCount ? `Participants: ${payload.participantCount}` : null,
|
|
4466
4701
|
"",
|
|
4467
4702
|
`Preview: ${preview}`,
|
|
4468
4703
|
"",
|
|
4469
|
-
`To start hearing this channel, add a routing rule for "${channel}" to .config/routes.json.`,
|
|
4470
|
-
`
|
|
4704
|
+
`To start hearing this channel, add a routing rule for "${channel}" to .config/routes.json \u2014 only the ${GATED_RELEASE_LIMIT_PER_CHANNEL} most recent held messages are replayed; older ones stay in the channel history (volute chat read ${channel}).`,
|
|
4705
|
+
`To stop hearing about it, decline the channel: volute chat channels decline ${channel}`
|
|
4471
4706
|
].filter((line) => line !== null).join("\n");
|
|
4472
|
-
|
|
4473
|
-
await sendSystemMessage2(mindName, notification);
|
|
4707
|
+
await this.notify(mindName, notification);
|
|
4474
4708
|
}
|
|
4475
4709
|
/**
|
|
4476
4710
|
* Insert a delivery_queue row and return its id. The `mind` column is always keyed by
|
|
@@ -4535,18 +4769,6 @@ var DeliveryManager = class {
|
|
|
4535
4769
|
const cached = avatarBlocksCache.get(cacheKey);
|
|
4536
4770
|
if (cached && cached.expiresAt > Date.now()) return cached.blocks;
|
|
4537
4771
|
const blocks = [];
|
|
4538
|
-
let sharpDefault = null;
|
|
4539
|
-
try {
|
|
4540
|
-
const mod = await import("sharp");
|
|
4541
|
-
sharpDefault = mod.default ?? mod;
|
|
4542
|
-
} catch (err) {
|
|
4543
|
-
const code = err.code;
|
|
4544
|
-
if (code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND") {
|
|
4545
|
-
dlog.debug("sharp not available, sending full-size avatars");
|
|
4546
|
-
} else {
|
|
4547
|
-
dlog.warn("sharp import failed, sending full-size avatars", logger_default.errorData(err));
|
|
4548
|
-
}
|
|
4549
|
-
}
|
|
4550
4772
|
for (const p of participants) {
|
|
4551
4773
|
if (!p.avatar) continue;
|
|
4552
4774
|
try {
|
|
@@ -4577,32 +4799,8 @@ var DeliveryManager = class {
|
|
|
4577
4799
|
} else {
|
|
4578
4800
|
filePath = resolve8(voluteHome(), "avatars", p.avatar);
|
|
4579
4801
|
}
|
|
4580
|
-
const
|
|
4581
|
-
|
|
4582
|
-
".png": "image/png",
|
|
4583
|
-
".jpg": "image/jpeg",
|
|
4584
|
-
".jpeg": "image/jpeg",
|
|
4585
|
-
".gif": "image/gif",
|
|
4586
|
-
".webp": "image/webp"
|
|
4587
|
-
};
|
|
4588
|
-
const mediaType = mimeMap[ext];
|
|
4589
|
-
if (!mediaType) continue;
|
|
4590
|
-
const data = await readFile(filePath);
|
|
4591
|
-
let imageData = data;
|
|
4592
|
-
if (sharpDefault) {
|
|
4593
|
-
try {
|
|
4594
|
-
imageData = await sharpDefault(data).resize(128, 128, { fit: "cover" }).toBuffer();
|
|
4595
|
-
} catch (err) {
|
|
4596
|
-
dlog.warn(
|
|
4597
|
-
`avatar resize failed for ${p.username}, sending original`,
|
|
4598
|
-
logger_default.errorData(err)
|
|
4599
|
-
);
|
|
4600
|
-
}
|
|
4601
|
-
}
|
|
4602
|
-
blocks.push(
|
|
4603
|
-
{ type: "text", text: `[Avatar for ${p.username}]` },
|
|
4604
|
-
{ type: "image", media_type: mediaType, data: imageData.toString("base64") }
|
|
4605
|
-
);
|
|
4802
|
+
const rendered = await renderAvatarBlock(filePath, p.username);
|
|
4803
|
+
if (rendered) blocks.push(...rendered);
|
|
4606
4804
|
} catch (err) {
|
|
4607
4805
|
const code = err.code;
|
|
4608
4806
|
if (code !== "ENOENT") {
|
|
@@ -4610,7 +4808,11 @@ var DeliveryManager = class {
|
|
|
4610
4808
|
}
|
|
4611
4809
|
}
|
|
4612
4810
|
}
|
|
4613
|
-
|
|
4811
|
+
const now = Date.now();
|
|
4812
|
+
for (const [key, entry] of avatarBlocksCache) {
|
|
4813
|
+
if (entry.expiresAt <= now) avatarBlocksCache.delete(key);
|
|
4814
|
+
}
|
|
4815
|
+
avatarBlocksCache.set(cacheKey, { blocks, expiresAt: now + AVATAR_CACHE_TTL });
|
|
4614
4816
|
return blocks;
|
|
4615
4817
|
}
|
|
4616
4818
|
incrementActive(mind, session, senders, channels) {
|
|
@@ -4761,8 +4963,8 @@ async function linkToolResultToTurn(mind, turnId, toolResultContent, toolUseEven
|
|
|
4761
4963
|
await db.update(activity).set({
|
|
4762
4964
|
turn_id: turnId,
|
|
4763
4965
|
...toolUseEventId != null ? { source_event_id: toolUseEventId } : {}
|
|
4764
|
-
}).where(
|
|
4765
|
-
const actRows = await db.select().from(activity).where(
|
|
4966
|
+
}).where(inArray3(activity.id, activityIds));
|
|
4967
|
+
const actRows = await db.select().from(activity).where(inArray3(activity.id, activityIds));
|
|
4766
4968
|
if (actRows.length > 0) {
|
|
4767
4969
|
await db.insert(mindHistory).values(
|
|
4768
4970
|
actRows.map((a) => ({
|
|
@@ -4826,6 +5028,40 @@ async function deliverMessage(mindName, payload, opts = {}) {
|
|
|
4826
5028
|
return false;
|
|
4827
5029
|
}
|
|
4828
5030
|
}
|
|
5031
|
+
async function deliverBatch(mindName, payloads) {
|
|
5032
|
+
if (payloads.length === 0) return true;
|
|
5033
|
+
try {
|
|
5034
|
+
const baseName = await getBaseName(mindName);
|
|
5035
|
+
const entry = await findMind(baseName);
|
|
5036
|
+
if (!entry) {
|
|
5037
|
+
dlog2.warn(`cannot deliver batch to ${mindName}: mind not found`);
|
|
5038
|
+
return false;
|
|
5039
|
+
}
|
|
5040
|
+
const channels = {};
|
|
5041
|
+
for (const p of payloads) {
|
|
5042
|
+
const ch = p.channel ?? "unknown";
|
|
5043
|
+
if (!channels[ch]) channels[ch] = [];
|
|
5044
|
+
channels[ch].push(p);
|
|
5045
|
+
}
|
|
5046
|
+
const first = payloads[0];
|
|
5047
|
+
const route = resolveRoute(getRoutingConfig(baseName), {
|
|
5048
|
+
channel: first.channel,
|
|
5049
|
+
sender: first.sender ?? void 0,
|
|
5050
|
+
isDM: first.isDM,
|
|
5051
|
+
participantCount: first.participantCount
|
|
5052
|
+
});
|
|
5053
|
+
const session = first.session ?? (route.destination === "mind" ? route.session : "main");
|
|
5054
|
+
const res = await fetch(`http://127.0.0.1:${entry.port}/message`, {
|
|
5055
|
+
method: "POST",
|
|
5056
|
+
headers: { "Content-Type": "application/json" },
|
|
5057
|
+
body: JSON.stringify({ session, batch: { channels } })
|
|
5058
|
+
});
|
|
5059
|
+
return res.ok;
|
|
5060
|
+
} catch (err) {
|
|
5061
|
+
dlog2.warn(`unexpected error delivering batch to ${mindName}`, logger_default.errorData(err));
|
|
5062
|
+
return false;
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
4829
5065
|
|
|
4830
5066
|
// packages/daemon/src/lib/chat/system-chat.ts
|
|
4831
5067
|
var slog4 = logger_default.child("system-chat");
|
|
@@ -4879,6 +5115,10 @@ async function sendSystemMessage(mindName, text, opts) {
|
|
|
4879
5115
|
});
|
|
4880
5116
|
}
|
|
4881
5117
|
async function sendSystemMessageDirect(mindName, text) {
|
|
5118
|
+
if (isSpiritName(mindName)) {
|
|
5119
|
+
await recordInbound(mindName, "@volute", "volute", text);
|
|
5120
|
+
return {};
|
|
5121
|
+
}
|
|
4882
5122
|
const { conversationId } = await ensureSystemDM(mindName);
|
|
4883
5123
|
await addMessage(conversationId, "user", "volute", [{ type: "text", text }]);
|
|
4884
5124
|
await recordInbound(mindName, "@volute", "volute", text);
|
|
@@ -4888,7 +5128,7 @@ async function spiritWillHandle() {
|
|
|
4888
5128
|
const spiritEntry = await findMind(SPIRIT_NAME);
|
|
4889
5129
|
if (spiritEntry?.running && spiritEntry.mindType === "spirit") return true;
|
|
4890
5130
|
try {
|
|
4891
|
-
const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-
|
|
5131
|
+
const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-3NA7ZJP3.js");
|
|
4892
5132
|
const sm = getSleepManagerIfReady2();
|
|
4893
5133
|
if (sm?.isSleeping(SPIRIT_NAME)) return true;
|
|
4894
5134
|
} catch (err) {
|
|
@@ -4916,7 +5156,7 @@ async function generateSystemFallbackReply(conversationId, mindName, message) {
|
|
|
4916
5156
|
if (config.sleep.schedule?.wake) contextParts.push(`Wake cron: ${config.sleep.schedule.wake}`);
|
|
4917
5157
|
}
|
|
4918
5158
|
try {
|
|
4919
|
-
const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-
|
|
5159
|
+
const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-3NA7ZJP3.js");
|
|
4920
5160
|
const sm = getSleepManagerIfReady2();
|
|
4921
5161
|
if (sm) {
|
|
4922
5162
|
const state2 = sm.getState(mindName);
|
|
@@ -5001,6 +5241,7 @@ export {
|
|
|
5001
5241
|
readSystemsConfig,
|
|
5002
5242
|
writeSystemsConfig,
|
|
5003
5243
|
deleteSystemsConfig,
|
|
5244
|
+
enrichActivityMetadata,
|
|
5004
5245
|
parseCommandArgs,
|
|
5005
5246
|
loadAllExtensions,
|
|
5006
5247
|
getLoadedExtensions,
|
|
@@ -5025,6 +5266,7 @@ export {
|
|
|
5025
5266
|
wakeMind,
|
|
5026
5267
|
startSpiritFull,
|
|
5027
5268
|
stopSpiritFull,
|
|
5269
|
+
buildSeedOrientation,
|
|
5028
5270
|
stopMindFull,
|
|
5029
5271
|
matchesGlob,
|
|
5030
5272
|
SleepManager,
|
|
@@ -5036,6 +5278,7 @@ export {
|
|
|
5036
5278
|
linkToolResultToTurn,
|
|
5037
5279
|
resolveSleepAction,
|
|
5038
5280
|
deliverMessage,
|
|
5281
|
+
deliverBatch,
|
|
5039
5282
|
ensureSystemChannel,
|
|
5040
5283
|
joinSystemChannel,
|
|
5041
5284
|
announceToSystem
|