volute 0.7.0 → 0.8.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 (77) hide show
  1. package/README.md +16 -14
  2. package/dist/{agent-7JF7MT73.js → agent-YORVRB6I.js} +10 -10
  3. package/dist/{agent-manager-IMZ7ZMBF.js → agent-manager-CMMH5KQQ.js} +4 -4
  4. package/dist/{channel-SMCNOIVQ.js → channel-RDGHBFSI.js} +16 -56
  5. package/dist/{chunk-JR4UXCTO.js → chunk-23L3MKEV.js} +1 -1
  6. package/dist/{chunk-5SKQ6J7T.js → chunk-5C5JWR2L.js} +15 -7
  7. package/dist/{chunk-UWHWAPGO.js → chunk-DP2DX4WV.js} +9 -1
  8. package/dist/{chunk-7ACDT3P2.js → chunk-ECPQXRLB.js} +1 -2
  9. package/dist/{chunk-LLJNZPCU.js → chunk-HZ5LTOEJ.js} +1 -1
  10. package/dist/{chunk-W76KWE23.js → chunk-IQXBMFZG.js} +6 -4
  11. package/dist/{chunk-ZZOOTYXK.js → chunk-LIPPXNIE.js} +60 -74
  12. package/dist/{chunk-BX7KI4S3.js → chunk-N6MLQ26B.js} +23 -96
  13. package/dist/{chunk-H7AMDUIA.js → chunk-QF22MYDJ.js} +6 -5
  14. package/dist/{chunk-NKXULRSW.js → chunk-RT6Y7AR3.js} +1 -1
  15. package/dist/{chunk-62X577Y7.js → chunk-W6TMWYU3.js} +126 -73
  16. package/dist/{chunk-EG45HBSJ.js → chunk-XSJ27WEM.js} +1 -1
  17. package/dist/cli.js +22 -20
  18. package/dist/{connector-Y7JPNROO.js → connector-ZP6MEFF4.js} +3 -3
  19. package/dist/connectors/discord.js +18 -59
  20. package/dist/connectors/slack.js +21 -38
  21. package/dist/connectors/telegram.js +31 -49
  22. package/dist/{create-G525LWEA.js → create-HGJHLABX.js} +22 -17
  23. package/dist/{daemon-client-442IV43D.js → daemon-client-54J3EIZD.js} +2 -2
  24. package/dist/{daemon-restart-4HVEKYFY.js → daemon-restart-CPBLMMRI.js} +3 -3
  25. package/dist/daemon.js +342 -402
  26. package/dist/{delete-UOU4AFQN.js → delete-45TGQC4N.js} +10 -5
  27. package/dist/{down-AZVH5TCD.js → down-O4EWZTVA.js} +2 -2
  28. package/dist/{env-7GLUJCWS.js → env-KMNYGVZ2.js} +7 -9
  29. package/dist/{history-H72ZUIBN.js → history-PXJVYLVY.js} +2 -2
  30. package/dist/{import-AVKQJDYC.js → import-CNEDF3TD.js} +6 -6
  31. package/dist/{logs-EDGK26AK.js → logs-TZB3MTLZ.js} +5 -4
  32. package/dist/{package-T2WAVJOU.js → package-RJSONENE.js} +1 -1
  33. package/dist/{restart-O4ETYLJF.js → restart-KVH3TK5N.js} +2 -2
  34. package/dist/{schedule-S6QVC5ON.js → schedule-HCUCBNQI.js} +2 -2
  35. package/dist/send-BNC2S5BY.js +162 -0
  36. package/dist/{service-HZNIDNJF.js → service-XCADRKIS.js} +8 -1
  37. package/dist/{setup-F4TCWVSP.js → setup-32KH5KLN.js} +85 -26
  38. package/dist/{start-VHQ7LNWM.js → start-QU73YTJW.js} +2 -2
  39. package/dist/{status-QAJWXKMZ.js → status-Q6ZQJXNI.js} +2 -2
  40. package/dist/{stop-CAGCT5NI.js → stop-N7U5N6A7.js} +2 -2
  41. package/dist/{up-RWZF6MLT.js → up-V6EAA7OZ.js} +2 -2
  42. package/dist/{update-F7QWV2LB.js → update-EUCZ7XGG.js} +3 -3
  43. package/dist/{update-check-B4J6IEQ4.js → update-check-SM4244SU.js} +2 -2
  44. package/dist/{upgrade-YXKPWDRU.js → upgrade-CZF6PN7Y.js} +4 -4
  45. package/dist/{variant-4Z6W3PP6.js → variant-RKXPN5DH.js} +20 -46
  46. package/dist/web-assets/assets/index-D-3zx6vs.js +307 -0
  47. package/dist/web-assets/index.html +1 -1
  48. package/drizzle/0004_magical_silverclaw.sql +1 -0
  49. package/drizzle/meta/0004_snapshot.json +410 -0
  50. package/drizzle/meta/_journal.json +7 -0
  51. package/package.json +1 -1
  52. package/templates/_base/_skills/volute-agent/SKILL.md +32 -16
  53. package/templates/_base/home/.config/routes.json +4 -8
  54. package/templates/_base/home/VOLUTE.md +16 -14
  55. package/templates/_base/src/lib/auto-reply.ts +38 -0
  56. package/templates/_base/src/lib/daemon-client.ts +53 -0
  57. package/templates/_base/src/lib/router.ts +66 -14
  58. package/templates/_base/src/lib/routing.ts +48 -9
  59. package/templates/_base/src/lib/startup.ts +1 -25
  60. package/templates/_base/src/lib/types.ts +2 -1
  61. package/templates/_base/src/lib/volute-server.ts +29 -14
  62. package/templates/agent-sdk/src/agent.ts +53 -111
  63. package/templates/agent-sdk/src/lib/content.ts +41 -0
  64. package/templates/agent-sdk/src/lib/session-store.ts +43 -0
  65. package/templates/agent-sdk/src/lib/stream-consumer.ts +66 -0
  66. package/templates/agent-sdk/src/server.ts +5 -13
  67. package/templates/pi/.init/AGENTS.md +5 -5
  68. package/templates/pi/src/agent.ts +32 -84
  69. package/templates/pi/src/lib/content.ts +15 -0
  70. package/templates/pi/src/lib/event-handler.ts +74 -0
  71. package/templates/pi/src/lib/resolve-model.ts +21 -0
  72. package/templates/pi/src/server.ts +3 -7
  73. package/dist/chunk-B3R6L2GW.js +0 -24
  74. package/dist/chunk-ZYGKG6VC.js +0 -22
  75. package/dist/message-SCOQDR3P.js +0 -32
  76. package/dist/send-G7PE4DOJ.js +0 -72
  77. package/dist/web-assets/assets/index-B1CqjUYD.js +0 -308
package/dist/daemon.js CHANGED
@@ -6,18 +6,15 @@ import {
6
6
  initAgentManager,
7
7
  loadJsonMap,
8
8
  saveJsonMap
9
- } from "./chunk-62X577Y7.js";
9
+ } from "./chunk-W6TMWYU3.js";
10
10
  import {
11
11
  checkForUpdate,
12
12
  checkForUpdateCached,
13
13
  getCurrentVersion
14
- } from "./chunk-NKXULRSW.js";
15
- import {
16
- collectPart
17
- } from "./chunk-B3R6L2GW.js";
14
+ } from "./chunk-RT6Y7AR3.js";
18
15
  import {
19
16
  CHANNELS
20
- } from "./chunk-ZZOOTYXK.js";
17
+ } from "./chunk-LIPPXNIE.js";
21
18
  import {
22
19
  agentMessages,
23
20
  approveUser,
@@ -36,24 +33,24 @@ import {
36
33
  sessions,
37
34
  users,
38
35
  verifyUser
39
- } from "./chunk-7ACDT3P2.js";
36
+ } from "./chunk-ECPQXRLB.js";
40
37
  import {
41
38
  readVoluteConfig,
42
39
  writeVoluteConfig
43
40
  } from "./chunk-NETNFBA5.js";
44
41
  import {
45
42
  loadMergedEnv
46
- } from "./chunk-H7AMDUIA.js";
43
+ } from "./chunk-QF22MYDJ.js";
47
44
  import {
48
45
  slugify,
49
46
  writeChannelEntry
50
- } from "./chunk-BX7KI4S3.js";
47
+ } from "./chunk-N6MLQ26B.js";
51
48
  import {
52
49
  applyIsolation
53
- } from "./chunk-W76KWE23.js";
50
+ } from "./chunk-IQXBMFZG.js";
54
51
  import {
55
52
  resolveVoluteBin
56
- } from "./chunk-5SKQ6J7T.js";
53
+ } from "./chunk-5C5JWR2L.js";
57
54
  import {
58
55
  agentDir,
59
56
  checkHealth,
@@ -67,15 +64,17 @@ import {
67
64
  removeAllVariants,
68
65
  setAgentRunning,
69
66
  setVariantRunning,
67
+ stateDir,
68
+ validateBranchName,
70
69
  voluteHome
71
- } from "./chunk-UWHWAPGO.js";
70
+ } from "./chunk-DP2DX4WV.js";
72
71
  import "./chunk-K3NQKI34.js";
73
72
 
74
73
  // src/daemon.ts
75
74
  import { randomBytes } from "crypto";
76
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
75
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
77
76
  import { homedir } from "os";
78
- import { resolve as resolve9 } from "path";
77
+ import { resolve as resolve10 } from "path";
79
78
  import { format } from "util";
80
79
 
81
80
  // src/lib/connector-manager.ts
@@ -186,13 +185,13 @@ var ConnectorManager = class {
186
185
  }
187
186
  }
188
187
  }
189
- checkConnectorEnv(type, agentDir2) {
188
+ checkConnectorEnv(type, agentName, agentDir2) {
190
189
  const agentConnectorDir = resolve2(agentDir2, "connectors", type);
191
190
  const userConnectorDir = resolve2(voluteHome(), "connectors", type);
192
191
  const connectorDir = existsSync2(agentConnectorDir) ? agentConnectorDir : existsSync2(userConnectorDir) ? userConnectorDir : void 0;
193
192
  const def = getConnectorDef(type, connectorDir);
194
193
  if (!def) return null;
195
- const env = loadMergedEnv(agentDir2);
194
+ const env = loadMergedEnv(agentName);
196
195
  const missing = checkMissingEnvVars(def, env);
197
196
  if (missing.length === 0) return null;
198
197
  return {
@@ -220,7 +219,7 @@ var ConnectorManager = class {
220
219
  });
221
220
  this.connectors.get(agentName)?.delete(type);
222
221
  }
223
- this.killOrphanConnector(agentDir2, type);
222
+ this.killOrphanConnector(agentName, type);
224
223
  const agentConnector = resolve2(agentDir2, "connectors", type, "index.ts");
225
224
  const userConnector = resolve2(voluteHome(), "connectors", type, "index.ts");
226
225
  const builtinConnector = this.resolveBuiltinConnector(type);
@@ -238,10 +237,10 @@ var ConnectorManager = class {
238
237
  } else {
239
238
  throw new Error(`No connector code found for type: ${type}`);
240
239
  }
241
- const logsDir = resolve2(agentDir2, ".volute", "logs");
240
+ const logsDir = resolve2(stateDir(agentName), "logs");
242
241
  mkdirSync(logsDir, { recursive: true });
243
242
  const logStream = new RotatingLog(resolve2(logsDir, `${type}.log`));
244
- const agentEnv = loadMergedEnv(agentDir2);
243
+ const agentEnv = loadMergedEnv(agentName);
245
244
  const prefix = `${type.toUpperCase()}_`;
246
245
  const connectorEnv = Object.fromEntries(
247
246
  Object.entries(agentEnv).filter(([k]) => k.startsWith(prefix))
@@ -269,7 +268,7 @@ var ConnectorManager = class {
269
268
  lastStderr = chunk.toString().trim();
270
269
  });
271
270
  if (child.pid) {
272
- this.saveConnectorPid(agentDir2, type, child.pid);
271
+ this.saveConnectorPid(agentName, type, child.pid);
273
272
  }
274
273
  if (!this.connectors.has(agentName)) {
275
274
  this.connectors.set(agentName, /* @__PURE__ */ new Map());
@@ -315,26 +314,27 @@ var ConnectorManager = class {
315
314
  const stopKey = `${agentName}:${type}`;
316
315
  this.stopping.add(stopKey);
317
316
  agentMap.delete(type);
318
- await new Promise((resolve10) => {
319
- tracked.child.on("exit", () => resolve10());
317
+ await new Promise((resolve11) => {
318
+ tracked.child.on("exit", () => resolve11());
320
319
  try {
321
320
  tracked.child.kill("SIGTERM");
322
321
  } catch {
323
- resolve10();
322
+ resolve11();
324
323
  }
325
324
  setTimeout(() => {
326
325
  try {
327
326
  tracked.child.kill("SIGKILL");
328
327
  } catch {
329
328
  }
330
- resolve10();
329
+ resolve11();
331
330
  }, 5e3);
332
331
  });
333
332
  this.stopping.delete(stopKey);
334
333
  this.restartAttempts.delete(stopKey);
335
334
  try {
336
- this.removeConnectorPid(agentDir(agentName), type);
337
- } catch {
335
+ this.removeConnectorPid(agentName, type);
336
+ } catch (err) {
337
+ console.error(`[daemon] failed to remove PID file for ${type}/${agentName}:`, err);
338
338
  }
339
339
  console.error(`[daemon] stopped connector ${type} for ${agentName}`);
340
340
  }
@@ -358,22 +358,22 @@ var ConnectorManager = class {
358
358
  running: !tracked.child.killed
359
359
  }));
360
360
  }
361
- connectorPidPath(agentDir2, type) {
362
- return resolve2(agentDir2, ".volute", "connectors", `${type}.pid`);
361
+ connectorPidPath(agentName, type) {
362
+ return resolve2(stateDir(agentName), "connectors", `${type}.pid`);
363
363
  }
364
- saveConnectorPid(agentDir2, type, pid) {
365
- const pidPath = this.connectorPidPath(agentDir2, type);
364
+ saveConnectorPid(agentName, type, pid) {
365
+ const pidPath = this.connectorPidPath(agentName, type);
366
366
  mkdirSync(dirname(pidPath), { recursive: true });
367
367
  writeFileSync(pidPath, String(pid));
368
368
  }
369
- removeConnectorPid(agentDir2, type) {
369
+ removeConnectorPid(agentName, type) {
370
370
  try {
371
- unlinkSync(this.connectorPidPath(agentDir2, type));
371
+ unlinkSync(this.connectorPidPath(agentName, type));
372
372
  } catch {
373
373
  }
374
374
  }
375
- killOrphanConnector(agentDir2, type) {
376
- const pidPath = this.connectorPidPath(agentDir2, type);
375
+ killOrphanConnector(agentName, type) {
376
+ const pidPath = this.connectorPidPath(agentName, type);
377
377
  if (!existsSync2(pidPath)) return;
378
378
  try {
379
379
  const pid = parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
@@ -406,8 +406,37 @@ function getConnectorManager() {
406
406
  return instance;
407
407
  }
408
408
 
409
- // src/lib/scheduler.ts
409
+ // src/lib/migrate-state.ts
410
+ import { copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync } from "fs";
410
411
  import { resolve as resolve3 } from "path";
412
+ function migrateAgentState(name) {
413
+ const src = resolve3(agentDir(name), ".volute");
414
+ if (!existsSync3(src)) return;
415
+ const dest = stateDir(name);
416
+ mkdirSync2(dest, { recursive: true });
417
+ for (const file of ["env.json", "channels.json"]) {
418
+ const srcPath = resolve3(src, file);
419
+ const destPath = resolve3(dest, file);
420
+ if (existsSync3(srcPath) && !existsSync3(destPath)) {
421
+ copyFileSync(srcPath, destPath);
422
+ }
423
+ }
424
+ const srcLogs = resolve3(src, "logs");
425
+ const destLogs = resolve3(dest, "logs");
426
+ if (existsSync3(srcLogs) && !existsSync3(destLogs)) {
427
+ mkdirSync2(destLogs, { recursive: true });
428
+ for (const file of readdirSync(srcLogs)) {
429
+ try {
430
+ copyFileSync(resolve3(srcLogs, file), resolve3(destLogs, file));
431
+ } catch (err) {
432
+ console.error(`[migrate] failed to copy log ${file} for ${name}:`, err);
433
+ }
434
+ }
435
+ }
436
+ }
437
+
438
+ // src/lib/scheduler.ts
439
+ import { resolve as resolve4 } from "path";
411
440
  import { CronExpressionParser } from "cron-parser";
412
441
  var Scheduler = class {
413
442
  schedules = /* @__PURE__ */ new Map();
@@ -417,7 +446,7 @@ var Scheduler = class {
417
446
  daemonPort = null;
418
447
  daemonToken = null;
419
448
  get statePath() {
420
- return resolve3(voluteHome(), "scheduler-state.json");
449
+ return resolve4(voluteHome(), "scheduler-state.json");
421
450
  }
422
451
  start(daemonPort, daemonToken) {
423
452
  this.daemonPort = daemonPort ?? null;
@@ -524,18 +553,8 @@ var Scheduler = class {
524
553
  } else {
525
554
  console.error(`[scheduler] fired "${schedule.id}" for ${agentName}`);
526
555
  }
527
- try {
528
- const reader = res.body?.getReader();
529
- if (reader) {
530
- try {
531
- while (!(await reader.read()).done) {
532
- }
533
- } finally {
534
- reader.releaseLock();
535
- }
536
- }
537
- } catch {
538
- }
556
+ await res.text().catch(() => {
557
+ });
539
558
  } catch (err) {
540
559
  console.error(`[scheduler] failed to fire "${schedule.id}" for ${agentName}:`, err);
541
560
  } finally {
@@ -696,18 +715,8 @@ ${summary}`
696
715
  `[token-budget] replayed ${messages2.length} queued message(s) for ${agentName}`
697
716
  );
698
717
  }
699
- try {
700
- const reader = res.body?.getReader();
701
- if (reader) {
702
- try {
703
- while (!(await reader.read()).done) {
704
- }
705
- } finally {
706
- reader.releaseLock();
707
- }
708
- }
709
- } catch {
710
- }
718
+ await res.text().catch(() => {
719
+ });
711
720
  } catch (err) {
712
721
  console.error(`[token-budget] failed to replay for ${agentName}:`, err);
713
722
  const state = this.budgets.get(agentName);
@@ -788,9 +797,9 @@ var authMiddleware = createMiddleware(async (c, next) => {
788
797
  });
789
798
 
790
799
  // src/web/server.ts
791
- import { existsSync as existsSync6 } from "fs";
800
+ import { existsSync as existsSync7 } from "fs";
792
801
  import { readFile as readFile2, stat } from "fs/promises";
793
- import { dirname as dirname2, extname, resolve as resolve8 } from "path";
802
+ import { dirname as dirname2, extname, resolve as resolve9 } from "path";
794
803
  import { serve } from "@hono/node-server";
795
804
 
796
805
  // src/lib/log-buffer.ts
@@ -844,50 +853,12 @@ import { csrf } from "hono/csrf";
844
853
  import { HTTPException } from "hono/http-exception";
845
854
 
846
855
  // src/web/routes/agents.ts
847
- import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
848
- import { resolve as resolve4 } from "path";
856
+ import { execFile } from "child_process";
857
+ import { existsSync as existsSync4, readFileSync as readFileSync3, rmSync } from "fs";
858
+ import { resolve as resolve5 } from "path";
859
+ import { promisify } from "util";
849
860
  import { and, desc, eq as eq2 } from "drizzle-orm";
850
861
  import { Hono } from "hono";
851
- import { stream } from "hono/streaming";
852
-
853
- // src/lib/ndjson.ts
854
- var MAX_BUFFER_SIZE = 1e6;
855
- async function* readNdjson(body) {
856
- const reader = body.getReader();
857
- const decoder = new TextDecoder();
858
- let buffer = "";
859
- try {
860
- while (true) {
861
- const { done, value } = await reader.read();
862
- if (done) break;
863
- buffer += decoder.decode(value, { stream: true });
864
- if (buffer.length > MAX_BUFFER_SIZE) {
865
- logger_default.warn("ndjson: buffer exceeded 1MB, resetting");
866
- buffer = "";
867
- continue;
868
- }
869
- const lines = buffer.split("\n");
870
- buffer = lines.pop() || "";
871
- for (const line of lines) {
872
- if (!line.trim()) continue;
873
- try {
874
- yield JSON.parse(line);
875
- } catch {
876
- logger_default.warn("ndjson: skipping invalid line", { line: line.slice(0, 100) });
877
- }
878
- }
879
- }
880
- if (buffer.trim()) {
881
- try {
882
- yield JSON.parse(buffer);
883
- } catch {
884
- logger_default.warn("ndjson: skipping invalid line", { line: buffer.slice(0, 100) });
885
- }
886
- }
887
- } finally {
888
- reader.releaseLock();
889
- }
890
- }
891
862
 
892
863
  // src/lib/typing.ts
893
864
  var DEFAULT_TTL_MS = 1e4;
@@ -957,11 +928,38 @@ function getTypingMap() {
957
928
  }
958
929
 
959
930
  // src/web/routes/agents.ts
931
+ var execFileAsync = promisify(execFile);
932
+ async function startAgentFull(name, baseName, variantName) {
933
+ await getAgentManager().startAgent(name);
934
+ if (variantName) return;
935
+ const dir = agentDir(baseName);
936
+ const entry = findAgent(baseName);
937
+ await getConnectorManager().startConnectors(baseName, dir, entry.port, getDaemonPort());
938
+ getScheduler().loadSchedules(baseName);
939
+ const config = readVoluteConfig(dir);
940
+ if (config?.tokenBudget) {
941
+ getTokenBudget().setBudget(
942
+ baseName,
943
+ config.tokenBudget,
944
+ config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
945
+ );
946
+ }
947
+ }
948
+ function extractTextContent(content) {
949
+ if (typeof content === "string") return content;
950
+ if (Array.isArray(content)) {
951
+ return content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
952
+ }
953
+ return JSON.stringify(content);
954
+ }
960
955
  function getDaemonPort() {
961
956
  try {
962
- const data = JSON.parse(readFileSync3(resolve4(voluteHome(), "daemon.json"), "utf-8"));
957
+ const data = JSON.parse(readFileSync3(resolve5(voluteHome(), "daemon.json"), "utf-8"));
963
958
  return data.port;
964
- } catch {
959
+ } catch (err) {
960
+ if (err?.code !== "ENOENT") {
961
+ console.error("[daemon] failed to read daemon.json:", err);
962
+ }
965
963
  return void 0;
966
964
  }
967
965
  }
@@ -1008,7 +1006,7 @@ var app = new Hono().get("/", async (c) => {
1008
1006
  const name = c.req.param("name");
1009
1007
  const entry = findAgent(name);
1010
1008
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1011
- if (!existsSync3(agentDir(name))) return c.json({ error: "Agent directory missing" }, 404);
1009
+ if (!existsSync4(agentDir(name))) return c.json({ error: "Agent directory missing" }, 404);
1012
1010
  const { status, channels } = await getAgentStatus(name, entry.port);
1013
1011
  const variants = readVariants(name);
1014
1012
  const manager = getAgentManager();
@@ -1034,27 +1032,13 @@ var app = new Hono().get("/", async (c) => {
1034
1032
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
1035
1033
  } else {
1036
1034
  const dir = agentDir(baseName);
1037
- if (!existsSync3(dir)) return c.json({ error: "Agent directory missing" }, 404);
1035
+ if (!existsSync4(dir)) return c.json({ error: "Agent directory missing" }, 404);
1038
1036
  }
1039
- const manager = getAgentManager();
1040
- if (manager.isRunning(name)) {
1037
+ if (getAgentManager().isRunning(name)) {
1041
1038
  return c.json({ error: "Agent already running" }, 409);
1042
1039
  }
1043
1040
  try {
1044
- await manager.startAgent(name);
1045
- if (!variantName) {
1046
- const dir = agentDir(baseName);
1047
- await getConnectorManager().startConnectors(baseName, dir, entry.port, getDaemonPort());
1048
- getScheduler().loadSchedules(baseName);
1049
- const config = readVoluteConfig(dir);
1050
- if (config?.tokenBudget) {
1051
- getTokenBudget().setBudget(
1052
- baseName,
1053
- config.tokenBudget,
1054
- config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
1055
- );
1056
- }
1057
- }
1041
+ await startAgentFull(name, baseName, variantName);
1058
1042
  return c.json({ ok: true });
1059
1043
  } catch (err) {
1060
1044
  return c.json({ error: err instanceof Error ? err.message : "Failed to start agent" }, 500);
@@ -1069,32 +1053,54 @@ var app = new Hono().get("/", async (c) => {
1069
1053
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
1070
1054
  } else {
1071
1055
  const dir = agentDir(baseName);
1072
- if (!existsSync3(dir)) return c.json({ error: "Agent directory missing" }, 404);
1056
+ if (!existsSync4(dir)) return c.json({ error: "Agent directory missing" }, 404);
1057
+ }
1058
+ let context;
1059
+ const contentType = c.req.header("content-type");
1060
+ if (contentType?.includes("application/json")) {
1061
+ try {
1062
+ const body = await c.req.json();
1063
+ if (body?.context) context = body.context;
1064
+ } catch (err) {
1065
+ console.error(`[daemon] failed to parse restart context for ${name}:`, err);
1066
+ }
1073
1067
  }
1074
1068
  const manager = getAgentManager();
1075
- const connectorManager = getConnectorManager();
1076
1069
  try {
1077
1070
  if (manager.isRunning(name)) {
1078
1071
  if (!variantName) {
1079
- await connectorManager.stopConnectors(baseName);
1072
+ await getConnectorManager().stopConnectors(baseName);
1080
1073
  getTokenBudget().removeBudget(baseName);
1081
1074
  }
1082
1075
  await manager.stopAgent(name);
1083
1076
  }
1084
- await manager.startAgent(name);
1085
- if (!variantName) {
1086
- const dir = agentDir(baseName);
1087
- await connectorManager.startConnectors(baseName, dir, entry.port, getDaemonPort());
1088
- getScheduler().loadSchedules(baseName);
1089
- const config = readVoluteConfig(dir);
1090
- if (config?.tokenBudget) {
1091
- getTokenBudget().setBudget(
1092
- baseName,
1093
- config.tokenBudget,
1094
- config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
1095
- );
1077
+ if (context?.type === "merge" && context.name && !variantName) {
1078
+ const mergeVariantName = String(context.name);
1079
+ const branchErr = validateBranchName(mergeVariantName);
1080
+ if (branchErr) {
1081
+ return c.json({ error: `Invalid variant name: ${branchErr}` }, 400);
1096
1082
  }
1083
+ console.error(`[daemon] merging variant for ${baseName}: ${mergeVariantName}`);
1084
+ const mergeArgs = [
1085
+ "variant",
1086
+ "merge",
1087
+ mergeVariantName,
1088
+ "--agent",
1089
+ baseName,
1090
+ "--skip-verify"
1091
+ ];
1092
+ if (context.summary) mergeArgs.push("--summary", String(context.summary));
1093
+ if (context.justification) mergeArgs.push("--justification", String(context.justification));
1094
+ if (context.memory) mergeArgs.push("--memory", String(context.memory));
1095
+ await execFileAsync("volute", mergeArgs, {
1096
+ cwd: agentDir(baseName),
1097
+ env: { ...process.env, VOLUTE_SUPERVISOR: "1" }
1098
+ });
1097
1099
  }
1100
+ if (context) {
1101
+ manager.setPendingContext(name, context);
1102
+ }
1103
+ await startAgentFull(name, baseName, variantName);
1098
1104
  return c.json({ ok: true });
1099
1105
  } catch (err) {
1100
1106
  return c.json({ error: err instanceof Error ? err.message : "Failed to restart agent" }, 500);
@@ -1138,7 +1144,11 @@ var app = new Hono().get("/", async (c) => {
1138
1144
  removeAllVariants(name);
1139
1145
  removeAgent(name);
1140
1146
  await deleteAgentUser(name);
1141
- if (force && existsSync3(dir)) {
1147
+ const state = stateDir(name);
1148
+ if (existsSync4(state)) {
1149
+ rmSync(state, { recursive: true, force: true });
1150
+ }
1151
+ if (force && existsSync4(dir)) {
1142
1152
  rmSync(dir, { recursive: true, force: true });
1143
1153
  }
1144
1154
  return c.json({ ok: true });
@@ -1168,18 +1178,10 @@ var app = new Hono().get("/", async (c) => {
1168
1178
  if (parsed) {
1169
1179
  try {
1170
1180
  const sender2 = parsed.sender ?? null;
1171
- let content;
1172
- if (typeof parsed.content === "string") {
1173
- content = parsed.content;
1174
- } else if (Array.isArray(parsed.content)) {
1175
- content = parsed.content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
1176
- } else {
1177
- content = JSON.stringify(parsed.content);
1178
- }
1181
+ const content = extractTextContent(parsed.content);
1179
1182
  await db.insert(agentMessages).values({
1180
1183
  agent: baseName,
1181
1184
  channel,
1182
- role: "user",
1183
1185
  sender: sender2,
1184
1186
  content
1185
1187
  });
@@ -1190,31 +1192,13 @@ var app = new Hono().get("/", async (c) => {
1190
1192
  const budget = getTokenBudget();
1191
1193
  const budgetStatus = budget.checkBudget(baseName);
1192
1194
  if (budgetStatus === "exceeded") {
1193
- let textContent = "";
1194
- if (parsed) {
1195
- if (typeof parsed.content === "string") {
1196
- textContent = parsed.content;
1197
- } else if (Array.isArray(parsed.content)) {
1198
- textContent = parsed.content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
1199
- }
1200
- }
1195
+ const textContent = parsed ? extractTextContent(parsed.content) : "";
1201
1196
  budget.enqueue(baseName, {
1202
1197
  channel,
1203
1198
  sender: parsed?.sender ?? null,
1204
1199
  textContent
1205
1200
  });
1206
- c.header("Content-Type", "application/x-ndjson");
1207
- const encoder2 = new TextEncoder();
1208
- return stream(c, async (s) => {
1209
- await s.write(
1210
- encoder2.encode(
1211
- `${JSON.stringify({ type: "text", content: "[Token budget exceeded \u2014 message queued for next period]" })}
1212
- `
1213
- )
1214
- );
1215
- await s.write(encoder2.encode(`${JSON.stringify({ type: "done" })}
1216
- `));
1217
- });
1201
+ return c.json({ error: "Token budget exceeded \u2014 message queued for next period" }, 429);
1218
1202
  }
1219
1203
  const typingMap = getTypingMap();
1220
1204
  const sender = parsed?.sender ?? "";
@@ -1238,63 +1222,38 @@ var app = new Hono().get("/", async (c) => {
1238
1222
  budget.acknowledgeWarning(baseName);
1239
1223
  forwardBody = JSON.stringify(parsed);
1240
1224
  }
1241
- let res;
1225
+ typingMap.set(channel, baseName, { persistent: true });
1226
+ const conversationId = parsed?.conversationId ?? null;
1227
+ if (conversationId) typingMap.set(`volute:${conversationId}`, baseName, { persistent: true });
1242
1228
  try {
1243
- res = await fetch(`http://127.0.0.1:${port}/message`, {
1229
+ const res = await fetch(`http://127.0.0.1:${port}/message`, {
1244
1230
  method: "POST",
1245
1231
  headers: { "Content-Type": "application/json" },
1246
1232
  body: forwardBody
1247
1233
  });
1234
+ if (!res.ok) {
1235
+ const text = await res.text().catch(() => "");
1236
+ console.error(`[daemon] agent ${name} responded with ${res.status}: ${text}`);
1237
+ return c.json({ error: `Agent responded with ${res.status}` }, res.status);
1238
+ }
1239
+ let result;
1240
+ try {
1241
+ result = await res.json();
1242
+ } catch (parseErr) {
1243
+ console.error(`[daemon] agent ${name} returned non-JSON response:`, parseErr);
1244
+ return c.json({ error: "Agent returned invalid response" }, 502);
1245
+ }
1246
+ if (result.usage) {
1247
+ budget.recordUsage(baseName, result.usage.input_tokens, result.usage.output_tokens);
1248
+ }
1249
+ return c.json({ ok: true });
1248
1250
  } catch (err) {
1249
1251
  console.error(`[daemon] agent ${name} unreachable on port ${port}:`, err);
1250
1252
  return c.json({ error: "Agent is not reachable" }, 502);
1253
+ } finally {
1254
+ typingMap.delete(channel, baseName);
1255
+ if (conversationId) typingMap.delete(`volute:${conversationId}`, baseName);
1251
1256
  }
1252
- if (!res.ok) {
1253
- return c.json({ error: `Agent responded with ${res.status}` }, res.status);
1254
- }
1255
- if (!res.body) {
1256
- return c.json({ error: "No response body from agent" }, 502);
1257
- }
1258
- c.header("Content-Type", "application/x-ndjson");
1259
- const encoder = new TextEncoder();
1260
- typingMap.set(channel, baseName, { persistent: true });
1261
- return stream(c, async (s) => {
1262
- try {
1263
- const textParts = [];
1264
- const toolParts = [];
1265
- for await (const event of readNdjson(res.body)) {
1266
- if (event.type === "usage") {
1267
- const input = typeof event.input_tokens === "number" ? event.input_tokens : 0;
1268
- const output = typeof event.output_tokens === "number" ? event.output_tokens : 0;
1269
- budget.recordUsage(baseName, input, output);
1270
- continue;
1271
- }
1272
- await s.write(encoder.encode(`${JSON.stringify(event)}
1273
- `));
1274
- const part = collectPart(event);
1275
- if (part != null) {
1276
- if (event.type === "tool_use") toolParts.push(part);
1277
- else textParts.push(part);
1278
- }
1279
- }
1280
- const content = [textParts.join(""), ...toolParts].filter(Boolean).join("\n");
1281
- if (content) {
1282
- try {
1283
- await db.insert(agentMessages).values({
1284
- agent: baseName,
1285
- channel,
1286
- role: "assistant",
1287
- sender: baseName,
1288
- content
1289
- });
1290
- } catch (err) {
1291
- console.error(`[daemon] failed to persist assistant response for ${baseName}:`, err);
1292
- }
1293
- }
1294
- } finally {
1295
- typingMap.delete(channel, baseName);
1296
- }
1297
- });
1298
1257
  }).get("/:name/budget", async (c) => {
1299
1258
  const name = c.req.param("name");
1300
1259
  const [baseName] = name.split("@", 2);
@@ -1318,8 +1277,7 @@ var app = new Hono().get("/", async (c) => {
1318
1277
  await db.insert(agentMessages).values({
1319
1278
  agent: baseName,
1320
1279
  channel: body.channel,
1321
- role: "assistant",
1322
- sender: baseName,
1280
+ sender: body.sender ?? baseName,
1323
1281
  content: body.content
1324
1282
  });
1325
1283
  } catch (err) {
@@ -1445,7 +1403,7 @@ var app3 = new Hono3().get("/:name/connectors", (c) => {
1445
1403
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1446
1404
  const dir = agentDir(name);
1447
1405
  const manager = getConnectorManager();
1448
- const envCheck = manager.checkConnectorEnv(type, dir);
1406
+ const envCheck = manager.checkConnectorEnv(type, name, dir);
1449
1407
  if (envCheck) {
1450
1408
  return c.json(
1451
1409
  {
@@ -1490,21 +1448,18 @@ var app3 = new Hono3().get("/:name/connectors", (c) => {
1490
1448
  var connectors_default = app3;
1491
1449
 
1492
1450
  // src/web/routes/files.ts
1493
- import { existsSync as existsSync4 } from "fs";
1494
- import { readdir, readFile, writeFile } from "fs/promises";
1495
- import { resolve as resolve5 } from "path";
1496
- import { zValidator as zValidator2 } from "@hono/zod-validator";
1451
+ import { existsSync as existsSync5 } from "fs";
1452
+ import { readdir, readFile } from "fs/promises";
1453
+ import { resolve as resolve6 } from "path";
1497
1454
  import { Hono as Hono4 } from "hono";
1498
- import { z as z2 } from "zod";
1499
1455
  var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
1500
- var saveFileSchema = z2.object({ content: z2.string() });
1501
1456
  var app4 = new Hono4().get("/:name/files", async (c) => {
1502
1457
  const name = c.req.param("name");
1503
1458
  const entry = findAgent(name);
1504
1459
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1505
1460
  const dir = agentDir(name);
1506
- const homeDir = resolve5(dir, "home");
1507
- if (!existsSync4(homeDir)) return c.json({ error: "Home directory missing" }, 404);
1461
+ const homeDir = resolve6(dir, "home");
1462
+ if (!existsSync5(homeDir)) return c.json({ error: "Home directory missing" }, 404);
1508
1463
  const allFiles = await readdir(homeDir);
1509
1464
  const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
1510
1465
  return c.json(files);
@@ -1517,61 +1472,47 @@ var app4 = new Hono4().get("/:name/files", async (c) => {
1517
1472
  const entry = findAgent(name);
1518
1473
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1519
1474
  const dir = agentDir(name);
1520
- const filePath = resolve5(dir, "home", filename);
1521
- if (!existsSync4(filePath)) {
1475
+ const filePath = resolve6(dir, "home", filename);
1476
+ if (!existsSync5(filePath)) {
1522
1477
  return c.json({ error: "File not found" }, 404);
1523
1478
  }
1524
1479
  const content = await readFile(filePath, "utf-8");
1525
1480
  return c.json({ filename, content });
1526
- }).put("/:name/files/:filename", zValidator2("json", saveFileSchema), async (c) => {
1527
- const name = c.req.param("name");
1528
- const filename = c.req.param("filename");
1529
- if (!ALLOWED_FILES.has(filename)) {
1530
- return c.json({ error: "File not allowed" }, 403);
1531
- }
1532
- const entry = findAgent(name);
1533
- if (!entry) return c.json({ error: "Agent not found" }, 404);
1534
- const dir = agentDir(name);
1535
- const filePath = resolve5(dir, "home", filename);
1536
- const { content } = c.req.valid("json");
1537
- await writeFile(filePath, content);
1538
- return c.json({ ok: true });
1539
1481
  });
1540
1482
  var files_default = app4;
1541
1483
 
1542
1484
  // src/web/routes/logs.ts
1543
1485
  import { spawn as spawn2 } from "child_process";
1544
- import { existsSync as existsSync5 } from "fs";
1545
- import { resolve as resolve6 } from "path";
1486
+ import { existsSync as existsSync6 } from "fs";
1487
+ import { resolve as resolve7 } from "path";
1546
1488
  import { Hono as Hono5 } from "hono";
1547
1489
  import { streamSSE } from "hono/streaming";
1548
1490
  var app5 = new Hono5().get("/:name/logs", async (c) => {
1549
1491
  const name = c.req.param("name");
1550
1492
  const entry = findAgent(name);
1551
1493
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1552
- const dir = agentDir(name);
1553
- const logFile = resolve6(dir, ".volute", "logs", "agent.log");
1554
- if (!existsSync5(logFile)) {
1494
+ const logFile = resolve7(stateDir(name), "logs", "agent.log");
1495
+ if (!existsSync6(logFile)) {
1555
1496
  return c.json({ error: "No log file found" }, 404);
1556
1497
  }
1557
- return streamSSE(c, async (stream2) => {
1498
+ return streamSSE(c, async (stream) => {
1558
1499
  const tail = spawn2("tail", ["-n", "200", "-f", logFile]);
1559
1500
  const onData = (data) => {
1560
1501
  const lines = data.toString().split("\n");
1561
1502
  for (const line of lines) {
1562
1503
  if (line) {
1563
- stream2.writeSSE({ data: line }).catch(() => {
1504
+ stream.writeSSE({ data: line }).catch(() => {
1564
1505
  });
1565
1506
  }
1566
1507
  }
1567
1508
  };
1568
1509
  tail.stdout.on("data", onData);
1569
- stream2.onAbort(() => {
1510
+ stream.onAbort(() => {
1570
1511
  tail.kill();
1571
1512
  });
1572
- await new Promise((resolve10) => {
1573
- tail.on("exit", resolve10);
1574
- stream2.onAbort(resolve10);
1513
+ await new Promise((resolve11) => {
1514
+ tail.on("exit", resolve11);
1515
+ stream.onAbort(resolve11);
1575
1516
  });
1576
1517
  });
1577
1518
  });
@@ -1665,18 +1606,18 @@ import { streamSSE as streamSSE2 } from "hono/streaming";
1665
1606
  var app7 = new Hono7().get("/logs", async (c) => {
1666
1607
  const user = c.get("user");
1667
1608
  if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
1668
- return streamSSE2(c, async (stream2) => {
1609
+ return streamSSE2(c, async (stream) => {
1669
1610
  for (const entry of logBuffer.getEntries()) {
1670
- await stream2.writeSSE({ data: JSON.stringify(entry) });
1611
+ await stream.writeSSE({ data: JSON.stringify(entry) });
1671
1612
  }
1672
1613
  const unsubscribe = logBuffer.subscribe((entry) => {
1673
- stream2.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
1614
+ stream.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
1674
1615
  });
1675
1616
  });
1676
- await new Promise((resolve10) => {
1677
- stream2.onAbort(() => {
1617
+ await new Promise((resolve11) => {
1618
+ stream.onAbort(() => {
1678
1619
  unsubscribe();
1679
- resolve10();
1620
+ resolve11();
1680
1621
  });
1681
1622
  });
1682
1623
  });
@@ -1684,15 +1625,15 @@ var app7 = new Hono7().get("/logs", async (c) => {
1684
1625
  var system_default = app7;
1685
1626
 
1686
1627
  // src/web/routes/typing.ts
1687
- import { zValidator as zValidator3 } from "@hono/zod-validator";
1628
+ import { zValidator as zValidator2 } from "@hono/zod-validator";
1688
1629
  import { Hono as Hono8 } from "hono";
1689
- import { z as z3 } from "zod";
1690
- var typingSchema = z3.object({
1691
- channel: z3.string().min(1),
1692
- sender: z3.string().min(1),
1693
- active: z3.boolean()
1630
+ import { z as z2 } from "zod";
1631
+ var typingSchema = z2.object({
1632
+ channel: z2.string().min(1),
1633
+ sender: z2.string().min(1),
1634
+ active: z2.boolean()
1694
1635
  });
1695
- var app8 = new Hono8().post("/:name/typing", zValidator3("json", typingSchema), (c) => {
1636
+ var app8 = new Hono8().post("/:name/typing", zValidator2("json", typingSchema), (c) => {
1696
1637
  const { channel, sender, active } = c.req.valid("json");
1697
1638
  const map = getTypingMap();
1698
1639
  if (active) {
@@ -1752,11 +1693,39 @@ var variants_default = app10;
1752
1693
 
1753
1694
  // src/web/routes/volute/chat.ts
1754
1695
  import { readFileSync as readFileSync4 } from "fs";
1755
- import { resolve as resolve7 } from "path";
1756
- import { zValidator as zValidator4 } from "@hono/zod-validator";
1696
+ import { resolve as resolve8 } from "path";
1697
+ import { zValidator as zValidator3 } from "@hono/zod-validator";
1757
1698
  import { Hono as Hono11 } from "hono";
1758
1699
  import { streamSSE as streamSSE3 } from "hono/streaming";
1759
- import { z as z4 } from "zod";
1700
+ import { z as z3 } from "zod";
1701
+
1702
+ // src/lib/conversation-events.ts
1703
+ var subscribers = /* @__PURE__ */ new Map();
1704
+ function subscribe(conversationId, callback) {
1705
+ let set = subscribers.get(conversationId);
1706
+ if (!set) {
1707
+ set = /* @__PURE__ */ new Set();
1708
+ subscribers.set(conversationId, set);
1709
+ }
1710
+ set.add(callback);
1711
+ return () => {
1712
+ set.delete(callback);
1713
+ if (set.size === 0) subscribers.delete(conversationId);
1714
+ };
1715
+ }
1716
+ function publish(conversationId, event) {
1717
+ const set = subscribers.get(conversationId);
1718
+ if (!set) return;
1719
+ for (const cb of set) {
1720
+ try {
1721
+ cb(event);
1722
+ } catch (err) {
1723
+ console.error("[conversation-events] subscriber threw:", err);
1724
+ set.delete(cb);
1725
+ if (set.size === 0) subscribers.delete(conversationId);
1726
+ }
1727
+ }
1728
+ }
1760
1729
 
1761
1730
  // src/lib/conversations.ts
1762
1731
  import { randomUUID } from "crypto";
@@ -1845,7 +1814,7 @@ async function addMessage(conversationId, role, senderName, content) {
1845
1814
  await db.update(conversations).set({ title }).where(and2(eq3(conversations.id, conversationId), isNull(conversations.title)));
1846
1815
  }
1847
1816
  }
1848
- return {
1817
+ const msg = {
1849
1818
  id: result.id,
1850
1819
  conversation_id: conversationId,
1851
1820
  role,
@@ -1853,6 +1822,15 @@ async function addMessage(conversationId, role, senderName, content) {
1853
1822
  content,
1854
1823
  created_at: result.created_at
1855
1824
  };
1825
+ publish(conversationId, {
1826
+ type: "message",
1827
+ id: msg.id,
1828
+ role: msg.role,
1829
+ senderName: msg.sender_name,
1830
+ content: msg.content,
1831
+ createdAt: msg.created_at
1832
+ });
1833
+ return msg;
1856
1834
  }
1857
1835
  async function getMessages(conversationId) {
1858
1836
  const db = await getDb();
@@ -1915,20 +1893,24 @@ async function deleteConversation(id) {
1915
1893
  }
1916
1894
 
1917
1895
  // src/web/routes/volute/chat.ts
1918
- var chatSchema = z4.object({
1919
- message: z4.string().optional(),
1920
- conversationId: z4.string().optional(),
1921
- sender: z4.string().optional(),
1922
- images: z4.array(
1923
- z4.object({
1924
- media_type: z4.string(),
1925
- data: z4.string()
1896
+ var chatSchema = z3.object({
1897
+ message: z3.string().optional(),
1898
+ conversationId: z3.string().optional(),
1899
+ sender: z3.string().optional(),
1900
+ images: z3.array(
1901
+ z3.object({
1902
+ media_type: z3.string(),
1903
+ data: z3.string()
1926
1904
  })
1927
1905
  ).optional()
1928
1906
  });
1929
1907
  function getDaemonUrl() {
1930
- const data = JSON.parse(readFileSync4(resolve7(voluteHome(), "daemon.json"), "utf-8"));
1931
- return `http://${daemonLoopback()}:${data.port}`;
1908
+ try {
1909
+ const data = JSON.parse(readFileSync4(resolve8(voluteHome(), "daemon.json"), "utf-8"));
1910
+ return `http://${daemonLoopback()}:${data.port}`;
1911
+ } catch (err) {
1912
+ throw new Error(`Failed to read daemon config: ${err instanceof Error ? err.message : err}`);
1913
+ }
1932
1914
  }
1933
1915
  function daemonFetchInternal(path, body) {
1934
1916
  const daemonUrl = getDaemonUrl();
@@ -1940,40 +1922,7 @@ function daemonFetchInternal(path, body) {
1940
1922
  if (token) headers.Authorization = `Bearer ${token}`;
1941
1923
  return fetch(`${daemonUrl}${path}`, { method: "POST", headers, body });
1942
1924
  }
1943
- function accumulateEvent(content, event) {
1944
- if (event.type === "text") {
1945
- const last = content[content.length - 1];
1946
- if (last && last.type === "text") last.text += event.content;
1947
- else content.push({ type: "text", text: event.content });
1948
- } else if (event.type === "tool_use") {
1949
- content.push({ type: "tool_use", name: event.name, input: event.input });
1950
- } else if (event.type === "tool_result") {
1951
- content.push({
1952
- type: "tool_result",
1953
- output: event.output,
1954
- ...event.is_error ? { is_error: true } : {}
1955
- });
1956
- }
1957
- }
1958
- async function consumeAndPersist(res, conversationId, agentName) {
1959
- if (!res.body) {
1960
- console.warn(`[chat] no response body from ${agentName}`);
1961
- return [];
1962
- }
1963
- const assistantContent = [];
1964
- for await (const event of readNdjson(res.body)) {
1965
- accumulateEvent(assistantContent, event);
1966
- if (event.type === "done") break;
1967
- }
1968
- if (assistantContent.length === 0) return [];
1969
- try {
1970
- await addMessage(conversationId, "assistant", agentName, assistantContent);
1971
- } catch (err) {
1972
- console.error(`[chat] failed to persist conversation message from ${agentName}:`, err);
1973
- }
1974
- return assistantContent;
1975
- }
1976
- var app11 = new Hono11().post("/:name/chat", zValidator4("json", chatSchema), async (c) => {
1925
+ var app11 = new Hono11().post("/:name/chat", zValidator3("json", chatSchema), async (c) => {
1977
1926
  const name = c.req.param("name");
1978
1927
  const [baseName] = name.split("@", 2);
1979
1928
  const entry = findAgent(baseName);
@@ -1991,7 +1940,6 @@ var app11 = new Hono11().post("/:name/chat", zValidator4("json", chatSchema), as
1991
1940
  return c.json({ error: "Conversation not found" }, 404);
1992
1941
  }
1993
1942
  } else {
1994
- const title = body.message ? body.message.slice(0, 80) : "Image message";
1995
1943
  const participantIds = [];
1996
1944
  if (user.id !== 0) {
1997
1945
  participantIds.push(user.id);
@@ -2010,6 +1958,8 @@ var app11 = new Hono11().post("/:name/chat", zValidator4("json", chatSchema), as
2010
1958
  }
2011
1959
  }
2012
1960
  if (!conversationId) {
1961
+ const participantNames2 = /* @__PURE__ */ new Set([senderName, baseName]);
1962
+ const title = [...participantNames2].join(", ");
2013
1963
  const conv2 = await createConversation(baseName, "volute", {
2014
1964
  userId: user.id !== 0 ? user.id : void 0,
2015
1965
  title,
@@ -2034,108 +1984,86 @@ var app11 = new Hono11().post("/:name/chat", zValidator4("json", chatSchema), as
2034
1984
  const participants = await getParticipants(conversationId);
2035
1985
  const agentParticipants = participants.filter((p) => p.userType === "agent");
2036
1986
  const participantNames = participants.map((p) => p.username);
2037
- const { getAgentManager: getAgentManager2 } = await import("./agent-manager-IMZ7ZMBF.js");
1987
+ const { getAgentManager: getAgentManager2 } = await import("./agent-manager-CMMH5KQQ.js");
2038
1988
  const manager = getAgentManager2();
2039
1989
  const runningAgents = agentParticipants.map((ap) => {
2040
1990
  const agentKey = ap.username === baseName ? name : ap.username;
2041
1991
  return manager.isRunning(agentKey) ? ap.username : null;
2042
1992
  }).filter((n) => n !== null && n !== senderName);
2043
1993
  const isDM = participants.length === 2;
2044
- const dir = agentDir(baseName);
2045
- writeChannelEntry(dir, channel, {
1994
+ const channelEntry = {
2046
1995
  platformId: conversationId,
2047
1996
  platform: "volute",
2048
1997
  name: convTitle ?? void 0,
2049
1998
  type: isDM ? "dm" : "group"
2050
- });
1999
+ };
2000
+ for (const ap of agentParticipants) {
2001
+ try {
2002
+ writeChannelEntry(ap.username, channel, channelEntry);
2003
+ } catch (err) {
2004
+ console.warn(`[chat] failed to write channel entry for ${ap.username}:`, err);
2005
+ }
2006
+ }
2051
2007
  const typingMap = getTypingMap();
2052
2008
  const currentlyTyping = typingMap.get(channel);
2053
2009
  const payload = JSON.stringify({
2054
2010
  content: contentBlocks,
2055
2011
  channel,
2012
+ conversationId,
2056
2013
  sender: senderName,
2057
2014
  participants: participantNames,
2058
2015
  participantCount: participants.length,
2059
2016
  isDM,
2060
2017
  ...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
2061
2018
  });
2062
- const responses = [];
2063
2019
  for (const agentName of runningAgents) {
2064
2020
  const targetName = agentName === baseName ? name : agentName;
2065
- try {
2066
- const res = await daemonFetchInternal(
2067
- `/api/agents/${encodeURIComponent(targetName)}/message`,
2068
- payload
2069
- );
2070
- if (res.ok && res.body) {
2071
- responses.push({ name: agentName, res });
2072
- } else {
2073
- const errorBody = await res.text().catch(() => "");
2074
- console.error(
2075
- `[chat] agent ${agentName} responded with ${res.status}: ${errorBody.slice(0, 500)}`
2076
- );
2021
+ daemonFetchInternal(`/api/agents/${encodeURIComponent(targetName)}/message`, payload).then(async (res) => {
2022
+ if (!res.ok) {
2023
+ const text = await res.text().catch(() => "");
2024
+ console.error(`[chat] agent ${agentName} responded ${res.status}: ${text}`);
2077
2025
  }
2078
- } catch (err) {
2026
+ }).catch((err) => {
2079
2027
  console.error(`[chat] agent ${agentName} unreachable via daemon:`, err);
2080
- }
2081
- }
2082
- if (responses.length === 0) {
2083
- return streamSSE3(c, async (stream2) => {
2084
- await stream2.writeSSE({
2085
- data: JSON.stringify({ type: "meta", conversationId })
2086
- });
2087
- await stream2.writeSSE({ data: JSON.stringify({ type: "sync" }) });
2088
2028
  });
2089
2029
  }
2090
- const primary = responses[0];
2091
- const secondary = responses.slice(1);
2092
- const secondaryPromises = secondary.map((s) => consumeAndPersist(s.res, conversationId, s.name));
2093
- return streamSSE3(c, async (stream2) => {
2094
- await stream2.writeSSE({
2095
- data: JSON.stringify({ type: "meta", conversationId, senderName: primary.name })
2030
+ return c.json({ ok: true, conversationId });
2031
+ }).get("/:name/conversations/:id/events", async (c) => {
2032
+ const conversationId = c.req.param("id");
2033
+ const user = c.get("user");
2034
+ if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
2035
+ return c.json({ error: "Conversation not found" }, 404);
2036
+ }
2037
+ return streamSSE3(c, async (stream) => {
2038
+ const unsubscribe = subscribe(conversationId, (event) => {
2039
+ stream.writeSSE({ data: JSON.stringify(event) }).catch((err) => {
2040
+ if (!stream.aborted) console.error("[chat] SSE write error:", err);
2041
+ });
2096
2042
  });
2097
- const assistantContent = [];
2098
- try {
2099
- for await (const event of readNdjson(primary.res.body)) {
2100
- await stream2.writeSSE({ data: JSON.stringify(event) });
2101
- accumulateEvent(assistantContent, event);
2102
- if (event.type === "done") break;
2103
- }
2104
- } catch (err) {
2105
- console.error(`[chat] error streaming response from ${primary.name}:`, err);
2106
- await stream2.writeSSE({
2107
- data: JSON.stringify({ type: "error", message: "Stream interrupted" })
2043
+ const keepAlive = setInterval(() => {
2044
+ stream.writeSSE({ data: "" }).catch((err) => {
2045
+ if (!stream.aborted) console.error("[chat] SSE ping error:", err);
2108
2046
  });
2109
- }
2110
- if (assistantContent.length > 0) {
2111
- try {
2112
- await addMessage(conversationId, "assistant", primary.name, assistantContent);
2113
- } catch (err) {
2114
- console.error(`[chat] failed to persist response from ${primary.name}:`, err);
2115
- }
2116
- }
2117
- const results = await Promise.allSettled(secondaryPromises);
2118
- for (let i = 0; i < results.length; i++) {
2119
- if (results[i].status === "rejected") {
2120
- console.error(
2121
- `[chat] secondary agent ${secondary[i].name} response failed:`,
2122
- results[i].reason
2123
- );
2124
- }
2125
- }
2126
- await stream2.writeSSE({ data: JSON.stringify({ type: "sync" }) });
2047
+ }, 15e3);
2048
+ await new Promise((resolve11) => {
2049
+ stream.onAbort(() => {
2050
+ unsubscribe();
2051
+ clearInterval(keepAlive);
2052
+ resolve11();
2053
+ });
2054
+ });
2127
2055
  });
2128
2056
  });
2129
2057
  var chat_default = app11;
2130
2058
 
2131
2059
  // src/web/routes/volute/conversations.ts
2132
- import { zValidator as zValidator5 } from "@hono/zod-validator";
2060
+ import { zValidator as zValidator4 } from "@hono/zod-validator";
2133
2061
  import { Hono as Hono12 } from "hono";
2134
- import { z as z5 } from "zod";
2135
- var createConvSchema = z5.object({
2136
- title: z5.string().optional(),
2137
- participantIds: z5.array(z5.number()).optional(),
2138
- participantNames: z5.array(z5.string()).optional()
2062
+ import { z as z4 } from "zod";
2063
+ var createConvSchema = z4.object({
2064
+ title: z4.string().optional(),
2065
+ participantIds: z4.array(z4.number()).optional(),
2066
+ participantNames: z4.array(z4.string()).optional()
2139
2067
  });
2140
2068
  var app12 = new Hono12().get("/:name/conversations", async (c) => {
2141
2069
  const name = c.req.param("name");
@@ -2148,7 +2076,7 @@ var app12 = new Hono12().get("/:name/conversations", async (c) => {
2148
2076
  const all = await listConversationsForUser(lookupId);
2149
2077
  const convs = all.filter((c2) => c2.agent_name === name);
2150
2078
  return c.json(convs);
2151
- }).post("/:name/conversations", zValidator5("json", createConvSchema), async (c) => {
2079
+ }).post("/:name/conversations", zValidator4("json", createConvSchema), async (c) => {
2152
2080
  const name = c.req.param("name");
2153
2081
  const user = c.get("user");
2154
2082
  const body = c.req.valid("json");
@@ -2189,9 +2117,13 @@ var app12 = new Hono12().get("/:name/conversations", async (c) => {
2189
2117
  console.warn(`[conversations] DM conversation ${existingId} found but not retrievable`);
2190
2118
  }
2191
2119
  }
2120
+ let title = body.title;
2121
+ if (!title && body.participantNames?.length) {
2122
+ title = body.participantNames.join(", ");
2123
+ }
2192
2124
  const conv = await createConversation(name, "volute", {
2193
2125
  userId: user.id !== 0 ? user.id : void 0,
2194
- title: body.title,
2126
+ title,
2195
2127
  participantIds
2196
2128
  });
2197
2129
  return c.json(conv, 201);
@@ -2221,12 +2153,12 @@ var app12 = new Hono12().get("/:name/conversations", async (c) => {
2221
2153
  var conversations_default = app12;
2222
2154
 
2223
2155
  // src/web/routes/volute/user-conversations.ts
2224
- import { zValidator as zValidator6 } from "@hono/zod-validator";
2156
+ import { zValidator as zValidator5 } from "@hono/zod-validator";
2225
2157
  import { Hono as Hono13 } from "hono";
2226
- import { z as z6 } from "zod";
2227
- var createSchema = z6.object({
2228
- title: z6.string().optional(),
2229
- participantNames: z6.array(z6.string()).min(1)
2158
+ import { z as z5 } from "zod";
2159
+ var createSchema = z5.object({
2160
+ title: z5.string().optional(),
2161
+ participantNames: z5.array(z5.string()).min(1)
2230
2162
  });
2231
2163
  var app13 = new Hono13().use("*", authMiddleware).get("/", async (c) => {
2232
2164
  const user = c.get("user");
@@ -2240,7 +2172,7 @@ var app13 = new Hono13().use("*", authMiddleware).get("/", async (c) => {
2240
2172
  }
2241
2173
  const msgs = await getMessages(id);
2242
2174
  return c.json(msgs);
2243
- }).post("/", zValidator6("json", createSchema), async (c) => {
2175
+ }).post("/", zValidator5("json", createSchema), async (c) => {
2244
2176
  const user = c.get("user");
2245
2177
  const body = c.req.valid("json");
2246
2178
  const participantIds = /* @__PURE__ */ new Set();
@@ -2346,8 +2278,8 @@ async function startServer({
2346
2278
  let assetsDir = "";
2347
2279
  let searchDir = dirname2(new URL(import.meta.url).pathname);
2348
2280
  for (let i = 0; i < 5; i++) {
2349
- const candidate = resolve8(searchDir, "dist", "web-assets");
2350
- if (existsSync6(candidate)) {
2281
+ const candidate = resolve9(searchDir, "dist", "web-assets");
2282
+ if (existsSync7(candidate)) {
2351
2283
  assetsDir = candidate;
2352
2284
  break;
2353
2285
  }
@@ -2357,7 +2289,7 @@ async function startServer({
2357
2289
  app_default.get("*", async (c) => {
2358
2290
  const urlPath = new URL(c.req.url).pathname;
2359
2291
  if (urlPath.startsWith("/api/")) return c.notFound();
2360
- const filePath = resolve8(assetsDir, urlPath.slice(1));
2292
+ const filePath = resolve9(assetsDir, urlPath.slice(1));
2361
2293
  if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
2362
2294
  const s = await stat(filePath).catch(() => null);
2363
2295
  if (s?.isFile()) {
@@ -2366,7 +2298,7 @@ async function startServer({
2366
2298
  const body = await readFile2(filePath);
2367
2299
  return c.body(body, 200, { "Content-Type": mime });
2368
2300
  }
2369
- const indexPath = resolve8(assetsDir, "index.html");
2301
+ const indexPath = resolve9(assetsDir, "index.html");
2370
2302
  const indexStat = await stat(indexPath).catch(() => null);
2371
2303
  if (indexStat?.isFile()) {
2372
2304
  const body = await readFile2(indexPath, "utf-8");
@@ -2376,10 +2308,10 @@ async function startServer({
2376
2308
  });
2377
2309
  }
2378
2310
  const server = serve({ fetch: app_default.fetch, port, hostname });
2379
- await new Promise((resolve10, reject) => {
2311
+ await new Promise((resolve11, reject) => {
2380
2312
  server.on("listening", () => {
2381
2313
  logger_default.info("Volute UI running", { hostname, port });
2382
- resolve10();
2314
+ resolve11();
2383
2315
  });
2384
2316
  server.on("error", (err) => {
2385
2317
  reject(err);
@@ -2390,14 +2322,14 @@ async function startServer({
2390
2322
 
2391
2323
  // src/daemon.ts
2392
2324
  if (!process.env.VOLUTE_HOME) {
2393
- process.env.VOLUTE_HOME = resolve9(homedir(), ".volute");
2325
+ process.env.VOLUTE_HOME = resolve10(homedir(), ".volute");
2394
2326
  }
2395
2327
  async function startDaemon(opts) {
2396
2328
  const { port, hostname } = opts;
2397
2329
  const myPid = String(process.pid);
2398
2330
  const home = voluteHome();
2399
2331
  if (!opts.foreground) {
2400
- const log2 = new RotatingLog(resolve9(home, "daemon.log"));
2332
+ const log2 = new RotatingLog(resolve10(home, "daemon.log"));
2401
2333
  const write2 = (...args) => log2.write(`${format(...args)}
2402
2334
  `);
2403
2335
  console.log = write2;
@@ -2405,11 +2337,12 @@ async function startDaemon(opts) {
2405
2337
  console.warn = write2;
2406
2338
  console.info = write2;
2407
2339
  }
2408
- const DAEMON_PID_PATH = resolve9(home, "daemon.pid");
2409
- const DAEMON_JSON_PATH = resolve9(home, "daemon.json");
2410
- mkdirSync2(home, { recursive: true });
2340
+ const DAEMON_PID_PATH = resolve10(home, "daemon.pid");
2341
+ const DAEMON_JSON_PATH = resolve10(home, "daemon.json");
2342
+ mkdirSync3(home, { recursive: true });
2411
2343
  const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
2412
2344
  process.env.VOLUTE_DAEMON_TOKEN = token;
2345
+ process.env.VOLUTE_DAEMON_PORT = String(port);
2413
2346
  process.env.VOLUTE_DAEMON_HOSTNAME = hostname;
2414
2347
  let server;
2415
2348
  try {
@@ -2435,6 +2368,13 @@ async function startDaemon(opts) {
2435
2368
  const tokenBudget = getTokenBudget();
2436
2369
  tokenBudget.start(port, token);
2437
2370
  const registry = readRegistry();
2371
+ for (const entry of registry) {
2372
+ try {
2373
+ migrateAgentState(entry.name);
2374
+ } catch (err) {
2375
+ console.error(`[daemon] failed to migrate state for ${entry.name}:`, err);
2376
+ }
2377
+ }
2438
2378
  for (const entry of registry) {
2439
2379
  if (!entry.running) continue;
2440
2380
  try {