volute 0.42.1 → 0.43.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/dist/{activity-events-KFKVRZE3.js → activity-events-UISXY4Y5.js} +3 -3
- package/dist/{ai-service-3GBOJGUA.js → ai-service-FBCCEYF5.js} +8 -4
- package/dist/{archive-4PPYF7LL.js → archive-I32WHCJB.js} +3 -3
- package/dist/{auth-EAASQPTN.js → auth-3RIG6GKN.js} +4 -4
- package/dist/{chat-LYL5CWJV.js → chat-SDSX5TVV.js} +2 -2
- package/dist/{chunk-M2YMNTHK.js → chunk-2HIVYZDB.js} +1 -1
- package/dist/{chunk-BWZHX46O.js → chunk-3WLWKOGM.js} +21 -6
- package/dist/{chunk-QPTQKGGF.js → chunk-57LBQ7JX.js} +1 -1
- package/dist/{chunk-43K5UWF7.js → chunk-65OLHBCL.js} +13 -12
- package/dist/{chunk-K547QOCM.js → chunk-7G2EEYXX.js} +2 -2
- package/dist/{chunk-E7XWFDZL.js → chunk-AD46AYZQ.js} +1 -1
- package/dist/{chunk-P6DPB33N.js → chunk-ATEX3ZPG.js} +2 -2
- package/dist/{chunk-WR7CSHQ4.js → chunk-C6ESRQZ7.js} +1 -1
- package/dist/{chunk-BR6KFHVT.js → chunk-EE7SZJON.js} +2 -2
- package/dist/{chunk-5QG36RV7.js → chunk-EFL5UOY4.js} +3 -3
- package/dist/{chunk-M3T3Y6FT.js → chunk-FSSUANZJ.js} +5 -5
- package/dist/{chunk-JPDN6ZRJ.js → chunk-FZ5TH5PF.js} +8 -8
- package/dist/{chunk-OEJFZHZZ.js → chunk-GZJFC76M.js} +1 -1
- package/dist/{chunk-KUJVCDWT.js → chunk-JVK6QQIC.js} +2 -2
- package/dist/{chunk-YLTIEEVL.js → chunk-K4EHBHPE.js} +3 -3
- package/dist/{chunk-MHFAWJCC.js → chunk-L3IV23ZG.js} +12 -1
- package/dist/{chunk-VBKRZCR7.js → chunk-M3PGGSBH.js} +1 -1
- package/dist/{chunk-NSGIVS5T.js → chunk-O3DXAR44.js} +2 -2
- package/dist/{chunk-KZAL7HEQ.js → chunk-ONM2AZFL.js} +1 -1
- package/dist/{chunk-6G7R2LPI.js → chunk-PF2AB7VJ.js} +568 -378
- package/dist/chunk-PGPLCJPK.js +272 -0
- package/dist/{chunk-62ICA2FF.js → chunk-SPLPKBES.js} +17 -1
- package/dist/{chunk-BLOAJLGI.js → chunk-XDTKJNW5.js} +1 -1
- package/dist/{chunk-IUZ3EV2D.js → chunk-YAMO2U3N.js} +1 -1
- package/dist/{chunk-4ZSUUBG5.js → chunk-YSV3VJQK.js} +55 -2
- package/dist/{chunk-JU5GWO77.js → chunk-ZO7IQJKO.js} +3 -3
- package/dist/cli.js +15 -15
- package/dist/{cloud-sync-E3ZL5SZ4.js → cloud-sync-FRYNWKCR.js} +16 -17
- package/dist/{conversations-FSZCUUYO.js → conversations-67WWW25C.js} +4 -4
- package/dist/{create-ERNQYOCK.js → create-7W3LV3J7.js} +1 -1
- package/dist/{daemon-restart-QBZOMILO.js → daemon-restart-5GVZ5V3X.js} +7 -8
- package/dist/daemon.js +524 -433
- package/dist/{db-ZJJJYJBU.js → db-CZV62BOQ.js} +2 -2
- package/dist/delivery-manager-5J2BE22P.js +32 -0
- package/dist/{delivery-router-FDBZJQOY.js → delivery-router-HXBHYHJM.js} +7 -5
- package/dist/down-RRNYIGCU.js +16 -0
- package/dist/echo-text-XKBJM6RV.js +32 -0
- package/dist/{exec-ZDD7NEB4.js → exec-SJK7VUTC.js} +3 -4
- package/dist/{export-WYI3E3TB.js → export-7LPCIB5J.js} +3 -3
- package/dist/{extensions-X6MKBJLB.js → extensions-76EGTMFY.js} +16 -17
- package/dist/{files-COTCVBGO.js → files-LV7VAKPC.js} +3 -3
- package/dist/{import-EPK6WCYS.js → import-XPECCRQ7.js} +6 -6
- package/dist/{isolation-F7ABZMDA.js → isolation-5AYEY55P.js} +5 -3
- package/dist/{login-GPBXOU4D.js → login-KJSN4JDA.js} +2 -2
- package/dist/{logout-DNNTIYE2.js → logout-TMJGUQV2.js} +2 -2
- package/dist/message-delivery-WJBZFALY.js +34 -0
- package/dist/{mind-K5PZHPBR.js → mind-QEWCR7FA.js} +5 -5
- package/dist/{mind-activity-tracker-HWVFE533.js → mind-activity-tracker-JHAT6456.js} +4 -4
- package/dist/mind-service-ZBVUMJ5H.js +36 -0
- package/dist/{package-G4AM2IDZ.js → package-ZOI2KAYC.js} +1 -1
- package/dist/{registry-MDLNPTTI.js → registry-7LQHTRUX.js} +2 -2
- package/dist/{sandbox-E7RWZDVG.js → sandbox-SJH3XOPZ.js} +4 -4
- package/dist/scheduler-BR6C6B3E.js +30 -0
- package/dist/{schema-HHYJDG7B.js → schema-LRUTFOER.js} +1 -1
- package/dist/{seed-SKVSWF7S.js → seed-KOG6KJAO.js} +1 -1
- package/dist/{seed-cmd-D2H5VLB4.js → seed-cmd-GJFENEJH.js} +2 -2
- package/dist/{seed-create-BUSKQ5II.js → seed-create-YXO6R4KE.js} +3 -3
- package/dist/{seed-sprout-R2IZPITD.js → seed-sprout-BBU64QXP.js} +7 -8
- package/dist/{send-SLDFRSTL.js → send-ZBZWIH5V.js} +3 -3
- package/dist/{service-ZDOSEQCE.js → service-4EVZKGSA.js} +4 -5
- package/dist/{service-install-2HZPDHEY.js → service-install-SHRM6NHK.js} +5 -6
- package/dist/{setup-IYOUPCIK.js → setup-25MMJ3J4.js} +3 -3
- package/dist/{setup-IU3ID7II.js → setup-354PDHUF.js} +8 -10
- package/dist/{skills-P6RGGASV.js → skills-DPI3WZPQ.js} +5 -6
- package/dist/sleep-manager-BVU5D2AE.js +34 -0
- package/dist/{spirit-XLD6QXSS.js → spirit-JQQHUVWR.js} +7 -8
- package/dist/{sprout-QV2RJM5W.js → sprout-4WAYBUVV.js} +1 -1
- package/dist/{status-QAKAPDYW.js → status-WTU2WHXY.js} +5 -6
- package/dist/system-chat-KMQIP7VH.js +34 -0
- package/dist/{tailscale-TIRTGM6E.js → tailscale-Q7ZJL7S7.js} +2 -2
- package/dist/{turn-tracker-Z5UJC4BY.js → turn-tracker-2GMKJQTN.js} +7 -5
- package/dist/up-7RSBEKW6.js +18 -0
- package/dist/{update-NY3XBVAJ.js → update-EJ7Q77ZX.js} +5 -6
- package/dist/{update-check-OG27AL4Z.js → update-check-ACYYNKTW.js} +3 -3
- package/dist/{version-notify-P7UAR7HZ.js → version-notify-DCAIXXZB.js} +17 -18
- package/dist/web-assets/assets/{index-CrFAsfiE.js → index-DTDmEfPU.js} +2 -2
- package/dist/web-assets/index.html +1 -1
- package/drizzle/0010_delivery_queue_redrive.sql +10 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/templates/codex/src/server.ts +2 -8
- package/dist/chunk-GN52E2DP.js +0 -59
- package/dist/chunk-TFBQESH7.js +0 -179
- package/dist/delivery-manager-J3BRHXUF.js +0 -33
- package/dist/down-DSRWSCXY.js +0 -17
- package/dist/echo-text-3HBQUWPX.js +0 -33
- package/dist/message-delivery-4ZWFDBAY.js +0 -41
- package/dist/mind-manager-6DSY5IJW.js +0 -33
- package/dist/mind-service-R2FM53KD.js +0 -37
- package/dist/scheduler-2FKQCRCM.js +0 -31
- package/dist/sleep-manager-F5KWIFOX.js +0 -35
- package/dist/system-chat-EQIH6JVL.js +0 -35
- package/dist/up-ZTPGC7HB.js +0 -19
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
loadMergedEnv
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XDTKJNW5.js";
|
|
5
5
|
import {
|
|
6
6
|
spiritDir
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-65OLHBCL.js";
|
|
8
8
|
import {
|
|
9
9
|
hashSkillDir,
|
|
10
10
|
importSkillFromDir,
|
|
11
11
|
removeSharedSkill,
|
|
12
12
|
sharedSkillsDir
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-FSSUANZJ.js";
|
|
14
14
|
import {
|
|
15
15
|
addMessage,
|
|
16
16
|
createChannel,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
getParticipants,
|
|
21
21
|
joinChannel,
|
|
22
22
|
publish as publish2
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-K4EHBHPE.js";
|
|
24
24
|
import {
|
|
25
25
|
readVoluteConfig,
|
|
26
26
|
writeVoluteConfig
|
|
@@ -28,53 +28,52 @@ import {
|
|
|
28
28
|
import {
|
|
29
29
|
isSandboxEnabled,
|
|
30
30
|
wrapForSandbox
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-C6ESRQZ7.js";
|
|
32
32
|
import {
|
|
33
33
|
markIdle,
|
|
34
34
|
onMindEvent
|
|
35
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-M3PGGSBH.js";
|
|
36
36
|
import {
|
|
37
37
|
extractTextContent,
|
|
38
38
|
getRoutingConfig,
|
|
39
39
|
resolveDeliveryMode,
|
|
40
|
-
resolveRoute
|
|
41
|
-
|
|
40
|
+
resolveRoute,
|
|
41
|
+
setRoutesChangeListener
|
|
42
|
+
} from "./chunk-SPLPKBES.js";
|
|
42
43
|
import {
|
|
43
44
|
clearMind,
|
|
44
|
-
getActiveTurnId,
|
|
45
45
|
getPrompt,
|
|
46
|
+
linkInboundToActiveTurn,
|
|
46
47
|
publish as publish3,
|
|
47
48
|
summarizeOrphanedTurns
|
|
48
|
-
} from "./chunk-
|
|
49
|
-
import {
|
|
50
|
-
exec
|
|
51
|
-
} from "./chunk-GN52E2DP.js";
|
|
49
|
+
} from "./chunk-3WLWKOGM.js";
|
|
52
50
|
import {
|
|
53
51
|
chownMindDir,
|
|
52
|
+
exec,
|
|
54
53
|
isIsolationEnabled,
|
|
55
54
|
mindUserName,
|
|
56
55
|
wrapForIsolation
|
|
57
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-PGPLCJPK.js";
|
|
58
57
|
import {
|
|
59
58
|
aiCompleteUtility,
|
|
60
59
|
getAiConfig,
|
|
61
60
|
resolveApiKey
|
|
62
|
-
} from "./chunk-
|
|
61
|
+
} from "./chunk-YSV3VJQK.js";
|
|
63
62
|
import {
|
|
64
63
|
readGlobalConfig,
|
|
65
64
|
writeGlobalConfig
|
|
66
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-ONM2AZFL.js";
|
|
67
66
|
import {
|
|
68
67
|
getOrCreateMindUser,
|
|
69
68
|
getOrCreateSystemUser,
|
|
70
69
|
getUser,
|
|
71
70
|
getUserByUsername,
|
|
72
71
|
syncMindProfile
|
|
73
|
-
} from "./chunk-
|
|
72
|
+
} from "./chunk-EFL5UOY4.js";
|
|
74
73
|
import {
|
|
75
74
|
publish,
|
|
76
75
|
subscribe
|
|
77
|
-
} from "./chunk-
|
|
76
|
+
} from "./chunk-7G2EEYXX.js";
|
|
78
77
|
import {
|
|
79
78
|
logger_default
|
|
80
79
|
} from "./chunk-T2TP6ZC6.js";
|
|
@@ -88,21 +87,18 @@ import {
|
|
|
88
87
|
stateDir,
|
|
89
88
|
voluteHome,
|
|
90
89
|
voluteSystemDir
|
|
91
|
-
} from "./chunk-
|
|
90
|
+
} from "./chunk-YAMO2U3N.js";
|
|
92
91
|
import {
|
|
93
92
|
activity,
|
|
94
|
-
conversationParticipants,
|
|
95
93
|
deliveryQueue,
|
|
96
94
|
messages,
|
|
97
95
|
mindHistory,
|
|
98
96
|
mindNotices,
|
|
99
|
-
sessions
|
|
100
|
-
|
|
101
|
-
users
|
|
102
|
-
} from "./chunk-MHFAWJCC.js";
|
|
97
|
+
sessions
|
|
98
|
+
} from "./chunk-L3IV23ZG.js";
|
|
103
99
|
|
|
104
100
|
// packages/daemon/src/lib/delivery/message-delivery.ts
|
|
105
|
-
import { and as and4,
|
|
101
|
+
import { and as and4, eq as eq5, inArray as inArray3 } from "drizzle-orm";
|
|
106
102
|
|
|
107
103
|
// packages/daemon/src/lib/daemon/sleep-manager.ts
|
|
108
104
|
import { execFile as execFile2, spawn as spawnChild } from "child_process";
|
|
@@ -126,6 +122,20 @@ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as rea
|
|
|
126
122
|
import { resolve as resolve2 } from "path";
|
|
127
123
|
import { promisify } from "util";
|
|
128
124
|
|
|
125
|
+
// packages/daemon/src/lib/util/health.ts
|
|
126
|
+
async function checkHealth(port) {
|
|
127
|
+
try {
|
|
128
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
129
|
+
signal: AbortSignal.timeout(2e3)
|
|
130
|
+
});
|
|
131
|
+
if (!res.ok) return { ok: false };
|
|
132
|
+
const data = await res.json();
|
|
133
|
+
return { ok: true, name: data.name };
|
|
134
|
+
} catch {
|
|
135
|
+
return { ok: false };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
129
139
|
// packages/daemon/src/lib/util/json-state.ts
|
|
130
140
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
131
141
|
import { writeFile } from "fs/promises";
|
|
@@ -236,7 +246,7 @@ var RotatingLog = class extends Writable {
|
|
|
236
246
|
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
237
247
|
import { resolve } from "path";
|
|
238
248
|
var slog = logger_default.child("cred-sync");
|
|
239
|
-
function writeClaudeCredentials(homeDir, baseName, oauth) {
|
|
249
|
+
async function writeClaudeCredentials(homeDir, baseName, oauth) {
|
|
240
250
|
const claudeDir = resolve(homeDir, ".claude");
|
|
241
251
|
mkdirSync(claudeDir, { recursive: true });
|
|
242
252
|
writeFileSync2(
|
|
@@ -252,18 +262,18 @@ function writeClaudeCredentials(homeDir, baseName, oauth) {
|
|
|
252
262
|
{ mode: 384 }
|
|
253
263
|
);
|
|
254
264
|
if (isIsolationEnabled()) {
|
|
255
|
-
chownMindDir(claudeDir, baseName);
|
|
265
|
+
await chownMindDir(claudeDir, baseName);
|
|
256
266
|
}
|
|
257
267
|
return claudeDir;
|
|
258
268
|
}
|
|
259
|
-
function writePiProviderKey(piAgentDir, baseName, provider, key) {
|
|
269
|
+
async function writePiProviderKey(piAgentDir, baseName, provider, key) {
|
|
260
270
|
mkdirSync(piAgentDir, { recursive: true });
|
|
261
271
|
const authPath = resolve(piAgentDir, "auth.json");
|
|
262
272
|
const authData = existsSync3(authPath) ? JSON.parse(readFileSync2(authPath, "utf-8")) : {};
|
|
263
273
|
authData[provider] = { type: "api_key", key };
|
|
264
274
|
writeFileSync2(authPath, JSON.stringify(authData, null, 2), { mode: 384 });
|
|
265
275
|
if (isIsolationEnabled()) {
|
|
266
|
-
chownMindDir(piAgentDir, baseName);
|
|
276
|
+
await chownMindDir(piAgentDir, baseName);
|
|
267
277
|
}
|
|
268
278
|
}
|
|
269
279
|
function piUsesProvider(piAgentDir, provider) {
|
|
@@ -291,11 +301,11 @@ async function syncProviderToMinds(provider, deps = {}) {
|
|
|
291
301
|
const baseName = entry.parent ?? name;
|
|
292
302
|
const template = entry.template;
|
|
293
303
|
if (template === "claude" || !template) {
|
|
294
|
-
writeClaudeCredentials(resolve(dir, "home"), baseName, oauth);
|
|
304
|
+
await writeClaudeCredentials(resolve(dir, "home"), baseName, oauth);
|
|
295
305
|
} else if (template === "pi") {
|
|
296
306
|
const piAgentDir = resolve(dir, ".mind", "pi-agent");
|
|
297
307
|
if (piUsesProvider(piAgentDir, "anthropic")) {
|
|
298
|
-
writePiProviderKey(piAgentDir, baseName, "anthropic", oauth.access);
|
|
308
|
+
await writePiProviderKey(piAgentDir, baseName, "anthropic", oauth.access);
|
|
299
309
|
}
|
|
300
310
|
}
|
|
301
311
|
} catch (err) {
|
|
@@ -486,6 +496,13 @@ function buildMindBaseEnv(source = process.env) {
|
|
|
486
496
|
}
|
|
487
497
|
return base;
|
|
488
498
|
}
|
|
499
|
+
var MindStartupError = class extends Error {
|
|
500
|
+
constructor(message, stderr) {
|
|
501
|
+
super(message);
|
|
502
|
+
this.stderr = stderr;
|
|
503
|
+
this.name = "MindStartupError";
|
|
504
|
+
}
|
|
505
|
+
};
|
|
489
506
|
function mindPidPath(name) {
|
|
490
507
|
return resolve2(stateDir(name), "mind.pid");
|
|
491
508
|
}
|
|
@@ -495,6 +512,26 @@ var MindManager = class {
|
|
|
495
512
|
shuttingDown = false;
|
|
496
513
|
restartTracker = new RestartTracker();
|
|
497
514
|
pendingContext = /* @__PURE__ */ new Map();
|
|
515
|
+
// Per-name lifecycle mutex: start/stop/restart for a given mind serialize so
|
|
516
|
+
// concurrent callers can't double-spawn or have a loser's cleanup delete the
|
|
517
|
+
// winner's tracked child.
|
|
518
|
+
locks = /* @__PURE__ */ new Map();
|
|
519
|
+
/** Run `fn` after any in-flight lifecycle op for `name` settles, serializing per name. */
|
|
520
|
+
withLock(name, fn) {
|
|
521
|
+
const prev = this.locks.get(name) ?? Promise.resolve();
|
|
522
|
+
const result = prev.then(fn, fn);
|
|
523
|
+
const tail = result.then(
|
|
524
|
+
() => {
|
|
525
|
+
},
|
|
526
|
+
() => {
|
|
527
|
+
}
|
|
528
|
+
);
|
|
529
|
+
this.locks.set(name, tail);
|
|
530
|
+
tail.finally(() => {
|
|
531
|
+
if (this.locks.get(name) === tail) this.locks.delete(name);
|
|
532
|
+
});
|
|
533
|
+
return result;
|
|
534
|
+
}
|
|
498
535
|
async resolveTarget(name) {
|
|
499
536
|
const entry = await findMind(name);
|
|
500
537
|
if (!entry) throw new Error(`Unknown mind: ${name}`);
|
|
@@ -507,6 +544,9 @@ var MindManager = class {
|
|
|
507
544
|
return { dir, port: entry.port, baseName: name, template: entry.template };
|
|
508
545
|
}
|
|
509
546
|
async startMind(name) {
|
|
547
|
+
return this.withLock(name, () => this._startMind(name));
|
|
548
|
+
}
|
|
549
|
+
async _startMind(name) {
|
|
510
550
|
if (this.minds.has(name)) {
|
|
511
551
|
throw new Error(`Mind ${name} is already running`);
|
|
512
552
|
}
|
|
@@ -540,7 +580,9 @@ var MindManager = class {
|
|
|
540
580
|
mlog.warn(`failed to read PID file for ${name}`, logger_default.errorData(err));
|
|
541
581
|
}
|
|
542
582
|
try {
|
|
543
|
-
const res = await fetch(`http://127.0.0.1:${port}/health
|
|
583
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
584
|
+
signal: AbortSignal.timeout(2e3)
|
|
585
|
+
});
|
|
544
586
|
if (res.ok) {
|
|
545
587
|
mlog.warn(`killing orphan process on port ${port}`);
|
|
546
588
|
await killProcessOnPort(port);
|
|
@@ -555,8 +597,8 @@ var MindManager = class {
|
|
|
555
597
|
mkdirSync2(mindTmp, { recursive: true });
|
|
556
598
|
if (isIsolationEnabled()) {
|
|
557
599
|
try {
|
|
558
|
-
chownMindDir(mindStateDir, baseName);
|
|
559
|
-
chownMindDir(mindTmp, baseName);
|
|
600
|
+
await chownMindDir(mindStateDir, baseName);
|
|
601
|
+
await chownMindDir(mindTmp, baseName);
|
|
560
602
|
} catch (err) {
|
|
561
603
|
throw new Error(
|
|
562
604
|
`Cannot start mind ${name}: failed to set ownership on state directory ${mindStateDir}: ${err instanceof Error ? err.message : err}`
|
|
@@ -592,7 +634,7 @@ var MindManager = class {
|
|
|
592
634
|
const apiKey = await resolveApiKey(provider);
|
|
593
635
|
if (apiKey) {
|
|
594
636
|
const piAgentDir = resolve2(dir, ".mind", "pi-agent");
|
|
595
|
-
writePiProviderKey(piAgentDir, baseName, provider, apiKey);
|
|
637
|
+
await writePiProviderKey(piAgentDir, baseName, provider, apiKey);
|
|
596
638
|
env.PI_CODING_AGENT_DIR = piAgentDir;
|
|
597
639
|
const providerEnvVars = {
|
|
598
640
|
openrouter: "OPENROUTER_API_KEY",
|
|
@@ -645,7 +687,7 @@ var MindManager = class {
|
|
|
645
687
|
writeFileSync3(configTomlPath, 'cli_auth_credentials_store = "file"\n');
|
|
646
688
|
}
|
|
647
689
|
if (isIsolationEnabled()) {
|
|
648
|
-
chownMindDir(codexDir, baseName);
|
|
690
|
+
await chownMindDir(codexDir, baseName);
|
|
649
691
|
}
|
|
650
692
|
} else {
|
|
651
693
|
const apiKey = await resolveApiKey("openai-codex");
|
|
@@ -675,7 +717,7 @@ var MindManager = class {
|
|
|
675
717
|
const key = await resolveApiKey("anthropic");
|
|
676
718
|
const oauth = getAiConfig()?.providers.anthropic?.oauth;
|
|
677
719
|
if (key && oauth) {
|
|
678
|
-
const claudeDir = writeClaudeCredentials(resolve2(dir, "home"), baseName, oauth);
|
|
720
|
+
const claudeDir = await writeClaudeCredentials(resolve2(dir, "home"), baseName, oauth);
|
|
679
721
|
env.CLAUDE_CONFIG_DIR = claudeDir;
|
|
680
722
|
}
|
|
681
723
|
} else {
|
|
@@ -736,29 +778,52 @@ var MindManager = class {
|
|
|
736
778
|
});
|
|
737
779
|
try {
|
|
738
780
|
await new Promise((resolve9, reject) => {
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
if (data.toString().match(/listening on :\d+/)) {
|
|
744
|
-
clearTimeout(timeout);
|
|
745
|
-
resolve9();
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
child.stdout?.on("data", checkOutput);
|
|
749
|
-
child.stderr?.on("data", checkOutput);
|
|
750
|
-
child.on("error", (err) => {
|
|
751
|
-
clearTimeout(timeout);
|
|
752
|
-
reject(err);
|
|
753
|
-
});
|
|
754
|
-
child.on("exit", (code) => {
|
|
755
|
-
clearTimeout(timeout);
|
|
756
|
-
const errorLine = recentStderr.find(
|
|
757
|
-
(l) => l.startsWith("Error:") || l.includes("Error:")
|
|
758
|
-
);
|
|
781
|
+
let settled = false;
|
|
782
|
+
const deadline = Date.now() + 3e4;
|
|
783
|
+
const onExit = (code) => {
|
|
784
|
+
const errorLine = recentStderr.find((l) => l.includes("Error:"));
|
|
759
785
|
const detail = errorLine ? `: ${errorLine.trim()}` : "";
|
|
760
|
-
|
|
761
|
-
|
|
786
|
+
finish(
|
|
787
|
+
() => reject(
|
|
788
|
+
new MindStartupError(
|
|
789
|
+
`Mind ${name} exited with code ${code} during startup${detail}`,
|
|
790
|
+
recentStderr.join("\n")
|
|
791
|
+
)
|
|
792
|
+
)
|
|
793
|
+
);
|
|
794
|
+
};
|
|
795
|
+
const onError = (err) => finish(() => reject(err));
|
|
796
|
+
function finish(action) {
|
|
797
|
+
if (settled) return;
|
|
798
|
+
settled = true;
|
|
799
|
+
child.off("exit", onExit);
|
|
800
|
+
child.off("error", onError);
|
|
801
|
+
action();
|
|
802
|
+
}
|
|
803
|
+
child.on("exit", onExit);
|
|
804
|
+
child.on("error", onError);
|
|
805
|
+
const poll = async () => {
|
|
806
|
+
if (settled) return;
|
|
807
|
+
const { ok } = await checkHealth(port);
|
|
808
|
+
if (settled) return;
|
|
809
|
+
if (ok) {
|
|
810
|
+
finish(() => resolve9());
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
if (Date.now() >= deadline) {
|
|
814
|
+
finish(
|
|
815
|
+
() => reject(
|
|
816
|
+
new MindStartupError(
|
|
817
|
+
`Mind ${name} did not become healthy within 30s`,
|
|
818
|
+
recentStderr.join("\n")
|
|
819
|
+
)
|
|
820
|
+
)
|
|
821
|
+
);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
setTimeout(poll, 250);
|
|
825
|
+
};
|
|
826
|
+
void poll();
|
|
762
827
|
});
|
|
763
828
|
} catch (err) {
|
|
764
829
|
this.minds.delete(name);
|
|
@@ -826,7 +891,8 @@ var MindManager = class {
|
|
|
826
891
|
participants: ["volute", name],
|
|
827
892
|
participantCount: 2,
|
|
828
893
|
...conversationId ? { conversationId } : {}
|
|
829
|
-
})
|
|
894
|
+
}),
|
|
895
|
+
signal: AbortSignal.timeout(1e4)
|
|
830
896
|
});
|
|
831
897
|
} catch (err) {
|
|
832
898
|
mlog.warn(`failed to deliver pending context to ${name}`, logger_default.errorData(err));
|
|
@@ -834,14 +900,21 @@ var MindManager = class {
|
|
|
834
900
|
}
|
|
835
901
|
setupCrashRecovery(name, child) {
|
|
836
902
|
child.on("exit", async (code) => {
|
|
903
|
+
if (this.minds.get(name)?.child !== child) return;
|
|
837
904
|
this.minds.delete(name);
|
|
838
905
|
if (this.shuttingDown || this.stopping.has(name)) return;
|
|
839
906
|
mlog.error(`mind ${name} exited with code ${code}`);
|
|
840
907
|
try {
|
|
841
|
-
const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-
|
|
842
|
-
const
|
|
908
|
+
const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-BVU5D2AE.js");
|
|
909
|
+
const sleepMgr = getSleepManagerIfReady2();
|
|
910
|
+
const sleepState = sleepMgr?.getState(name);
|
|
843
911
|
if (sleepState?.sleeping) {
|
|
844
|
-
|
|
912
|
+
if (sleepState.wokenByTrigger && sleepMgr) {
|
|
913
|
+
mlog.info(`${name} crashed during trigger wake \u2014 returning to sleep`);
|
|
914
|
+
void sleepMgr.returnToSleepAfterCrash(name);
|
|
915
|
+
} else {
|
|
916
|
+
mlog.info(`${name} is sleeping \u2014 skipping crash recovery`);
|
|
917
|
+
}
|
|
845
918
|
return;
|
|
846
919
|
}
|
|
847
920
|
} catch (err) {
|
|
@@ -862,15 +935,15 @@ var MindManager = class {
|
|
|
862
935
|
(err) => mlog.warn(`failed to clear turn state for ${name} after crash`, logger_default.errorData(err))
|
|
863
936
|
);
|
|
864
937
|
try {
|
|
865
|
-
const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-
|
|
938
|
+
const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-5J2BE22P.js");
|
|
866
939
|
getDeliveryManager2().clearMindSessions(name);
|
|
867
940
|
} catch (err) {
|
|
868
941
|
if (!(err instanceof Error && err.message.includes("not initialized"))) {
|
|
869
942
|
mlog.warn(`failed to clear delivery state for ${name} after crash`, logger_default.errorData(err));
|
|
870
943
|
}
|
|
871
944
|
}
|
|
872
|
-
import("./mind-activity-tracker-
|
|
873
|
-
import("./activity-events-
|
|
945
|
+
import("./mind-activity-tracker-JHAT6456.js").then(({ markIdle: markIdle2 }) => markIdle2(name)).catch((err) => mlog.warn(`failed to mark ${name} idle after crash`, logger_default.errorData(err)));
|
|
946
|
+
import("./activity-events-UISXY4Y5.js").then(
|
|
874
947
|
({ publish: publish4 }) => publish4({ type: "mind_stopped", mind: name, summary: `${name} crashed (exit ${code})` })
|
|
875
948
|
).catch((err) => mlog.warn(`failed to publish crash event for ${name}`, logger_default.errorData(err)));
|
|
876
949
|
const { shouldRestart, delay, attempt } = this.restartTracker.recordCrash(name);
|
|
@@ -892,25 +965,32 @@ var MindManager = class {
|
|
|
892
965
|
});
|
|
893
966
|
}
|
|
894
967
|
async stopMind(name) {
|
|
968
|
+
return this.withLock(name, () => this._stopMind(name));
|
|
969
|
+
}
|
|
970
|
+
async _stopMind(name) {
|
|
895
971
|
const tracked = this.minds.get(name);
|
|
896
972
|
if (!tracked) return;
|
|
897
973
|
this.stopping.add(name);
|
|
898
974
|
const { child } = tracked;
|
|
899
975
|
this.minds.delete(name);
|
|
900
976
|
await new Promise((resolve9) => {
|
|
901
|
-
|
|
902
|
-
try {
|
|
903
|
-
process.kill(-child.pid, "SIGTERM");
|
|
904
|
-
} catch {
|
|
905
|
-
resolve9();
|
|
906
|
-
}
|
|
907
|
-
setTimeout(() => {
|
|
977
|
+
const killTimer = setTimeout(() => {
|
|
908
978
|
try {
|
|
909
979
|
process.kill(-child.pid, "SIGKILL");
|
|
910
980
|
} catch {
|
|
911
981
|
}
|
|
912
982
|
resolve9();
|
|
913
983
|
}, 5e3);
|
|
984
|
+
child.on("exit", () => {
|
|
985
|
+
clearTimeout(killTimer);
|
|
986
|
+
resolve9();
|
|
987
|
+
});
|
|
988
|
+
try {
|
|
989
|
+
process.kill(-child.pid, "SIGTERM");
|
|
990
|
+
} catch {
|
|
991
|
+
clearTimeout(killTimer);
|
|
992
|
+
resolve9();
|
|
993
|
+
}
|
|
914
994
|
});
|
|
915
995
|
this.stopping.delete(name);
|
|
916
996
|
revokeMindToken(name);
|
|
@@ -921,7 +1001,7 @@ var MindManager = class {
|
|
|
921
1001
|
mlog.warn(`failed to clear turn state for ${name} on stop`, logger_default.errorData(err));
|
|
922
1002
|
}
|
|
923
1003
|
try {
|
|
924
|
-
const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-
|
|
1004
|
+
const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-5J2BE22P.js");
|
|
925
1005
|
getDeliveryManager2().clearMindSessions(name);
|
|
926
1006
|
} catch (err) {
|
|
927
1007
|
if (!(err instanceof Error && err.message.includes("not initialized"))) {
|
|
@@ -929,7 +1009,7 @@ var MindManager = class {
|
|
|
929
1009
|
}
|
|
930
1010
|
}
|
|
931
1011
|
try {
|
|
932
|
-
const { clearEchoTextCache } = await import("./echo-text-
|
|
1012
|
+
const { clearEchoTextCache } = await import("./echo-text-XKBJM6RV.js");
|
|
933
1013
|
clearEchoTextCache(name);
|
|
934
1014
|
} catch (err) {
|
|
935
1015
|
mlog.debug(`failed to clear echo-text cache for ${name}`, logger_default.errorData(err));
|
|
@@ -942,8 +1022,10 @@ var MindManager = class {
|
|
|
942
1022
|
mlog.info(`stopped mind ${name}`);
|
|
943
1023
|
}
|
|
944
1024
|
async restartMind(name) {
|
|
945
|
-
|
|
946
|
-
|
|
1025
|
+
return this.withLock(name, async () => {
|
|
1026
|
+
await this._stopMind(name);
|
|
1027
|
+
await this._startMind(name);
|
|
1028
|
+
});
|
|
947
1029
|
}
|
|
948
1030
|
async stopAll() {
|
|
949
1031
|
this.shuttingDown = true;
|
|
@@ -1007,6 +1089,9 @@ function getMindManager() {
|
|
|
1007
1089
|
if (!instance) throw new Error("MindManager not initialized \u2014 call initMindManager() first");
|
|
1008
1090
|
return instance;
|
|
1009
1091
|
}
|
|
1092
|
+
function tryGetMindManager() {
|
|
1093
|
+
return instance;
|
|
1094
|
+
}
|
|
1010
1095
|
|
|
1011
1096
|
// packages/daemon/src/lib/chat/system-channel.ts
|
|
1012
1097
|
var SYSTEM_CHANNEL_NAME = "system";
|
|
@@ -1803,7 +1888,7 @@ async function installNpmExtension(pkg) {
|
|
|
1803
1888
|
throw new Error(`Extension "${pkg}" is already installed`);
|
|
1804
1889
|
}
|
|
1805
1890
|
const dir = ensureExtensionsNpmDir();
|
|
1806
|
-
const { exec: exec2 } = await import("./exec-
|
|
1891
|
+
const { exec: exec2 } = await import("./exec-SJK7VUTC.js");
|
|
1807
1892
|
try {
|
|
1808
1893
|
await exec2("npm", ["install", "--ignore-scripts", pkg], { cwd: dir });
|
|
1809
1894
|
} catch (err) {
|
|
@@ -1824,7 +1909,7 @@ async function uninstallNpmExtension(pkg) {
|
|
|
1824
1909
|
packages.splice(idx, 1);
|
|
1825
1910
|
writeExtensionsConfig(packages);
|
|
1826
1911
|
try {
|
|
1827
|
-
const { exec: exec2 } = await import("./exec-
|
|
1912
|
+
const { exec: exec2 } = await import("./exec-SJK7VUTC.js");
|
|
1828
1913
|
await exec2("npm", ["uninstall", "--ignore-scripts", pkg], { cwd: extensionsNpmDir() });
|
|
1829
1914
|
} catch (err) {
|
|
1830
1915
|
logger_default.warn(
|
|
@@ -2625,7 +2710,7 @@ async function wakeMind(name) {
|
|
|
2625
2710
|
async function startSpiritFull(name) {
|
|
2626
2711
|
const entry = await findMind(name);
|
|
2627
2712
|
if (entry?.dir) {
|
|
2628
|
-
const { registerMindDir } = await import("./delivery-router-
|
|
2713
|
+
const { registerMindDir } = await import("./delivery-router-HXBHYHJM.js");
|
|
2629
2714
|
registerMindDir(name, entry.dir);
|
|
2630
2715
|
}
|
|
2631
2716
|
await getMindManager().startMind(name);
|
|
@@ -2647,8 +2732,8 @@ async function stopSpiritFull(name) {
|
|
|
2647
2732
|
}).catch((err) => logger_default.error("failed to publish spirit_stopped activity", logger_default.errorData(err)));
|
|
2648
2733
|
}
|
|
2649
2734
|
async function ensureCreatorDM(mindName, creatorUsername) {
|
|
2650
|
-
const { getOrCreateMindUser: getOrCreateMindUser2, getUserByUsername: getUserByUsername2 } = await import("./auth-
|
|
2651
|
-
const { findDMConversation: findDMConversation2, createConversation: createConversation2 } = await import("./conversations-
|
|
2735
|
+
const { getOrCreateMindUser: getOrCreateMindUser2, getUserByUsername: getUserByUsername2 } = await import("./auth-3RIG6GKN.js");
|
|
2736
|
+
const { findDMConversation: findDMConversation2, createConversation: createConversation2 } = await import("./conversations-67WWW25C.js");
|
|
2652
2737
|
const mindUser = await getOrCreateMindUser2(mindName);
|
|
2653
2738
|
const creatorUser = await getUserByUsername2(creatorUsername);
|
|
2654
2739
|
if (!creatorUser) {
|
|
@@ -2782,6 +2867,37 @@ var SleepManager = class {
|
|
|
2782
2867
|
this.markAwake(name);
|
|
2783
2868
|
slog3.info(`${name} trigger-wake converted to full wake`);
|
|
2784
2869
|
}
|
|
2870
|
+
/**
|
|
2871
|
+
* A trigger-woken mind's process crashed while still in the brief trigger-wake
|
|
2872
|
+
* window. The mind-manager crash handler calls this to return it to sleep —
|
|
2873
|
+
* archive the (now-dead) session and restore sleeping state — rather than
|
|
2874
|
+
* leaving it in sleeping+wokenByTrigger limbo. Mirrors the idle-driven
|
|
2875
|
+
* return-to-sleep in onActivityEvent, minus stopping an already-dead process.
|
|
2876
|
+
*/
|
|
2877
|
+
async returnToSleepAfterCrash(name) {
|
|
2878
|
+
const state = this.states.get(name);
|
|
2879
|
+
if (!state?.sleeping || !state.wokenByTrigger) return;
|
|
2880
|
+
if (this.transitioning.has(name)) return;
|
|
2881
|
+
this.transitioning.add(name);
|
|
2882
|
+
state.wokenByTrigger = false;
|
|
2883
|
+
try {
|
|
2884
|
+
try {
|
|
2885
|
+
await sleepMind(name);
|
|
2886
|
+
} catch (err) {
|
|
2887
|
+
slog3.warn(`sleepMind during crash-return failed for ${name}`, logger_default.errorData(err));
|
|
2888
|
+
}
|
|
2889
|
+
await this.archiveSessions(name);
|
|
2890
|
+
state.sleeping = true;
|
|
2891
|
+
state.sleepingSince = (/* @__PURE__ */ new Date()).toISOString();
|
|
2892
|
+
state.scheduledWakeAt = this.getNextWakeTime(this.getSleepConfig(name));
|
|
2893
|
+
this.saveState();
|
|
2894
|
+
slog3.info(`${name} returned to sleep after crashing during trigger wake`);
|
|
2895
|
+
} catch (err) {
|
|
2896
|
+
slog3.error(`failed to return ${name} to sleep after crash`, logger_default.errorData(err));
|
|
2897
|
+
} finally {
|
|
2898
|
+
this.transitioning.delete(name);
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2785
2901
|
getSleepConfig(name) {
|
|
2786
2902
|
if (this.sleepConfigs.has(name)) {
|
|
2787
2903
|
return this.sleepConfigs.get(name) ?? null;
|
|
@@ -2992,7 +3108,7 @@ var SleepManager = class {
|
|
|
2992
3108
|
const db = await getDb();
|
|
2993
3109
|
const rows = await db.select().from(deliveryQueue).where(and2(eq3(deliveryQueue.mind, name), eq3(deliveryQueue.status, "sleep-queued"))).all();
|
|
2994
3110
|
if (rows.length === 0) return 0;
|
|
2995
|
-
const { deliverMessage: deliverMessage2 } = await import("./message-delivery-
|
|
3111
|
+
const { deliverMessage: deliverMessage2 } = await import("./message-delivery-WJBZFALY.js");
|
|
2996
3112
|
const delivered = [];
|
|
2997
3113
|
for (const row of rows) {
|
|
2998
3114
|
try {
|
|
@@ -3313,7 +3429,7 @@ function getSleepManagerIfReady() {
|
|
|
3313
3429
|
// packages/daemon/src/lib/delivery/delivery-manager.ts
|
|
3314
3430
|
import { readFile, realpath } from "fs/promises";
|
|
3315
3431
|
import { extname, resolve as resolve8 } from "path";
|
|
3316
|
-
import { and as and3, eq as eq4, sql as sql2 } from "drizzle-orm";
|
|
3432
|
+
import { and as and3, eq as eq4, inArray as inArray2, sql as sql2 } from "drizzle-orm";
|
|
3317
3433
|
|
|
3318
3434
|
// packages/daemon/src/lib/chat/typing.ts
|
|
3319
3435
|
var DEFAULT_TTL_MS = 1e4;
|
|
@@ -3409,12 +3525,41 @@ function publishTypingForChannels(channels, map) {
|
|
|
3409
3525
|
// packages/daemon/src/lib/delivery/delivery-manager.ts
|
|
3410
3526
|
var dlog = logger_default.child("delivery-manager");
|
|
3411
3527
|
var MAX_BATCH_SIZE = 50;
|
|
3528
|
+
var REDRIVE_INTERVAL_MS = 15e3;
|
|
3529
|
+
var RETRY_BASE_MS = 5e3;
|
|
3530
|
+
var RETRY_MAX_MS = 5 * 6e4;
|
|
3531
|
+
var REDRIVE_BATCH_LIMIT = 200;
|
|
3412
3532
|
var mentionRegexCache = /* @__PURE__ */ new Map();
|
|
3413
3533
|
var avatarBlocksCache = /* @__PURE__ */ new Map();
|
|
3414
3534
|
var AVATAR_CACHE_TTL = 5 * 60 * 1e3;
|
|
3415
3535
|
var DeliveryManager = class {
|
|
3416
3536
|
sessionStates = /* @__PURE__ */ new Map();
|
|
3417
3537
|
batchBuffers = /* @__PURE__ */ new Map();
|
|
3538
|
+
/**
|
|
3539
|
+
* delivery_queue row ids currently owned in-memory — either buffered in a batch
|
|
3540
|
+
* buffer or actively being POSTed. The redrive sweep skips these so it never
|
|
3541
|
+
* double-delivers a row that the normal path is already handling.
|
|
3542
|
+
*/
|
|
3543
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
3544
|
+
/**
|
|
3545
|
+
* Per-`(baseName:session)` promise chain that serializes POSTs so two rapid
|
|
3546
|
+
* messages to the same session can't be reordered by resolvePort/enrichment latency.
|
|
3547
|
+
*/
|
|
3548
|
+
drainChains = /* @__PURE__ */ new Map();
|
|
3549
|
+
redriveTimer = null;
|
|
3550
|
+
/** Predicate for whether a mind is up; overridable in tests. */
|
|
3551
|
+
isMindRunning = (name) => tryGetMindManager()?.isRunning(name) ?? false;
|
|
3552
|
+
constructor() {
|
|
3553
|
+
setRoutesChangeListener((mind) => {
|
|
3554
|
+
this.releaseGated(mind).catch(
|
|
3555
|
+
(err) => dlog.warn(`failed to release gated messages for ${mind}`, logger_default.errorData(err))
|
|
3556
|
+
);
|
|
3557
|
+
});
|
|
3558
|
+
}
|
|
3559
|
+
/** Test seam: override the mind-running predicate. */
|
|
3560
|
+
setRunningCheck(fn) {
|
|
3561
|
+
this.isMindRunning = fn;
|
|
3562
|
+
}
|
|
3418
3563
|
// --- Public API ---
|
|
3419
3564
|
/**
|
|
3420
3565
|
* Route and deliver a message to a mind. This is the main entry point.
|
|
@@ -3434,7 +3579,8 @@ var DeliveryManager = class {
|
|
|
3434
3579
|
await this.enqueueBatch(mindName, sessionName2, payload, sessionConfig2);
|
|
3435
3580
|
return { routed: true, session: sessionName2, destination: "mind", mode: "batch" };
|
|
3436
3581
|
}
|
|
3437
|
-
await this.
|
|
3582
|
+
const queueId2 = await this.persistToQueue(mindName, sessionName2, payload);
|
|
3583
|
+
await this.deliverToMind(mindName, sessionName2, payload, sessionConfig2, queueId2);
|
|
3438
3584
|
return { routed: true, session: sessionName2, destination: "mind", mode: "immediate" };
|
|
3439
3585
|
}
|
|
3440
3586
|
const meta = {
|
|
@@ -3472,16 +3618,14 @@ var DeliveryManager = class {
|
|
|
3472
3618
|
if (sessionName === "$new") {
|
|
3473
3619
|
sessionName = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3474
3620
|
}
|
|
3475
|
-
tagRecentInbound(baseName, sessionName, payload.channel).catch((err) => {
|
|
3476
|
-
dlog.warn(`tagRecentInbound failed for ${baseName}`, logger_default.errorData(err));
|
|
3477
|
-
});
|
|
3478
3621
|
const sessionConfig = resolveDeliveryMode(config, sessionName, route.rule);
|
|
3479
3622
|
if (sessionConfig.delivery.mode === "batch") {
|
|
3480
3623
|
dlog.debug(`enqueueing batch message for ${mindName}/${sessionName}`);
|
|
3481
3624
|
await this.enqueueBatch(mindName, sessionName, payload, sessionConfig);
|
|
3482
3625
|
return { routed: true, session: sessionName, destination: "mind", mode: "batch" };
|
|
3483
3626
|
}
|
|
3484
|
-
await this.
|
|
3627
|
+
const queueId = await this.persistToQueue(mindName, sessionName, payload);
|
|
3628
|
+
await this.deliverToMind(mindName, sessionName, payload, sessionConfig, queueId);
|
|
3485
3629
|
return { routed: true, session: sessionName, destination: "mind", mode: "immediate" };
|
|
3486
3630
|
}
|
|
3487
3631
|
/**
|
|
@@ -3506,44 +3650,121 @@ var DeliveryManager = class {
|
|
|
3506
3650
|
}
|
|
3507
3651
|
}
|
|
3508
3652
|
/**
|
|
3509
|
-
* Restore queued messages from DB on daemon restart.
|
|
3653
|
+
* Restore queued messages from DB on daemon restart — a single redrive pass.
|
|
3654
|
+
* All accepted deliveries are persisted to delivery_queue before their POST and
|
|
3655
|
+
* only deleted on mind-ack, so pending rows are exactly the undelivered messages.
|
|
3510
3656
|
*/
|
|
3511
3657
|
async restoreFromDb() {
|
|
3658
|
+
await this.redrive();
|
|
3659
|
+
}
|
|
3660
|
+
/** Start the periodic redrive sweep (idempotent). */
|
|
3661
|
+
startRedrive() {
|
|
3662
|
+
if (this.redriveTimer) return;
|
|
3663
|
+
this.redriveTimer = setInterval(() => {
|
|
3664
|
+
this.redrive().catch((err) => dlog.warn("redrive sweep failed", logger_default.errorData(err)));
|
|
3665
|
+
}, REDRIVE_INTERVAL_MS);
|
|
3666
|
+
this.redriveTimer.unref();
|
|
3667
|
+
}
|
|
3668
|
+
/**
|
|
3669
|
+
* Re-read pending delivery_queue rows that are eligible (past their backoff window)
|
|
3670
|
+
* and re-deliver them through the normal path. Rows already owned in-memory
|
|
3671
|
+
* (`inFlight`) and minds that are down are skipped so we never hot-loop or double-send.
|
|
3672
|
+
*/
|
|
3673
|
+
async redrive() {
|
|
3674
|
+
let rows;
|
|
3512
3675
|
try {
|
|
3513
3676
|
const db = await getDb();
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3677
|
+
rows = await db.select().from(deliveryQueue).where(
|
|
3678
|
+
and3(
|
|
3679
|
+
eq4(deliveryQueue.status, "pending"),
|
|
3680
|
+
sql2`(${deliveryQueue.next_attempt_at} IS NULL OR ${deliveryQueue.next_attempt_at} <= datetime('now'))`
|
|
3681
|
+
)
|
|
3682
|
+
).orderBy(deliveryQueue.id).limit(REDRIVE_BATCH_LIMIT);
|
|
3683
|
+
} catch (err) {
|
|
3684
|
+
dlog.warn("failed to read delivery queue for redrive", logger_default.errorData(err));
|
|
3685
|
+
return;
|
|
3686
|
+
}
|
|
3687
|
+
let redriven = 0;
|
|
3688
|
+
for (const row of rows) {
|
|
3689
|
+
if (this.inFlight.has(row.id)) continue;
|
|
3690
|
+
if (!this.isMindRunning(row.mind)) continue;
|
|
3691
|
+
let payload;
|
|
3692
|
+
try {
|
|
3693
|
+
payload = JSON.parse(row.payload);
|
|
3694
|
+
} catch (parseErr) {
|
|
3695
|
+
dlog.warn(
|
|
3696
|
+
`corrupt payload in delivery queue row ${row.id}, dropping`,
|
|
3697
|
+
logger_default.errorData(parseErr)
|
|
3698
|
+
);
|
|
3699
|
+
await this.deleteQueueRows([row.id]);
|
|
3700
|
+
continue;
|
|
3701
|
+
}
|
|
3702
|
+
const config = getRoutingConfig(row.mind);
|
|
3703
|
+
const sessionConfig = resolveDeliveryMode(config, row.session);
|
|
3704
|
+
const target = row.target_mind ?? row.mind;
|
|
3705
|
+
if (sessionConfig.delivery.mode === "batch") {
|
|
3706
|
+
this.inFlight.add(row.id);
|
|
3707
|
+
this.addToBatchBuffer(target, row.session, sessionConfig, {
|
|
3708
|
+
payload,
|
|
3709
|
+
channel: payload.channel,
|
|
3710
|
+
sender: payload.sender ?? null,
|
|
3711
|
+
createdAt: Date.now(),
|
|
3712
|
+
queueId: row.id
|
|
3713
|
+
});
|
|
3714
|
+
} else {
|
|
3715
|
+
this.deliverToMind(target, row.session, payload, sessionConfig, row.id).catch((err) => {
|
|
3716
|
+
dlog.warn(`failed to redrive delivery for ${target}`, logger_default.errorData(err));
|
|
3717
|
+
});
|
|
3718
|
+
}
|
|
3719
|
+
redriven++;
|
|
3720
|
+
}
|
|
3721
|
+
if (redriven > 0) dlog.info(`redrove ${redriven} pending delivery queue rows`);
|
|
3722
|
+
}
|
|
3723
|
+
/**
|
|
3724
|
+
* Re-evaluate a mind's `gated` rows against its current routes.json and promote any
|
|
3725
|
+
* that now match to `pending` (delivered by the next redrive sweep). Called when the
|
|
3726
|
+
* routing config changes.
|
|
3727
|
+
*/
|
|
3728
|
+
async releaseGated(mindName) {
|
|
3729
|
+
const baseName = await getBaseName(mindName);
|
|
3730
|
+
const config = getRoutingConfig(baseName);
|
|
3731
|
+
let rows;
|
|
3732
|
+
try {
|
|
3733
|
+
const db = await getDb();
|
|
3734
|
+
rows = await db.select().from(deliveryQueue).where(and3(eq4(deliveryQueue.mind, baseName), eq4(deliveryQueue.status, "gated")));
|
|
3735
|
+
} catch (err) {
|
|
3736
|
+
dlog.warn(`failed to read gated rows for ${baseName}`, logger_default.errorData(err));
|
|
3737
|
+
return;
|
|
3738
|
+
}
|
|
3739
|
+
const promote = [];
|
|
3740
|
+
for (const row of rows) {
|
|
3741
|
+
let payload;
|
|
3742
|
+
try {
|
|
3743
|
+
payload = JSON.parse(row.payload);
|
|
3744
|
+
} catch {
|
|
3745
|
+
continue;
|
|
3540
3746
|
}
|
|
3541
|
-
|
|
3542
|
-
|
|
3747
|
+
const meta = {
|
|
3748
|
+
channel: payload.channel,
|
|
3749
|
+
sender: payload.sender ?? void 0,
|
|
3750
|
+
isDM: payload.isDM,
|
|
3751
|
+
participantCount: payload.participantCount
|
|
3752
|
+
};
|
|
3753
|
+
const route = resolveRoute(config, meta);
|
|
3754
|
+
if (route.matched && route.destination === "mind") {
|
|
3755
|
+
promote.push(row.id);
|
|
3543
3756
|
}
|
|
3757
|
+
}
|
|
3758
|
+
if (promote.length === 0) return;
|
|
3759
|
+
try {
|
|
3760
|
+
const db = await getDb();
|
|
3761
|
+
await db.update(deliveryQueue).set({ status: "pending", attempts: 0, next_attempt_at: null }).where(inArray2(deliveryQueue.id, promote));
|
|
3762
|
+
dlog.info(`released ${promote.length} gated message(s) for ${baseName} after route change`);
|
|
3544
3763
|
} catch (err) {
|
|
3545
|
-
dlog.warn(
|
|
3764
|
+
dlog.warn(`failed to promote gated rows for ${baseName}`, logger_default.errorData(err));
|
|
3765
|
+
return;
|
|
3546
3766
|
}
|
|
3767
|
+
await this.redrive();
|
|
3547
3768
|
}
|
|
3548
3769
|
/**
|
|
3549
3770
|
* Get pending (gated) messages for a mind.
|
|
@@ -3600,6 +3821,9 @@ var DeliveryManager = class {
|
|
|
3600
3821
|
if (bufferKey.startsWith(`${mindName}:`)) {
|
|
3601
3822
|
if (buffer.debounceTimer) clearTimeout(buffer.debounceTimer);
|
|
3602
3823
|
if (buffer.maxWaitTimer) clearTimeout(buffer.maxWaitTimer);
|
|
3824
|
+
for (const msg of buffer.messages) {
|
|
3825
|
+
if (msg.queueId != null) this.inFlight.delete(msg.queueId);
|
|
3826
|
+
}
|
|
3603
3827
|
toDelete.push(bufferKey);
|
|
3604
3828
|
}
|
|
3605
3829
|
}
|
|
@@ -3636,6 +3860,13 @@ var DeliveryManager = class {
|
|
|
3636
3860
|
}
|
|
3637
3861
|
this.batchBuffers.clear();
|
|
3638
3862
|
this.sessionStates.clear();
|
|
3863
|
+
this.inFlight.clear();
|
|
3864
|
+
this.drainChains.clear();
|
|
3865
|
+
if (this.redriveTimer) {
|
|
3866
|
+
clearInterval(this.redriveTimer);
|
|
3867
|
+
this.redriveTimer = null;
|
|
3868
|
+
}
|
|
3869
|
+
setRoutesChangeListener(void 0);
|
|
3639
3870
|
if (instance7 === this) instance7 = void 0;
|
|
3640
3871
|
}
|
|
3641
3872
|
// --- Private ---
|
|
@@ -3667,162 +3898,213 @@ var DeliveryManager = class {
|
|
|
3667
3898
|
clearTimeout(timeout);
|
|
3668
3899
|
}
|
|
3669
3900
|
}
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
const
|
|
3677
|
-
const
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
if (payload.conversationId) {
|
|
3687
|
-
typingMap.set(payload.conversationId, baseName, { persistent: true });
|
|
3688
|
-
}
|
|
3689
|
-
onMindEvent(baseName, "delivery", payload.channel);
|
|
3690
|
-
const enrichedPayload = await this.enrichWithProfiles(baseName, session, payload);
|
|
3691
|
-
const body = JSON.stringify({
|
|
3692
|
-
...enrichedPayload,
|
|
3693
|
-
session,
|
|
3694
|
-
interrupt: sessionConfig.interrupt,
|
|
3695
|
-
instructions: sessionConfig.instructions
|
|
3901
|
+
/**
|
|
3902
|
+
* Serialize `fn` on a per-key promise chain so calls for the same key run one at a
|
|
3903
|
+
* time in submission order. Used to drain a `(mind, session)` sequentially.
|
|
3904
|
+
*/
|
|
3905
|
+
runSequential(key, fn) {
|
|
3906
|
+
const prev = this.drainChains.get(key) ?? Promise.resolve();
|
|
3907
|
+
const run = prev.then(fn, fn);
|
|
3908
|
+
const tail = run.then(
|
|
3909
|
+
() => {
|
|
3910
|
+
},
|
|
3911
|
+
() => {
|
|
3912
|
+
}
|
|
3913
|
+
);
|
|
3914
|
+
this.drainChains.set(key, tail);
|
|
3915
|
+
tail.then(() => {
|
|
3916
|
+
if (this.drainChains.get(key) === tail) this.drainChains.delete(key);
|
|
3696
3917
|
});
|
|
3918
|
+
return run;
|
|
3919
|
+
}
|
|
3920
|
+
/** Delete delivered queue rows by their specific ids. */
|
|
3921
|
+
async deleteQueueRows(ids) {
|
|
3922
|
+
const valid = [...new Set(ids.filter((id) => typeof id === "number"))];
|
|
3923
|
+
if (valid.length === 0) return;
|
|
3697
3924
|
try {
|
|
3698
|
-
const
|
|
3699
|
-
|
|
3700
|
-
this.decrementActive(baseName, session);
|
|
3701
|
-
publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
|
|
3702
|
-
}
|
|
3925
|
+
const db = await getDb();
|
|
3926
|
+
await db.delete(deliveryQueue).where(inArray2(deliveryQueue.id, valid));
|
|
3703
3927
|
} catch (err) {
|
|
3704
|
-
dlog.warn(
|
|
3705
|
-
this.decrementActive(baseName, session);
|
|
3706
|
-
publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
|
|
3928
|
+
dlog.warn("failed to delete delivered delivery queue rows", logger_default.errorData(err));
|
|
3707
3929
|
}
|
|
3708
3930
|
}
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
}
|
|
3723
|
-
const enrichedMessages = await Promise.all(
|
|
3724
|
-
messages2.map(async (msg, i) => {
|
|
3725
|
-
if (!isFirstForChannel[i]) return msg;
|
|
3726
|
-
const enrichedPayload = await this.enrichWithProfiles(baseName, session, msg.payload);
|
|
3727
|
-
return { ...msg, payload: enrichedPayload };
|
|
3728
|
-
})
|
|
3729
|
-
);
|
|
3730
|
-
const channels = {};
|
|
3731
|
-
for (const msg of enrichedMessages) {
|
|
3732
|
-
const ch = msg.channel ?? "unknown";
|
|
3733
|
-
if (!channels[ch]) channels[ch] = [];
|
|
3734
|
-
channels[ch].push(msg.payload);
|
|
3735
|
-
}
|
|
3736
|
-
const senders = /* @__PURE__ */ new Set();
|
|
3737
|
-
const channelSet = /* @__PURE__ */ new Set();
|
|
3738
|
-
for (const msg of messages2) {
|
|
3739
|
-
if (msg.sender) senders.add(msg.sender);
|
|
3740
|
-
if (msg.channel) channelSet.add(msg.channel);
|
|
3741
|
-
}
|
|
3742
|
-
this.incrementActive(baseName, session, senders, channelSet);
|
|
3743
|
-
const typingMap = getTypingMap();
|
|
3744
|
-
for (const ch of Object.keys(channels)) {
|
|
3745
|
-
if (ch !== "unknown") typingMap.set(ch, baseName, { persistent: true });
|
|
3746
|
-
}
|
|
3747
|
-
const seenConvIds = /* @__PURE__ */ new Set();
|
|
3748
|
-
for (const msg of messages2) {
|
|
3749
|
-
if (msg.payload.conversationId && !seenConvIds.has(msg.payload.conversationId)) {
|
|
3750
|
-
seenConvIds.add(msg.payload.conversationId);
|
|
3751
|
-
typingMap.set(msg.payload.conversationId, baseName, { persistent: true });
|
|
3931
|
+
/** Bump attempt counters and set a backoff window so a down mind isn't hot-looped. */
|
|
3932
|
+
async scheduleRetry(ids) {
|
|
3933
|
+
const valid = [...new Set(ids.filter((id) => typeof id === "number"))];
|
|
3934
|
+
if (valid.length === 0) return;
|
|
3935
|
+
try {
|
|
3936
|
+
const db = await getDb();
|
|
3937
|
+
const rows = await db.select({ id: deliveryQueue.id, attempts: deliveryQueue.attempts }).from(deliveryQueue).where(inArray2(deliveryQueue.id, valid));
|
|
3938
|
+
for (const row of rows) {
|
|
3939
|
+
const attempts = row.attempts + 1;
|
|
3940
|
+
const backoffSec = Math.round(
|
|
3941
|
+
Math.min(RETRY_MAX_MS, RETRY_BASE_MS * 2 ** Math.min(attempts, 20)) / 1e3
|
|
3942
|
+
);
|
|
3943
|
+
await db.update(deliveryQueue).set({ attempts, next_attempt_at: sql2`datetime('now', ${`+${backoffSec} seconds`})` }).where(eq4(deliveryQueue.id, row.id));
|
|
3752
3944
|
}
|
|
3945
|
+
} catch (err) {
|
|
3946
|
+
dlog.warn("failed to schedule delivery retry", logger_default.errorData(err));
|
|
3753
3947
|
}
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3948
|
+
}
|
|
3949
|
+
async deliverToMind(mindName, session, payload, sessionConfig, queueId) {
|
|
3950
|
+
if (queueId != null) this.inFlight.add(queueId);
|
|
3951
|
+
await this.runSequential(`${mindName}:${session}`, async () => {
|
|
3952
|
+
const resolved = await this.resolvePort(mindName);
|
|
3953
|
+
if (!resolved) {
|
|
3954
|
+
dlog.warn(`cannot deliver to ${mindName}: mind not found`);
|
|
3955
|
+
if (queueId != null) this.inFlight.delete(queueId);
|
|
3956
|
+
return;
|
|
3957
|
+
}
|
|
3958
|
+
const { baseName, port } = resolved;
|
|
3959
|
+
const senders = /* @__PURE__ */ new Set();
|
|
3960
|
+
if (payload.sender) senders.add(payload.sender);
|
|
3961
|
+
const channels = /* @__PURE__ */ new Set();
|
|
3962
|
+
if (payload.channel) channels.add(payload.channel);
|
|
3963
|
+
this.incrementActive(baseName, session, senders, channels);
|
|
3964
|
+
linkInboundToActiveTurn(baseName, session, payload.channel).catch(
|
|
3965
|
+
(err) => dlog.warn(`failed to link mid-turn inbound for ${baseName}`, logger_default.errorData(err))
|
|
3966
|
+
);
|
|
3967
|
+
const typingMap = getTypingMap();
|
|
3968
|
+
if (payload.channel) {
|
|
3969
|
+
typingMap.set(payload.channel, baseName, { persistent: true });
|
|
3970
|
+
}
|
|
3971
|
+
if (payload.conversationId) {
|
|
3972
|
+
typingMap.set(payload.conversationId, baseName, { persistent: true });
|
|
3973
|
+
}
|
|
3974
|
+
onMindEvent(baseName, "delivery", payload.channel);
|
|
3975
|
+
const enrichedPayload = await this.enrichWithProfiles(baseName, session, payload);
|
|
3976
|
+
const body = JSON.stringify({
|
|
3977
|
+
...enrichedPayload,
|
|
3978
|
+
session,
|
|
3979
|
+
interrupt: sessionConfig.interrupt,
|
|
3980
|
+
instructions: sessionConfig.instructions
|
|
3981
|
+
});
|
|
3982
|
+
try {
|
|
3983
|
+
const ok = await this.postToMind(port, body);
|
|
3984
|
+
if (!ok) {
|
|
3985
|
+
this.decrementActive(baseName, session);
|
|
3986
|
+
publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
|
|
3987
|
+
await this.scheduleRetry([queueId]);
|
|
3988
|
+
} else {
|
|
3989
|
+
await this.deleteQueueRows([queueId]);
|
|
3990
|
+
}
|
|
3991
|
+
} catch (err) {
|
|
3992
|
+
dlog.warn(`failed to deliver to ${mindName}`, logger_default.errorData(err));
|
|
3763
3993
|
this.decrementActive(baseName, session);
|
|
3764
3994
|
publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3995
|
+
await this.scheduleRetry([queueId]);
|
|
3996
|
+
} finally {
|
|
3997
|
+
if (queueId != null) this.inFlight.delete(queueId);
|
|
3998
|
+
}
|
|
3999
|
+
});
|
|
4000
|
+
}
|
|
4001
|
+
async deliverBatchToMind(mindName, session, messages2, sessionConfig, interruptOverride) {
|
|
4002
|
+
const queueIds = messages2.map((m) => m.queueId).filter((id) => typeof id === "number");
|
|
4003
|
+
await this.runSequential(`${mindName}:${session}`, async () => {
|
|
4004
|
+
const resolved = await this.resolvePort(mindName);
|
|
4005
|
+
if (!resolved) {
|
|
4006
|
+
dlog.warn(`cannot deliver batch to ${mindName}: mind not found`);
|
|
4007
|
+
for (const id of queueIds) this.inFlight.delete(id);
|
|
4008
|
+
return;
|
|
4009
|
+
}
|
|
4010
|
+
const { baseName, port } = resolved;
|
|
4011
|
+
const firstPerChannel = /* @__PURE__ */ new Set();
|
|
4012
|
+
const isFirstForChannel = [];
|
|
4013
|
+
for (const msg of messages2) {
|
|
4014
|
+
const ch = msg.channel ?? "unknown";
|
|
4015
|
+
isFirstForChannel.push(!firstPerChannel.has(ch));
|
|
4016
|
+
firstPerChannel.add(ch);
|
|
4017
|
+
}
|
|
4018
|
+
const enrichedMessages = await Promise.all(
|
|
4019
|
+
messages2.map(async (msg, i) => {
|
|
4020
|
+
if (!isFirstForChannel[i]) return msg;
|
|
4021
|
+
const enrichedPayload = await this.enrichWithProfiles(baseName, session, msg.payload);
|
|
4022
|
+
return { ...msg, payload: enrichedPayload };
|
|
4023
|
+
})
|
|
4024
|
+
);
|
|
4025
|
+
const channels = {};
|
|
4026
|
+
for (const msg of enrichedMessages) {
|
|
4027
|
+
const ch = msg.channel ?? "unknown";
|
|
4028
|
+
if (!channels[ch]) channels[ch] = [];
|
|
4029
|
+
channels[ch].push(msg.payload);
|
|
4030
|
+
}
|
|
4031
|
+
const senders = /* @__PURE__ */ new Set();
|
|
4032
|
+
const channelSet = /* @__PURE__ */ new Set();
|
|
4033
|
+
for (const msg of messages2) {
|
|
4034
|
+
if (msg.sender) senders.add(msg.sender);
|
|
4035
|
+
if (msg.channel) channelSet.add(msg.channel);
|
|
4036
|
+
}
|
|
4037
|
+
this.incrementActive(baseName, session, senders, channelSet);
|
|
4038
|
+
for (const ch of channelSet) {
|
|
4039
|
+
linkInboundToActiveTurn(baseName, session, ch).catch(
|
|
4040
|
+
(err) => dlog.warn(`failed to link mid-turn inbound for ${baseName}`, logger_default.errorData(err))
|
|
4041
|
+
);
|
|
4042
|
+
}
|
|
4043
|
+
const typingMap = getTypingMap();
|
|
4044
|
+
for (const ch of Object.keys(channels)) {
|
|
4045
|
+
if (ch !== "unknown") typingMap.set(ch, baseName, { persistent: true });
|
|
4046
|
+
}
|
|
4047
|
+
const seenConvIds = /* @__PURE__ */ new Set();
|
|
4048
|
+
for (const msg of messages2) {
|
|
4049
|
+
if (msg.payload.conversationId && !seenConvIds.has(msg.payload.conversationId)) {
|
|
4050
|
+
seenConvIds.add(msg.payload.conversationId);
|
|
4051
|
+
typingMap.set(msg.payload.conversationId, baseName, { persistent: true });
|
|
3780
4052
|
}
|
|
3781
4053
|
}
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
4054
|
+
const body = JSON.stringify({
|
|
4055
|
+
session,
|
|
4056
|
+
batch: { channels },
|
|
4057
|
+
interrupt: interruptOverride ?? sessionConfig.interrupt,
|
|
4058
|
+
instructions: sessionConfig.instructions
|
|
4059
|
+
});
|
|
4060
|
+
try {
|
|
4061
|
+
const ok = await this.postToMind(port, body);
|
|
4062
|
+
if (!ok) {
|
|
4063
|
+
this.decrementActive(baseName, session);
|
|
4064
|
+
publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
|
|
4065
|
+
await this.scheduleRetry(queueIds);
|
|
4066
|
+
} else {
|
|
4067
|
+
await this.deleteQueueRows(queueIds);
|
|
4068
|
+
}
|
|
4069
|
+
} catch (err) {
|
|
4070
|
+
dlog.warn(`failed to deliver batch to ${mindName}`, logger_default.errorData(err));
|
|
4071
|
+
this.decrementActive(baseName, session);
|
|
4072
|
+
publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
|
|
4073
|
+
await this.scheduleRetry(queueIds);
|
|
4074
|
+
} finally {
|
|
4075
|
+
for (const id of queueIds) this.inFlight.delete(id);
|
|
4076
|
+
}
|
|
4077
|
+
});
|
|
3787
4078
|
}
|
|
3788
4079
|
async enqueueBatch(mindName, session, payload, sessionConfig) {
|
|
3789
4080
|
const delivery = sessionConfig.delivery;
|
|
4081
|
+
const baseName = await getBaseName(mindName);
|
|
4082
|
+
const queueId = await this.persistToQueue(mindName, session, payload);
|
|
4083
|
+
if (queueId != null) this.inFlight.add(queueId);
|
|
4084
|
+
const msg = {
|
|
4085
|
+
payload,
|
|
4086
|
+
channel: payload.channel,
|
|
4087
|
+
sender: payload.sender ?? null,
|
|
4088
|
+
createdAt: Date.now(),
|
|
4089
|
+
queueId
|
|
4090
|
+
};
|
|
3790
4091
|
if (delivery.triggers?.length) {
|
|
3791
4092
|
const text = extractTextContent(payload.content);
|
|
3792
4093
|
const lower = text.toLowerCase();
|
|
3793
4094
|
if (delivery.triggers.some((t) => lower.includes(t.toLowerCase()))) {
|
|
3794
|
-
await this.flushBatch(mindName, session, [
|
|
3795
|
-
{
|
|
3796
|
-
payload,
|
|
3797
|
-
channel: payload.channel,
|
|
3798
|
-
sender: payload.sender ?? null,
|
|
3799
|
-
createdAt: Date.now()
|
|
3800
|
-
}
|
|
3801
|
-
]);
|
|
4095
|
+
await this.flushBatch(mindName, session, [msg]);
|
|
3802
4096
|
return;
|
|
3803
4097
|
}
|
|
3804
4098
|
}
|
|
3805
|
-
const baseName = await getBaseName(mindName);
|
|
3806
4099
|
const state = this.sessionStates.get(baseName)?.get(session);
|
|
3807
4100
|
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) {
|
|
3808
4101
|
state.lastInterruptAt = Date.now();
|
|
3809
|
-
this.
|
|
3810
|
-
dlog.warn(`failed to persist batch message for ${mindName}/${session}`, logger_default.errorData(err));
|
|
3811
|
-
});
|
|
3812
|
-
await this.flushBatch(
|
|
3813
|
-
mindName,
|
|
3814
|
-
session,
|
|
3815
|
-
[{ payload, channel: payload.channel, sender: payload.sender, createdAt: Date.now() }],
|
|
3816
|
-
true
|
|
3817
|
-
);
|
|
4102
|
+
await this.flushBatch(mindName, session, [msg], true);
|
|
3818
4103
|
return;
|
|
3819
4104
|
}
|
|
3820
|
-
this.
|
|
3821
|
-
dlog.warn(`failed to persist batch message for ${mindName}/${session}`, logger_default.errorData(err));
|
|
3822
|
-
});
|
|
3823
|
-
this.addToBatchBuffer(mindName, session, payload, sessionConfig);
|
|
4105
|
+
this.addToBatchBuffer(mindName, session, sessionConfig, msg);
|
|
3824
4106
|
}
|
|
3825
|
-
addToBatchBuffer(mindName, session,
|
|
4107
|
+
addToBatchBuffer(mindName, session, sessionConfig, msg) {
|
|
3826
4108
|
const delivery = sessionConfig.delivery;
|
|
3827
4109
|
const bufferKey = `${mindName}:${session}`;
|
|
3828
4110
|
let buffer = this.batchBuffers.get(bufferKey);
|
|
@@ -3835,12 +4117,7 @@ var DeliveryManager = class {
|
|
|
3835
4117
|
};
|
|
3836
4118
|
this.batchBuffers.set(bufferKey, buffer);
|
|
3837
4119
|
}
|
|
3838
|
-
buffer.messages.push(
|
|
3839
|
-
payload,
|
|
3840
|
-
channel: payload.channel,
|
|
3841
|
-
sender: payload.sender ?? null,
|
|
3842
|
-
createdAt: Date.now()
|
|
3843
|
-
});
|
|
4120
|
+
buffer.messages.push(msg);
|
|
3844
4121
|
if (buffer.messages.length >= MAX_BATCH_SIZE) {
|
|
3845
4122
|
this.flushBatch(mindName, session);
|
|
3846
4123
|
return;
|
|
@@ -3892,7 +4169,7 @@ var DeliveryManager = class {
|
|
|
3892
4169
|
}
|
|
3893
4170
|
async gateMessage(mindName, session, payload) {
|
|
3894
4171
|
const baseName = await getBaseName(mindName);
|
|
3895
|
-
await this.persistToQueue(
|
|
4172
|
+
await this.persistToQueue(mindName, session, payload, "gated");
|
|
3896
4173
|
try {
|
|
3897
4174
|
const db = await getDb();
|
|
3898
4175
|
const count = await db.select({ count: sql2`count(*)` }).from(deliveryQueue).where(
|
|
@@ -3924,25 +4201,37 @@ var DeliveryManager = class {
|
|
|
3924
4201
|
`To accept this channel, add a routing rule for "${channel}" to your routes.json.`,
|
|
3925
4202
|
`Messages are being held until a route is configured.`
|
|
3926
4203
|
].filter((line) => line !== null).join("\n");
|
|
3927
|
-
const { sendSystemMessage: sendSystemMessage2 } = await import("./system-chat-
|
|
4204
|
+
const { sendSystemMessage: sendSystemMessage2 } = await import("./system-chat-KMQIP7VH.js");
|
|
3928
4205
|
await sendSystemMessage2(mindName, notification);
|
|
3929
4206
|
}
|
|
4207
|
+
/**
|
|
4208
|
+
* Insert a delivery_queue row and return its id. The `mind` column is always keyed by
|
|
4209
|
+
* `baseName` so inserts under a variant name and the id-scoped cleanup use the same key
|
|
4210
|
+
* (fixes the variant mismatch where variant-keyed rows were never matched by the base
|
|
4211
|
+
* cleanup). `target_mind` records the original delivery target (`mindName`, which may be a
|
|
4212
|
+
* variant) so redrive resolves the port from it — a variant's stranded row is re-delivered
|
|
4213
|
+
* to the variant, not the parent.
|
|
4214
|
+
*/
|
|
3930
4215
|
async persistToQueue(mindName, session, payload, status = "pending") {
|
|
3931
4216
|
try {
|
|
4217
|
+
const baseName = await getBaseName(mindName);
|
|
3932
4218
|
const db = await getDb();
|
|
3933
|
-
await db.insert(deliveryQueue).values({
|
|
3934
|
-
mind:
|
|
4219
|
+
const result = await db.insert(deliveryQueue).values({
|
|
4220
|
+
mind: baseName,
|
|
4221
|
+
target_mind: mindName,
|
|
3935
4222
|
session,
|
|
3936
4223
|
channel: payload.channel ?? null,
|
|
3937
4224
|
sender: payload.sender ?? null,
|
|
3938
4225
|
status,
|
|
3939
4226
|
payload: JSON.stringify(payload)
|
|
3940
|
-
});
|
|
4227
|
+
}).returning({ id: deliveryQueue.id });
|
|
4228
|
+
return result[0]?.id;
|
|
3941
4229
|
} catch (err) {
|
|
3942
4230
|
dlog.warn(
|
|
3943
4231
|
`failed to persist to delivery queue for ${mindName}/${session}`,
|
|
3944
4232
|
logger_default.errorData(err)
|
|
3945
4233
|
);
|
|
4234
|
+
return void 0;
|
|
3946
4235
|
}
|
|
3947
4236
|
}
|
|
3948
4237
|
async enrichWithProfiles(mindName, session, payload) {
|
|
@@ -4162,27 +4451,33 @@ async function linkToolResultToTurn(mind, turnId, toolResultContent, toolUseEven
|
|
|
4162
4451
|
id: mindHistory.id,
|
|
4163
4452
|
channel: mindHistory.channel,
|
|
4164
4453
|
content: mindHistory.content,
|
|
4165
|
-
message_id: mindHistory.message_id
|
|
4454
|
+
message_id: mindHistory.message_id,
|
|
4455
|
+
turn_id: mindHistory.turn_id
|
|
4166
4456
|
}).from(mindHistory).where(and4(eq5(mindHistory.id, outboundId), eq5(mindHistory.mind, mind))).limit(1);
|
|
4167
4457
|
const row = rows[0];
|
|
4168
4458
|
if (!row) {
|
|
4169
4459
|
dlog2.warn(`outbound marker references missing record: mind=${mind} id=${outboundId}`);
|
|
4170
4460
|
continue;
|
|
4171
4461
|
}
|
|
4172
|
-
|
|
4462
|
+
const alreadyTagged = row.turn_id != null;
|
|
4463
|
+
if (!alreadyTagged) {
|
|
4464
|
+
await db.update(mindHistory).set({ turn_id: turnId }).where(eq5(mindHistory.id, outboundId));
|
|
4465
|
+
}
|
|
4173
4466
|
if (row.message_id) {
|
|
4174
4467
|
await db.update(messages).set({
|
|
4175
4468
|
turn_id: turnId,
|
|
4176
4469
|
...toolUseEventId != null ? { source_event_id: toolUseEventId } : {}
|
|
4177
4470
|
}).where(eq5(messages.id, Number(row.message_id)));
|
|
4178
4471
|
}
|
|
4179
|
-
|
|
4180
|
-
mind,
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4472
|
+
if (!alreadyTagged) {
|
|
4473
|
+
publish3(mind, {
|
|
4474
|
+
mind,
|
|
4475
|
+
type: "outbound",
|
|
4476
|
+
channel: row.channel ?? void 0,
|
|
4477
|
+
content: row.content ?? void 0,
|
|
4478
|
+
turnId
|
|
4479
|
+
});
|
|
4480
|
+
}
|
|
4186
4481
|
} catch (err) {
|
|
4187
4482
|
dlog2.warn(`failed to link outbound ${outboundId} to turn ${turnId}`, logger_default.errorData(err));
|
|
4188
4483
|
}
|
|
@@ -4196,8 +4491,8 @@ async function linkToolResultToTurn(mind, turnId, toolResultContent, toolUseEven
|
|
|
4196
4491
|
await db.update(activity).set({
|
|
4197
4492
|
turn_id: turnId,
|
|
4198
4493
|
...toolUseEventId != null ? { source_event_id: toolUseEventId } : {}
|
|
4199
|
-
}).where(
|
|
4200
|
-
const actRows = await db.select().from(activity).where(
|
|
4494
|
+
}).where(inArray3(activity.id, activityIds));
|
|
4495
|
+
const actRows = await db.select().from(activity).where(inArray3(activity.id, activityIds));
|
|
4201
4496
|
if (actRows.length > 0) {
|
|
4202
4497
|
await db.insert(mindHistory).values(
|
|
4203
4498
|
actRows.map((a) => ({
|
|
@@ -4215,108 +4510,6 @@ async function linkToolResultToTurn(mind, turnId, toolResultContent, toolUseEven
|
|
|
4215
4510
|
}
|
|
4216
4511
|
}
|
|
4217
4512
|
}
|
|
4218
|
-
async function tagUntaggedOutbound(mind, turnId) {
|
|
4219
|
-
const db = await getDb();
|
|
4220
|
-
const range = await db.select({
|
|
4221
|
-
minId: sql3`MIN(${mindHistory.id})`,
|
|
4222
|
-
maxId: sql3`MAX(${mindHistory.id})`
|
|
4223
|
-
}).from(mindHistory).where(and4(eq5(mindHistory.mind, mind), eq5(mindHistory.turn_id, turnId)));
|
|
4224
|
-
const minId = range[0]?.minId;
|
|
4225
|
-
const maxId = range[0]?.maxId;
|
|
4226
|
-
if (minId == null || maxId == null) return;
|
|
4227
|
-
const orphans = await db.select({ id: mindHistory.id, message_id: mindHistory.message_id }).from(mindHistory).where(
|
|
4228
|
-
and4(
|
|
4229
|
-
eq5(mindHistory.mind, mind),
|
|
4230
|
-
eq5(mindHistory.type, "outbound"),
|
|
4231
|
-
sql3`${mindHistory.turn_id} IS NULL`,
|
|
4232
|
-
sql3`${mindHistory.id} >= ${minId}`,
|
|
4233
|
-
sql3`${mindHistory.id} <= ${maxId}`
|
|
4234
|
-
)
|
|
4235
|
-
);
|
|
4236
|
-
if (orphans.length === 0) return;
|
|
4237
|
-
const orphanIds = orphans.map((r) => r.id);
|
|
4238
|
-
await db.update(mindHistory).set({ turn_id: turnId }).where(inArray2(mindHistory.id, orphanIds));
|
|
4239
|
-
const orphansWithMessages = orphans.filter((o) => o.message_id);
|
|
4240
|
-
if (orphansWithMessages.length > 0) {
|
|
4241
|
-
const toolUseEvents = await db.select({ id: mindHistory.id }).from(mindHistory).where(
|
|
4242
|
-
and4(
|
|
4243
|
-
eq5(mindHistory.mind, mind),
|
|
4244
|
-
eq5(mindHistory.turn_id, turnId),
|
|
4245
|
-
eq5(mindHistory.type, "tool_use")
|
|
4246
|
-
)
|
|
4247
|
-
).orderBy(mindHistory.id);
|
|
4248
|
-
for (const orphan of orphansWithMessages) {
|
|
4249
|
-
let sourceEventId = null;
|
|
4250
|
-
for (let i = toolUseEvents.length - 1; i >= 0; i--) {
|
|
4251
|
-
if (toolUseEvents[i].id < orphan.id) {
|
|
4252
|
-
sourceEventId = toolUseEvents[i].id;
|
|
4253
|
-
break;
|
|
4254
|
-
}
|
|
4255
|
-
}
|
|
4256
|
-
await db.update(messages).set({
|
|
4257
|
-
turn_id: turnId,
|
|
4258
|
-
...sourceEventId != null ? { source_event_id: sourceEventId } : {}
|
|
4259
|
-
}).where(eq5(messages.id, Number(orphan.message_id)));
|
|
4260
|
-
}
|
|
4261
|
-
}
|
|
4262
|
-
dlog2.info(`tagged ${orphans.length} orphaned outbound record(s) for ${mind} with turn ${turnId}`);
|
|
4263
|
-
}
|
|
4264
|
-
async function tagUntaggedInbound(mind, turnId, {
|
|
4265
|
-
limit = 5,
|
|
4266
|
-
setTrigger = false,
|
|
4267
|
-
channel
|
|
4268
|
-
} = {}) {
|
|
4269
|
-
const db = await getDb();
|
|
4270
|
-
const tagHistoryPromise = (async () => {
|
|
4271
|
-
if (!channel) return;
|
|
4272
|
-
const historyConditions = [
|
|
4273
|
-
eq5(mindHistory.mind, mind),
|
|
4274
|
-
eq5(mindHistory.type, "inbound"),
|
|
4275
|
-
sql3`${mindHistory.turn_id} IS NULL`,
|
|
4276
|
-
sql3`${mindHistory.created_at} > datetime('now', '-60 seconds')`,
|
|
4277
|
-
eq5(mindHistory.channel, channel)
|
|
4278
|
-
];
|
|
4279
|
-
const recentInbounds = await db.select({ id: mindHistory.id }).from(mindHistory).where(and4(...historyConditions)).orderBy(desc(mindHistory.id)).limit(limit);
|
|
4280
|
-
if (recentInbounds.length > 0) {
|
|
4281
|
-
const ids = recentInbounds.map((r) => r.id);
|
|
4282
|
-
await db.update(mindHistory).set({ turn_id: turnId }).where(inArray2(mindHistory.id, ids));
|
|
4283
|
-
if (setTrigger) {
|
|
4284
|
-
await db.update(turns).set({ trigger_event_id: recentInbounds[0].id }).where(eq5(turns.id, turnId));
|
|
4285
|
-
}
|
|
4286
|
-
}
|
|
4287
|
-
})();
|
|
4288
|
-
const mindUserPromise = db.select({ id: users.id }).from(users).where(and4(eq5(users.username, mind), eq5(users.user_type, "mind"))).get();
|
|
4289
|
-
const [tagResult, mindUserResult] = await Promise.allSettled([
|
|
4290
|
-
tagHistoryPromise,
|
|
4291
|
-
mindUserPromise
|
|
4292
|
-
]);
|
|
4293
|
-
if (tagResult.status === "rejected") {
|
|
4294
|
-
dlog2.error("failed to tag history inbound", logger_default.errorData(tagResult.reason));
|
|
4295
|
-
}
|
|
4296
|
-
const mindUser = mindUserResult.status === "fulfilled" ? mindUserResult.value : void 0;
|
|
4297
|
-
const mindConvIds = mindUser ? (await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq5(conversationParticipants.user_id, mindUser.id))).map((r) => r.conversation_id) : [];
|
|
4298
|
-
const recentMsgs = mindConvIds.length > 0 ? await db.select({ id: messages.id }).from(messages).where(
|
|
4299
|
-
and4(
|
|
4300
|
-
inArray2(messages.conversation_id, mindConvIds),
|
|
4301
|
-
sql3`${messages.turn_id} IS NULL`,
|
|
4302
|
-
sql3`${messages.sender_name} != ${mind}`,
|
|
4303
|
-
sql3`${messages.created_at} > datetime('now', '-60 seconds')`
|
|
4304
|
-
)
|
|
4305
|
-
).orderBy(desc(messages.id)).limit(limit) : [];
|
|
4306
|
-
if (recentMsgs.length > 0) {
|
|
4307
|
-
const ids = recentMsgs.map((r) => r.id);
|
|
4308
|
-
await db.update(messages).set({ turn_id: turnId }).where(inArray2(messages.id, ids));
|
|
4309
|
-
}
|
|
4310
|
-
}
|
|
4311
|
-
async function tagRecentInbound(mind, session, channel) {
|
|
4312
|
-
const turnId = getActiveTurnId(mind, session);
|
|
4313
|
-
if (!turnId) return;
|
|
4314
|
-
try {
|
|
4315
|
-
await tagUntaggedInbound(mind, turnId, { limit: 1, channel });
|
|
4316
|
-
} catch (err) {
|
|
4317
|
-
dlog2.warn(`failed to tag recent inbound for ${mind} with turn ${turnId}`, logger_default.errorData(err));
|
|
4318
|
-
}
|
|
4319
|
-
}
|
|
4320
4513
|
function resolveSleepAction(sleepBehavior, wokenByTrigger, wakeTriggerMatches) {
|
|
4321
4514
|
if (sleepBehavior === "skip") return "skip";
|
|
4322
4515
|
if (sleepBehavior === "trigger-wake" && !wokenByTrigger) return "queue-and-wake";
|
|
@@ -4450,7 +4643,7 @@ async function generateSystemReply(conversationId, mindName, message) {
|
|
|
4450
4643
|
if (config.sleep.schedule?.wake) contextParts.push(`Wake cron: ${config.sleep.schedule.wake}`);
|
|
4451
4644
|
}
|
|
4452
4645
|
try {
|
|
4453
|
-
const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-
|
|
4646
|
+
const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-BVU5D2AE.js");
|
|
4454
4647
|
const sm = getSleepManagerIfReady2();
|
|
4455
4648
|
if (sm) {
|
|
4456
4649
|
const state = sm.getState(mindName);
|
|
@@ -4500,6 +4693,7 @@ export {
|
|
|
4500
4693
|
sendSystemMessage,
|
|
4501
4694
|
sendSystemMessageDirect,
|
|
4502
4695
|
generateSystemReply,
|
|
4696
|
+
checkHealth,
|
|
4503
4697
|
RotatingLog,
|
|
4504
4698
|
syncProviderToMinds,
|
|
4505
4699
|
recordNotice,
|
|
@@ -4514,8 +4708,7 @@ export {
|
|
|
4514
4708
|
initDeliveryManager,
|
|
4515
4709
|
getDeliveryManager,
|
|
4516
4710
|
tryGetDeliveryManager,
|
|
4517
|
-
|
|
4518
|
-
MindManager,
|
|
4711
|
+
MindStartupError,
|
|
4519
4712
|
initMindManager,
|
|
4520
4713
|
getMindManager,
|
|
4521
4714
|
invalidateMindUserCache,
|
|
@@ -4565,9 +4758,6 @@ export {
|
|
|
4565
4758
|
recordInbound,
|
|
4566
4759
|
recordOutbound,
|
|
4567
4760
|
linkToolResultToTurn,
|
|
4568
|
-
tagUntaggedOutbound,
|
|
4569
|
-
tagUntaggedInbound,
|
|
4570
|
-
tagRecentInbound,
|
|
4571
4761
|
resolveSleepAction,
|
|
4572
4762
|
deliverMessage,
|
|
4573
4763
|
ensureSystemChannel,
|