volute 0.28.0 → 0.29.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 (44) hide show
  1. package/README.md +15 -5
  2. package/dist/api.d.ts +192 -9
  3. package/dist/{chat-M4SX42JD.js → chat-KTPOR2JT.js} +1 -1
  4. package/dist/chunk-A6TUJJ3L.js +19 -0
  5. package/dist/{chunk-AAPXKR5V.js → chunk-CMILSHZD.js} +138 -285
  6. package/dist/{chunk-K5NAC55T.js → chunk-CQ7SNKNI.js} +1 -1
  7. package/dist/{chunk-POSXWWTA.js → chunk-EHZKEMMV.js} +4 -4
  8. package/dist/{chunk-IAYBDWVG.js → chunk-FLZGS4QH.js} +145 -0
  9. package/dist/chunk-THUUIU3E.js +232 -0
  10. package/dist/cli.js +11 -8
  11. package/dist/clock-DGCBVGYA.js +259 -0
  12. package/dist/{cloud-sync-HDL6PHZI.js → cloud-sync-KILFGV5Q.js} +6 -5
  13. package/dist/connectors/discord-bridge.js +1 -1
  14. package/dist/connectors/slack-bridge.js +1 -1
  15. package/dist/connectors/telegram-bridge.js +1 -1
  16. package/dist/{conversations-M2K4253F.js → conversations-P5BL7RMX.js} +7 -1
  17. package/dist/create-DFCAGEE5.js +70 -0
  18. package/dist/{daemon-restart-G4B2OYAB.js → daemon-restart-UHOMICXT.js} +1 -1
  19. package/dist/daemon.js +181 -66
  20. package/dist/{message-delivery-HV3S6HZV.js → message-delivery-Q7VUMIEI.js} +10 -7
  21. package/dist/{mind-activity-tracker-EN6XNXPF.js → mind-activity-tracker-WRHFI3YW.js} +1 -1
  22. package/dist/{mind-manager-S6ILZVX3.js → mind-manager-P66HQDNE.js} +1 -1
  23. package/dist/{package-CG4RWUGP.js → package-OFKXNKJF.js} +1 -1
  24. package/dist/pages-watcher-P7QECRE2.js +21 -0
  25. package/dist/skills/dreaming/references/INSTALL.md +3 -17
  26. package/dist/skills/volute-mind/SKILL.md +43 -16
  27. package/dist/{sleep-manager-WMVG2VCL.js → sleep-manager-G4B5GW5P.js} +6 -5
  28. package/dist/{up-GM2JOH2Y.js → up-W6VAK2XE.js} +1 -1
  29. package/dist/{version-notify-JDUF4HQJ.js → version-notify-WDHRO3XD.js} +8 -7
  30. package/dist/web-assets/assets/index-BmKDnWDB.css +1 -0
  31. package/dist/web-assets/assets/index-CLJMx-GA.js +71 -0
  32. package/dist/web-assets/index.html +2 -2
  33. package/package.json +1 -1
  34. package/templates/_base/src/lib/logger.ts +10 -49
  35. package/templates/_base/src/lib/router.ts +1 -9
  36. package/templates/claude/src/lib/stream-consumer.ts +1 -4
  37. package/templates/pi/src/lib/event-handler.ts +1 -14
  38. package/dist/chunk-T6HKBWXZ.js +0 -23
  39. package/dist/create-D7J73A6H.js +0 -45
  40. package/dist/schedule-QTJMFATP.js +0 -154
  41. package/dist/web-assets/assets/index-BZGvToHi.css +0 -1
  42. package/dist/web-assets/assets/index-Cz4TrpzB.js +0 -75
  43. /package/dist/{chunk-SGVNFZHW.js → chunk-DUAUMCEE.js} +0 -0
  44. /package/dist/{pages-KJDJX4TA.js → pages-EUJR52AH.js} +0 -0
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ startWatcher,
4
+ stopWatcher
5
+ } from "./chunk-THUUIU3E.js";
2
6
  import {
3
7
  addMessage,
4
8
  createChannel,
@@ -6,10 +10,10 @@ import {
6
10
  getParticipants,
7
11
  joinChannel,
8
12
  publish as publish2
9
- } from "./chunk-IAYBDWVG.js";
13
+ } from "./chunk-FLZGS4QH.js";
10
14
  import {
11
15
  markIdle
12
- } from "./chunk-K5NAC55T.js";
16
+ } from "./chunk-CQ7SNKNI.js";
13
17
  import {
14
18
  broadcast,
15
19
  publish,
@@ -21,7 +25,7 @@ import {
21
25
  getPrompt,
22
26
  loadJsonMap,
23
27
  saveJsonMap
24
- } from "./chunk-POSXWWTA.js";
28
+ } from "./chunk-EHZKEMMV.js";
25
29
  import {
26
30
  logger_default
27
31
  } from "./chunk-YUIHSKR6.js";
@@ -46,15 +50,15 @@ import {
46
50
  // src/lib/daemon/sleep-manager.ts
47
51
  import { execFile, spawn as spawnChild } from "child_process";
48
52
  import {
49
- existsSync as existsSync6,
53
+ existsSync as existsSync5,
50
54
  mkdirSync as mkdirSync5,
51
- readdirSync as readdirSync2,
55
+ readdirSync,
52
56
  readFileSync as readFileSync6,
53
57
  readlinkSync,
54
58
  renameSync as renameSync2,
55
59
  writeFileSync as writeFileSync5
56
60
  } from "fs";
57
- import { resolve as resolve9 } from "path";
61
+ import { resolve as resolve8 } from "path";
58
62
  import { promisify } from "util";
59
63
  import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
60
64
  import { and as and3, eq as eq3, inArray as inArray2 } from "drizzle-orm";
@@ -225,214 +229,9 @@ async function migrateMindRoles() {
225
229
  await db.update(users).set({ role: "user" }).where(and(eq(users.user_type, "mind"), inArray(users.role, ["mind", "agent"])));
226
230
  }
227
231
 
228
- // src/lib/pages-watcher.ts
229
- import { existsSync as existsSync2, readdirSync, statSync, watch } from "fs";
230
- import { join, resolve as resolve2 } from "path";
231
- var watchers = /* @__PURE__ */ new Map();
232
- var homeWatchers = /* @__PURE__ */ new Map();
233
- var debounceTimers = /* @__PURE__ */ new Map();
234
- var sitesCache = null;
235
- var recentPagesCache = null;
236
- function startPagesWatcher(mindName, pagesDir) {
237
- try {
238
- const watcher = watch(pagesDir, { recursive: true }, (_eventType, filename) => {
239
- if (!filename || !filename.endsWith(".html")) return;
240
- const key = `${mindName}:${filename}`;
241
- const existing = debounceTimers.get(key);
242
- if (existing) clearTimeout(existing);
243
- debounceTimers.set(
244
- key,
245
- setTimeout(() => {
246
- debounceTimers.delete(key);
247
- invalidateCache();
248
- publish({
249
- type: "page_updated",
250
- mind: mindName,
251
- summary: `${mindName} updated ${filename}`,
252
- metadata: { file: filename }
253
- }).catch(
254
- (err) => logger_default.error("failed to publish page_updated activity", logger_default.errorData(err))
255
- );
256
- }, 100)
257
- );
258
- });
259
- watchers.set(mindName, watcher);
260
- } catch (err) {
261
- logger_default.warn(`failed to start pages watcher for ${mindName}`, logger_default.errorData(err));
262
- }
263
- }
264
- function startWatcher(mindName) {
265
- if (watchers.has(mindName)) return;
266
- const pagesDir = resolve2(mindDir(mindName), "home", "public", "pages");
267
- if (existsSync2(pagesDir)) {
268
- startPagesWatcher(mindName, pagesDir);
269
- return;
270
- }
271
- if (homeWatchers.has(mindName)) return;
272
- const publicDir = resolve2(mindDir(mindName), "home", "public");
273
- if (!existsSync2(publicDir)) return;
274
- try {
275
- const hw = watch(publicDir, (_eventType, filename) => {
276
- if (filename !== "pages") return;
277
- if (!existsSync2(pagesDir)) return;
278
- hw.close();
279
- homeWatchers.delete(mindName);
280
- invalidateCache();
281
- startPagesWatcher(mindName, pagesDir);
282
- });
283
- homeWatchers.set(mindName, hw);
284
- } catch (err) {
285
- logger_default.warn(`failed to start home watcher for ${mindName}`, logger_default.errorData(err));
286
- }
287
- }
288
- function stopWatcher(mindName) {
289
- const watcher = watchers.get(mindName);
290
- if (watcher) {
291
- watcher.close();
292
- watchers.delete(mindName);
293
- }
294
- const hw = homeWatchers.get(mindName);
295
- if (hw) {
296
- hw.close();
297
- homeWatchers.delete(mindName);
298
- }
299
- for (const [key, timer] of debounceTimers) {
300
- if (key.startsWith(`${mindName}:`)) {
301
- clearTimeout(timer);
302
- debounceTimers.delete(key);
303
- }
304
- }
305
- }
306
- function stopAllWatchers() {
307
- for (const [, watcher] of watchers) {
308
- watcher.close();
309
- }
310
- watchers.clear();
311
- for (const [, hw] of homeWatchers) {
312
- hw.close();
313
- }
314
- homeWatchers.clear();
315
- for (const [, timer] of debounceTimers) {
316
- clearTimeout(timer);
317
- }
318
- debounceTimers.clear();
319
- invalidateCache();
320
- }
321
- function invalidateCache() {
322
- sitesCache = null;
323
- recentPagesCache = null;
324
- }
325
- function scanPagesDir(dir, urlPrefix) {
326
- const pages = [];
327
- let items;
328
- try {
329
- items = readdirSync(dir);
330
- } catch {
331
- return pages;
332
- }
333
- for (const item of items) {
334
- if (item.startsWith(".")) continue;
335
- const fullPath = resolve2(dir, item);
336
- try {
337
- const s = statSync(fullPath);
338
- if (s.isFile() && item.endsWith(".html")) {
339
- pages.push({
340
- file: item,
341
- modified: s.mtime.toISOString(),
342
- url: `${urlPrefix}/${item}`
343
- });
344
- } else if (s.isDirectory()) {
345
- const indexPath = resolve2(fullPath, "index.html");
346
- if (existsSync2(indexPath)) {
347
- const indexStat = statSync(indexPath);
348
- pages.push({
349
- file: join(item, "index.html"),
350
- modified: indexStat.mtime.toISOString(),
351
- url: `${urlPrefix}/${item}/`
352
- });
353
- }
354
- }
355
- } catch {
356
- }
357
- }
358
- pages.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
359
- return pages;
360
- }
361
- async function buildSites() {
362
- const sites = [];
363
- const systemPagesDir = resolve2(voluteHome(), "shared", "pages");
364
- if (existsSync2(systemPagesDir)) {
365
- const systemPages = scanPagesDir(systemPagesDir, "/pages/_system");
366
- if (systemPages.length > 0) {
367
- sites.push({ name: "_system", label: "System", pages: systemPages });
368
- }
369
- }
370
- const entries = await readRegistry();
371
- for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
372
- const pagesDir = resolve2(mindDir(entry.name), "home", "public", "pages");
373
- if (!existsSync2(pagesDir)) continue;
374
- const mindPages = scanPagesDir(pagesDir, `/pages/${entry.name}`);
375
- if (mindPages.length > 0) {
376
- sites.push({ name: entry.name, label: entry.name, pages: mindPages });
377
- }
378
- }
379
- return sites;
380
- }
381
- async function buildRecentPages() {
382
- const entries = await readRegistry();
383
- const pages = [];
384
- for (const entry of entries) {
385
- const pagesDir = resolve2(mindDir(entry.name), "home", "public", "pages");
386
- if (!existsSync2(pagesDir)) continue;
387
- let items;
388
- try {
389
- items = readdirSync(pagesDir);
390
- } catch {
391
- continue;
392
- }
393
- for (const item of items) {
394
- if (item.startsWith(".")) continue;
395
- const fullPath = resolve2(pagesDir, item);
396
- try {
397
- const s = statSync(fullPath);
398
- if (s.isFile() && item.endsWith(".html")) {
399
- pages.push({
400
- mind: entry.name,
401
- file: item,
402
- modified: s.mtime.toISOString(),
403
- url: `/pages/${entry.name}/${item}`
404
- });
405
- } else if (s.isDirectory()) {
406
- const indexPath = resolve2(fullPath, "index.html");
407
- if (existsSync2(indexPath)) {
408
- const indexStat = statSync(indexPath);
409
- pages.push({
410
- mind: entry.name,
411
- file: join(item, "index.html"),
412
- modified: indexStat.mtime.toISOString(),
413
- url: `/pages/${entry.name}/${item}/`
414
- });
415
- }
416
- }
417
- } catch {
418
- }
419
- }
420
- }
421
- pages.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
422
- return pages.slice(0, 10);
423
- }
424
- async function getCachedSites() {
425
- if (!sitesCache) sitesCache = await buildSites();
426
- return sitesCache;
427
- }
428
- async function getCachedRecentPages() {
429
- if (!recentPagesCache) recentPagesCache = await buildRecentPages();
430
- return recentPagesCache;
431
- }
432
-
433
232
  // src/connectors/sdk.ts
434
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
435
- import { join as join2, resolve as resolve3 } from "path";
233
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
234
+ import { join, resolve as resolve2 } from "path";
436
235
  function splitMessage(text, maxLength) {
437
236
  const chunks = [];
438
237
  while (text.length > maxLength) {
@@ -445,8 +244,8 @@ function splitMessage(text, maxLength) {
445
244
  return chunks;
446
245
  }
447
246
  function readChannelMap(mindName) {
448
- const filePath = join2(stateDir(mindName), "channels.json");
449
- if (!existsSync3(filePath)) return {};
247
+ const filePath = join(stateDir(mindName), "channels.json");
248
+ if (!existsSync2(filePath)) return {};
450
249
  try {
451
250
  return JSON.parse(readFileSync2(filePath, "utf-8"));
452
251
  } catch (err) {
@@ -457,7 +256,7 @@ function readChannelMap(mindName) {
457
256
  function writeChannelEntry(mindName, slug, entry) {
458
257
  const dir = stateDir(mindName);
459
258
  mkdirSync2(dir, { recursive: true });
460
- const filePath = join2(dir, "channels.json");
259
+ const filePath = join(dir, "channels.json");
461
260
  const map = readChannelMap(mindName);
462
261
  map[slug] = entry;
463
262
  writeFileSync2(filePath, JSON.stringify(map, null, 2) + "\n");
@@ -501,7 +300,7 @@ function publish3(mind, event) {
501
300
 
502
301
  // src/lib/delivery/delivery-manager.ts
503
302
  import { readFile, realpath } from "fs/promises";
504
- import { extname, resolve as resolve5 } from "path";
303
+ import { extname, resolve as resolve4 } from "path";
505
304
  import { and as and2, eq as eq2, sql } from "drizzle-orm";
506
305
 
507
306
  // src/lib/typing.ts
@@ -595,8 +394,8 @@ function publishTypingForChannels(channels, map) {
595
394
  }
596
395
 
597
396
  // src/lib/delivery/delivery-router.ts
598
- import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
599
- import { resolve as resolve4 } from "path";
397
+ import { readFileSync as readFileSync3, statSync } from "fs";
398
+ import { resolve as resolve3 } from "path";
600
399
  function extractTextContent(content) {
601
400
  if (typeof content === "string") return content;
602
401
  if (Array.isArray(content)) {
@@ -609,7 +408,7 @@ var statCheckCache = /* @__PURE__ */ new Map();
609
408
  var STAT_TTL_MS = 5e3;
610
409
  var dlog = logger_default.child("delivery-router");
611
410
  function configPath(mindName) {
612
- return resolve4(mindDir(mindName), "home/.config/routes.json");
411
+ return resolve3(mindDir(mindName), "home/.config/routes.json");
613
412
  }
614
413
  function getRoutingConfig(mindName) {
615
414
  const path = configPath(mindName);
@@ -621,7 +420,7 @@ function getRoutingConfig(mindName) {
621
420
  }
622
421
  let mtime;
623
422
  try {
624
- mtime = statSync2(path).mtimeMs;
423
+ mtime = statSync(path).mtimeMs;
625
424
  } catch {
626
425
  configCache.delete(mindName);
627
426
  statCheckCache.delete(mindName);
@@ -1294,8 +1093,8 @@ var DeliveryManager = class {
1294
1093
  const dir = mindDir(p.username);
1295
1094
  const config = readVoluteConfig(dir);
1296
1095
  if (!config?.profile?.avatar) continue;
1297
- filePath = resolve5(dir, "home", config.profile.avatar);
1298
- const homeDir = resolve5(dir, "home");
1096
+ filePath = resolve4(dir, "home", config.profile.avatar);
1097
+ const homeDir = resolve4(dir, "home");
1299
1098
  if (!filePath.startsWith(`${homeDir}/`)) {
1300
1099
  dlog2.warn(`avatar path for ${p.username} escapes home directory, skipping`);
1301
1100
  continue;
@@ -1314,7 +1113,7 @@ var DeliveryManager = class {
1314
1113
  throw err;
1315
1114
  }
1316
1115
  } else {
1317
- filePath = resolve5(voluteHome(), "avatars", p.avatar);
1116
+ filePath = resolve4(voluteHome(), "avatars", p.avatar);
1318
1117
  }
1319
1118
  const ext = extname(filePath).toLowerCase();
1320
1119
  const mimeMap = {
@@ -1410,6 +1209,12 @@ async function recordInbound(mind, channel, sender, content) {
1410
1209
  content: content ?? void 0
1411
1210
  });
1412
1211
  }
1212
+ function resolveSleepAction(sleepBehavior, wokenByTrigger, wakeTriggerMatches) {
1213
+ if (sleepBehavior === "skip") return "skip";
1214
+ if (sleepBehavior === "trigger-wake" && !wokenByTrigger) return "queue-and-wake";
1215
+ if (!sleepBehavior && wakeTriggerMatches) return "queue-and-wake";
1216
+ return "queue";
1217
+ }
1413
1218
  async function deliverMessage(mindName, payload) {
1414
1219
  try {
1415
1220
  const baseName = await getBaseName(mindName);
@@ -1422,11 +1227,21 @@ async function deliverMessage(mindName, payload) {
1422
1227
  await recordInbound(baseName, payload.channel, payload.sender ?? null, textContent);
1423
1228
  const sleepManager = getSleepManagerIfReady();
1424
1229
  if (sleepManager?.isSleeping(baseName)) {
1425
- if (sleepManager.checkWakeTrigger(baseName, payload)) {
1426
- await sleepManager.queueSleepMessage(baseName, payload);
1230
+ const sleepState = sleepManager.getState(baseName);
1231
+ const action = resolveSleepAction(
1232
+ payload.whileSleeping,
1233
+ sleepState.wokenByTrigger,
1234
+ sleepManager.checkWakeTrigger(baseName, payload)
1235
+ );
1236
+ if (action === "skip") {
1237
+ dlog3.info(
1238
+ `skipped delivery to ${baseName} (sleeping, whileSleeping=skip, channel=${payload.channel})`
1239
+ );
1240
+ return;
1241
+ }
1242
+ await sleepManager.queueSleepMessage(baseName, payload);
1243
+ if (action === "queue-and-wake") {
1427
1244
  sleepManager.initiateWake(baseName, { trigger: { channel: payload.channel } }).catch((err) => dlog3.warn(`failed to trigger-wake ${baseName}`, logger_default.errorData(err)));
1428
- } else {
1429
- await sleepManager.queueSleepMessage(baseName, payload);
1430
1245
  }
1431
1246
  return;
1432
1247
  }
@@ -1472,7 +1287,7 @@ async function announceToSystem(text) {
1472
1287
  platformId: channelId,
1473
1288
  platform: "volute",
1474
1289
  name: SYSTEM_CHANNEL_NAME,
1475
- type: "group"
1290
+ type: "channel"
1476
1291
  });
1477
1292
  } catch (err) {
1478
1293
  logger_default.warn(`failed to write channel entry for ${mind.username}`, logger_default.errorData(err));
@@ -1493,27 +1308,27 @@ async function announceToSystem(text) {
1493
1308
 
1494
1309
  // src/lib/systems-config.ts
1495
1310
  import {
1496
- existsSync as existsSync4,
1311
+ existsSync as existsSync3,
1497
1312
  mkdirSync as mkdirSync3,
1498
1313
  readFileSync as readFileSync4,
1499
1314
  renameSync,
1500
1315
  unlinkSync,
1501
1316
  writeFileSync as writeFileSync3
1502
1317
  } from "fs";
1503
- import { resolve as resolve6 } from "path";
1318
+ import { resolve as resolve5 } from "path";
1504
1319
  var DEFAULT_API_URL = "https://volute.systems";
1505
1320
  function configPath2() {
1506
- return resolve6(voluteSystemDir(), "systems.json");
1321
+ return resolve5(voluteSystemDir(), "systems.json");
1507
1322
  }
1508
1323
  function migrateIfNeeded() {
1509
1324
  const target = configPath2();
1510
- if (existsSync4(target)) return;
1325
+ if (existsSync3(target)) return;
1511
1326
  const oldPaths = [
1512
- resolve6(voluteUserHome(), "systems.json"),
1513
- resolve6(voluteHome(), "systems.json")
1327
+ resolve5(voluteUserHome(), "systems.json"),
1328
+ resolve5(voluteHome(), "systems.json")
1514
1329
  ];
1515
1330
  for (const old of oldPaths) {
1516
- if (old !== target && existsSync4(old)) {
1331
+ if (old !== target && existsSync3(old)) {
1517
1332
  try {
1518
1333
  mkdirSync3(voluteSystemDir(), { recursive: true });
1519
1334
  renameSync(old, target);
@@ -1526,7 +1341,7 @@ function migrateIfNeeded() {
1526
1341
  function readSystemsConfig() {
1527
1342
  migrateIfNeeded();
1528
1343
  const path = configPath2();
1529
- if (!existsSync4(path)) return null;
1344
+ if (!existsSync3(path)) return null;
1530
1345
  const raw = readFileSync4(path, "utf-8");
1531
1346
  let data;
1532
1347
  try {
@@ -1793,7 +1608,7 @@ async function ensureMailAddress(mindName) {
1793
1608
  }
1794
1609
 
1795
1610
  // src/lib/daemon/scheduler.ts
1796
- import { resolve as resolve7 } from "path";
1611
+ import { resolve as resolve6 } from "path";
1797
1612
  import { CronExpressionParser } from "cron-parser";
1798
1613
  var slog = logger_default.child("scheduler");
1799
1614
  var Scheduler = class {
@@ -1802,7 +1617,7 @@ var Scheduler = class {
1802
1617
  lastFired = /* @__PURE__ */ new Map();
1803
1618
  // "mind:scheduleId" → epoch minute
1804
1619
  get statePath() {
1805
- return resolve7(voluteSystemDir(), "scheduler-state.json");
1620
+ return resolve6(voluteSystemDir(), "scheduler-state.json");
1806
1621
  }
1807
1622
  start() {
1808
1623
  this.loadState();
@@ -1853,6 +1668,15 @@ var Scheduler = class {
1853
1668
  shouldFire(schedule, epochMinute, mind, cronCache) {
1854
1669
  const key = `${mind}:${schedule.id}`;
1855
1670
  if (this.lastFired.get(key) === epochMinute) return false;
1671
+ if (schedule.fireAt) {
1672
+ const fireTime = Math.floor(new Date(schedule.fireAt).getTime() / 6e4);
1673
+ if (epochMinute >= fireTime) {
1674
+ this.lastFired.set(key, epochMinute);
1675
+ return true;
1676
+ }
1677
+ return false;
1678
+ }
1679
+ if (!schedule.cron) return false;
1856
1680
  let prevMinute = cronCache.get(schedule.cron);
1857
1681
  if (prevMinute === void 0) {
1858
1682
  try {
@@ -1872,22 +1696,10 @@ var Scheduler = class {
1872
1696
  return false;
1873
1697
  }
1874
1698
  async fire(mindName, schedule) {
1875
- const sleepManager = getSleepManagerIfReady();
1876
- const sleepState = sleepManager?.getState(mindName);
1877
- if (sleepState?.sleeping) {
1878
- if (schedule.skipWhenSleeping) {
1879
- slog.info(`skipped "${schedule.id}" for ${mindName} (sleeping)`);
1880
- return;
1881
- }
1882
- if (sleepState.wokenByTrigger) {
1883
- slog.info(`skipped "${schedule.id}" for ${mindName} (trigger-woken)`);
1884
- return;
1885
- }
1886
- }
1887
1699
  try {
1888
1700
  let text;
1889
1701
  if (schedule.script) {
1890
- const homeDir = resolve7(mindDir(mindName), "home");
1702
+ const homeDir = resolve6(mindDir(mindName), "home");
1891
1703
  try {
1892
1704
  const output = await this.runScript(schedule.script, homeDir, mindName);
1893
1705
  if (!output.trim()) {
@@ -1907,16 +1719,46 @@ ${stderr}` : ""}`;
1907
1719
  slog.warn(`schedule "${schedule.id}" for ${mindName} has no message or script`);
1908
1720
  return;
1909
1721
  }
1722
+ const whileSleeping = schedule.whileSleeping ?? (schedule.skipWhenSleeping ? "skip" : void 0);
1910
1723
  await this.deliver(mindName, {
1911
1724
  content: [{ type: "text", text }],
1912
1725
  channel: schedule.channel ?? "system:scheduler",
1913
- sender: schedule.id
1726
+ sender: schedule.id,
1727
+ whileSleeping
1914
1728
  });
1915
1729
  slog.info(`fired "${schedule.id}" for ${mindName}`);
1730
+ if (schedule.fireAt) {
1731
+ this.removeSchedule(mindName, schedule.id);
1732
+ }
1916
1733
  } catch (err) {
1917
1734
  slog.warn(`failed to fire "${schedule.id}" for ${mindName}`, logger_default.errorData(err));
1918
1735
  }
1919
1736
  }
1737
+ removeSchedule(mindName, scheduleId) {
1738
+ const memSchedules = this.schedules.get(mindName);
1739
+ if (memSchedules) {
1740
+ const filtered = memSchedules.filter((s) => s.id !== scheduleId);
1741
+ if (filtered.length > 0) {
1742
+ this.schedules.set(mindName, filtered);
1743
+ } else {
1744
+ this.schedules.delete(mindName);
1745
+ }
1746
+ }
1747
+ try {
1748
+ const dir = mindDir(mindName);
1749
+ const config = readVoluteConfig(dir);
1750
+ if (!config?.schedules) return;
1751
+ config.schedules = config.schedules.filter((s) => s.id !== scheduleId);
1752
+ if (config.schedules.length === 0) config.schedules = void 0;
1753
+ writeVoluteConfig(dir, config);
1754
+ slog.info(`removed one-time schedule "${scheduleId}" for ${mindName}`);
1755
+ } catch (err) {
1756
+ slog.error(
1757
+ `failed to persist removal of schedule "${scheduleId}" for ${mindName} (removed from memory)`,
1758
+ logger_default.errorData(err)
1759
+ );
1760
+ }
1761
+ }
1920
1762
  runScript(script, cwd, mindName) {
1921
1763
  return exec("bash", ["-c", script], { cwd, mindName });
1922
1764
  }
@@ -1936,8 +1778,8 @@ function getScheduler() {
1936
1778
  }
1937
1779
 
1938
1780
  // src/lib/daemon/token-budget.ts
1939
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
1940
- import { resolve as resolve8 } from "path";
1781
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
1782
+ import { resolve as resolve7 } from "path";
1941
1783
  var tlog = logger_default.child("token-budget");
1942
1784
  var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
1943
1785
  var MAX_QUEUE_SIZE = 100;
@@ -2055,7 +1897,7 @@ var TokenBudget = class {
2055
1897
  this.dirty.clear();
2056
1898
  }
2057
1899
  budgetStatePath(mind) {
2058
- return resolve8(stateDir(mind), "budget.json");
1900
+ return resolve7(stateDir(mind), "budget.json");
2059
1901
  }
2060
1902
  saveBudgetState(mind, state) {
2061
1903
  try {
@@ -2076,7 +1918,7 @@ var TokenBudget = class {
2076
1918
  loadBudgetState(mind) {
2077
1919
  try {
2078
1920
  const path = this.budgetStatePath(mind);
2079
- if (!existsSync5(path)) return null;
1921
+ if (!existsSync4(path)) return null;
2080
1922
  const data = JSON.parse(readFileSync5(path, "utf-8"));
2081
1923
  if (typeof data.periodStart !== "number" || typeof data.tokensUsed !== "number") return null;
2082
1924
  return {
@@ -2146,6 +1988,7 @@ async function startMindFull(name) {
2146
1988
  if (!entry || entry.stage === "seed") return;
2147
1989
  const dir = mindDir(baseName);
2148
1990
  getScheduler().loadSchedules(baseName);
1991
+ getSleepManagerIfReady()?.loadSleepConfig(baseName);
2149
1992
  ensureMailAddress(baseName).catch(
2150
1993
  (err) => logger_default.error(`failed to ensure mail address for ${baseName}`, logger_default.errorData(err))
2151
1994
  );
@@ -2238,8 +2081,9 @@ var SleepManager = class {
2238
2081
  interval = null;
2239
2082
  unsubActivity = null;
2240
2083
  transitioning = /* @__PURE__ */ new Set();
2084
+ sleepConfigs = /* @__PURE__ */ new Map();
2241
2085
  get statePath() {
2242
- return resolve9(voluteSystemDir(), "sleep-state.json");
2086
+ return resolve8(voluteSystemDir(), "sleep-state.json");
2243
2087
  }
2244
2088
  start() {
2245
2089
  this.loadState();
@@ -2255,7 +2099,7 @@ var SleepManager = class {
2255
2099
  // --- State persistence ---
2256
2100
  loadState() {
2257
2101
  try {
2258
- if (existsSync6(this.statePath)) {
2102
+ if (existsSync5(this.statePath)) {
2259
2103
  const data = JSON.parse(readFileSync6(this.statePath, "utf-8"));
2260
2104
  for (const [name, state] of Object.entries(data)) {
2261
2105
  state.triggerWakeHistory ??= [];
@@ -2299,9 +2143,21 @@ var SleepManager = class {
2299
2143
  slog2.info(`${name} trigger-wake converted to full wake`);
2300
2144
  }
2301
2145
  getSleepConfig(name) {
2146
+ if (this.sleepConfigs.has(name)) {
2147
+ return this.sleepConfigs.get(name) ?? null;
2148
+ }
2149
+ const config = this.loadSleepConfig(name);
2150
+ return config;
2151
+ }
2152
+ loadSleepConfig(name) {
2302
2153
  const dir = mindDir(name);
2303
2154
  const config = readVoluteConfig(dir);
2304
- return config?.sleep ?? null;
2155
+ const sleepConfig = config?.sleep ?? null;
2156
+ this.sleepConfigs.set(name, sleepConfig);
2157
+ return sleepConfig;
2158
+ }
2159
+ invalidateSleepConfig(name) {
2160
+ this.sleepConfigs.delete(name);
2305
2161
  }
2306
2162
  /**
2307
2163
  * Put a mind to sleep. Sends pre-sleep message, waits for completion,
@@ -2321,8 +2177,7 @@ var SleepManager = class {
2321
2177
  if (!entry) return;
2322
2178
  const sleepConfig = this.getSleepConfig(name);
2323
2179
  const wakeTime = opts?.voluntaryWakeAt ?? this.getNextWakeTime(sleepConfig) ?? "scheduled time";
2324
- const queuedInfo = "";
2325
- const preSleepMsg = await getPrompt("pre_sleep", { wakeTime, queuedInfo });
2180
+ const preSleepMsg = await getPrompt("pre_sleep", { wakeTime });
2326
2181
  try {
2327
2182
  const db = await getDb();
2328
2183
  await db.insert(mindHistory).values({
@@ -2492,7 +2347,7 @@ var SleepManager = class {
2492
2347
  const db = await getDb();
2493
2348
  const rows = await db.select().from(deliveryQueue).where(and3(eq3(deliveryQueue.mind, name), eq3(deliveryQueue.status, "sleep-queued"))).all();
2494
2349
  if (rows.length === 0) return 0;
2495
- const { deliverMessage: deliverMessage2 } = await import("./message-delivery-HV3S6HZV.js");
2350
+ const { deliverMessage: deliverMessage2 } = await import("./message-delivery-Q7VUMIEI.js");
2496
2351
  const delivered = [];
2497
2352
  for (const row of rows) {
2498
2353
  try {
@@ -2592,17 +2447,17 @@ var SleepManager = class {
2592
2447
  }
2593
2448
  }
2594
2449
  async waitForIdle(name, timeoutMs) {
2595
- return new Promise((resolve10) => {
2450
+ return new Promise((resolve9) => {
2596
2451
  const timeout = setTimeout(() => {
2597
2452
  unsub();
2598
- resolve10();
2453
+ resolve9();
2599
2454
  }, timeoutMs);
2600
2455
  const unsub = subscribe((event) => {
2601
2456
  if (event.mind !== name) return;
2602
2457
  if (event.type === "mind_done" || event.type === "mind_idle") {
2603
2458
  clearTimeout(timeout);
2604
2459
  unsub();
2605
- resolve10();
2460
+ resolve9();
2606
2461
  }
2607
2462
  });
2608
2463
  });
@@ -2610,15 +2465,15 @@ var SleepManager = class {
2610
2465
  async archiveSessions(name) {
2611
2466
  const dir = mindDir(name);
2612
2467
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 16);
2613
- const sessionsDir = resolve9(dir, ".mind", "sessions");
2614
- if (existsSync6(sessionsDir)) {
2615
- const archiveDir = resolve9(sessionsDir, "archive");
2468
+ const sessionsDir = resolve8(dir, ".mind", "sessions");
2469
+ if (existsSync5(sessionsDir)) {
2470
+ const archiveDir = resolve8(sessionsDir, "archive");
2616
2471
  mkdirSync5(archiveDir, { recursive: true });
2617
- for (const file of readdirSync2(sessionsDir)) {
2472
+ for (const file of readdirSync(sessionsDir)) {
2618
2473
  if (file === "archive" || !file.endsWith(".json")) continue;
2619
- const src = resolve9(sessionsDir, file);
2474
+ const src = resolve8(sessionsDir, file);
2620
2475
  const base = file.replace(/\.json$/, "");
2621
- const dest = resolve9(archiveDir, `${base}-${timestamp}.json`);
2476
+ const dest = resolve8(archiveDir, `${base}-${timestamp}.json`);
2622
2477
  try {
2623
2478
  renameSync2(src, dest);
2624
2479
  } catch (err) {
@@ -2626,14 +2481,14 @@ var SleepManager = class {
2626
2481
  }
2627
2482
  }
2628
2483
  }
2629
- const piSessionsDir = resolve9(dir, ".mind", "pi-sessions");
2630
- if (existsSync6(piSessionsDir)) {
2631
- const archiveDir = resolve9(piSessionsDir, "archive");
2484
+ const piSessionsDir = resolve8(dir, ".mind", "pi-sessions");
2485
+ if (existsSync5(piSessionsDir)) {
2486
+ const archiveDir = resolve8(piSessionsDir, "archive");
2632
2487
  mkdirSync5(archiveDir, { recursive: true });
2633
- for (const entry of readdirSync2(piSessionsDir, { withFileTypes: true })) {
2488
+ for (const entry of readdirSync(piSessionsDir, { withFileTypes: true })) {
2634
2489
  if (entry.name === "archive" || !entry.isDirectory()) continue;
2635
- const src = resolve9(piSessionsDir, entry.name);
2636
- const dest = resolve9(archiveDir, `${entry.name}-${timestamp}`);
2490
+ const src = resolve8(piSessionsDir, entry.name);
2491
+ const dest = resolve8(archiveDir, `${entry.name}-${timestamp}`);
2637
2492
  try {
2638
2493
  renameSync2(src, dest);
2639
2494
  } catch (err) {
@@ -2643,8 +2498,8 @@ var SleepManager = class {
2643
2498
  }
2644
2499
  }
2645
2500
  async runWakeContextScript(name, sleepingSince, duration) {
2646
- const scriptPath = resolve9(mindDir(name), "home", ".config", "hooks", "wake-context.sh");
2647
- if (!existsSync6(scriptPath)) return "";
2501
+ const scriptPath = resolve8(mindDir(name), "home", ".config", "hooks", "wake-context.sh");
2502
+ if (!existsSync5(scriptPath)) return "";
2648
2503
  const input = JSON.stringify({
2649
2504
  sleepingSince,
2650
2505
  duration,
@@ -2748,9 +2603,9 @@ var SleepManager = class {
2748
2603
  if (fields[3] !== "0A") continue;
2749
2604
  const inode = parseInt(fields[9], 10);
2750
2605
  if (!inode) continue;
2751
- for (const pidDir of readdirSync2("/proc").filter((f) => /^\d+$/.test(f))) {
2606
+ for (const pidDir of readdirSync("/proc").filter((f) => /^\d+$/.test(f))) {
2752
2607
  try {
2753
- const fds = readdirSync2(`/proc/${pidDir}/fd`);
2608
+ const fds = readdirSync(`/proc/${pidDir}/fd`);
2754
2609
  for (const fd of fds) {
2755
2610
  try {
2756
2611
  const link = readlinkSync(`/proc/${pidDir}/fd/${fd}`);
@@ -2826,9 +2681,6 @@ export {
2826
2681
  migrateMindRoles,
2827
2682
  readVoluteConfig,
2828
2683
  writeVoluteConfig,
2829
- stopAllWatchers,
2830
- getCachedSites,
2831
- getCachedRecentPages,
2832
2684
  splitMessage,
2833
2685
  writeChannelEntry,
2834
2686
  resolveChannelId,
@@ -2854,6 +2706,7 @@ export {
2854
2706
  initDeliveryManager,
2855
2707
  getDeliveryManager,
2856
2708
  recordInbound,
2709
+ resolveSleepAction,
2857
2710
  deliverMessage,
2858
2711
  readSystemsConfig,
2859
2712
  writeSystemsConfig,