volute 0.6.0 → 0.8.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 (77) hide show
  1. package/README.md +13 -13
  2. package/dist/{agent-X7GJLBLW.js → agent-YORVRB6I.js} +10 -10
  3. package/dist/{agent-manager-JDVXU3ON.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-ECPQXRLB.js +264 -0
  9. package/dist/{down-FXWAN66A.js → chunk-HZ5LTOEJ.js} +48 -13
  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-AOKAQGO4.js → chunk-RT6Y7AR3.js} +2 -1
  15. package/dist/{chunk-G6ZNGLUX.js → chunk-W6TMWYU3.js} +133 -78
  16. package/dist/{up-CSX3ZUIU.js → chunk-XSJ27WEM.js} +2 -2
  17. package/dist/cli.js +25 -19
  18. package/dist/{connector-Y7JPNROO.js → connector-ZP6MEFF4.js} +3 -3
  19. package/dist/connectors/discord.js +24 -61
  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-CPBLMMRI.js +23 -0
  25. package/dist/daemon.js +397 -661
  26. package/dist/{delete-2PH2CGDY.js → delete-45TGQC4N.js} +13 -4
  27. package/dist/down-O4EWZTVA.js +11 -0
  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-4DP4Y4UO.js → package-5UCKNK6J.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-R4MCNBOA.js} +1 -1
  37. package/dist/{setup-F4TCWVSP.js → setup-JXDCJX7W.js} +25 -6
  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-V6EAA7OZ.js +12 -0
  42. package/dist/{update-XSIX3GGP.js → update-EUCZ7XGG.js} +3 -3
  43. package/dist/{update-check-5ZADDHCK.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-D5PzIndO.js +0 -308
package/dist/daemon.js CHANGED
@@ -6,32 +6,51 @@ import {
6
6
  initAgentManager,
7
7
  loadJsonMap,
8
8
  saveJsonMap
9
- } from "./chunk-G6ZNGLUX.js";
9
+ } from "./chunk-W6TMWYU3.js";
10
10
  import {
11
11
  checkForUpdate,
12
12
  checkForUpdateCached,
13
13
  getCurrentVersion
14
- } from "./chunk-AOKAQGO4.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";
18
+ import {
19
+ agentMessages,
20
+ approveUser,
21
+ conversationParticipants,
22
+ conversations,
23
+ createUser,
24
+ deleteAgentUser,
25
+ getDb,
26
+ getOrCreateAgentUser,
27
+ getUser,
28
+ getUserByUsername,
29
+ listPendingUsers,
30
+ listUsers,
31
+ listUsersByType,
32
+ messages,
33
+ sessions,
34
+ users,
35
+ verifyUser
36
+ } from "./chunk-ECPQXRLB.js";
21
37
  import {
22
38
  readVoluteConfig,
23
39
  writeVoluteConfig
24
40
  } from "./chunk-NETNFBA5.js";
25
41
  import {
26
42
  loadMergedEnv
27
- } from "./chunk-H7AMDUIA.js";
28
- import "./chunk-BX7KI4S3.js";
43
+ } from "./chunk-QF22MYDJ.js";
44
+ import {
45
+ slugify,
46
+ writeChannelEntry
47
+ } from "./chunk-N6MLQ26B.js";
29
48
  import {
30
49
  applyIsolation
31
- } from "./chunk-W76KWE23.js";
50
+ } from "./chunk-IQXBMFZG.js";
32
51
  import {
33
52
  resolveVoluteBin
34
- } from "./chunk-5SKQ6J7T.js";
53
+ } from "./chunk-5C5JWR2L.js";
35
54
  import {
36
55
  agentDir,
37
56
  checkHealth,
@@ -45,15 +64,15 @@ import {
45
64
  removeAllVariants,
46
65
  setAgentRunning,
47
66
  setVariantRunning,
67
+ stateDir,
68
+ validateBranchName,
48
69
  voluteHome
49
- } from "./chunk-UWHWAPGO.js";
50
- import {
51
- __export
52
- } from "./chunk-K3NQKI34.js";
70
+ } from "./chunk-DP2DX4WV.js";
71
+ import "./chunk-K3NQKI34.js";
53
72
 
54
73
  // src/daemon.ts
55
74
  import { randomBytes } from "crypto";
56
- 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";
57
76
  import { homedir } from "os";
58
77
  import { resolve as resolve10 } from "path";
59
78
  import { format } from "util";
@@ -166,13 +185,13 @@ var ConnectorManager = class {
166
185
  }
167
186
  }
168
187
  }
169
- checkConnectorEnv(type, agentDir2) {
188
+ checkConnectorEnv(type, agentName, agentDir2) {
170
189
  const agentConnectorDir = resolve2(agentDir2, "connectors", type);
171
190
  const userConnectorDir = resolve2(voluteHome(), "connectors", type);
172
191
  const connectorDir = existsSync2(agentConnectorDir) ? agentConnectorDir : existsSync2(userConnectorDir) ? userConnectorDir : void 0;
173
192
  const def = getConnectorDef(type, connectorDir);
174
193
  if (!def) return null;
175
- const env = loadMergedEnv(agentDir2);
194
+ const env = loadMergedEnv(agentName);
176
195
  const missing = checkMissingEnvVars(def, env);
177
196
  if (missing.length === 0) return null;
178
197
  return {
@@ -200,7 +219,7 @@ var ConnectorManager = class {
200
219
  });
201
220
  this.connectors.get(agentName)?.delete(type);
202
221
  }
203
- this.killOrphanConnector(agentDir2, type);
222
+ this.killOrphanConnector(agentName, type);
204
223
  const agentConnector = resolve2(agentDir2, "connectors", type, "index.ts");
205
224
  const userConnector = resolve2(voluteHome(), "connectors", type, "index.ts");
206
225
  const builtinConnector = this.resolveBuiltinConnector(type);
@@ -218,10 +237,10 @@ var ConnectorManager = class {
218
237
  } else {
219
238
  throw new Error(`No connector code found for type: ${type}`);
220
239
  }
221
- const logsDir = resolve2(agentDir2, ".volute", "logs");
240
+ const logsDir = resolve2(stateDir(agentName), "logs");
222
241
  mkdirSync(logsDir, { recursive: true });
223
242
  const logStream = new RotatingLog(resolve2(logsDir, `${type}.log`));
224
- const agentEnv = loadMergedEnv(agentDir2);
243
+ const agentEnv = loadMergedEnv(agentName);
225
244
  const prefix = `${type.toUpperCase()}_`;
226
245
  const connectorEnv = Object.fromEntries(
227
246
  Object.entries(agentEnv).filter(([k]) => k.startsWith(prefix))
@@ -249,7 +268,7 @@ var ConnectorManager = class {
249
268
  lastStderr = chunk.toString().trim();
250
269
  });
251
270
  if (child.pid) {
252
- this.saveConnectorPid(agentDir2, type, child.pid);
271
+ this.saveConnectorPid(agentName, type, child.pid);
253
272
  }
254
273
  if (!this.connectors.has(agentName)) {
255
274
  this.connectors.set(agentName, /* @__PURE__ */ new Map());
@@ -313,8 +332,9 @@ var ConnectorManager = class {
313
332
  this.stopping.delete(stopKey);
314
333
  this.restartAttempts.delete(stopKey);
315
334
  try {
316
- this.removeConnectorPid(agentDir(agentName), type);
317
- } catch {
335
+ this.removeConnectorPid(agentName, type);
336
+ } catch (err) {
337
+ console.error(`[daemon] failed to remove PID file for ${type}/${agentName}:`, err);
318
338
  }
319
339
  console.error(`[daemon] stopped connector ${type} for ${agentName}`);
320
340
  }
@@ -338,22 +358,22 @@ var ConnectorManager = class {
338
358
  running: !tracked.child.killed
339
359
  }));
340
360
  }
341
- connectorPidPath(agentDir2, type) {
342
- return resolve2(agentDir2, ".volute", "connectors", `${type}.pid`);
361
+ connectorPidPath(agentName, type) {
362
+ return resolve2(stateDir(agentName), "connectors", `${type}.pid`);
343
363
  }
344
- saveConnectorPid(agentDir2, type, pid) {
345
- const pidPath = this.connectorPidPath(agentDir2, type);
364
+ saveConnectorPid(agentName, type, pid) {
365
+ const pidPath = this.connectorPidPath(agentName, type);
346
366
  mkdirSync(dirname(pidPath), { recursive: true });
347
367
  writeFileSync(pidPath, String(pid));
348
368
  }
349
- removeConnectorPid(agentDir2, type) {
369
+ removeConnectorPid(agentName, type) {
350
370
  try {
351
- unlinkSync(this.connectorPidPath(agentDir2, type));
371
+ unlinkSync(this.connectorPidPath(agentName, type));
352
372
  } catch {
353
373
  }
354
374
  }
355
- killOrphanConnector(agentDir2, type) {
356
- const pidPath = this.connectorPidPath(agentDir2, type);
375
+ killOrphanConnector(agentName, type) {
376
+ const pidPath = this.connectorPidPath(agentName, type);
357
377
  if (!existsSync2(pidPath)) return;
358
378
  try {
359
379
  const pid = parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
@@ -386,8 +406,37 @@ function getConnectorManager() {
386
406
  return instance;
387
407
  }
388
408
 
389
- // src/lib/scheduler.ts
409
+ // src/lib/migrate-state.ts
410
+ import { copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync } from "fs";
390
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";
391
440
  import { CronExpressionParser } from "cron-parser";
392
441
  var Scheduler = class {
393
442
  schedules = /* @__PURE__ */ new Map();
@@ -397,7 +446,7 @@ var Scheduler = class {
397
446
  daemonPort = null;
398
447
  daemonToken = null;
399
448
  get statePath() {
400
- return resolve3(voluteHome(), "scheduler-state.json");
449
+ return resolve4(voluteHome(), "scheduler-state.json");
401
450
  }
402
451
  start(daemonPort, daemonToken) {
403
452
  this.daemonPort = daemonPort ?? null;
@@ -504,18 +553,8 @@ var Scheduler = class {
504
553
  } else {
505
554
  console.error(`[scheduler] fired "${schedule.id}" for ${agentName}`);
506
555
  }
507
- try {
508
- const reader = res.body?.getReader();
509
- if (reader) {
510
- try {
511
- while (!(await reader.read()).done) {
512
- }
513
- } finally {
514
- reader.releaseLock();
515
- }
516
- }
517
- } catch {
518
- }
556
+ await res.text().catch(() => {
557
+ });
519
558
  } catch (err) {
520
559
  console.error(`[scheduler] failed to fire "${schedule.id}" for ${agentName}:`, err);
521
560
  } finally {
@@ -676,18 +715,8 @@ ${summary}`
676
715
  `[token-budget] replayed ${messages2.length} queued message(s) for ${agentName}`
677
716
  );
678
717
  }
679
- try {
680
- const reader = res.body?.getReader();
681
- if (reader) {
682
- try {
683
- while (!(await reader.read()).done) {
684
- }
685
- } finally {
686
- reader.releaseLock();
687
- }
688
- }
689
- } catch {
690
- }
718
+ await res.text().catch(() => {
719
+ });
691
720
  } catch (err) {
692
721
  console.error(`[token-budget] failed to replay for ${agentName}:`, err);
693
722
  const state = this.budgets.get(agentName);
@@ -705,245 +734,9 @@ function getTokenBudget() {
705
734
 
706
735
  // src/web/middleware/auth.ts
707
736
  import { timingSafeEqual } from "crypto";
708
- import { eq as eq2, lt } from "drizzle-orm";
737
+ import { eq, lt } from "drizzle-orm";
709
738
  import { getCookie } from "hono/cookie";
710
739
  import { createMiddleware } from "hono/factory";
711
-
712
- // src/lib/auth.ts
713
- import { compareSync, hashSync } from "bcryptjs";
714
- import { and, count, eq } from "drizzle-orm";
715
-
716
- // src/lib/db.ts
717
- import { chmodSync, existsSync as existsSync3 } from "fs";
718
- import { dirname as dirname2, resolve as resolve4 } from "path";
719
- import { fileURLToPath } from "url";
720
- import { drizzle } from "drizzle-orm/libsql";
721
- import { migrate } from "drizzle-orm/libsql/migrator";
722
-
723
- // src/lib/schema.ts
724
- var schema_exports = {};
725
- __export(schema_exports, {
726
- agentMessages: () => agentMessages,
727
- conversationParticipants: () => conversationParticipants,
728
- conversations: () => conversations,
729
- messages: () => messages,
730
- sessions: () => sessions,
731
- users: () => users
732
- });
733
- import { sql } from "drizzle-orm";
734
- import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
735
- var users = sqliteTable("users", {
736
- id: integer("id").primaryKey({ autoIncrement: true }),
737
- username: text("username").unique().notNull(),
738
- password_hash: text("password_hash").notNull(),
739
- role: text("role").notNull().default("pending"),
740
- user_type: text("user_type").notNull().default("human"),
741
- created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
742
- });
743
- var conversations = sqliteTable(
744
- "conversations",
745
- {
746
- id: text("id").primaryKey(),
747
- agent_name: text("agent_name").notNull(),
748
- channel: text("channel").notNull(),
749
- user_id: integer("user_id").references(() => users.id),
750
- title: text("title"),
751
- created_at: text("created_at").notNull().default(sql`(datetime('now'))`),
752
- updated_at: text("updated_at").notNull().default(sql`(datetime('now'))`)
753
- },
754
- (table) => [
755
- index("idx_conversations_agent_name").on(table.agent_name),
756
- index("idx_conversations_user_id").on(table.user_id),
757
- index("idx_conversations_updated_at").on(table.updated_at)
758
- ]
759
- );
760
- var agentMessages = sqliteTable(
761
- "agent_messages",
762
- {
763
- id: integer("id").primaryKey({ autoIncrement: true }),
764
- agent: text("agent").notNull(),
765
- channel: text("channel").notNull(),
766
- role: text("role").notNull(),
767
- sender: text("sender"),
768
- content: text("content").notNull(),
769
- created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
770
- },
771
- (table) => [
772
- index("idx_agent_messages_agent").on(table.agent),
773
- index("idx_agent_messages_channel").on(table.agent, table.channel)
774
- ]
775
- );
776
- var conversationParticipants = sqliteTable(
777
- "conversation_participants",
778
- {
779
- conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
780
- user_id: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
781
- role: text("role").notNull().default("member"),
782
- joined_at: text("joined_at").notNull().default(sql`(datetime('now'))`)
783
- },
784
- (table) => [
785
- uniqueIndex("idx_cp_unique").on(table.conversation_id, table.user_id),
786
- index("idx_cp_user_id").on(table.user_id)
787
- ]
788
- );
789
- var sessions = sqliteTable("sessions", {
790
- id: text("id").primaryKey(),
791
- userId: integer("user_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
792
- createdAt: integer("created_at").notNull()
793
- });
794
- var messages = sqliteTable(
795
- "messages",
796
- {
797
- id: integer("id").primaryKey({ autoIncrement: true }),
798
- conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
799
- role: text("role").notNull(),
800
- sender_name: text("sender_name"),
801
- content: text("content").notNull(),
802
- created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
803
- },
804
- (table) => [index("idx_messages_conversation_id").on(table.conversation_id)]
805
- );
806
-
807
- // src/lib/db.ts
808
- var __dirname = dirname2(fileURLToPath(import.meta.url));
809
- var migrationsFolder = existsSync3(resolve4(__dirname, "../drizzle")) ? resolve4(__dirname, "../drizzle") : resolve4(__dirname, "../../drizzle");
810
- var db = null;
811
- async function getDb() {
812
- if (db) return db;
813
- const dbPath = process.env.VOLUTE_DB_PATH || resolve4(voluteHome(), "volute.db");
814
- db = drizzle({ connection: { url: `file:${dbPath}` }, schema: schema_exports });
815
- await migrate(db, { migrationsFolder });
816
- try {
817
- chmodSync(dbPath, 384);
818
- } catch (err) {
819
- console.error(
820
- `[volute] WARNING: Failed to restrict database file permissions on ${dbPath}:`,
821
- err
822
- );
823
- }
824
- return db;
825
- }
826
-
827
- // src/lib/auth.ts
828
- async function createUser(username, password) {
829
- const db2 = await getDb();
830
- const hash = hashSync(password, 10);
831
- const [{ value }] = await db2.select({ value: count() }).from(users).where(eq(users.user_type, "human"));
832
- const role = value === 0 ? "admin" : "pending";
833
- const [result] = await db2.insert(users).values({ username, password_hash: hash, role }).returning({
834
- id: users.id,
835
- username: users.username,
836
- role: users.role,
837
- user_type: users.user_type,
838
- created_at: users.created_at
839
- });
840
- return result;
841
- }
842
- async function verifyUser(username, password) {
843
- const db2 = await getDb();
844
- const row = await db2.select().from(users).where(eq(users.username, username)).get();
845
- if (!row) return null;
846
- if (row.user_type === "agent") return null;
847
- if (!compareSync(password, row.password_hash)) return null;
848
- const { password_hash: _, ...user } = row;
849
- return user;
850
- }
851
- async function getUser(id) {
852
- const db2 = await getDb();
853
- const row = await db2.select({
854
- id: users.id,
855
- username: users.username,
856
- role: users.role,
857
- user_type: users.user_type,
858
- created_at: users.created_at
859
- }).from(users).where(eq(users.id, id)).get();
860
- return row ?? null;
861
- }
862
- async function getUserByUsername(username) {
863
- const db2 = await getDb();
864
- const row = await db2.select({
865
- id: users.id,
866
- username: users.username,
867
- role: users.role,
868
- user_type: users.user_type,
869
- created_at: users.created_at
870
- }).from(users).where(eq(users.username, username)).get();
871
- return row ?? null;
872
- }
873
- async function listUsers() {
874
- const db2 = await getDb();
875
- return db2.select({
876
- id: users.id,
877
- username: users.username,
878
- role: users.role,
879
- user_type: users.user_type,
880
- created_at: users.created_at
881
- }).from(users).orderBy(users.created_at).all();
882
- }
883
- async function listPendingUsers() {
884
- const db2 = await getDb();
885
- return db2.select({
886
- id: users.id,
887
- username: users.username,
888
- role: users.role,
889
- user_type: users.user_type,
890
- created_at: users.created_at
891
- }).from(users).where(eq(users.role, "pending")).orderBy(users.created_at).all();
892
- }
893
- async function listUsersByType(userType) {
894
- const db2 = await getDb();
895
- return db2.select({
896
- id: users.id,
897
- username: users.username,
898
- role: users.role,
899
- user_type: users.user_type,
900
- created_at: users.created_at
901
- }).from(users).where(eq(users.user_type, userType)).orderBy(users.created_at).all();
902
- }
903
- async function getOrCreateAgentUser(agentName) {
904
- const db2 = await getDb();
905
- const existing = await db2.select({
906
- id: users.id,
907
- username: users.username,
908
- role: users.role,
909
- user_type: users.user_type,
910
- created_at: users.created_at
911
- }).from(users).where(and(eq(users.username, agentName), eq(users.user_type, "agent"))).get();
912
- if (existing) return existing;
913
- try {
914
- const [result] = await db2.insert(users).values({
915
- username: agentName,
916
- password_hash: "!agent",
917
- role: "agent",
918
- user_type: "agent"
919
- }).returning({
920
- id: users.id,
921
- username: users.username,
922
- role: users.role,
923
- user_type: users.user_type,
924
- created_at: users.created_at
925
- });
926
- return result;
927
- } catch (err) {
928
- if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
929
- const retried = await db2.select({
930
- id: users.id,
931
- username: users.username,
932
- role: users.role,
933
- user_type: users.user_type,
934
- created_at: users.created_at
935
- }).from(users).where(and(eq(users.username, agentName), eq(users.user_type, "agent"))).get();
936
- if (retried) return retried;
937
- }
938
- throw err;
939
- }
940
- }
941
- async function approveUser(id) {
942
- const db2 = await getDb();
943
- await db2.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
944
- }
945
-
946
- // src/web/middleware/auth.ts
947
740
  function isValidDaemonToken(token) {
948
741
  const expected = process.env.VOLUTE_DAEMON_TOKEN;
949
742
  if (!expected || token.length !== expected.length) return false;
@@ -951,29 +744,29 @@ function isValidDaemonToken(token) {
951
744
  }
952
745
  var SESSION_MAX_AGE = 864e5;
953
746
  async function createSession(userId) {
954
- const db2 = await getDb();
747
+ const db = await getDb();
955
748
  const sessionId = crypto.randomUUID();
956
- await db2.insert(sessions).values({ id: sessionId, userId, createdAt: Date.now() });
749
+ await db.insert(sessions).values({ id: sessionId, userId, createdAt: Date.now() });
957
750
  return sessionId;
958
751
  }
959
752
  async function deleteSession(sessionId) {
960
- const db2 = await getDb();
961
- await db2.delete(sessions).where(eq2(sessions.id, sessionId));
753
+ const db = await getDb();
754
+ await db.delete(sessions).where(eq(sessions.id, sessionId));
962
755
  }
963
756
  async function getSessionUserId(sessionId) {
964
- const db2 = await getDb();
965
- const row = await db2.select().from(sessions).where(eq2(sessions.id, sessionId)).get();
757
+ const db = await getDb();
758
+ const row = await db.select().from(sessions).where(eq(sessions.id, sessionId)).get();
966
759
  if (!row) return void 0;
967
760
  if (Date.now() - row.createdAt > SESSION_MAX_AGE) {
968
- await db2.delete(sessions).where(eq2(sessions.id, sessionId));
761
+ await db.delete(sessions).where(eq(sessions.id, sessionId));
969
762
  return void 0;
970
763
  }
971
764
  return row.userId;
972
765
  }
973
766
  async function cleanExpiredSessions() {
974
- const db2 = await getDb();
767
+ const db = await getDb();
975
768
  const cutoff = Date.now() - SESSION_MAX_AGE;
976
- await db2.delete(sessions).where(lt(sessions.createdAt, cutoff));
769
+ await db.delete(sessions).where(lt(sessions.createdAt, cutoff));
977
770
  }
978
771
  var requireAdmin = createMiddleware(async (c, next) => {
979
772
  const user = c.get("user");
@@ -1006,7 +799,7 @@ var authMiddleware = createMiddleware(async (c, next) => {
1006
799
  // src/web/server.ts
1007
800
  import { existsSync as existsSync7 } from "fs";
1008
801
  import { readFile as readFile2, stat } from "fs/promises";
1009
- import { dirname as dirname3, extname, resolve as resolve9 } from "path";
802
+ import { dirname as dirname2, extname, resolve as resolve9 } from "path";
1010
803
  import { serve } from "@hono/node-server";
1011
804
 
1012
805
  // src/lib/log-buffer.ts
@@ -1060,50 +853,12 @@ import { csrf } from "hono/csrf";
1060
853
  import { HTTPException } from "hono/http-exception";
1061
854
 
1062
855
  // src/web/routes/agents.ts
856
+ import { execFile } from "child_process";
1063
857
  import { existsSync as existsSync4, readFileSync as readFileSync3, rmSync } from "fs";
1064
858
  import { resolve as resolve5 } from "path";
1065
- import { and as and2, desc, eq as eq3 } from "drizzle-orm";
859
+ import { promisify } from "util";
860
+ import { and, desc, eq as eq2 } from "drizzle-orm";
1066
861
  import { Hono } from "hono";
1067
- import { stream } from "hono/streaming";
1068
-
1069
- // src/lib/ndjson.ts
1070
- var MAX_BUFFER_SIZE = 1e6;
1071
- async function* readNdjson(body) {
1072
- const reader = body.getReader();
1073
- const decoder = new TextDecoder();
1074
- let buffer = "";
1075
- try {
1076
- while (true) {
1077
- const { done, value } = await reader.read();
1078
- if (done) break;
1079
- buffer += decoder.decode(value, { stream: true });
1080
- if (buffer.length > MAX_BUFFER_SIZE) {
1081
- logger_default.warn("ndjson: buffer exceeded 1MB, resetting");
1082
- buffer = "";
1083
- continue;
1084
- }
1085
- const lines = buffer.split("\n");
1086
- buffer = lines.pop() || "";
1087
- for (const line of lines) {
1088
- if (!line.trim()) continue;
1089
- try {
1090
- yield JSON.parse(line);
1091
- } catch {
1092
- logger_default.warn("ndjson: skipping invalid line", { line: line.slice(0, 100) });
1093
- }
1094
- }
1095
- }
1096
- if (buffer.trim()) {
1097
- try {
1098
- yield JSON.parse(buffer);
1099
- } catch {
1100
- logger_default.warn("ndjson: skipping invalid line", { line: buffer.slice(0, 100) });
1101
- }
1102
- }
1103
- } finally {
1104
- reader.releaseLock();
1105
- }
1106
- }
1107
862
 
1108
863
  // src/lib/typing.ts
1109
864
  var DEFAULT_TTL_MS = 1e4;
@@ -1173,11 +928,38 @@ function getTypingMap() {
1173
928
  }
1174
929
 
1175
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
+ }
1176
955
  function getDaemonPort() {
1177
956
  try {
1178
957
  const data = JSON.parse(readFileSync3(resolve5(voluteHome(), "daemon.json"), "utf-8"));
1179
958
  return data.port;
1180
- } catch {
959
+ } catch (err) {
960
+ if (err?.code !== "ENOENT") {
961
+ console.error("[daemon] failed to read daemon.json:", err);
962
+ }
1181
963
  return void 0;
1182
964
  }
1183
965
  }
@@ -1252,25 +1034,11 @@ var app = new Hono().get("/", async (c) => {
1252
1034
  const dir = agentDir(baseName);
1253
1035
  if (!existsSync4(dir)) return c.json({ error: "Agent directory missing" }, 404);
1254
1036
  }
1255
- const manager = getAgentManager();
1256
- if (manager.isRunning(name)) {
1037
+ if (getAgentManager().isRunning(name)) {
1257
1038
  return c.json({ error: "Agent already running" }, 409);
1258
1039
  }
1259
1040
  try {
1260
- await manager.startAgent(name);
1261
- if (!variantName) {
1262
- const dir = agentDir(baseName);
1263
- await getConnectorManager().startConnectors(baseName, dir, entry.port, getDaemonPort());
1264
- getScheduler().loadSchedules(baseName);
1265
- const config = readVoluteConfig(dir);
1266
- if (config?.tokenBudget) {
1267
- getTokenBudget().setBudget(
1268
- baseName,
1269
- config.tokenBudget,
1270
- config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
1271
- );
1272
- }
1273
- }
1041
+ await startAgentFull(name, baseName, variantName);
1274
1042
  return c.json({ ok: true });
1275
1043
  } catch (err) {
1276
1044
  return c.json({ error: err instanceof Error ? err.message : "Failed to start agent" }, 500);
@@ -1287,30 +1055,52 @@ var app = new Hono().get("/", async (c) => {
1287
1055
  const dir = agentDir(baseName);
1288
1056
  if (!existsSync4(dir)) return c.json({ error: "Agent directory missing" }, 404);
1289
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
+ }
1067
+ }
1290
1068
  const manager = getAgentManager();
1291
- const connectorManager = getConnectorManager();
1292
1069
  try {
1293
1070
  if (manager.isRunning(name)) {
1294
1071
  if (!variantName) {
1295
- await connectorManager.stopConnectors(baseName);
1072
+ await getConnectorManager().stopConnectors(baseName);
1296
1073
  getTokenBudget().removeBudget(baseName);
1297
1074
  }
1298
1075
  await manager.stopAgent(name);
1299
1076
  }
1300
- await manager.startAgent(name);
1301
- if (!variantName) {
1302
- const dir = agentDir(baseName);
1303
- await connectorManager.startConnectors(baseName, dir, entry.port, getDaemonPort());
1304
- getScheduler().loadSchedules(baseName);
1305
- const config = readVoluteConfig(dir);
1306
- if (config?.tokenBudget) {
1307
- getTokenBudget().setBudget(
1308
- baseName,
1309
- config.tokenBudget,
1310
- config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
1311
- );
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);
1312
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
+ });
1099
+ }
1100
+ if (context) {
1101
+ manager.setPendingContext(name, context);
1313
1102
  }
1103
+ await startAgentFull(name, baseName, variantName);
1314
1104
  return c.json({ ok: true });
1315
1105
  } catch (err) {
1316
1106
  return c.json({ error: err instanceof Error ? err.message : "Failed to restart agent" }, 500);
@@ -1353,6 +1143,11 @@ var app = new Hono().get("/", async (c) => {
1353
1143
  }
1354
1144
  removeAllVariants(name);
1355
1145
  removeAgent(name);
1146
+ await deleteAgentUser(name);
1147
+ const state = stateDir(name);
1148
+ if (existsSync4(state)) {
1149
+ rmSync(state, { recursive: true, force: true });
1150
+ }
1356
1151
  if (force && existsSync4(dir)) {
1357
1152
  rmSync(dir, { recursive: true, force: true });
1358
1153
  }
@@ -1379,23 +1174,15 @@ var app = new Hono().get("/", async (c) => {
1379
1174
  console.error(`[daemon] failed to parse message body for ${baseName}:`, err);
1380
1175
  }
1381
1176
  const channel = parsed?.channel ?? "unknown";
1382
- const db2 = await getDb();
1177
+ const db = await getDb();
1383
1178
  if (parsed) {
1384
1179
  try {
1385
- const sender = parsed.sender ?? null;
1386
- let content;
1387
- if (typeof parsed.content === "string") {
1388
- content = parsed.content;
1389
- } else if (Array.isArray(parsed.content)) {
1390
- content = parsed.content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
1391
- } else {
1392
- content = JSON.stringify(parsed.content);
1393
- }
1394
- await db2.insert(agentMessages).values({
1180
+ const sender2 = parsed.sender ?? null;
1181
+ const content = extractTextContent(parsed.content);
1182
+ await db.insert(agentMessages).values({
1395
1183
  agent: baseName,
1396
1184
  channel,
1397
- role: "user",
1398
- sender,
1185
+ sender: sender2,
1399
1186
  content
1400
1187
  });
1401
1188
  } catch (err) {
@@ -1405,33 +1192,17 @@ var app = new Hono().get("/", async (c) => {
1405
1192
  const budget = getTokenBudget();
1406
1193
  const budgetStatus = budget.checkBudget(baseName);
1407
1194
  if (budgetStatus === "exceeded") {
1408
- let textContent = "";
1409
- if (parsed) {
1410
- if (typeof parsed.content === "string") {
1411
- textContent = parsed.content;
1412
- } else if (Array.isArray(parsed.content)) {
1413
- textContent = parsed.content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
1414
- }
1415
- }
1195
+ const textContent = parsed ? extractTextContent(parsed.content) : "";
1416
1196
  budget.enqueue(baseName, {
1417
1197
  channel,
1418
1198
  sender: parsed?.sender ?? null,
1419
1199
  textContent
1420
1200
  });
1421
- c.header("Content-Type", "application/x-ndjson");
1422
- const encoder2 = new TextEncoder();
1423
- return stream(c, async (s) => {
1424
- await s.write(
1425
- encoder2.encode(
1426
- `${JSON.stringify({ type: "text", content: "[Token budget exceeded \u2014 message queued for next period]" })}
1427
- `
1428
- )
1429
- );
1430
- await s.write(encoder2.encode(`${JSON.stringify({ type: "done" })}
1431
- `));
1432
- });
1201
+ return c.json({ error: "Token budget exceeded \u2014 message queued for next period" }, 429);
1433
1202
  }
1434
1203
  const typingMap = getTypingMap();
1204
+ const sender = parsed?.sender ?? "";
1205
+ if (sender) typingMap.delete(channel, sender);
1435
1206
  const currentlyTyping = typingMap.get(channel).filter((s) => s !== baseName);
1436
1207
  let forwardBody = body;
1437
1208
  if (parsed && currentlyTyping.length > 0) {
@@ -1451,63 +1222,38 @@ var app = new Hono().get("/", async (c) => {
1451
1222
  budget.acknowledgeWarning(baseName);
1452
1223
  forwardBody = JSON.stringify(parsed);
1453
1224
  }
1454
- 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 });
1455
1228
  try {
1456
- res = await fetch(`http://127.0.0.1:${port}/message`, {
1229
+ const res = await fetch(`http://127.0.0.1:${port}/message`, {
1457
1230
  method: "POST",
1458
1231
  headers: { "Content-Type": "application/json" },
1459
1232
  body: forwardBody
1460
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 });
1461
1250
  } catch (err) {
1462
1251
  console.error(`[daemon] agent ${name} unreachable on port ${port}:`, err);
1463
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);
1464
1256
  }
1465
- if (!res.ok) {
1466
- return c.json({ error: `Agent responded with ${res.status}` }, res.status);
1467
- }
1468
- if (!res.body) {
1469
- return c.json({ error: "No response body from agent" }, 502);
1470
- }
1471
- c.header("Content-Type", "application/x-ndjson");
1472
- const encoder = new TextEncoder();
1473
- typingMap.set(channel, baseName, { persistent: true });
1474
- return stream(c, async (s) => {
1475
- try {
1476
- const textParts = [];
1477
- const toolParts = [];
1478
- for await (const event of readNdjson(res.body)) {
1479
- if (event.type === "usage") {
1480
- const input = typeof event.input_tokens === "number" ? event.input_tokens : 0;
1481
- const output = typeof event.output_tokens === "number" ? event.output_tokens : 0;
1482
- budget.recordUsage(baseName, input, output);
1483
- continue;
1484
- }
1485
- await s.write(encoder.encode(`${JSON.stringify(event)}
1486
- `));
1487
- const part = collectPart(event);
1488
- if (part != null) {
1489
- if (event.type === "tool_use") toolParts.push(part);
1490
- else textParts.push(part);
1491
- }
1492
- }
1493
- const content = [textParts.join(""), ...toolParts].filter(Boolean).join("\n");
1494
- if (content) {
1495
- try {
1496
- await db2.insert(agentMessages).values({
1497
- agent: baseName,
1498
- channel,
1499
- role: "assistant",
1500
- sender: baseName,
1501
- content
1502
- });
1503
- } catch (err) {
1504
- console.error(`[daemon] failed to persist assistant response for ${baseName}:`, err);
1505
- }
1506
- }
1507
- } finally {
1508
- typingMap.delete(channel, baseName);
1509
- }
1510
- });
1511
1257
  }).get("/:name/budget", async (c) => {
1512
1258
  const name = c.req.param("name");
1513
1259
  const [baseName] = name.split("@", 2);
@@ -1526,13 +1272,12 @@ var app = new Hono().get("/", async (c) => {
1526
1272
  if (!body.channel || !body.content) {
1527
1273
  return c.json({ error: "channel and content required" }, 400);
1528
1274
  }
1529
- const db2 = await getDb();
1275
+ const db = await getDb();
1530
1276
  try {
1531
- await db2.insert(agentMessages).values({
1277
+ await db.insert(agentMessages).values({
1532
1278
  agent: baseName,
1533
1279
  channel: body.channel,
1534
- role: "assistant",
1535
- sender: baseName,
1280
+ sender: body.sender ?? baseName,
1536
1281
  content: body.content
1537
1282
  });
1538
1283
  } catch (err) {
@@ -1542,20 +1287,20 @@ var app = new Hono().get("/", async (c) => {
1542
1287
  return c.json({ ok: true });
1543
1288
  }).get("/:name/history/channels", async (c) => {
1544
1289
  const name = c.req.param("name");
1545
- const db2 = await getDb();
1546
- const rows = await db2.selectDistinct({ channel: agentMessages.channel }).from(agentMessages).where(eq3(agentMessages.agent, name));
1290
+ const db = await getDb();
1291
+ const rows = await db.selectDistinct({ channel: agentMessages.channel }).from(agentMessages).where(eq2(agentMessages.agent, name));
1547
1292
  return c.json(rows.map((r) => r.channel));
1548
1293
  }).get("/:name/history", async (c) => {
1549
1294
  const name = c.req.param("name");
1550
1295
  const channel = c.req.query("channel");
1551
1296
  const limit = Math.min(Math.max(parseInt(c.req.query("limit") ?? "50", 10) || 50, 1), 200);
1552
1297
  const offset = Math.max(parseInt(c.req.query("offset") ?? "0", 10) || 0, 0);
1553
- const db2 = await getDb();
1554
- const conditions = [eq3(agentMessages.agent, name)];
1298
+ const db = await getDb();
1299
+ const conditions = [eq2(agentMessages.agent, name)];
1555
1300
  if (channel) {
1556
- conditions.push(eq3(agentMessages.channel, channel));
1301
+ conditions.push(eq2(agentMessages.channel, channel));
1557
1302
  }
1558
- const rows = await db2.select().from(agentMessages).where(and2(...conditions)).orderBy(desc(agentMessages.created_at)).limit(limit).offset(offset);
1303
+ const rows = await db.select().from(agentMessages).where(and(...conditions)).orderBy(desc(agentMessages.created_at)).limit(limit).offset(offset);
1559
1304
  return c.json(rows);
1560
1305
  });
1561
1306
  var agents_default = app;
@@ -1658,7 +1403,7 @@ var app3 = new Hono3().get("/:name/connectors", (c) => {
1658
1403
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1659
1404
  const dir = agentDir(name);
1660
1405
  const manager = getConnectorManager();
1661
- const envCheck = manager.checkConnectorEnv(type, dir);
1406
+ const envCheck = manager.checkConnectorEnv(type, name, dir);
1662
1407
  if (envCheck) {
1663
1408
  return c.json(
1664
1409
  {
@@ -1704,13 +1449,10 @@ var connectors_default = app3;
1704
1449
 
1705
1450
  // src/web/routes/files.ts
1706
1451
  import { existsSync as existsSync5 } from "fs";
1707
- import { readdir, readFile, writeFile } from "fs/promises";
1452
+ import { readdir, readFile } from "fs/promises";
1708
1453
  import { resolve as resolve6 } from "path";
1709
- import { zValidator as zValidator2 } from "@hono/zod-validator";
1710
1454
  import { Hono as Hono4 } from "hono";
1711
- import { z as z2 } from "zod";
1712
1455
  var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
1713
- var saveFileSchema = z2.object({ content: z2.string() });
1714
1456
  var app4 = new Hono4().get("/:name/files", async (c) => {
1715
1457
  const name = c.req.param("name");
1716
1458
  const entry = findAgent(name);
@@ -1736,19 +1478,6 @@ var app4 = new Hono4().get("/:name/files", async (c) => {
1736
1478
  }
1737
1479
  const content = await readFile(filePath, "utf-8");
1738
1480
  return c.json({ filename, content });
1739
- }).put("/:name/files/:filename", zValidator2("json", saveFileSchema), async (c) => {
1740
- const name = c.req.param("name");
1741
- const filename = c.req.param("filename");
1742
- if (!ALLOWED_FILES.has(filename)) {
1743
- return c.json({ error: "File not allowed" }, 403);
1744
- }
1745
- const entry = findAgent(name);
1746
- if (!entry) return c.json({ error: "Agent not found" }, 404);
1747
- const dir = agentDir(name);
1748
- const filePath = resolve6(dir, "home", filename);
1749
- const { content } = c.req.valid("json");
1750
- await writeFile(filePath, content);
1751
- return c.json({ ok: true });
1752
1481
  });
1753
1482
  var files_default = app4;
1754
1483
 
@@ -1762,29 +1491,28 @@ var app5 = new Hono5().get("/:name/logs", async (c) => {
1762
1491
  const name = c.req.param("name");
1763
1492
  const entry = findAgent(name);
1764
1493
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1765
- const dir = agentDir(name);
1766
- const logFile = resolve7(dir, ".volute", "logs", "agent.log");
1494
+ const logFile = resolve7(stateDir(name), "logs", "agent.log");
1767
1495
  if (!existsSync6(logFile)) {
1768
1496
  return c.json({ error: "No log file found" }, 404);
1769
1497
  }
1770
- return streamSSE(c, async (stream2) => {
1498
+ return streamSSE(c, async (stream) => {
1771
1499
  const tail = spawn2("tail", ["-n", "200", "-f", logFile]);
1772
1500
  const onData = (data) => {
1773
1501
  const lines = data.toString().split("\n");
1774
1502
  for (const line of lines) {
1775
1503
  if (line) {
1776
- stream2.writeSSE({ data: line }).catch(() => {
1504
+ stream.writeSSE({ data: line }).catch(() => {
1777
1505
  });
1778
1506
  }
1779
1507
  }
1780
1508
  };
1781
1509
  tail.stdout.on("data", onData);
1782
- stream2.onAbort(() => {
1510
+ stream.onAbort(() => {
1783
1511
  tail.kill();
1784
1512
  });
1785
1513
  await new Promise((resolve11) => {
1786
1514
  tail.on("exit", resolve11);
1787
- stream2.onAbort(resolve11);
1515
+ stream.onAbort(resolve11);
1788
1516
  });
1789
1517
  });
1790
1518
  });
@@ -1878,16 +1606,16 @@ import { streamSSE as streamSSE2 } from "hono/streaming";
1878
1606
  var app7 = new Hono7().get("/logs", async (c) => {
1879
1607
  const user = c.get("user");
1880
1608
  if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
1881
- return streamSSE2(c, async (stream2) => {
1609
+ return streamSSE2(c, async (stream) => {
1882
1610
  for (const entry of logBuffer.getEntries()) {
1883
- await stream2.writeSSE({ data: JSON.stringify(entry) });
1611
+ await stream.writeSSE({ data: JSON.stringify(entry) });
1884
1612
  }
1885
1613
  const unsubscribe = logBuffer.subscribe((entry) => {
1886
- stream2.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
1614
+ stream.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
1887
1615
  });
1888
1616
  });
1889
1617
  await new Promise((resolve11) => {
1890
- stream2.onAbort(() => {
1618
+ stream.onAbort(() => {
1891
1619
  unsubscribe();
1892
1620
  resolve11();
1893
1621
  });
@@ -1897,15 +1625,15 @@ var app7 = new Hono7().get("/logs", async (c) => {
1897
1625
  var system_default = app7;
1898
1626
 
1899
1627
  // src/web/routes/typing.ts
1900
- import { zValidator as zValidator3 } from "@hono/zod-validator";
1628
+ import { zValidator as zValidator2 } from "@hono/zod-validator";
1901
1629
  import { Hono as Hono8 } from "hono";
1902
- import { z as z3 } from "zod";
1903
- var typingSchema = z3.object({
1904
- channel: z3.string().min(1),
1905
- sender: z3.string().min(1),
1906
- 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()
1907
1635
  });
1908
- var app8 = new Hono8().post("/:name/typing", zValidator3("json", typingSchema), (c) => {
1636
+ var app8 = new Hono8().post("/:name/typing", zValidator2("json", typingSchema), (c) => {
1909
1637
  const { channel, sender, active } = c.req.valid("json");
1910
1638
  const map = getTypingMap();
1911
1639
  if (active) {
@@ -1966,18 +1694,46 @@ var variants_default = app10;
1966
1694
  // src/web/routes/volute/chat.ts
1967
1695
  import { readFileSync as readFileSync4 } from "fs";
1968
1696
  import { resolve as resolve8 } from "path";
1969
- import { zValidator as zValidator4 } from "@hono/zod-validator";
1697
+ import { zValidator as zValidator3 } from "@hono/zod-validator";
1970
1698
  import { Hono as Hono11 } from "hono";
1971
1699
  import { streamSSE as streamSSE3 } from "hono/streaming";
1972
- 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
+ }
1973
1729
 
1974
1730
  // src/lib/conversations.ts
1975
1731
  import { randomUUID } from "crypto";
1976
- import { and as and3, desc as desc2, eq as eq4, inArray, isNull, sql as sql2 } from "drizzle-orm";
1732
+ import { and as and2, desc as desc2, eq as eq3, inArray, isNull, sql } from "drizzle-orm";
1977
1733
  async function createConversation(agentName, channel, opts) {
1978
- const db2 = await getDb();
1734
+ const db = await getDb();
1979
1735
  const id = randomUUID();
1980
- await db2.insert(conversations).values({
1736
+ await db.insert(conversations).values({
1981
1737
  id,
1982
1738
  agent_name: agentName,
1983
1739
  channel,
@@ -1985,7 +1741,7 @@ async function createConversation(agentName, channel, opts) {
1985
1741
  title: opts?.title ?? null
1986
1742
  });
1987
1743
  if (opts?.participantIds && opts.participantIds.length > 0) {
1988
- await db2.insert(conversationParticipants).values(
1744
+ await db.insert(conversationParticipants).values(
1989
1745
  opts.participantIds.map((uid, i) => ({
1990
1746
  conversation_id: id,
1991
1747
  user_id: uid,
@@ -2004,41 +1760,41 @@ async function createConversation(agentName, channel, opts) {
2004
1760
  };
2005
1761
  }
2006
1762
  async function getConversation(id) {
2007
- const db2 = await getDb();
2008
- const row = await db2.select().from(conversations).where(eq4(conversations.id, id)).get();
1763
+ const db = await getDb();
1764
+ const row = await db.select().from(conversations).where(eq3(conversations.id, id)).get();
2009
1765
  return row ?? null;
2010
1766
  }
2011
1767
  async function getParticipants(conversationId) {
2012
- const db2 = await getDb();
2013
- const rows = await db2.select({
1768
+ const db = await getDb();
1769
+ const rows = await db.select({
2014
1770
  userId: conversationParticipants.user_id,
2015
1771
  username: users.username,
2016
1772
  userType: users.user_type,
2017
1773
  role: conversationParticipants.role
2018
- }).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(eq4(conversationParticipants.conversation_id, conversationId)).all();
1774
+ }).from(conversationParticipants).innerJoin(users, eq3(conversationParticipants.user_id, users.id)).where(eq3(conversationParticipants.conversation_id, conversationId)).all();
2019
1775
  return rows;
2020
1776
  }
2021
1777
  async function isParticipant(conversationId, userId) {
2022
- const db2 = await getDb();
2023
- const row = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
2024
- and3(
2025
- eq4(conversationParticipants.conversation_id, conversationId),
2026
- eq4(conversationParticipants.user_id, userId)
1778
+ const db = await getDb();
1779
+ const row = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
1780
+ and2(
1781
+ eq3(conversationParticipants.conversation_id, conversationId),
1782
+ eq3(conversationParticipants.user_id, userId)
2027
1783
  )
2028
1784
  ).get();
2029
1785
  return row != null;
2030
1786
  }
2031
1787
  async function listConversationsForUser(userId) {
2032
- const db2 = await getDb();
2033
- const participantRows = await db2.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq4(conversationParticipants.user_id, userId)).all();
1788
+ const db = await getDb();
1789
+ const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq3(conversationParticipants.user_id, userId)).all();
2034
1790
  if (participantRows.length === 0) return [];
2035
1791
  const convIds = participantRows.map((r) => r.conversation_id);
2036
- return db2.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc2(conversations.updated_at)).all();
1792
+ return db.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc2(conversations.updated_at)).all();
2037
1793
  }
2038
1794
  async function isParticipantOrOwner(conversationId, userId) {
2039
1795
  if (await isParticipant(conversationId, userId)) return true;
2040
- const db2 = await getDb();
2041
- const row = await db2.select().from(conversations).where(and3(eq4(conversations.id, conversationId), eq4(conversations.user_id, userId))).get();
1796
+ const db = await getDb();
1797
+ const row = await db.select().from(conversations).where(and2(eq3(conversations.id, conversationId), eq3(conversations.user_id, userId))).get();
2042
1798
  return row != null;
2043
1799
  }
2044
1800
  async function deleteConversationForUser(id, userId) {
@@ -2047,18 +1803,18 @@ async function deleteConversationForUser(id, userId) {
2047
1803
  return true;
2048
1804
  }
2049
1805
  async function addMessage(conversationId, role, senderName, content) {
2050
- const db2 = await getDb();
1806
+ const db = await getDb();
2051
1807
  const serialized = JSON.stringify(content);
2052
- const [result] = await db2.insert(messages).values({ conversation_id: conversationId, role, sender_name: senderName, content: serialized }).returning({ id: messages.id, created_at: messages.created_at });
2053
- await db2.update(conversations).set({ updated_at: sql2`datetime('now')` }).where(eq4(conversations.id, conversationId));
1808
+ const [result] = await db.insert(messages).values({ conversation_id: conversationId, role, sender_name: senderName, content: serialized }).returning({ id: messages.id, created_at: messages.created_at });
1809
+ await db.update(conversations).set({ updated_at: sql`datetime('now')` }).where(eq3(conversations.id, conversationId));
2054
1810
  if (role === "user") {
2055
1811
  const firstText = content.find((b) => b.type === "text");
2056
1812
  const title = firstText ? firstText.text.slice(0, 80) : "";
2057
1813
  if (title) {
2058
- await db2.update(conversations).set({ title }).where(and3(eq4(conversations.id, conversationId), isNull(conversations.title)));
1814
+ await db.update(conversations).set({ title }).where(and2(eq3(conversations.id, conversationId), isNull(conversations.title)));
2059
1815
  }
2060
1816
  }
2061
- return {
1817
+ const msg = {
2062
1818
  id: result.id,
2063
1819
  conversation_id: conversationId,
2064
1820
  role,
@@ -2066,10 +1822,19 @@ async function addMessage(conversationId, role, senderName, content) {
2066
1822
  content,
2067
1823
  created_at: result.created_at
2068
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;
2069
1834
  }
2070
1835
  async function getMessages(conversationId) {
2071
- const db2 = await getDb();
2072
- const rows = await db2.select().from(messages).where(eq4(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
1836
+ const db = await getDb();
1837
+ const rows = await db.select().from(messages).where(eq3(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
2073
1838
  return rows.map((row) => {
2074
1839
  let content;
2075
1840
  try {
@@ -2084,15 +1849,15 @@ async function getMessages(conversationId) {
2084
1849
  async function listConversationsWithParticipants(userId) {
2085
1850
  const convs = await listConversationsForUser(userId);
2086
1851
  if (convs.length === 0) return [];
2087
- const db2 = await getDb();
1852
+ const db = await getDb();
2088
1853
  const convIds = convs.map((c) => c.id);
2089
- const rows = await db2.select({
1854
+ const rows = await db.select({
2090
1855
  conversationId: conversationParticipants.conversation_id,
2091
1856
  userId: users.id,
2092
1857
  username: users.username,
2093
1858
  userType: users.user_type,
2094
1859
  role: conversationParticipants.role
2095
- }).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
1860
+ }).from(conversationParticipants).innerJoin(users, eq3(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
2096
1861
  const byConv = /* @__PURE__ */ new Map();
2097
1862
  for (const r of rows) {
2098
1863
  let arr = byConv.get(r.conversationId);
@@ -2110,10 +1875,10 @@ async function listConversationsWithParticipants(userId) {
2110
1875
  return convs.map((c) => ({ ...c, participants: byConv.get(c.id) ?? [] }));
2111
1876
  }
2112
1877
  async function findDMConversation(agentName, participantIds) {
2113
- const db2 = await getDb();
2114
- const agentConvs = await db2.select({ id: conversations.id }).from(conversations).where(eq4(conversations.agent_name, agentName)).all();
1878
+ const db = await getDb();
1879
+ const agentConvs = await db.select({ id: conversations.id }).from(conversations).where(eq3(conversations.agent_name, agentName)).all();
2115
1880
  for (const conv of agentConvs) {
2116
- const rows = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq4(conversationParticipants.conversation_id, conv.id)).all();
1881
+ const rows = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq3(conversationParticipants.conversation_id, conv.id)).all();
2117
1882
  if (rows.length !== 2) continue;
2118
1883
  const ids = new Set(rows.map((r) => r.user_id));
2119
1884
  if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
@@ -2123,25 +1888,29 @@ async function findDMConversation(agentName, participantIds) {
2123
1888
  return null;
2124
1889
  }
2125
1890
  async function deleteConversation(id) {
2126
- const db2 = await getDb();
2127
- await db2.delete(conversations).where(eq4(conversations.id, id));
1891
+ const db = await getDb();
1892
+ await db.delete(conversations).where(eq3(conversations.id, id));
2128
1893
  }
2129
1894
 
2130
1895
  // src/web/routes/volute/chat.ts
2131
- var chatSchema = z4.object({
2132
- message: z4.string().optional(),
2133
- conversationId: z4.string().optional(),
2134
- sender: z4.string().optional(),
2135
- images: z4.array(
2136
- z4.object({
2137
- media_type: z4.string(),
2138
- 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()
2139
1904
  })
2140
1905
  ).optional()
2141
1906
  });
2142
1907
  function getDaemonUrl() {
2143
- const data = JSON.parse(readFileSync4(resolve8(voluteHome(), "daemon.json"), "utf-8"));
2144
- 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
+ }
2145
1914
  }
2146
1915
  function daemonFetchInternal(path, body) {
2147
1916
  const daemonUrl = getDaemonUrl();
@@ -2153,40 +1922,7 @@ function daemonFetchInternal(path, body) {
2153
1922
  if (token) headers.Authorization = `Bearer ${token}`;
2154
1923
  return fetch(`${daemonUrl}${path}`, { method: "POST", headers, body });
2155
1924
  }
2156
- function accumulateEvent(content, event) {
2157
- if (event.type === "text") {
2158
- const last = content[content.length - 1];
2159
- if (last && last.type === "text") last.text += event.content;
2160
- else content.push({ type: "text", text: event.content });
2161
- } else if (event.type === "tool_use") {
2162
- content.push({ type: "tool_use", name: event.name, input: event.input });
2163
- } else if (event.type === "tool_result") {
2164
- content.push({
2165
- type: "tool_result",
2166
- output: event.output,
2167
- ...event.is_error ? { is_error: true } : {}
2168
- });
2169
- }
2170
- }
2171
- async function consumeAndPersist(res, conversationId, agentName) {
2172
- if (!res.body) {
2173
- console.warn(`[chat] no response body from ${agentName}`);
2174
- return [];
2175
- }
2176
- const assistantContent = [];
2177
- for await (const event of readNdjson(res.body)) {
2178
- accumulateEvent(assistantContent, event);
2179
- if (event.type === "done") break;
2180
- }
2181
- if (assistantContent.length === 0) return [];
2182
- try {
2183
- await addMessage(conversationId, "assistant", agentName, assistantContent);
2184
- } catch (err) {
2185
- console.error(`[chat] failed to persist conversation message from ${agentName}:`, err);
2186
- }
2187
- return assistantContent;
2188
- }
2189
- 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) => {
2190
1926
  const name = c.req.param("name");
2191
1927
  const [baseName] = name.split("@", 2);
2192
1928
  const entry = findAgent(baseName);
@@ -2204,7 +1940,6 @@ var app11 = new Hono11().post("/:name/chat", zValidator4("json", chatSchema), as
2204
1940
  return c.json({ error: "Conversation not found" }, 404);
2205
1941
  }
2206
1942
  } else {
2207
- const title = body.message ? body.message.slice(0, 80) : "Image message";
2208
1943
  const participantIds = [];
2209
1944
  if (user.id !== 0) {
2210
1945
  participantIds.push(user.id);
@@ -2223,15 +1958,19 @@ var app11 = new Hono11().post("/:name/chat", zValidator4("json", chatSchema), as
2223
1958
  }
2224
1959
  }
2225
1960
  if (!conversationId) {
2226
- const conv = await createConversation(baseName, "volute", {
1961
+ const participantNames2 = /* @__PURE__ */ new Set([senderName, baseName]);
1962
+ const title = [...participantNames2].join(", ");
1963
+ const conv2 = await createConversation(baseName, "volute", {
2227
1964
  userId: user.id !== 0 ? user.id : void 0,
2228
1965
  title,
2229
1966
  participantIds
2230
1967
  });
2231
- conversationId = conv.id;
1968
+ conversationId = conv2.id;
2232
1969
  }
2233
1970
  }
2234
- const channel = `volute:${conversationId}`;
1971
+ const conv = await getConversation(conversationId);
1972
+ const convTitle = conv?.title;
1973
+ const channel = convTitle ? `volute:${slugify(convTitle)}` : `volute:${conversationId}`;
2235
1974
  const contentBlocks = [];
2236
1975
  if (body.message) {
2237
1976
  contentBlocks.push({ type: "text", text: body.message });
@@ -2245,101 +1984,86 @@ var app11 = new Hono11().post("/:name/chat", zValidator4("json", chatSchema), as
2245
1984
  const participants = await getParticipants(conversationId);
2246
1985
  const agentParticipants = participants.filter((p) => p.userType === "agent");
2247
1986
  const participantNames = participants.map((p) => p.username);
2248
- const { getAgentManager: getAgentManager2 } = await import("./agent-manager-JDVXU3ON.js");
1987
+ const { getAgentManager: getAgentManager2 } = await import("./agent-manager-CMMH5KQQ.js");
2249
1988
  const manager = getAgentManager2();
2250
1989
  const runningAgents = agentParticipants.map((ap) => {
2251
1990
  const agentKey = ap.username === baseName ? name : ap.username;
2252
1991
  return manager.isRunning(agentKey) ? ap.username : null;
2253
1992
  }).filter((n) => n !== null && n !== senderName);
2254
1993
  const isDM = participants.length === 2;
1994
+ const channelEntry = {
1995
+ platformId: conversationId,
1996
+ platform: "volute",
1997
+ name: convTitle ?? void 0,
1998
+ type: isDM ? "dm" : "group"
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
+ }
2255
2007
  const typingMap = getTypingMap();
2256
2008
  const currentlyTyping = typingMap.get(channel);
2257
2009
  const payload = JSON.stringify({
2258
2010
  content: contentBlocks,
2259
2011
  channel,
2012
+ conversationId,
2260
2013
  sender: senderName,
2261
2014
  participants: participantNames,
2262
2015
  participantCount: participants.length,
2263
2016
  isDM,
2264
2017
  ...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
2265
2018
  });
2266
- const responses = [];
2267
2019
  for (const agentName of runningAgents) {
2268
2020
  const targetName = agentName === baseName ? name : agentName;
2269
- try {
2270
- const res = await daemonFetchInternal(
2271
- `/api/agents/${encodeURIComponent(targetName)}/message`,
2272
- payload
2273
- );
2274
- if (res.ok && res.body) {
2275
- responses.push({ name: agentName, res });
2276
- } else {
2277
- const errorBody = await res.text().catch(() => "");
2278
- console.error(
2279
- `[chat] agent ${agentName} responded with ${res.status}: ${errorBody.slice(0, 500)}`
2280
- );
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}`);
2281
2025
  }
2282
- } catch (err) {
2026
+ }).catch((err) => {
2283
2027
  console.error(`[chat] agent ${agentName} unreachable via daemon:`, err);
2284
- }
2285
- }
2286
- if (responses.length === 0) {
2287
- return streamSSE3(c, async (stream2) => {
2288
- await stream2.writeSSE({
2289
- data: JSON.stringify({ type: "meta", conversationId })
2290
- });
2291
- await stream2.writeSSE({ data: JSON.stringify({ type: "sync" }) });
2292
2028
  });
2293
2029
  }
2294
- const primary = responses[0];
2295
- const secondary = responses.slice(1);
2296
- const secondaryPromises = secondary.map((s) => consumeAndPersist(s.res, conversationId, s.name));
2297
- return streamSSE3(c, async (stream2) => {
2298
- await stream2.writeSSE({
2299
- 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
+ });
2300
2042
  });
2301
- const assistantContent = [];
2302
- try {
2303
- for await (const event of readNdjson(primary.res.body)) {
2304
- await stream2.writeSSE({ data: JSON.stringify(event) });
2305
- accumulateEvent(assistantContent, event);
2306
- if (event.type === "done") break;
2307
- }
2308
- } catch (err) {
2309
- console.error(`[chat] error streaming response from ${primary.name}:`, err);
2310
- await stream2.writeSSE({
2311
- 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);
2312
2046
  });
2313
- }
2314
- if (assistantContent.length > 0) {
2315
- try {
2316
- await addMessage(conversationId, "assistant", primary.name, assistantContent);
2317
- } catch (err) {
2318
- console.error(`[chat] failed to persist response from ${primary.name}:`, err);
2319
- }
2320
- }
2321
- const results = await Promise.allSettled(secondaryPromises);
2322
- for (let i = 0; i < results.length; i++) {
2323
- if (results[i].status === "rejected") {
2324
- console.error(
2325
- `[chat] secondary agent ${secondary[i].name} response failed:`,
2326
- results[i].reason
2327
- );
2328
- }
2329
- }
2330
- 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
+ });
2331
2055
  });
2332
2056
  });
2333
2057
  var chat_default = app11;
2334
2058
 
2335
2059
  // src/web/routes/volute/conversations.ts
2336
- import { zValidator as zValidator5 } from "@hono/zod-validator";
2060
+ import { zValidator as zValidator4 } from "@hono/zod-validator";
2337
2061
  import { Hono as Hono12 } from "hono";
2338
- import { z as z5 } from "zod";
2339
- var createConvSchema = z5.object({
2340
- title: z5.string().optional(),
2341
- participantIds: z5.array(z5.number()).optional(),
2342
- 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()
2343
2067
  });
2344
2068
  var app12 = new Hono12().get("/:name/conversations", async (c) => {
2345
2069
  const name = c.req.param("name");
@@ -2352,7 +2076,7 @@ var app12 = new Hono12().get("/:name/conversations", async (c) => {
2352
2076
  const all = await listConversationsForUser(lookupId);
2353
2077
  const convs = all.filter((c2) => c2.agent_name === name);
2354
2078
  return c.json(convs);
2355
- }).post("/:name/conversations", zValidator5("json", createConvSchema), async (c) => {
2079
+ }).post("/:name/conversations", zValidator4("json", createConvSchema), async (c) => {
2356
2080
  const name = c.req.param("name");
2357
2081
  const user = c.get("user");
2358
2082
  const body = c.req.valid("json");
@@ -2393,9 +2117,13 @@ var app12 = new Hono12().get("/:name/conversations", async (c) => {
2393
2117
  console.warn(`[conversations] DM conversation ${existingId} found but not retrievable`);
2394
2118
  }
2395
2119
  }
2120
+ let title = body.title;
2121
+ if (!title && body.participantNames?.length) {
2122
+ title = body.participantNames.join(", ");
2123
+ }
2396
2124
  const conv = await createConversation(name, "volute", {
2397
2125
  userId: user.id !== 0 ? user.id : void 0,
2398
- title: body.title,
2126
+ title,
2399
2127
  participantIds
2400
2128
  });
2401
2129
  return c.json(conv, 201);
@@ -2425,12 +2153,12 @@ var app12 = new Hono12().get("/:name/conversations", async (c) => {
2425
2153
  var conversations_default = app12;
2426
2154
 
2427
2155
  // src/web/routes/volute/user-conversations.ts
2428
- import { zValidator as zValidator6 } from "@hono/zod-validator";
2156
+ import { zValidator as zValidator5 } from "@hono/zod-validator";
2429
2157
  import { Hono as Hono13 } from "hono";
2430
- import { z as z6 } from "zod";
2431
- var createSchema = z6.object({
2432
- title: z6.string().optional(),
2433
- 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)
2434
2162
  });
2435
2163
  var app13 = new Hono13().use("*", authMiddleware).get("/", async (c) => {
2436
2164
  const user = c.get("user");
@@ -2444,7 +2172,7 @@ var app13 = new Hono13().use("*", authMiddleware).get("/", async (c) => {
2444
2172
  }
2445
2173
  const msgs = await getMessages(id);
2446
2174
  return c.json(msgs);
2447
- }).post("/", zValidator6("json", createSchema), async (c) => {
2175
+ }).post("/", zValidator5("json", createSchema), async (c) => {
2448
2176
  const user = c.get("user");
2449
2177
  const body = c.req.valid("json");
2450
2178
  const participantIds = /* @__PURE__ */ new Set();
@@ -2548,14 +2276,14 @@ async function startServer({
2548
2276
  hostname = "127.0.0.1"
2549
2277
  }) {
2550
2278
  let assetsDir = "";
2551
- let searchDir = dirname3(new URL(import.meta.url).pathname);
2279
+ let searchDir = dirname2(new URL(import.meta.url).pathname);
2552
2280
  for (let i = 0; i < 5; i++) {
2553
2281
  const candidate = resolve9(searchDir, "dist", "web-assets");
2554
2282
  if (existsSync7(candidate)) {
2555
2283
  assetsDir = candidate;
2556
2284
  break;
2557
2285
  }
2558
- searchDir = dirname3(searchDir);
2286
+ searchDir = dirname2(searchDir);
2559
2287
  }
2560
2288
  if (assetsDir) {
2561
2289
  app_default.get("*", async (c) => {
@@ -2611,9 +2339,10 @@ async function startDaemon(opts) {
2611
2339
  }
2612
2340
  const DAEMON_PID_PATH = resolve10(home, "daemon.pid");
2613
2341
  const DAEMON_JSON_PATH = resolve10(home, "daemon.json");
2614
- mkdirSync2(home, { recursive: true });
2342
+ mkdirSync3(home, { recursive: true });
2615
2343
  const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
2616
2344
  process.env.VOLUTE_DAEMON_TOKEN = token;
2345
+ process.env.VOLUTE_DAEMON_PORT = String(port);
2617
2346
  process.env.VOLUTE_DAEMON_HOSTNAME = hostname;
2618
2347
  let server;
2619
2348
  try {
@@ -2639,6 +2368,13 @@ async function startDaemon(opts) {
2639
2368
  const tokenBudget = getTokenBudget();
2640
2369
  tokenBudget.start(port, token);
2641
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
+ }
2642
2378
  for (const entry of registry) {
2643
2379
  if (!entry.running) continue;
2644
2380
  try {