volute 0.42.1 → 0.43.1

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.
Files changed (101) hide show
  1. package/dist/{activity-events-KFKVRZE3.js → activity-events-UISXY4Y5.js} +3 -3
  2. package/dist/{ai-service-3GBOJGUA.js → ai-service-FBCCEYF5.js} +8 -4
  3. package/dist/{archive-4PPYF7LL.js → archive-I32WHCJB.js} +3 -3
  4. package/dist/{auth-EAASQPTN.js → auth-3RIG6GKN.js} +4 -4
  5. package/dist/{chat-LYL5CWJV.js → chat-SDSX5TVV.js} +2 -2
  6. package/dist/{chunk-M2YMNTHK.js → chunk-2HIVYZDB.js} +1 -1
  7. package/dist/{chunk-6G7R2LPI.js → chunk-4SVONUTC.js} +569 -379
  8. package/dist/{chunk-QPTQKGGF.js → chunk-57LBQ7JX.js} +1 -1
  9. package/dist/{chunk-K547QOCM.js → chunk-7G2EEYXX.js} +2 -2
  10. package/dist/{chunk-E7XWFDZL.js → chunk-AD46AYZQ.js} +1 -1
  11. package/dist/{chunk-P6DPB33N.js → chunk-ATEX3ZPG.js} +2 -2
  12. package/dist/{chunk-WR7CSHQ4.js → chunk-C6ESRQZ7.js} +1 -1
  13. package/dist/{chunk-JU5GWO77.js → chunk-DE4EKQFH.js} +3 -3
  14. package/dist/{chunk-JPDN6ZRJ.js → chunk-DMUPZQWP.js} +8 -8
  15. package/dist/{chunk-BR6KFHVT.js → chunk-EE7SZJON.js} +2 -2
  16. package/dist/{chunk-5QG36RV7.js → chunk-EFL5UOY4.js} +3 -3
  17. package/dist/{chunk-OEJFZHZZ.js → chunk-GZJFC76M.js} +1 -1
  18. package/dist/{chunk-KUJVCDWT.js → chunk-JVK6QQIC.js} +2 -2
  19. package/dist/{chunk-YLTIEEVL.js → chunk-K4EHBHPE.js} +3 -3
  20. package/dist/{chunk-M3T3Y6FT.js → chunk-KINGVQSR.js} +5 -5
  21. package/dist/{chunk-MHFAWJCC.js → chunk-L3IV23ZG.js} +12 -1
  22. package/dist/{chunk-VBKRZCR7.js → chunk-M3PGGSBH.js} +1 -1
  23. package/dist/{chunk-NSGIVS5T.js → chunk-O3DXAR44.js} +2 -2
  24. package/dist/{chunk-KZAL7HEQ.js → chunk-ONM2AZFL.js} +1 -1
  25. package/dist/chunk-PGPLCJPK.js +272 -0
  26. package/dist/{chunk-43K5UWF7.js → chunk-QQ2MVQU3.js} +13 -12
  27. package/dist/{chunk-62ICA2FF.js → chunk-SPLPKBES.js} +17 -1
  28. package/dist/{chunk-BLOAJLGI.js → chunk-XDTKJNW5.js} +1 -1
  29. package/dist/{chunk-BWZHX46O.js → chunk-Y6O35D7K.js} +21 -6
  30. package/dist/{chunk-IUZ3EV2D.js → chunk-YAMO2U3N.js} +1 -1
  31. package/dist/{chunk-4ZSUUBG5.js → chunk-YSV3VJQK.js} +55 -2
  32. package/dist/cli.js +15 -15
  33. package/dist/{cloud-sync-E3ZL5SZ4.js → cloud-sync-KOEBUFW5.js} +16 -17
  34. package/dist/{conversations-FSZCUUYO.js → conversations-67WWW25C.js} +4 -4
  35. package/dist/{create-ERNQYOCK.js → create-7W3LV3J7.js} +1 -1
  36. package/dist/{daemon-restart-QBZOMILO.js → daemon-restart-EZXB73W7.js} +7 -8
  37. package/dist/daemon.js +524 -433
  38. package/dist/{db-ZJJJYJBU.js → db-CZV62BOQ.js} +2 -2
  39. package/dist/delivery-manager-OMCCJEVP.js +32 -0
  40. package/dist/{delivery-router-FDBZJQOY.js → delivery-router-HXBHYHJM.js} +7 -5
  41. package/dist/down-RRNYIGCU.js +16 -0
  42. package/dist/echo-text-KFSUCZXL.js +32 -0
  43. package/dist/{exec-ZDD7NEB4.js → exec-SJK7VUTC.js} +3 -4
  44. package/dist/{export-WYI3E3TB.js → export-7LPCIB5J.js} +3 -3
  45. package/dist/{extensions-X6MKBJLB.js → extensions-PQPZT6LS.js} +16 -17
  46. package/dist/{files-COTCVBGO.js → files-LV7VAKPC.js} +3 -3
  47. package/dist/{import-EPK6WCYS.js → import-XPECCRQ7.js} +6 -6
  48. package/dist/{isolation-F7ABZMDA.js → isolation-5AYEY55P.js} +5 -3
  49. package/dist/{login-GPBXOU4D.js → login-KJSN4JDA.js} +2 -2
  50. package/dist/{logout-DNNTIYE2.js → logout-TMJGUQV2.js} +2 -2
  51. package/dist/message-delivery-HRGFNWO7.js +34 -0
  52. package/dist/{mind-K5PZHPBR.js → mind-G3WEWDM4.js} +5 -5
  53. package/dist/{mind-activity-tracker-HWVFE533.js → mind-activity-tracker-JHAT6456.js} +4 -4
  54. package/dist/mind-service-2C7AINBO.js +36 -0
  55. package/dist/{package-G4AM2IDZ.js → package-24TL6DTF.js} +1 -1
  56. package/dist/{registry-MDLNPTTI.js → registry-7LQHTRUX.js} +2 -2
  57. package/dist/{sandbox-E7RWZDVG.js → sandbox-SJH3XOPZ.js} +4 -4
  58. package/dist/scheduler-VON2TB52.js +30 -0
  59. package/dist/{schema-HHYJDG7B.js → schema-LRUTFOER.js} +1 -1
  60. package/dist/{seed-SKVSWF7S.js → seed-HHSJTM6O.js} +1 -1
  61. package/dist/{seed-cmd-D2H5VLB4.js → seed-cmd-UPQY35RG.js} +2 -2
  62. package/dist/{seed-create-BUSKQ5II.js → seed-create-MDRMXMSB.js} +3 -3
  63. package/dist/{seed-sprout-R2IZPITD.js → seed-sprout-4M7IQWMH.js} +7 -8
  64. package/dist/{send-SLDFRSTL.js → send-ZBZWIH5V.js} +3 -3
  65. package/dist/{service-ZDOSEQCE.js → service-4EVZKGSA.js} +4 -5
  66. package/dist/{service-install-2HZPDHEY.js → service-install-SHRM6NHK.js} +5 -6
  67. package/dist/{setup-IYOUPCIK.js → setup-25MMJ3J4.js} +3 -3
  68. package/dist/{setup-IU3ID7II.js → setup-ZD2LMBR3.js} +8 -10
  69. package/dist/{skills-P6RGGASV.js → skills-4KQ377GN.js} +5 -6
  70. package/dist/sleep-manager-AG5SQDDI.js +34 -0
  71. package/dist/{spirit-XLD6QXSS.js → spirit-53DQPRNR.js} +7 -8
  72. package/dist/{sprout-QV2RJM5W.js → sprout-YKM2M6D6.js} +1 -1
  73. package/dist/{src-TJ7RBU5H.js → src-3EGHHKZK.js} +8 -8
  74. package/dist/{status-QAKAPDYW.js → status-WTU2WHXY.js} +5 -6
  75. package/dist/system-chat-54RN3K67.js +34 -0
  76. package/dist/{tailscale-TIRTGM6E.js → tailscale-Q7ZJL7S7.js} +2 -2
  77. package/dist/{turn-tracker-Z5UJC4BY.js → turn-tracker-JZYERJ76.js} +7 -5
  78. package/dist/up-LFZA4GI5.js +18 -0
  79. package/dist/{update-NY3XBVAJ.js → update-EJ7Q77ZX.js} +5 -6
  80. package/dist/{update-check-OG27AL4Z.js → update-check-ACYYNKTW.js} +3 -3
  81. package/dist/{version-notify-P7UAR7HZ.js → version-notify-SRQJCQEK.js} +17 -18
  82. package/dist/web-assets/assets/{index-CrFAsfiE.js → index-DTDmEfPU.js} +2 -2
  83. package/dist/web-assets/index.html +1 -1
  84. package/drizzle/0010_delivery_queue_redrive.sql +10 -0
  85. package/drizzle/meta/_journal.json +7 -0
  86. package/package.json +1 -1
  87. package/templates/claude/src/agent.ts +8 -1
  88. package/templates/claude/src/lib/message-channel.ts +28 -6
  89. package/templates/codex/src/server.ts +2 -8
  90. package/dist/chunk-GN52E2DP.js +0 -59
  91. package/dist/chunk-TFBQESH7.js +0 -179
  92. package/dist/delivery-manager-J3BRHXUF.js +0 -33
  93. package/dist/down-DSRWSCXY.js +0 -17
  94. package/dist/echo-text-3HBQUWPX.js +0 -33
  95. package/dist/message-delivery-4ZWFDBAY.js +0 -41
  96. package/dist/mind-manager-6DSY5IJW.js +0 -33
  97. package/dist/mind-service-R2FM53KD.js +0 -37
  98. package/dist/scheduler-2FKQCRCM.js +0 -31
  99. package/dist/sleep-manager-F5KWIFOX.js +0 -35
  100. package/dist/system-chat-EQIH6JVL.js +0 -35
  101. 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-BLOAJLGI.js";
4
+ } from "./chunk-XDTKJNW5.js";
5
5
  import {
6
6
  spiritDir
7
- } from "./chunk-43K5UWF7.js";
7
+ } from "./chunk-QQ2MVQU3.js";
8
8
  import {
9
9
  hashSkillDir,
10
10
  importSkillFromDir,
11
11
  removeSharedSkill,
12
12
  sharedSkillsDir
13
- } from "./chunk-M3T3Y6FT.js";
13
+ } from "./chunk-KINGVQSR.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-YLTIEEVL.js";
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-WR7CSHQ4.js";
31
+ } from "./chunk-C6ESRQZ7.js";
32
32
  import {
33
33
  markIdle,
34
34
  onMindEvent
35
- } from "./chunk-VBKRZCR7.js";
35
+ } from "./chunk-M3PGGSBH.js";
36
36
  import {
37
37
  extractTextContent,
38
38
  getRoutingConfig,
39
39
  resolveDeliveryMode,
40
- resolveRoute
41
- } from "./chunk-62ICA2FF.js";
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-BWZHX46O.js";
49
- import {
50
- exec
51
- } from "./chunk-GN52E2DP.js";
49
+ } from "./chunk-Y6O35D7K.js";
52
50
  import {
53
51
  chownMindDir,
52
+ exec,
54
53
  isIsolationEnabled,
55
54
  mindUserName,
56
55
  wrapForIsolation
57
- } from "./chunk-TFBQESH7.js";
56
+ } from "./chunk-PGPLCJPK.js";
58
57
  import {
59
58
  aiCompleteUtility,
60
59
  getAiConfig,
61
60
  resolveApiKey
62
- } from "./chunk-4ZSUUBG5.js";
61
+ } from "./chunk-YSV3VJQK.js";
63
62
  import {
64
63
  readGlobalConfig,
65
64
  writeGlobalConfig
66
- } from "./chunk-KZAL7HEQ.js";
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-5QG36RV7.js";
72
+ } from "./chunk-EFL5UOY4.js";
74
73
  import {
75
74
  publish,
76
75
  subscribe
77
- } from "./chunk-K547QOCM.js";
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-IUZ3EV2D.js";
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
- turns,
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, desc, eq as eq5, inArray as inArray2, sql as sql3 } from "drizzle-orm";
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
- const timeout = setTimeout(() => {
740
- reject(new Error(`Mind ${name} did not start within 30s`));
741
- }, 3e4);
742
- function checkOutput(data) {
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
- reject(new Error(`Mind ${name} exited with code ${code} during startup${detail}`));
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-F5KWIFOX.js");
842
- const sleepState = getSleepManagerIfReady2()?.getState(name);
908
+ const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-AG5SQDDI.js");
909
+ const sleepMgr = getSleepManagerIfReady2();
910
+ const sleepState = sleepMgr?.getState(name);
843
911
  if (sleepState?.sleeping) {
844
- mlog.info(`${name} is sleeping \u2014 skipping crash recovery`);
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-J3BRHXUF.js");
938
+ const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-OMCCJEVP.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-HWVFE533.js").then(({ markIdle: markIdle2 }) => markIdle2(name)).catch((err) => mlog.warn(`failed to mark ${name} idle after crash`, logger_default.errorData(err)));
873
- import("./activity-events-KFKVRZE3.js").then(
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
- child.on("exit", () => resolve9());
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-J3BRHXUF.js");
1004
+ const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-OMCCJEVP.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-3HBQUWPX.js");
1012
+ const { clearEchoTextCache } = await import("./echo-text-KFSUCZXL.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
- await this.stopMind(name);
946
- await this.startMind(name);
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";
@@ -1562,7 +1647,7 @@ function resolveSkillsDir(manifest) {
1562
1647
  async function discoverBuiltinExtensions(disabledIds) {
1563
1648
  const builtins = [
1564
1649
  { id: "notes", load: async () => (await import("./src-L2Z7X5UT.js")).default },
1565
- { id: "pages", load: async () => (await import("./src-TJ7RBU5H.js")).default },
1650
+ { id: "pages", load: async () => (await import("./src-3EGHHKZK.js")).default },
1566
1651
  { id: "plan", load: async () => (await import("./src-O4PRLMKM.js")).default }
1567
1652
  ];
1568
1653
  const results = [];
@@ -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-ZDD7NEB4.js");
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-ZDD7NEB4.js");
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-FDBZJQOY.js");
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-EAASQPTN.js");
2651
- const { findDMConversation: findDMConversation2, createConversation: createConversation2 } = await import("./conversations-FSZCUUYO.js");
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-4ZWFDBAY.js");
3111
+ const { deliverMessage: deliverMessage2 } = await import("./message-delivery-HRGFNWO7.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.deliverToMind(mindName, sessionName2, payload, sessionConfig2);
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.deliverToMind(mindName, sessionName, payload, sessionConfig);
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
- const rows = await db.select().from(deliveryQueue).where(eq4(deliveryQueue.status, "pending"));
3515
- for (const row of rows) {
3516
- let payload;
3517
- try {
3518
- payload = JSON.parse(row.payload);
3519
- } catch (parseErr) {
3520
- dlog.warn(
3521
- `corrupt payload in delivery queue row ${row.id}, skipping`,
3522
- logger_default.errorData(parseErr)
3523
- );
3524
- continue;
3525
- }
3526
- const config = getRoutingConfig(row.mind);
3527
- const sessionConfig = resolveDeliveryMode(config, row.session);
3528
- if (sessionConfig.delivery.mode === "batch") {
3529
- this.addToBatchBuffer(row.mind, row.session, payload, sessionConfig);
3530
- } else {
3531
- try {
3532
- await db.delete(deliveryQueue).where(eq4(deliveryQueue.id, row.id));
3533
- } catch (err) {
3534
- dlog.warn(`failed to delete queue row ${row.id} for ${row.mind}`, logger_default.errorData(err));
3535
- }
3536
- this.deliverToMind(row.mind, row.session, payload, sessionConfig).catch((err) => {
3537
- dlog.warn(`failed to restore delivery for ${row.mind}`, logger_default.errorData(err));
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
- if (rows.length > 0) {
3542
- dlog.info(`restored ${rows.length} queued messages from DB`);
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("failed to restore delivery queue from DB", logger_default.errorData(err));
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
- async deliverToMind(mindName, session, payload, sessionConfig) {
3671
- const resolved = await this.resolvePort(mindName);
3672
- if (!resolved) {
3673
- dlog.warn(`cannot deliver to ${mindName}: mind not found`);
3674
- return;
3675
- }
3676
- const { baseName, port } = resolved;
3677
- const senders = /* @__PURE__ */ new Set();
3678
- if (payload.sender) senders.add(payload.sender);
3679
- const channels = /* @__PURE__ */ new Set();
3680
- if (payload.channel) channels.add(payload.channel);
3681
- this.incrementActive(baseName, session, senders, channels);
3682
- const typingMap = getTypingMap();
3683
- if (payload.channel) {
3684
- typingMap.set(payload.channel, baseName, { persistent: true });
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 ok = await this.postToMind(port, body);
3699
- if (!ok) {
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(`failed to deliver to ${mindName}`, logger_default.errorData(err));
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
- async deliverBatchToMind(mindName, session, messages2, sessionConfig, interruptOverride) {
3710
- const resolved = await this.resolvePort(mindName);
3711
- if (!resolved) {
3712
- dlog.warn(`cannot deliver batch to ${mindName}: mind not found`);
3713
- return;
3714
- }
3715
- const { baseName, port } = resolved;
3716
- const firstPerChannel = /* @__PURE__ */ new Set();
3717
- const isFirstForChannel = [];
3718
- for (const msg of messages2) {
3719
- const ch = msg.channel ?? "unknown";
3720
- isFirstForChannel.push(!firstPerChannel.has(ch));
3721
- firstPerChannel.add(ch);
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
- const body = JSON.stringify({
3755
- session,
3756
- batch: { channels },
3757
- interrupt: interruptOverride ?? sessionConfig.interrupt,
3758
- instructions: sessionConfig.instructions
3759
- });
3760
- try {
3761
- const ok = await this.postToMind(port, body);
3762
- if (!ok) {
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
- } else {
3766
- try {
3767
- const db = await getDb();
3768
- await db.delete(deliveryQueue).where(
3769
- and3(
3770
- eq4(deliveryQueue.mind, baseName),
3771
- eq4(deliveryQueue.session, session),
3772
- eq4(deliveryQueue.status, "pending")
3773
- )
3774
- );
3775
- } catch (err) {
3776
- dlog.warn(
3777
- `failed to clean delivery queue for ${baseName}/${session}`,
3778
- logger_default.errorData(err)
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
- } catch (err) {
3783
- dlog.warn(`failed to deliver batch to ${mindName}`, logger_default.errorData(err));
3784
- this.decrementActive(baseName, session);
3785
- publishTypingForChannels(typingMap.deleteSender(baseName), typingMap);
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.persistToQueue(mindName, session, payload).catch((err) => {
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.persistToQueue(mindName, session, payload).catch((err) => {
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, payload, sessionConfig) {
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(baseName, session, payload, "gated");
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-EQIH6JVL.js");
4204
+ const { sendSystemMessage: sendSystemMessage2 } = await import("./system-chat-54RN3K67.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: mindName,
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
- await db.update(mindHistory).set({ turn_id: turnId }).where(eq5(mindHistory.id, outboundId));
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
- publish3(mind, {
4180
- mind,
4181
- type: "outbound",
4182
- channel: row.channel ?? void 0,
4183
- content: row.content ?? void 0,
4184
- turnId
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(inArray2(activity.id, activityIds));
4200
- const actRows = await db.select().from(activity).where(inArray2(activity.id, activityIds));
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-F5KWIFOX.js");
4646
+ const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-AG5SQDDI.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
- buildMindBaseEnv,
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,