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.
- package/README.md +13 -13
- package/dist/{agent-X7GJLBLW.js → agent-YORVRB6I.js} +10 -10
- package/dist/{agent-manager-JDVXU3ON.js → agent-manager-CMMH5KQQ.js} +4 -4
- package/dist/{channel-SMCNOIVQ.js → channel-RDGHBFSI.js} +16 -56
- package/dist/{chunk-JR4UXCTO.js → chunk-23L3MKEV.js} +1 -1
- package/dist/{chunk-5SKQ6J7T.js → chunk-5C5JWR2L.js} +15 -7
- package/dist/{chunk-UWHWAPGO.js → chunk-DP2DX4WV.js} +9 -1
- package/dist/chunk-ECPQXRLB.js +264 -0
- package/dist/{down-FXWAN66A.js → chunk-HZ5LTOEJ.js} +48 -13
- package/dist/{chunk-W76KWE23.js → chunk-IQXBMFZG.js} +6 -4
- package/dist/{chunk-ZZOOTYXK.js → chunk-LIPPXNIE.js} +60 -74
- package/dist/{chunk-BX7KI4S3.js → chunk-N6MLQ26B.js} +23 -96
- package/dist/{chunk-H7AMDUIA.js → chunk-QF22MYDJ.js} +6 -5
- package/dist/{chunk-AOKAQGO4.js → chunk-RT6Y7AR3.js} +2 -1
- package/dist/{chunk-G6ZNGLUX.js → chunk-W6TMWYU3.js} +133 -78
- package/dist/{up-CSX3ZUIU.js → chunk-XSJ27WEM.js} +2 -2
- package/dist/cli.js +25 -19
- package/dist/{connector-Y7JPNROO.js → connector-ZP6MEFF4.js} +3 -3
- package/dist/connectors/discord.js +24 -61
- package/dist/connectors/slack.js +21 -38
- package/dist/connectors/telegram.js +31 -49
- package/dist/{create-G525LWEA.js → create-HGJHLABX.js} +22 -17
- package/dist/{daemon-client-442IV43D.js → daemon-client-54J3EIZD.js} +2 -2
- package/dist/daemon-restart-CPBLMMRI.js +23 -0
- package/dist/daemon.js +397 -661
- package/dist/{delete-2PH2CGDY.js → delete-45TGQC4N.js} +13 -4
- package/dist/down-O4EWZTVA.js +11 -0
- package/dist/{env-7GLUJCWS.js → env-KMNYGVZ2.js} +7 -9
- package/dist/{history-H72ZUIBN.js → history-PXJVYLVY.js} +2 -2
- package/dist/{import-AVKQJDYC.js → import-CNEDF3TD.js} +6 -6
- package/dist/{logs-EDGK26AK.js → logs-TZB3MTLZ.js} +5 -4
- package/dist/{package-4DP4Y4UO.js → package-5UCKNK6J.js} +1 -1
- package/dist/{restart-O4ETYLJF.js → restart-KVH3TK5N.js} +2 -2
- package/dist/{schedule-S6QVC5ON.js → schedule-HCUCBNQI.js} +2 -2
- package/dist/send-BNC2S5BY.js +162 -0
- package/dist/{service-HZNIDNJF.js → service-R4MCNBOA.js} +1 -1
- package/dist/{setup-F4TCWVSP.js → setup-JXDCJX7W.js} +25 -6
- package/dist/{start-VHQ7LNWM.js → start-QU73YTJW.js} +2 -2
- package/dist/{status-QAJWXKMZ.js → status-Q6ZQJXNI.js} +2 -2
- package/dist/{stop-CAGCT5NI.js → stop-N7U5N6A7.js} +2 -2
- package/dist/up-V6EAA7OZ.js +12 -0
- package/dist/{update-XSIX3GGP.js → update-EUCZ7XGG.js} +3 -3
- package/dist/{update-check-5ZADDHCK.js → update-check-SM4244SU.js} +2 -2
- package/dist/{upgrade-YXKPWDRU.js → upgrade-CZF6PN7Y.js} +4 -4
- package/dist/{variant-4Z6W3PP6.js → variant-RKXPN5DH.js} +20 -46
- package/dist/web-assets/assets/index-D-3zx6vs.js +307 -0
- package/dist/web-assets/index.html +1 -1
- package/drizzle/0004_magical_silverclaw.sql +1 -0
- package/drizzle/meta/0004_snapshot.json +410 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/templates/_base/_skills/volute-agent/SKILL.md +32 -16
- package/templates/_base/home/.config/routes.json +4 -8
- package/templates/_base/home/VOLUTE.md +16 -14
- package/templates/_base/src/lib/auto-reply.ts +38 -0
- package/templates/_base/src/lib/daemon-client.ts +53 -0
- package/templates/_base/src/lib/router.ts +66 -14
- package/templates/_base/src/lib/routing.ts +48 -9
- package/templates/_base/src/lib/startup.ts +1 -25
- package/templates/_base/src/lib/types.ts +2 -1
- package/templates/_base/src/lib/volute-server.ts +29 -14
- package/templates/agent-sdk/src/agent.ts +53 -111
- package/templates/agent-sdk/src/lib/content.ts +41 -0
- package/templates/agent-sdk/src/lib/session-store.ts +43 -0
- package/templates/agent-sdk/src/lib/stream-consumer.ts +66 -0
- package/templates/agent-sdk/src/server.ts +5 -13
- package/templates/pi/.init/AGENTS.md +5 -5
- package/templates/pi/src/agent.ts +32 -84
- package/templates/pi/src/lib/content.ts +15 -0
- package/templates/pi/src/lib/event-handler.ts +74 -0
- package/templates/pi/src/lib/resolve-model.ts +21 -0
- package/templates/pi/src/server.ts +3 -7
- package/dist/chunk-B3R6L2GW.js +0 -24
- package/dist/chunk-ZYGKG6VC.js +0 -22
- package/dist/message-SCOQDR3P.js +0 -32
- package/dist/send-G7PE4DOJ.js +0 -72
- 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-
|
|
9
|
+
} from "./chunk-W6TMWYU3.js";
|
|
10
10
|
import {
|
|
11
11
|
checkForUpdate,
|
|
12
12
|
checkForUpdateCached,
|
|
13
13
|
getCurrentVersion
|
|
14
|
-
} from "./chunk-
|
|
15
|
-
import {
|
|
16
|
-
collectPart
|
|
17
|
-
} from "./chunk-B3R6L2GW.js";
|
|
14
|
+
} from "./chunk-RT6Y7AR3.js";
|
|
18
15
|
import {
|
|
19
16
|
CHANNELS
|
|
20
|
-
} from "./chunk-
|
|
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-
|
|
28
|
-
import
|
|
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-
|
|
50
|
+
} from "./chunk-IQXBMFZG.js";
|
|
32
51
|
import {
|
|
33
52
|
resolveVoluteBin
|
|
34
|
-
} from "./chunk-
|
|
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-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
342
|
-
return resolve2(
|
|
361
|
+
connectorPidPath(agentName, type) {
|
|
362
|
+
return resolve2(stateDir(agentName), "connectors", `${type}.pid`);
|
|
343
363
|
}
|
|
344
|
-
saveConnectorPid(
|
|
345
|
-
const pidPath = this.connectorPidPath(
|
|
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(
|
|
369
|
+
removeConnectorPid(agentName, type) {
|
|
350
370
|
try {
|
|
351
|
-
unlinkSync(this.connectorPidPath(
|
|
371
|
+
unlinkSync(this.connectorPidPath(agentName, type));
|
|
352
372
|
} catch {
|
|
353
373
|
}
|
|
354
374
|
}
|
|
355
|
-
killOrphanConnector(
|
|
356
|
-
const pidPath = this.connectorPidPath(
|
|
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/
|
|
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
|
|
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
|
-
|
|
508
|
-
|
|
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
|
-
|
|
680
|
-
|
|
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
|
|
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
|
|
747
|
+
const db = await getDb();
|
|
955
748
|
const sessionId = crypto.randomUUID();
|
|
956
|
-
await
|
|
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
|
|
961
|
-
await
|
|
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
|
|
965
|
-
const row = await
|
|
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
|
|
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
|
|
767
|
+
const db = await getDb();
|
|
975
768
|
const cutoff = Date.now() - SESSION_MAX_AGE;
|
|
976
|
-
await
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
1072
|
+
await getConnectorManager().stopConnectors(baseName);
|
|
1296
1073
|
getTokenBudget().removeBudget(baseName);
|
|
1297
1074
|
}
|
|
1298
1075
|
await manager.stopAgent(name);
|
|
1299
1076
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1304
|
-
|
|
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
|
|
1177
|
+
const db = await getDb();
|
|
1383
1178
|
if (parsed) {
|
|
1384
1179
|
try {
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1387
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
1275
|
+
const db = await getDb();
|
|
1530
1276
|
try {
|
|
1531
|
-
await
|
|
1277
|
+
await db.insert(agentMessages).values({
|
|
1532
1278
|
agent: baseName,
|
|
1533
1279
|
channel: body.channel,
|
|
1534
|
-
|
|
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
|
|
1546
|
-
const rows = await
|
|
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
|
|
1554
|
-
const conditions = [
|
|
1298
|
+
const db = await getDb();
|
|
1299
|
+
const conditions = [eq2(agentMessages.agent, name)];
|
|
1555
1300
|
if (channel) {
|
|
1556
|
-
conditions.push(
|
|
1301
|
+
conditions.push(eq2(agentMessages.channel, channel));
|
|
1557
1302
|
}
|
|
1558
|
-
const rows = await
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
1504
|
+
stream.writeSSE({ data: line }).catch(() => {
|
|
1777
1505
|
});
|
|
1778
1506
|
}
|
|
1779
1507
|
}
|
|
1780
1508
|
};
|
|
1781
1509
|
tail.stdout.on("data", onData);
|
|
1782
|
-
|
|
1510
|
+
stream.onAbort(() => {
|
|
1783
1511
|
tail.kill();
|
|
1784
1512
|
});
|
|
1785
1513
|
await new Promise((resolve11) => {
|
|
1786
1514
|
tail.on("exit", resolve11);
|
|
1787
|
-
|
|
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 (
|
|
1609
|
+
return streamSSE2(c, async (stream) => {
|
|
1882
1610
|
for (const entry of logBuffer.getEntries()) {
|
|
1883
|
-
await
|
|
1611
|
+
await stream.writeSSE({ data: JSON.stringify(entry) });
|
|
1884
1612
|
}
|
|
1885
1613
|
const unsubscribe = logBuffer.subscribe((entry) => {
|
|
1886
|
-
|
|
1614
|
+
stream.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
|
|
1887
1615
|
});
|
|
1888
1616
|
});
|
|
1889
1617
|
await new Promise((resolve11) => {
|
|
1890
|
-
|
|
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
|
|
1628
|
+
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
1901
1629
|
import { Hono as Hono8 } from "hono";
|
|
1902
|
-
import { z as
|
|
1903
|
-
var typingSchema =
|
|
1904
|
-
channel:
|
|
1905
|
-
sender:
|
|
1906
|
-
active:
|
|
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",
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1734
|
+
const db = await getDb();
|
|
1979
1735
|
const id = randomUUID();
|
|
1980
|
-
await
|
|
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
|
|
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
|
|
2008
|
-
const row = await
|
|
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
|
|
2013
|
-
const rows = await
|
|
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,
|
|
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
|
|
2023
|
-
const row = await
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
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
|
|
2033
|
-
const participantRows = await
|
|
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
|
|
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
|
|
2041
|
-
const row = await
|
|
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
|
|
1806
|
+
const db = await getDb();
|
|
2051
1807
|
const serialized = JSON.stringify(content);
|
|
2052
|
-
const [result] = await
|
|
2053
|
-
await
|
|
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
|
|
1814
|
+
await db.update(conversations).set({ title }).where(and2(eq3(conversations.id, conversationId), isNull(conversations.title)));
|
|
2059
1815
|
}
|
|
2060
1816
|
}
|
|
2061
|
-
|
|
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
|
|
2072
|
-
const rows = await
|
|
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
|
|
1852
|
+
const db = await getDb();
|
|
2088
1853
|
const convIds = convs.map((c) => c.id);
|
|
2089
|
-
const rows = await
|
|
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,
|
|
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
|
|
2114
|
-
const agentConvs = await
|
|
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
|
|
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
|
|
2127
|
-
await
|
|
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 =
|
|
2132
|
-
message:
|
|
2133
|
-
conversationId:
|
|
2134
|
-
sender:
|
|
2135
|
-
images:
|
|
2136
|
-
|
|
2137
|
-
media_type:
|
|
2138
|
-
data:
|
|
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
|
-
|
|
2144
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
1968
|
+
conversationId = conv2.id;
|
|
2232
1969
|
}
|
|
2233
1970
|
}
|
|
2234
|
-
const
|
|
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-
|
|
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
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
2295
|
-
|
|
2296
|
-
const
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
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
|
|
2302
|
-
|
|
2303
|
-
|
|
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
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
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
|
|
2060
|
+
import { zValidator as zValidator4 } from "@hono/zod-validator";
|
|
2337
2061
|
import { Hono as Hono12 } from "hono";
|
|
2338
|
-
import { z as
|
|
2339
|
-
var createConvSchema =
|
|
2340
|
-
title:
|
|
2341
|
-
participantIds:
|
|
2342
|
-
participantNames:
|
|
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",
|
|
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
|
|
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
|
|
2156
|
+
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
2429
2157
|
import { Hono as Hono13 } from "hono";
|
|
2430
|
-
import { z as
|
|
2431
|
-
var createSchema =
|
|
2432
|
-
title:
|
|
2433
|
-
participantNames:
|
|
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("/",
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 {
|