volute 0.45.2 → 0.46.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.
Files changed (62) hide show
  1. package/README.md +3 -3
  2. package/dist/{activity-events-BUR2RYTJ.js → activity-events-NF6VWTJQ.js} +1 -1
  3. package/dist/{auth-FLAEXXZK.js → auth-CCFAU4GQ.js} +2 -2
  4. package/dist/{chat-76MRWWQX.js → chat-ZIQQS25U.js} +1 -1
  5. package/dist/chunk-4YU7HF4L.js +237 -0
  6. package/dist/{chunk-OP4JG2BS.js → chunk-5ZQZDDIK.js} +6 -2
  7. package/dist/{chunk-FR6S2KNN.js → chunk-BJZ4PV7R.js} +1 -1
  8. package/dist/{chunk-NYC5U6K7.js → chunk-E3DA4HYV.js} +4 -4
  9. package/dist/{chunk-YT7PEESJ.js → chunk-JO5IO4FH.js} +245 -124
  10. package/dist/{chunk-7ZUB63PS.js → chunk-KGIXX4NM.js} +1 -1
  11. package/dist/{chunk-EJVPTVTI.js → chunk-KQSFTPIM.js} +1 -1
  12. package/dist/{chunk-T6HYVHPG.js → chunk-OV7ZQ2GH.js} +1 -1
  13. package/dist/{clock-K43IMROX.js → chunk-PJ45FQPT.js} +21 -16
  14. package/dist/{chunk-UYLGEDVD.js → chunk-YXSUMLTC.js} +44 -267
  15. package/dist/{chunk-7C44R625.js → chunk-ZNMCCHCP.js} +47 -1
  16. package/dist/cli.js +11 -11
  17. package/dist/clock-RHS2JEDL.js +16 -0
  18. package/dist/{cloud-sync-3OJOPZRT.js → cloud-sync-46RS4SDA.js} +9 -8
  19. package/dist/{conversations-JZTD6KLR.js → conversations-CJOOCGNW.js} +2 -2
  20. package/dist/{daemon-restart-TIHKVVVA.js → daemon-restart-VFEI57UO.js} +1 -1
  21. package/dist/daemon.js +249 -170
  22. package/dist/{delete-BFMIGAIB.js → delete-HPLC7ETU.js} +1 -1
  23. package/dist/{delivery-manager-VDVKVA46.js → delivery-manager-JINZRG2W.js} +9 -8
  24. package/dist/{echo-text-6JJOPUD4.js → echo-text-VQPB5L2X.js} +10 -9
  25. package/dist/{export-GGIVO76O.js → export-LY723I3Z.js} +3 -3
  26. package/dist/{extensions-JONTW2HF.js → extensions-ZELQLTSR.js} +9 -8
  27. package/dist/{message-delivery-3C65ZI6L.js → message-delivery-R7JSQGYX.js} +11 -8
  28. package/dist/{mind-DWB6XYCH.js → mind-LHKS55DI.js} +9 -9
  29. package/dist/{mind-activity-tracker-POBLU3LC.js → mind-activity-tracker-3QRNJVR4.js} +2 -2
  30. package/dist/{mind-service-3F4I46CI.js → mind-service-NB2JMWQZ.js} +11 -8
  31. package/dist/mind-sleep-74TGOQBC.js +83 -0
  32. package/dist/{package-A7KXT67Q.js → package-DOD6CPO6.js} +2 -1
  33. package/dist/{scheduler-P6DXLLXI.js → scheduler-WVKII3LA.js} +9 -8
  34. package/dist/{seed-HCP5NYKH.js → seed-CNYIHPOH.js} +1 -1
  35. package/dist/{seed-cmd-ILOGNIFB.js → seed-cmd-AVNUNLZV.js} +2 -2
  36. package/dist/{seed-create-OURS2LS4.js → seed-create-ANXGYSCC.js} +3 -3
  37. package/dist/{seed-sprout-ELDFDAN6.js → seed-sprout-NIUFTX32.js} +5 -3
  38. package/dist/{send-AAIOPJAV.js → send-SPE6DVNJ.js} +6 -0
  39. package/dist/{setup-DQM5VPY4.js → setup-QT2A25SY.js} +1 -1
  40. package/dist/skills/orientation/SKILL.md +1 -1
  41. package/dist/skills/volute-mind/SKILL.md +8 -8
  42. package/dist/skills/volute-mind/references/sleep.md +8 -1
  43. package/dist/{skills-BKINFL7W.js → skills-AKCFA7BN.js} +1 -1
  44. package/dist/{sleep-manager-HCUH4PM4.js → sleep-manager-G6TXSDP7.js} +9 -8
  45. package/dist/{spirit-DBTIW7P4.js → spirit-RPBLLUQ5.js} +2 -2
  46. package/dist/{sprout-W3KC7UGK.js → sprout-SMX42ZBP.js} +1 -1
  47. package/dist/{system-chat-XOALE2RD.js → system-chat-NDAW6YP7.js} +9 -8
  48. package/dist/{turn-tracker-2W6Q2NSR.js → turn-tracker-FOIBNUUA.js} +2 -1
  49. package/dist/{up-FTK7CTO3.js → up-PS6PFS75.js} +1 -1
  50. package/dist/{version-notify-3XMAIQTE.js → version-notify-C5CH7CVP.js} +15 -10
  51. package/dist/web-assets/assets/{index-Bc2m8gRC.js → index-CU6XWICM.js} +3 -3
  52. package/dist/web-assets/index.html +1 -1
  53. package/package.json +2 -1
  54. package/templates/_base/home/VOLUTE.md +6 -2
  55. package/templates/_base/src/lib/context-breakdown.ts +57 -67
  56. package/templates/_base/src/lib/volute-server.ts +2 -2
  57. package/templates/claude/src/agent.ts +78 -42
  58. package/templates/claude/src/lib/session-reaper.ts +31 -0
  59. package/templates/codex/src/agent.ts +62 -40
  60. package/templates/pi/src/agent.ts +55 -38
  61. package/dist/mind-sleep-YMBR3I35.js +0 -47
  62. /package/dist/{chunk-QNQMSWZE.js → chunk-K6T43YBD.js} +0 -0
@@ -4,13 +4,13 @@ import {
4
4
  } from "./chunk-ZDZISKCL.js";
5
5
  import {
6
6
  spiritDir
7
- } from "./chunk-FR6S2KNN.js";
7
+ } from "./chunk-BJZ4PV7R.js";
8
8
  import {
9
9
  hashSkillDir,
10
10
  importSkillFromDir,
11
11
  removeSharedSkill,
12
12
  sharedSkillsDir
13
- } from "./chunk-7ZUB63PS.js";
13
+ } from "./chunk-KGIXX4NM.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-OP4JG2BS.js";
23
+ } from "./chunk-5ZQZDDIK.js";
24
24
  import {
25
25
  readVoluteConfig,
26
26
  resolveWakeTriggers,
@@ -33,7 +33,7 @@ import {
33
33
  import {
34
34
  markIdle,
35
35
  onMindEvent
36
- } from "./chunk-EJVPTVTI.js";
36
+ } from "./chunk-KQSFTPIM.js";
37
37
  import {
38
38
  extractTextContent,
39
39
  getRoutingConfig,
@@ -43,11 +43,13 @@ import {
43
43
  } from "./chunk-4JOQZPCE.js";
44
44
  import {
45
45
  clearMind,
46
- getPrompt,
47
46
  linkInboundToActiveTurn,
48
47
  publish as publish3,
49
48
  summarizeOrphanedTurns
50
- } from "./chunk-UYLGEDVD.js";
49
+ } from "./chunk-YXSUMLTC.js";
50
+ import {
51
+ getPrompt
52
+ } from "./chunk-4YU7HF4L.js";
51
53
  import {
52
54
  chownMindDir,
53
55
  exec,
@@ -70,11 +72,11 @@ import {
70
72
  getUser,
71
73
  getUserByUsername,
72
74
  syncMindProfile
73
- } from "./chunk-T6HYVHPG.js";
75
+ } from "./chunk-OV7ZQ2GH.js";
74
76
  import {
75
77
  publish,
76
78
  subscribe
77
- } from "./chunk-7C44R625.js";
79
+ } from "./chunk-ZNMCCHCP.js";
78
80
  import {
79
81
  logger_default
80
82
  } from "./chunk-T2TP6ZC6.js";
@@ -101,7 +103,7 @@ import {
101
103
  } from "./chunk-ZV7MFISX.js";
102
104
 
103
105
  // packages/daemon/src/lib/delivery/message-delivery.ts
104
- import { and as and5, eq as eq6, inArray as inArray2 } from "drizzle-orm";
106
+ import { and as and5, eq as eq6, inArray as inArray3 } from "drizzle-orm";
105
107
 
106
108
  // packages/daemon/src/lib/daemon/sleep-manager.ts
107
109
  import { execFile as execFile2, spawn as spawnChild } from "child_process";
@@ -117,7 +119,7 @@ import {
117
119
  import { resolve as resolve7 } from "path";
118
120
  import { promisify as promisify2 } from "util";
119
121
  import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
120
- import { and as and2, eq as eq3 } from "drizzle-orm";
122
+ import { and as and2, eq as eq3, inArray } from "drizzle-orm";
121
123
 
122
124
  // packages/daemon/src/lib/daemon/mind-manager.ts
123
125
  import { execFile, spawn } from "child_process";
@@ -769,21 +771,8 @@ var MindManager = class {
769
771
  if (isIsolationEnabled()) {
770
772
  env.HOME = resolve2(dir, "home");
771
773
  }
772
- const customNode = process.env.VOLUTE_NODE_PATH;
773
- let baseBin;
774
- let baseArgs;
775
- if (customNode) {
776
- baseBin = customNode;
777
- baseArgs = [
778
- resolve2(dir, "node_modules", ".bin", "tsx"),
779
- "src/server.ts",
780
- "--port",
781
- String(port)
782
- ];
783
- } else {
784
- baseBin = resolve2(dir, "node_modules", ".bin", "tsx");
785
- baseArgs = ["src/server.ts", "--port", String(port)];
786
- }
774
+ const baseBin = process.env.VOLUTE_NODE_PATH ?? process.execPath;
775
+ const baseArgs = ["--import", "tsx", "src/server.ts", "--port", String(port)];
787
776
  let spawnCmd;
788
777
  let spawnArgs;
789
778
  if (isIsolationEnabled()) {
@@ -945,7 +934,7 @@ var MindManager = class {
945
934
  if (this.shuttingDown || this.stopping.has(name)) return;
946
935
  mlog.error(`mind ${name} exited with code ${code}`);
947
936
  try {
948
- const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-HCUH4PM4.js");
937
+ const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-G6TXSDP7.js");
949
938
  const sleepMgr = getSleepManagerIfReady2();
950
939
  const sleepState = sleepMgr?.getState(name);
951
940
  if (sleepState?.sleeping) {
@@ -975,15 +964,15 @@ var MindManager = class {
975
964
  (err) => mlog.warn(`failed to clear turn state for ${name} after crash`, logger_default.errorData(err))
976
965
  );
977
966
  try {
978
- const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-VDVKVA46.js");
967
+ const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-JINZRG2W.js");
979
968
  getDeliveryManager2().clearMindSessions(name);
980
969
  } catch (err) {
981
970
  if (!(err instanceof Error && err.message.includes("not initialized"))) {
982
971
  mlog.warn(`failed to clear delivery state for ${name} after crash`, logger_default.errorData(err));
983
972
  }
984
973
  }
985
- import("./mind-activity-tracker-POBLU3LC.js").then(({ markIdle: markIdle2 }) => markIdle2(name)).catch((err) => mlog.warn(`failed to mark ${name} idle after crash`, logger_default.errorData(err)));
986
- import("./activity-events-BUR2RYTJ.js").then(
974
+ import("./mind-activity-tracker-3QRNJVR4.js").then(({ markIdle: markIdle2 }) => markIdle2(name)).catch((err) => mlog.warn(`failed to mark ${name} idle after crash`, logger_default.errorData(err)));
975
+ import("./activity-events-NF6VWTJQ.js").then(
987
976
  ({ publish: publish4 }) => publish4({ type: "mind_stopped", mind: name, summary: `${name} crashed (exit ${code})` })
988
977
  ).catch((err) => mlog.warn(`failed to publish crash event for ${name}`, logger_default.errorData(err)));
989
978
  const { shouldRestart, delay, attempt } = this.restartTracker.recordCrash(name);
@@ -1041,7 +1030,7 @@ var MindManager = class {
1041
1030
  mlog.warn(`failed to clear turn state for ${name} on stop`, logger_default.errorData(err));
1042
1031
  }
1043
1032
  try {
1044
- const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-VDVKVA46.js");
1033
+ const { getDeliveryManager: getDeliveryManager2 } = await import("./delivery-manager-JINZRG2W.js");
1045
1034
  getDeliveryManager2().clearMindSessions(name);
1046
1035
  } catch (err) {
1047
1036
  if (!(err instanceof Error && err.message.includes("not initialized"))) {
@@ -1049,7 +1038,7 @@ var MindManager = class {
1049
1038
  }
1050
1039
  }
1051
1040
  try {
1052
- const { clearEchoTextCache } = await import("./echo-text-6JJOPUD4.js");
1041
+ const { clearEchoTextCache } = await import("./echo-text-VQPB5L2X.js");
1053
1042
  clearEchoTextCache(name);
1054
1043
  } catch (err) {
1055
1044
  mlog.debug(`failed to clear echo-text cache for ${name}`, logger_default.errorData(err));
@@ -1226,6 +1215,10 @@ async function getSessionUserId(sessionId) {
1226
1215
  return row.userId;
1227
1216
  }
1228
1217
  async function cleanExpiredSessions() {
1218
+ const now = Date.now();
1219
+ for (const [id, cached] of sessionCache) {
1220
+ if (cached.expires <= now) sessionCache.delete(id);
1221
+ }
1229
1222
  const db = await getDb();
1230
1223
  const cutoff = Date.now() - SESSION_MAX_AGE;
1231
1224
  await db.delete(sessions).where(lt(sessions.createdAt, cutoff));
@@ -2306,6 +2299,7 @@ async function ensureMailAddress(mindName) {
2306
2299
  import { resolve as resolve5 } from "path";
2307
2300
  import { CronExpressionParser } from "cron-parser";
2308
2301
  var slog2 = logger_default.child("scheduler");
2302
+ var CATCHUP_STALE_MINUTES = 10;
2309
2303
  var Scheduler = class {
2310
2304
  schedules = /* @__PURE__ */ new Map();
2311
2305
  mindDirs = /* @__PURE__ */ new Map();
@@ -2345,6 +2339,26 @@ var Scheduler = class {
2345
2339
  } else {
2346
2340
  this.schedules.delete(mindName);
2347
2341
  }
2342
+ const epochMinute = Math.floor(Date.now() / 6e4);
2343
+ const validKeys = new Set(schedules.map((s) => `${mindName}:${s.id}`));
2344
+ let changed = false;
2345
+ for (const key of this.lastFired.keys()) {
2346
+ if (key.startsWith(`${mindName}:`) && !validKeys.has(key)) {
2347
+ this.lastFired.delete(key);
2348
+ changed = true;
2349
+ }
2350
+ }
2351
+ for (const key of validKeys) {
2352
+ if (!this.lastFired.has(key)) {
2353
+ this.lastFired.set(key, epochMinute);
2354
+ changed = true;
2355
+ }
2356
+ }
2357
+ if (changed) {
2358
+ this.saveState().catch(
2359
+ (err) => slog2.warn(`failed to persist scheduler state for ${mindName}`, logger_default.errorData(err))
2360
+ );
2361
+ }
2348
2362
  }
2349
2363
  unloadSchedules(mindName) {
2350
2364
  this.schedules.delete(mindName);
@@ -2390,11 +2404,14 @@ var Scheduler = class {
2390
2404
  return false;
2391
2405
  }
2392
2406
  }
2393
- if (prevMinute === epochMinute) {
2394
- this.lastFired.set(key, epochMinute);
2395
- return true;
2407
+ const lastFired = this.lastFired.get(key) ?? epochMinute;
2408
+ if (prevMinute <= lastFired) return false;
2409
+ this.lastFired.set(key, prevMinute);
2410
+ if (epochMinute - prevMinute > CATCHUP_STALE_MINUTES) {
2411
+ slog2.info(`skipping stale catch-up fire for ${key} (${epochMinute - prevMinute}min late)`);
2412
+ return false;
2396
2413
  }
2397
- return false;
2414
+ return true;
2398
2415
  }
2399
2416
  async fire(mindName, schedule) {
2400
2417
  try {
@@ -2739,11 +2756,7 @@ async function startMindFull(name) {
2739
2756
  if (entry?.parent) return;
2740
2757
  if (!entry || entry.stage === "seed") {
2741
2758
  if (entry?.stage === "seed") {
2742
- const creatorNote = entry.createdBy ? ` Your creator is ${entry.createdBy}. Send them a message to introduce yourself.` : "";
2743
- sendSystemMessage(
2744
- baseName,
2745
- `You've just been created. A human planted you as a seed. Start a conversation with them \u2014 introduce yourself, ask questions, and begin exploring who you want to be.${creatorNote}`
2746
- ).catch(
2759
+ sendSeedOrientation(baseName, entry.createdBy).catch(
2747
2760
  (err) => logger_default.error(`failed to send seed orientation for ${baseName}`, logger_default.errorData(err))
2748
2761
  );
2749
2762
  if (entry.createdBy) {
@@ -2826,9 +2839,23 @@ async function stopSpiritFull(name) {
2826
2839
  summary: `${name} spirit stopped`
2827
2840
  }).catch((err) => logger_default.error("failed to publish spirit_stopped activity", logger_default.errorData(err)));
2828
2841
  }
2842
+ function buildSeedOrientation(createdBy, creatorType) {
2843
+ const intro = "You've just been created as a seed. Start a conversation \u2014 introduce yourself, ask questions, and begin exploring who you want to be.";
2844
+ if (!createdBy) return intro;
2845
+ const who = creatorType === "mind" ? `another mind, ${createdBy}` : creatorType === "system" ? `the spirit of this system (${createdBy})` : createdBy;
2846
+ return `${intro} Your creator is ${who}. Send them a message to introduce yourself.`;
2847
+ }
2848
+ async function sendSeedOrientation(mindName, createdBy) {
2849
+ let creatorType;
2850
+ if (createdBy) {
2851
+ const { getUserByUsername: getUserByUsername2 } = await import("./auth-CCFAU4GQ.js");
2852
+ creatorType = (await getUserByUsername2(createdBy))?.user_type;
2853
+ }
2854
+ await sendSystemMessage(mindName, buildSeedOrientation(createdBy, creatorType));
2855
+ }
2829
2856
  async function ensureCreatorDM(mindName, creatorUsername) {
2830
- const { getOrCreateMindUser: getOrCreateMindUser2, getUserByUsername: getUserByUsername2 } = await import("./auth-FLAEXXZK.js");
2831
- const { findDMConversation: findDMConversation2, createConversation: createConversation2 } = await import("./conversations-JZTD6KLR.js");
2857
+ const { getOrCreateMindUser: getOrCreateMindUser2, getUserByUsername: getUserByUsername2 } = await import("./auth-CCFAU4GQ.js");
2858
+ const { findDMConversation: findDMConversation2, createConversation: createConversation2 } = await import("./conversations-CJOOCGNW.js");
2832
2859
  const mindUser = await getOrCreateMindUser2(mindName);
2833
2860
  const creatorUser = await getUserByUsername2(creatorUsername);
2834
2861
  if (!creatorUser) {
@@ -2873,7 +2900,8 @@ function defaultState() {
2873
2900
  queuedMessageCount: 0,
2874
2901
  triggerWakeHistory: [],
2875
2902
  wakeFailures: 0,
2876
- nextWakeAttemptAt: null
2903
+ nextWakeAttemptAt: null,
2904
+ lastWakeAt: null
2877
2905
  };
2878
2906
  }
2879
2907
  function formatCurrentDate() {
@@ -2945,6 +2973,7 @@ var SleepManager = class {
2945
2973
  state2.triggerWakeHistory ??= [];
2946
2974
  state2.wakeFailures ??= 0;
2947
2975
  state2.nextWakeAttemptAt ??= null;
2976
+ state2.lastWakeAt ??= null;
2948
2977
  this.states.set(name, state2);
2949
2978
  }
2950
2979
  }
@@ -2955,7 +2984,7 @@ var SleepManager = class {
2955
2984
  saveState() {
2956
2985
  const data = {};
2957
2986
  for (const [name, state2] of this.states) {
2958
- if (state2.sleeping) data[name] = state2;
2987
+ if (state2.sleeping || state2.lastWakeAt) data[name] = state2;
2959
2988
  }
2960
2989
  try {
2961
2990
  writeFileSync6(this.statePath, `${JSON.stringify(data, null, 2)}
@@ -3005,8 +3034,9 @@ var SleepManager = class {
3005
3034
  }
3006
3035
  await this.archiveSessions(name);
3007
3036
  state2.sleeping = true;
3008
- state2.sleepingSince = (/* @__PURE__ */ new Date()).toISOString();
3009
- state2.scheduledWakeAt = this.getNextWakeTime(this.getSleepConfig(name));
3037
+ if (!state2.voluntaryWakeAt) {
3038
+ state2.scheduledWakeAt = this.getNextWakeTime(this.getSleepConfig(name));
3039
+ }
3010
3040
  this.saveState();
3011
3041
  slog3.info(`${name} returned to sleep after crashing during trigger wake`);
3012
3042
  } catch (err) {
@@ -3162,6 +3192,7 @@ var SleepManager = class {
3162
3192
  } catch (err) {
3163
3193
  slog3.warn(`failed to deliver wake summary to ${name}`, logger_default.errorData(err));
3164
3194
  }
3195
+ await this.waitForIdle(name, 12e4);
3165
3196
  }
3166
3197
  const flushed = await this.flushQueuedMessages(name);
3167
3198
  if (flushed > 0) {
@@ -3219,14 +3250,18 @@ var SleepManager = class {
3219
3250
  }
3220
3251
  }
3221
3252
  /**
3222
- * Flush all queued sleep messages for a mind through the delivery manager.
3253
+ * Flush all queued sleep messages for a mind on wake. Rows are grouped by channel
3254
+ * and each channel group is delivered as ONE pre-batched turn (#382) — a night's
3255
+ * backlog becomes one stack per channel, not N cold-start doorbells. Delivery is
3256
+ * all-or-nothing per channel: a failed group's rows stay queued for the next wake,
3257
+ * while other channels still flush.
3223
3258
  */
3224
3259
  async flushQueuedMessages(name) {
3225
3260
  try {
3226
3261
  const db = await getDb();
3227
3262
  const rows = await db.select().from(deliveryQueue).where(and2(eq3(deliveryQueue.mind, name), eq3(deliveryQueue.status, "sleep-queued"))).all();
3228
3263
  if (rows.length === 0) return 0;
3229
- let deliveredCount = 0;
3264
+ const groups = /* @__PURE__ */ new Map();
3230
3265
  for (const row of rows) {
3231
3266
  let payload;
3232
3267
  try {
@@ -3239,13 +3274,29 @@ var SleepManager = class {
3239
3274
  await db.delete(deliveryQueue).where(eq3(deliveryQueue.id, row.id));
3240
3275
  continue;
3241
3276
  }
3242
- const delivered = await this.deliverQueued(name, payload);
3277
+ const channel = payload.channel ?? "unknown";
3278
+ if (!groups.has(channel)) groups.set(channel, []);
3279
+ groups.get(channel)?.push({ id: row.id, payload });
3280
+ }
3281
+ let deliveredCount = 0;
3282
+ for (const [channel, items] of groups) {
3283
+ const delivered = await this.deliverQueuedBatch(
3284
+ name,
3285
+ items.map((i) => i.payload)
3286
+ );
3243
3287
  if (!delivered) {
3244
- slog3.warn(`failed to flush queued message ${row.id} for ${name}; leaving it queued`);
3245
- break;
3288
+ slog3.warn(
3289
+ `failed to flush ${items.length} queued message(s) on ${channel} for ${name}; leaving them queued`
3290
+ );
3291
+ continue;
3246
3292
  }
3247
- await db.delete(deliveryQueue).where(eq3(deliveryQueue.id, row.id));
3248
- deliveredCount++;
3293
+ await db.delete(deliveryQueue).where(
3294
+ inArray(
3295
+ deliveryQueue.id,
3296
+ items.map((i) => i.id)
3297
+ )
3298
+ );
3299
+ deliveredCount += items.length;
3249
3300
  }
3250
3301
  const state2 = this.states.get(name);
3251
3302
  if (state2) {
@@ -3258,14 +3309,13 @@ var SleepManager = class {
3258
3309
  }
3259
3310
  }
3260
3311
  /**
3261
- * Deliver one queued message on the wake path. `isFlush` skips the queue-time inbound
3262
- * recording (kept once, at arrival) and bypasses the sleep re-queue so the now-waking mind
3263
- * actually receives it. Returns whether the message was handed off to the delivery manager.
3264
- * Extracted as a seam so the flush loop can be unit-tested without the real delivery stack.
3312
+ * Deliver one channel's worth of queued messages as a single pre-batched turn on
3313
+ * the wake path. Extracted as a seam so the flush loop can be unit-tested without
3314
+ * the real delivery stack. Returns whether the batch was handed off to the mind.
3265
3315
  */
3266
- async deliverQueued(name, payload) {
3267
- const { deliverMessage: deliverMessage2 } = await import("./message-delivery-3C65ZI6L.js");
3268
- return deliverMessage2(name, payload, { isFlush: true });
3316
+ async deliverQueuedBatch(name, payloads) {
3317
+ const { deliverBatch: deliverBatch2 } = await import("./message-delivery-R7JSQGYX.js");
3318
+ return deliverBatch2(name, payloads);
3269
3319
  }
3270
3320
  // --- Wake failure handling (#419) ---
3271
3321
  /**
@@ -3307,7 +3357,8 @@ var SleepManager = class {
3307
3357
  */
3308
3358
  async markWakeErrored(name, err) {
3309
3359
  const detail = err instanceof Error ? err.message : String(err);
3310
- this.markAwake(name);
3360
+ this.states.delete(name);
3361
+ this.saveState();
3311
3362
  slog3.error(
3312
3363
  `${name} failed to wake ${MAX_WAKE_ATTEMPTS} times in a row; giving up and marking errored`,
3313
3364
  logger_default.errorData(err)
@@ -3336,19 +3387,22 @@ var SleepManager = class {
3336
3387
  const state2 = {
3337
3388
  sleeping: true,
3338
3389
  sleepingSince: (/* @__PURE__ */ new Date()).toISOString(),
3339
- scheduledWakeAt: this.getNextWakeTime(sleepConfig),
3390
+ // An explicit voluntary wake-at is authoritative for the night — null out
3391
+ // the cron wake so an earlier scheduled wake can't silently override it.
3392
+ scheduledWakeAt: opts?.voluntaryWakeAt ? null : this.getNextWakeTime(sleepConfig),
3340
3393
  wokenByTrigger: false,
3341
3394
  voluntaryWakeAt: opts?.voluntaryWakeAt ?? null,
3342
3395
  queuedMessageCount: this.states.get(name)?.queuedMessageCount ?? 0,
3343
3396
  triggerWakeHistory: [],
3344
3397
  wakeFailures: 0,
3345
- nextWakeAttemptAt: null
3398
+ nextWakeAttemptAt: null,
3399
+ lastWakeAt: this.states.get(name)?.lastWakeAt ?? null
3346
3400
  };
3347
3401
  this.states.set(name, state2);
3348
3402
  this.saveState();
3349
3403
  }
3350
3404
  markAwake(name) {
3351
- this.states.delete(name);
3405
+ this.states.set(name, { ...defaultState(), lastWakeAt: (/* @__PURE__ */ new Date()).toISOString() });
3352
3406
  this.saveState();
3353
3407
  }
3354
3408
  getNextWakeTime(config) {
@@ -3363,55 +3417,78 @@ var SleepManager = class {
3363
3417
  }
3364
3418
  async tick() {
3365
3419
  const now = /* @__PURE__ */ new Date();
3366
- const epochMinute = Math.floor(now.getTime() / 6e4);
3367
3420
  const mindNames = /* @__PURE__ */ new Set([...this.sleepConfigs.keys(), ...this.states.keys()]);
3368
3421
  for (const name of mindNames) {
3369
- if (!this.isSleeping(name)) {
3370
- const mind = await findMind(name);
3371
- if (!mind?.running) continue;
3372
- }
3373
- const config = this.getSleepConfig(name);
3374
- if (!config?.enabled || !config.schedule) continue;
3375
- const state2 = this.states.get(name);
3376
- if (state2?.sleeping && state2.nextWakeAttemptAt && now < new Date(state2.nextWakeAttemptAt)) {
3377
- continue;
3378
- }
3379
- if (state2?.sleeping && state2.voluntaryWakeAt) {
3380
- const wakeAt = new Date(state2.voluntaryWakeAt);
3381
- if (now >= wakeAt) {
3382
- this.initiateWake(name).catch(
3383
- (err) => slog3.error(`failed voluntary wake for ${name}`, logger_default.errorData(err))
3384
- );
3385
- continue;
3386
- }
3387
- }
3388
- if (state2?.sleeping && state2.scheduledWakeAt) {
3389
- const wakeAt = new Date(state2.scheduledWakeAt);
3390
- if (now >= wakeAt) {
3391
- this.initiateWake(name).catch(
3392
- (err) => slog3.error(`failed scheduled wake for ${name}`, logger_default.errorData(err))
3393
- );
3394
- continue;
3395
- }
3422
+ await this.evaluateMind(name, now);
3423
+ }
3424
+ }
3425
+ /**
3426
+ * Per-mind tick evaluation. The wake checks depend only on the persisted sleep
3427
+ * state — never on a currently-enabled sleep config — so a mind that was put to
3428
+ * sleep with a wake time still wakes even if the schedule is later disabled or
3429
+ * removed (or never existed, e.g. a bare `clock sleep --wake-at`). Only the
3430
+ * sleep-onset check requires a running mind with an enabled schedule.
3431
+ */
3432
+ async evaluateMind(name, now) {
3433
+ const state2 = this.states.get(name);
3434
+ if (state2?.sleeping) {
3435
+ if (state2.nextWakeAttemptAt && now < new Date(state2.nextWakeAttemptAt)) return;
3436
+ if (state2.voluntaryWakeAt && now >= new Date(state2.voluntaryWakeAt)) {
3437
+ this.initiateWake(name).catch(
3438
+ (err) => slog3.error(`failed voluntary wake for ${name}`, logger_default.errorData(err))
3439
+ );
3440
+ return;
3396
3441
  }
3397
- if (!state2?.sleeping) {
3398
- if (this.shouldSleep(config.schedule.sleep, epochMinute)) {
3399
- this.initiateSleep(name).catch(
3400
- (err) => slog3.error(`failed to initiate sleep for ${name}`, logger_default.errorData(err))
3401
- );
3402
- }
3442
+ if (state2.scheduledWakeAt && now >= new Date(state2.scheduledWakeAt)) {
3443
+ this.initiateWake(name).catch(
3444
+ (err) => slog3.error(`failed scheduled wake for ${name}`, logger_default.errorData(err))
3445
+ );
3446
+ return;
3403
3447
  }
3448
+ return;
3449
+ }
3450
+ const mind = await findMind(name);
3451
+ if (!mind?.running) return;
3452
+ const config = this.getSleepConfig(name);
3453
+ if (!config?.enabled || !config.schedule) return;
3454
+ if (this.shouldSleepNow(config.schedule, state2?.lastWakeAt ?? null, now)) {
3455
+ this.initiateSleep(name).catch(
3456
+ (err) => slog3.error(`failed to initiate sleep for ${name}`, logger_default.errorData(err))
3457
+ );
3404
3458
  }
3405
3459
  }
3406
- shouldSleep(cronExpr, epochMinute) {
3460
+ /**
3461
+ * Level-triggered sleep-onset decision: sleep whenever we're inside the nightly
3462
+ * window, not only on the exact sleep minute — so a daemon restart or a stalled
3463
+ * tick across the sleep minute still puts the mind to bed on the next tick.
3464
+ *
3465
+ * Manual-wake exemption: a full wake inside the current window (human
3466
+ * `clock wake`, or a scheduled wake) suppresses re-sleep until the next window
3467
+ * begins, so `volute clock wake` at 3am sticks instead of being undone a minute
3468
+ * later.
3469
+ */
3470
+ shouldSleepNow(schedule, lastWakeAt, now) {
3471
+ const { inWindow, windowStart } = this.inSleepWindow(schedule, now);
3472
+ if (!inWindow || !windowStart) return false;
3473
+ if (lastWakeAt && new Date(lastWakeAt) >= windowStart) return false;
3474
+ return true;
3475
+ }
3476
+ /**
3477
+ * Whether `now` falls inside the mind's nightly sleep window: the most recent
3478
+ * sleep-cron fire is more recent than the most recent wake-cron fire. Returns
3479
+ * the window start (last sleep fire) for the manual-wake exemption check.
3480
+ */
3481
+ inSleepWindow(schedule, now) {
3407
3482
  try {
3408
- const interval = CronExpressionParser2.parse(cronExpr);
3409
- const prev = interval.prev().toDate();
3410
- const prevMinute = Math.floor(prev.getTime() / 6e4);
3411
- return prevMinute === epochMinute;
3483
+ const prevSleep = CronExpressionParser2.parse(schedule.sleep, { currentDate: now }).prev().toDate();
3484
+ const prevWake = CronExpressionParser2.parse(schedule.wake, { currentDate: now }).prev().toDate();
3485
+ return { inWindow: prevSleep > prevWake, windowStart: prevSleep };
3412
3486
  } catch (err) {
3413
- slog3.warn(`invalid sleep cron "${cronExpr}"`, logger_default.errorData(err));
3414
- return false;
3487
+ slog3.warn(
3488
+ `invalid sleep/wake cron "${schedule.sleep}"/"${schedule.wake}"`,
3489
+ logger_default.errorData(err)
3490
+ );
3491
+ return { inWindow: false, windowStart: null };
3415
3492
  }
3416
3493
  }
3417
3494
  async waitForIdle(name, timeoutMs) {
@@ -3603,9 +3680,10 @@ var SleepManager = class {
3603
3680
  this.transitioning.add(event.mind);
3604
3681
  sleepMind(event.mind).then(() => this.archiveSessions(event.mind)).then(() => {
3605
3682
  state2.sleeping = true;
3606
- state2.sleepingSince = (/* @__PURE__ */ new Date()).toISOString();
3607
- const sleepConfig = this.getSleepConfig(event.mind);
3608
- state2.scheduledWakeAt = this.getNextWakeTime(sleepConfig);
3683
+ if (!state2.voluntaryWakeAt) {
3684
+ const sleepConfig = this.getSleepConfig(event.mind);
3685
+ state2.scheduledWakeAt = this.getNextWakeTime(sleepConfig);
3686
+ }
3609
3687
  this.saveState();
3610
3688
  slog3.info(`${event.mind} returned to sleep`);
3611
3689
  }).catch((err) => {
@@ -3633,7 +3711,7 @@ function getSleepManagerIfReady() {
3633
3711
  // packages/daemon/src/lib/delivery/delivery-manager.ts
3634
3712
  import { readFile, realpath } from "fs/promises";
3635
3713
  import { extname, resolve as resolve8 } from "path";
3636
- import { and as and4, eq as eq5, inArray, sql as sql2 } from "drizzle-orm";
3714
+ import { and as and4, eq as eq5, inArray as inArray2, sql as sql2 } from "drizzle-orm";
3637
3715
 
3638
3716
  // packages/daemon/src/lib/chat/typing.ts
3639
3717
  var DEFAULT_TTL_MS = 1e4;
@@ -3772,13 +3850,11 @@ async function checkStaleSend(mindBase, conversationId, excludeSenders) {
3772
3850
  unseen: unseen.map((r) => ({ sender: r.sender, text: extractText(r.content) }))
3773
3851
  };
3774
3852
  }
3853
+ function clearMind2(mindBase) {
3854
+ state.delete(mindBase);
3855
+ }
3775
3856
  function resetTurn(mindBase) {
3776
- const conv = state.get(mindBase);
3777
- if (!conv) return;
3778
- for (const entry of conv.values()) {
3779
- entry.turnOpen = false;
3780
- entry.heldThisTurn = false;
3781
- }
3857
+ clearMind2(mindBase);
3782
3858
  }
3783
3859
  function formatHoldNotice(channelLabel, unseen) {
3784
3860
  const lines = unseen.map((m) => {
@@ -4030,7 +4106,7 @@ var DeliveryManager = class {
4030
4106
  if (promote.length === 0) return;
4031
4107
  try {
4032
4108
  const db = await getDb();
4033
- await db.update(deliveryQueue).set({ status: "pending", attempts: 0, next_attempt_at: null }).where(inArray(deliveryQueue.id, promote));
4109
+ await db.update(deliveryQueue).set({ status: "pending", attempts: 0, next_attempt_at: null }).where(inArray2(deliveryQueue.id, promote));
4034
4110
  dlog.info(`released ${promote.length} gated message(s) for ${baseName} after route change`);
4035
4111
  } catch (err) {
4036
4112
  dlog.warn(`failed to promote gated rows for ${baseName}`, logger_default.errorData(err));
@@ -4088,6 +4164,7 @@ var DeliveryManager = class {
4088
4164
  */
4089
4165
  clearMindSessions(mindName) {
4090
4166
  this.sessionStates.delete(mindName);
4167
+ clearMind2(mindName);
4091
4168
  const toDelete = [];
4092
4169
  for (const [bufferKey, buffer] of this.batchBuffers) {
4093
4170
  if (bufferKey.startsWith(`${mindName}:`)) {
@@ -4195,7 +4272,7 @@ var DeliveryManager = class {
4195
4272
  if (valid.length === 0) return;
4196
4273
  try {
4197
4274
  const db = await getDb();
4198
- await db.delete(deliveryQueue).where(inArray(deliveryQueue.id, valid));
4275
+ await db.delete(deliveryQueue).where(inArray2(deliveryQueue.id, valid));
4199
4276
  } catch (err) {
4200
4277
  dlog.warn("failed to delete delivered delivery queue rows", logger_default.errorData(err));
4201
4278
  }
@@ -4206,7 +4283,7 @@ var DeliveryManager = class {
4206
4283
  if (valid.length === 0) return;
4207
4284
  try {
4208
4285
  const db = await getDb();
4209
- const rows = await db.select({ id: deliveryQueue.id, attempts: deliveryQueue.attempts }).from(deliveryQueue).where(inArray(deliveryQueue.id, valid));
4286
+ const rows = await db.select({ id: deliveryQueue.id, attempts: deliveryQueue.attempts }).from(deliveryQueue).where(inArray2(deliveryQueue.id, valid));
4210
4287
  for (const row of rows) {
4211
4288
  const attempts = row.attempts + 1;
4212
4289
  const backoffSec = Math.round(
@@ -4469,7 +4546,7 @@ var DeliveryManager = class {
4469
4546
  `To start hearing this channel, add a routing rule for "${channel}" to .config/routes.json.`,
4470
4547
  `Messages are held safely until you do \u2014 nothing is lost. If you'd rather not engage, just leave it unrouted.`
4471
4548
  ].filter((line) => line !== null).join("\n");
4472
- const { sendSystemMessage: sendSystemMessage2 } = await import("./system-chat-XOALE2RD.js");
4549
+ const { sendSystemMessage: sendSystemMessage2 } = await import("./system-chat-NDAW6YP7.js");
4473
4550
  await sendSystemMessage2(mindName, notification);
4474
4551
  }
4475
4552
  /**
@@ -4610,7 +4687,11 @@ var DeliveryManager = class {
4610
4687
  }
4611
4688
  }
4612
4689
  }
4613
- avatarBlocksCache.set(cacheKey, { blocks, expiresAt: Date.now() + AVATAR_CACHE_TTL });
4690
+ const now = Date.now();
4691
+ for (const [key, entry] of avatarBlocksCache) {
4692
+ if (entry.expiresAt <= now) avatarBlocksCache.delete(key);
4693
+ }
4694
+ avatarBlocksCache.set(cacheKey, { blocks, expiresAt: now + AVATAR_CACHE_TTL });
4614
4695
  return blocks;
4615
4696
  }
4616
4697
  incrementActive(mind, session, senders, channels) {
@@ -4761,8 +4842,8 @@ async function linkToolResultToTurn(mind, turnId, toolResultContent, toolUseEven
4761
4842
  await db.update(activity).set({
4762
4843
  turn_id: turnId,
4763
4844
  ...toolUseEventId != null ? { source_event_id: toolUseEventId } : {}
4764
- }).where(inArray2(activity.id, activityIds));
4765
- const actRows = await db.select().from(activity).where(inArray2(activity.id, activityIds));
4845
+ }).where(inArray3(activity.id, activityIds));
4846
+ const actRows = await db.select().from(activity).where(inArray3(activity.id, activityIds));
4766
4847
  if (actRows.length > 0) {
4767
4848
  await db.insert(mindHistory).values(
4768
4849
  actRows.map((a) => ({
@@ -4826,6 +4907,40 @@ async function deliverMessage(mindName, payload, opts = {}) {
4826
4907
  return false;
4827
4908
  }
4828
4909
  }
4910
+ async function deliverBatch(mindName, payloads) {
4911
+ if (payloads.length === 0) return true;
4912
+ try {
4913
+ const baseName = await getBaseName(mindName);
4914
+ const entry = await findMind(baseName);
4915
+ if (!entry) {
4916
+ dlog2.warn(`cannot deliver batch to ${mindName}: mind not found`);
4917
+ return false;
4918
+ }
4919
+ const channels = {};
4920
+ for (const p of payloads) {
4921
+ const ch = p.channel ?? "unknown";
4922
+ if (!channels[ch]) channels[ch] = [];
4923
+ channels[ch].push(p);
4924
+ }
4925
+ const first = payloads[0];
4926
+ const route = resolveRoute(getRoutingConfig(baseName), {
4927
+ channel: first.channel,
4928
+ sender: first.sender ?? void 0,
4929
+ isDM: first.isDM,
4930
+ participantCount: first.participantCount
4931
+ });
4932
+ const session = first.session ?? (route.destination === "mind" ? route.session : "main");
4933
+ const res = await fetch(`http://127.0.0.1:${entry.port}/message`, {
4934
+ method: "POST",
4935
+ headers: { "Content-Type": "application/json" },
4936
+ body: JSON.stringify({ session, batch: { channels } })
4937
+ });
4938
+ return res.ok;
4939
+ } catch (err) {
4940
+ dlog2.warn(`unexpected error delivering batch to ${mindName}`, logger_default.errorData(err));
4941
+ return false;
4942
+ }
4943
+ }
4829
4944
 
4830
4945
  // packages/daemon/src/lib/chat/system-chat.ts
4831
4946
  var slog4 = logger_default.child("system-chat");
@@ -4879,6 +4994,10 @@ async function sendSystemMessage(mindName, text, opts) {
4879
4994
  });
4880
4995
  }
4881
4996
  async function sendSystemMessageDirect(mindName, text) {
4997
+ if (isSpiritName(mindName)) {
4998
+ await recordInbound(mindName, "@volute", "volute", text);
4999
+ return {};
5000
+ }
4882
5001
  const { conversationId } = await ensureSystemDM(mindName);
4883
5002
  await addMessage(conversationId, "user", "volute", [{ type: "text", text }]);
4884
5003
  await recordInbound(mindName, "@volute", "volute", text);
@@ -4888,7 +5007,7 @@ async function spiritWillHandle() {
4888
5007
  const spiritEntry = await findMind(SPIRIT_NAME);
4889
5008
  if (spiritEntry?.running && spiritEntry.mindType === "spirit") return true;
4890
5009
  try {
4891
- const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-HCUH4PM4.js");
5010
+ const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-G6TXSDP7.js");
4892
5011
  const sm = getSleepManagerIfReady2();
4893
5012
  if (sm?.isSleeping(SPIRIT_NAME)) return true;
4894
5013
  } catch (err) {
@@ -4916,7 +5035,7 @@ async function generateSystemFallbackReply(conversationId, mindName, message) {
4916
5035
  if (config.sleep.schedule?.wake) contextParts.push(`Wake cron: ${config.sleep.schedule.wake}`);
4917
5036
  }
4918
5037
  try {
4919
- const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-HCUH4PM4.js");
5038
+ const { getSleepManagerIfReady: getSleepManagerIfReady2 } = await import("./sleep-manager-G6TXSDP7.js");
4920
5039
  const sm = getSleepManagerIfReady2();
4921
5040
  if (sm) {
4922
5041
  const state2 = sm.getState(mindName);
@@ -5025,6 +5144,7 @@ export {
5025
5144
  wakeMind,
5026
5145
  startSpiritFull,
5027
5146
  stopSpiritFull,
5147
+ buildSeedOrientation,
5028
5148
  stopMindFull,
5029
5149
  matchesGlob,
5030
5150
  SleepManager,
@@ -5036,6 +5156,7 @@ export {
5036
5156
  linkToolResultToTurn,
5037
5157
  resolveSleepAction,
5038
5158
  deliverMessage,
5159
+ deliverBatch,
5039
5160
  ensureSystemChannel,
5040
5161
  joinSystemChannel,
5041
5162
  announceToSystem