volute 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +1 -2
  2. package/dist/agent-manager-SSJUZWOV.js +13 -0
  3. package/dist/{channel-Q642YUZE.js → channel-2WJRM7PE.js} +2 -2
  4. package/dist/{chunk-H5XQARAP.js → chunk-4YXYAMFT.js} +3 -3
  5. package/dist/{chunk-5YW4B7CG.js → chunk-6UCG6MIX.js} +72 -23
  6. package/dist/{chunk-A5ZJEMHT.js → chunk-KFNNHQK7.js} +4 -4
  7. package/dist/chunk-L3BQEZ4Z.js +271 -0
  8. package/dist/{chunk-N4QN44LC.js → chunk-MY74SUOL.js} +29 -22
  9. package/dist/{chunk-KSMIWOCN.js → chunk-N4YNKR3Q.js} +6 -0
  10. package/dist/cli.js +23 -19
  11. package/dist/{connect-LW6G23AV.js → connect-X5V5IMRW.js} +3 -3
  12. package/dist/connectors/discord.js +9 -2
  13. package/dist/{create-3K6O2SDC.js → create-23AM7H5B.js} +1 -1
  14. package/dist/{daemon-client-ZTHW7ROS.js → daemon-client-VN24HM5T.js} +2 -2
  15. package/dist/daemon.js +394 -436
  16. package/dist/{delete-JNGY7ZFH.js → delete-GDMSOW3U.js} +2 -2
  17. package/dist/{disconnect-ACVTKTRE.js → disconnect-5JWFZ6RV.js} +2 -2
  18. package/dist/{down-FYCUYC5H.js → down-WTF73FE7.js} +5 -4
  19. package/dist/{env-7SLRN3MG.js → env-YKUJOFHE.js} +12 -5
  20. package/dist/{fork-BB3DZ426.js → fork-GRSVMBKI.js} +39 -32
  21. package/dist/history-7WVVKMUY.js +46 -0
  22. package/dist/{import-W2AMTEV5.js → import-42DOLBDT.js} +1 -1
  23. package/dist/{logs-BUHRIQ2L.js → logs-SYRQOL6B.js} +1 -1
  24. package/dist/{merge-446QTE7Q.js → merge-CSAVLSLY.js} +33 -36
  25. package/dist/{schedule-KKSOVUDF.js → schedule-J37XQM6E.js} +2 -2
  26. package/dist/{send-WQSVSRDD.js → send-PLOYEYER.js} +7 -5
  27. package/dist/{start-LKMWS6ZE.js → start-AG7QLULK.js} +2 -2
  28. package/dist/{status-CIEKUI3V.js → status-GCNU4M3K.js} +9 -2
  29. package/dist/{stop-YTOAGYE4.js → stop-IL5Q6NER.js} +2 -2
  30. package/dist/{up-AJJ4GCXY.js → up-ZC6G6K4K.js} +21 -37
  31. package/dist/{upgrade-JACA6YMO.js → upgrade-DD5TNJWU.js} +3 -5
  32. package/dist/{variants-HPY4DEWU.js → variants-QQIEKT6M.js} +2 -2
  33. package/drizzle/0000_flaky_mariko_yashida.sql +34 -0
  34. package/drizzle/0001_careless_warpath.sql +12 -0
  35. package/drizzle/meta/0000_snapshot.json +227 -0
  36. package/drizzle/meta/0001_snapshot.json +298 -0
  37. package/drizzle/meta/_journal.json +20 -0
  38. package/package.json +2 -1
  39. package/templates/_base/.init/.config/hooks/startup-context.sh +28 -0
  40. package/templates/_base/_skills/memory/SKILL.md +56 -13
  41. package/templates/_base/_skills/volute-agent/SKILL.md +27 -3
  42. package/templates/_base/home/VOLUTE.md +25 -0
  43. package/templates/_base/src/lib/format-prefix.ts +24 -0
  44. package/templates/_base/src/lib/sessions.ts +71 -0
  45. package/templates/_base/src/lib/startup.ts +132 -0
  46. package/templates/_base/src/lib/types.ts +3 -0
  47. package/templates/_base/src/lib/volute-server.ts +18 -2
  48. package/templates/agent-sdk/.init/.claude/settings.json +14 -0
  49. package/templates/agent-sdk/.init/.config/sessions.json +4 -0
  50. package/templates/agent-sdk/.init/CLAUDE.md +3 -2
  51. package/templates/agent-sdk/package.json.tmpl +1 -1
  52. package/templates/agent-sdk/src/agent.ts +101 -0
  53. package/templates/agent-sdk/src/lib/agent-sessions.ts +180 -0
  54. package/templates/agent-sdk/src/server.ts +33 -129
  55. package/templates/agent-sdk/volute-template.json +1 -1
  56. package/templates/pi/.init/.config/sessions.json +1 -0
  57. package/templates/pi/.init/AGENTS.md +2 -1
  58. package/templates/pi/src/agent.ts +61 -0
  59. package/templates/pi/src/lib/agent-sessions.ts +188 -0
  60. package/templates/pi/src/server.ts +28 -102
  61. package/templates/pi/volute-template.json +1 -1
  62. package/templates/agent-sdk/src/lib/agent.ts +0 -199
  63. package/templates/pi/src/lib/agent.ts +0 -205
  64. /package/templates/_base/.init/memory/{.gitkeep → journal/.gitkeep} +0 -0
  65. /package/templates/_base/{volute.json.tmpl → home/.config/volute.json.tmpl} +0 -0
  66. /package/templates/pi/{volute.json.tmpl → home/.config/volute.json.tmpl} +0 -0
package/README.md CHANGED
@@ -71,7 +71,6 @@ Responses stream back to your terminal in real time. The agent knows which chann
71
71
  │ ├── VOLUTE.md # channel routing docs
72
72
  │ └── memory/ # daily logs (YYYY-MM-DD.md)
73
73
  ├── src/ # agent server code
74
- ├── volute.json # agent config (model, etc.)
75
74
  └── .volute/ # runtime state, session, logs
76
75
  ```
77
76
 
@@ -210,7 +209,7 @@ volute create atlas --template pi
210
209
 
211
210
  ## Model configuration
212
211
 
213
- Set the model via `volute.json` in the agent's root directory, or the `VOLUTE_MODEL` env var.
212
+ Set the model via `home/.config/volute.json` in the agent directory, or the `VOLUTE_MODEL` env var.
214
213
 
215
214
  ## Development
216
215
 
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ AgentManager,
4
+ getAgentManager,
5
+ initAgentManager
6
+ } from "./chunk-L3BQEZ4Z.js";
7
+ import "./chunk-KFNNHQK7.js";
8
+ import "./chunk-6UCG6MIX.js";
9
+ export {
10
+ AgentManager,
11
+ getAgentManager,
12
+ initAgentManager
13
+ };
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  loadMergedEnv
4
- } from "./chunk-A5ZJEMHT.js";
4
+ } from "./chunk-KFNNHQK7.js";
5
5
  import {
6
6
  parseArgs
7
7
  } from "./chunk-D424ZQGI.js";
8
8
  import {
9
9
  resolveAgent
10
- } from "./chunk-5YW4B7CG.js";
10
+ } from "./chunk-6UCG6MIX.js";
11
11
 
12
12
  // src/lib/channels/discord.ts
13
13
  var API_BASE = "https://discord.com/api/v10";
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- VOLUTE_HOME
4
- } from "./chunk-5YW4B7CG.js";
3
+ voluteHome
4
+ } from "./chunk-6UCG6MIX.js";
5
5
 
6
6
  // src/lib/daemon-client.ts
7
7
  import { existsSync, readFileSync } from "fs";
8
8
  import { resolve } from "path";
9
9
  function readDaemonConfig() {
10
- const configPath = resolve(VOLUTE_HOME, "daemon.json");
10
+ const configPath = resolve(voluteHome(), "daemon.json");
11
11
  if (!existsSync(configPath)) {
12
12
  console.error("Volute is not running. Start with: volute up");
13
13
  process.exit(1);
@@ -9,19 +9,24 @@ var __export = (target, all) => {
9
9
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
10
10
  import { homedir } from "os";
11
11
  import { resolve } from "path";
12
- var VOLUTE_HOME = process.env.VOLUTE_HOME || resolve(homedir(), ".volute");
13
- var VARIANTS_PATH = resolve(VOLUTE_HOME, "variants.json");
12
+ function voluteHome() {
13
+ return process.env.VOLUTE_HOME || resolve(homedir(), ".volute");
14
+ }
15
+ function variantsPath() {
16
+ return resolve(voluteHome(), "variants.json");
17
+ }
14
18
  function readAllVariants() {
15
- if (!existsSync(VARIANTS_PATH)) return {};
19
+ const path = variantsPath();
20
+ if (!existsSync(path)) return {};
16
21
  try {
17
- return JSON.parse(readFileSync(VARIANTS_PATH, "utf-8"));
22
+ return JSON.parse(readFileSync(path, "utf-8"));
18
23
  } catch {
19
24
  return {};
20
25
  }
21
26
  }
22
27
  function writeAllVariants(all) {
23
- mkdirSync(VOLUTE_HOME, { recursive: true });
24
- writeFileSync(VARIANTS_PATH, `${JSON.stringify(all, null, 2)}
28
+ mkdirSync(voluteHome(), { recursive: true });
29
+ writeFileSync(variantsPath(), `${JSON.stringify(all, null, 2)}
25
30
  `);
26
31
  }
27
32
  function readVariants(agentName) {
@@ -52,6 +57,28 @@ function removeVariant(agentName, name) {
52
57
  function findVariant(agentName, name) {
53
58
  return readVariants(agentName).find((v) => v.name === name);
54
59
  }
60
+ function setVariantRunning(agentName, variantName, running) {
61
+ const all = readAllVariants();
62
+ const variants = all[agentName] ?? [];
63
+ const variant = variants.find((v) => v.name === variantName);
64
+ if (variant) {
65
+ variant.running = running;
66
+ all[agentName] = variants;
67
+ writeAllVariants(all);
68
+ }
69
+ }
70
+ function getAllRunningVariants() {
71
+ const all = readAllVariants();
72
+ const result = [];
73
+ for (const [agentName, variants] of Object.entries(all)) {
74
+ for (const variant of variants) {
75
+ if (variant.running) {
76
+ result.push({ agentName, variant });
77
+ }
78
+ }
79
+ }
80
+ return result;
81
+ }
55
82
  function removeAllVariants(agentName) {
56
83
  const all = readAllVariants();
57
84
  delete all[agentName];
@@ -81,19 +108,20 @@ function validateBranchName(branch) {
81
108
  }
82
109
 
83
110
  // src/lib/registry.ts
84
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
111
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync2 } from "fs";
85
112
  import { homedir as homedir2 } from "os";
86
113
  import { resolve as resolve2 } from "path";
87
- var VOLUTE_HOME2 = process.env.VOLUTE_HOME || resolve2(homedir2(), ".volute");
88
- var AGENTS_DIR = resolve2(VOLUTE_HOME2, "agents");
89
- var REGISTRY_PATH = resolve2(VOLUTE_HOME2, "agents.json");
114
+ function voluteHome2() {
115
+ return process.env.VOLUTE_HOME || resolve2(homedir2(), ".volute");
116
+ }
90
117
  function ensureVoluteHome() {
91
- mkdirSync2(AGENTS_DIR, { recursive: true });
118
+ mkdirSync2(resolve2(voluteHome2(), "agents"), { recursive: true });
92
119
  }
93
120
  function readRegistry() {
94
- if (!existsSync2(REGISTRY_PATH)) return [];
121
+ const registryPath = resolve2(voluteHome2(), "agents.json");
122
+ if (!existsSync2(registryPath)) return [];
95
123
  try {
96
- const entries = JSON.parse(readFileSync2(REGISTRY_PATH, "utf-8"));
124
+ const entries = JSON.parse(readFileSync2(registryPath, "utf-8"));
97
125
  return entries.map((e) => ({ ...e, running: e.running ?? false }));
98
126
  } catch {
99
127
  return [];
@@ -101,10 +129,26 @@ function readRegistry() {
101
129
  }
102
130
  function writeRegistry(entries) {
103
131
  ensureVoluteHome();
104
- writeFileSync2(REGISTRY_PATH, `${JSON.stringify(entries, null, 2)}
132
+ const registryPath = resolve2(voluteHome2(), "agents.json");
133
+ const tmpPath = `${registryPath}.tmp`;
134
+ writeFileSync2(tmpPath, `${JSON.stringify(entries, null, 2)}
105
135
  `);
136
+ renameSync(tmpPath, registryPath);
137
+ }
138
+ var AGENT_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
139
+ var AGENT_NAME_MAX = 64;
140
+ function validateAgentName(name) {
141
+ if (!name) return "Agent name is required";
142
+ if (name.length > AGENT_NAME_MAX)
143
+ return `Agent name must be at most ${AGENT_NAME_MAX} characters`;
144
+ if (!AGENT_NAME_RE.test(name)) {
145
+ return "Agent name must start with alphanumeric and contain only alphanumeric, dots, dashes, or underscores";
146
+ }
147
+ return null;
106
148
  }
107
149
  function addAgent(name, port) {
150
+ const err = validateAgentName(name);
151
+ if (err) throw new Error(err);
108
152
  const entries = readRegistry();
109
153
  const filtered = entries.filter((e) => e.name !== name);
110
154
  filtered.push({ name, port, created: (/* @__PURE__ */ new Date()).toISOString(), running: false });
@@ -126,34 +170,37 @@ function findAgent(name) {
126
170
  return readRegistry().find((e) => e.name === name);
127
171
  }
128
172
  function agentDir(name) {
129
- return resolve2(AGENTS_DIR, name);
173
+ return resolve2(voluteHome2(), "agents", name);
130
174
  }
131
175
  function nextPort() {
132
176
  const entries = readRegistry();
133
177
  const usedPorts = new Set(entries.map((e) => e.port));
178
+ for (const entry of entries) {
179
+ for (const v of readVariants(entry.name)) {
180
+ if (v.port) usedPorts.add(v.port);
181
+ }
182
+ }
134
183
  let port = 4100;
135
184
  while (usedPorts.has(port)) port++;
185
+ if (port > 65535) throw new Error("No available ports \u2014 all ports 4100-65535 are allocated");
136
186
  return port;
137
187
  }
138
188
  function resolveAgent(name) {
139
189
  const [baseName, variantName] = name.split("@", 2);
140
190
  const entry = findAgent(baseName);
141
191
  if (!entry) {
142
- console.error(`Unknown agent: ${baseName}`);
143
- process.exit(1);
192
+ throw new Error(`Unknown agent: ${baseName}`);
144
193
  }
145
194
  const dir = agentDir(baseName);
146
195
  if (!existsSync2(dir)) {
147
- console.error(`Agent directory missing: ${dir}`);
148
- process.exit(1);
196
+ throw new Error(`Agent directory missing: ${dir}`);
149
197
  }
150
198
  if (variantName) {
151
199
  const variant = findVariant(baseName, variantName);
152
200
  if (!variant) {
153
- console.error(`Unknown variant: ${variantName} (agent: ${baseName})`);
154
- process.exit(1);
201
+ throw new Error(`Unknown variant: ${variantName} (agent: ${baseName})`);
155
202
  }
156
- return { entry: { ...entry, port: variant.port }, dir };
203
+ return { entry: { ...entry, port: variant.port }, dir: variant.path };
157
204
  }
158
205
  return { entry, dir };
159
206
  }
@@ -165,10 +212,12 @@ export {
165
212
  addVariant,
166
213
  removeVariant,
167
214
  findVariant,
215
+ setVariantRunning,
216
+ getAllRunningVariants,
168
217
  removeAllVariants,
169
218
  checkHealth,
170
219
  validateBranchName,
171
- VOLUTE_HOME2 as VOLUTE_HOME,
220
+ voluteHome2 as voluteHome,
172
221
  ensureVoluteHome,
173
222
  readRegistry,
174
223
  addAgent,
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- VOLUTE_HOME
4
- } from "./chunk-5YW4B7CG.js";
3
+ voluteHome
4
+ } from "./chunk-6UCG6MIX.js";
5
5
 
6
6
  // src/lib/env.ts
7
7
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
8
8
  import { dirname, resolve } from "path";
9
9
  function sharedEnvPath() {
10
- return resolve(VOLUTE_HOME, "env.json");
10
+ return resolve(voluteHome(), "env.json");
11
11
  }
12
12
  function agentEnvPath(agentDir) {
13
13
  return resolve(agentDir, ".volute", "env.json");
@@ -23,7 +23,7 @@ function readEnv(path) {
23
23
  function writeEnv(path, env) {
24
24
  mkdirSync(dirname(path), { recursive: true });
25
25
  writeFileSync(path, `${JSON.stringify(env, null, 2)}
26
- `);
26
+ `, { mode: 384 });
27
27
  }
28
28
  function loadMergedEnv(agentDir) {
29
29
  const shared = readEnv(sharedEnvPath());
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ loadMergedEnv
4
+ } from "./chunk-KFNNHQK7.js";
5
+ import {
6
+ agentDir,
7
+ findAgent,
8
+ findVariant,
9
+ setAgentRunning,
10
+ setVariantRunning,
11
+ validateBranchName
12
+ } from "./chunk-6UCG6MIX.js";
13
+
14
+ // src/lib/agent-manager.ts
15
+ import { execFile, spawn } from "child_process";
16
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, unlinkSync } from "fs";
17
+ import { resolve } from "path";
18
+ import { promisify } from "util";
19
+ var execFileAsync = promisify(execFile);
20
+ var MAX_RESTART_ATTEMPTS = 5;
21
+ var BASE_RESTART_DELAY = 3e3;
22
+ var MAX_RESTART_DELAY = 6e4;
23
+ var AgentManager = class {
24
+ agents = /* @__PURE__ */ new Map();
25
+ stopping = /* @__PURE__ */ new Set();
26
+ shuttingDown = false;
27
+ restartAttempts = /* @__PURE__ */ new Map();
28
+ resolveTarget(name) {
29
+ const [baseName, variantName] = name.split("@", 2);
30
+ const entry = findAgent(baseName);
31
+ if (!entry) throw new Error(`Unknown agent: ${baseName}`);
32
+ if (variantName) {
33
+ const variant = findVariant(baseName, variantName);
34
+ if (!variant) throw new Error(`Unknown variant: ${variantName} (agent: ${baseName})`);
35
+ return { dir: variant.path, port: variant.port, isVariant: true, baseName, variantName };
36
+ }
37
+ const dir = agentDir(baseName);
38
+ if (!existsSync(dir)) throw new Error(`Agent directory missing: ${dir}`);
39
+ return { dir, port: entry.port, isVariant: false, baseName };
40
+ }
41
+ async startAgent(name) {
42
+ if (this.agents.has(name)) {
43
+ throw new Error(`Agent ${name} is already running`);
44
+ }
45
+ const target = this.resolveTarget(name);
46
+ const { dir, isVariant, baseName, variantName } = target;
47
+ const port = target.port;
48
+ try {
49
+ const res = await fetch(`http://localhost:${port}/health`);
50
+ if (res.ok) {
51
+ console.error(`[daemon] killing orphan process on port ${port}`);
52
+ await killProcessOnPort(port);
53
+ await new Promise((r) => setTimeout(r, 500));
54
+ }
55
+ } catch {
56
+ }
57
+ const voluteDir = resolve(dir, ".volute");
58
+ const logsDir = resolve(voluteDir, "logs");
59
+ mkdirSync(logsDir, { recursive: true });
60
+ const logStream = createWriteStream(resolve(logsDir, "agent.log"), {
61
+ flags: "a"
62
+ });
63
+ const agentEnv = loadMergedEnv(dir);
64
+ const { VOLUTE_DAEMON_TOKEN: _, ...parentEnv } = process.env;
65
+ const env = { ...parentEnv, ...agentEnv, VOLUTE_AGENT: name };
66
+ const tsxBin = resolve(dir, "node_modules", ".bin", "tsx");
67
+ const child = spawn(tsxBin, ["src/server.ts", "--port", String(port)], {
68
+ cwd: dir,
69
+ stdio: ["ignore", "pipe", "pipe"],
70
+ detached: true,
71
+ env
72
+ });
73
+ this.agents.set(name, { child, port });
74
+ child.stdout?.pipe(logStream);
75
+ child.stderr?.pipe(logStream);
76
+ try {
77
+ await new Promise((resolve2, reject) => {
78
+ const timeout = setTimeout(() => {
79
+ reject(new Error(`Agent ${name} did not start within 30s`));
80
+ }, 3e4);
81
+ function checkOutput(data) {
82
+ if (data.toString().match(/listening on :\d+/)) {
83
+ clearTimeout(timeout);
84
+ resolve2();
85
+ }
86
+ }
87
+ child.stdout?.on("data", checkOutput);
88
+ child.stderr?.on("data", checkOutput);
89
+ child.on("error", (err) => {
90
+ clearTimeout(timeout);
91
+ reject(err);
92
+ });
93
+ child.on("exit", (code) => {
94
+ clearTimeout(timeout);
95
+ reject(new Error(`Agent ${name} exited with code ${code} during startup`));
96
+ });
97
+ });
98
+ } catch (err) {
99
+ this.agents.delete(name);
100
+ try {
101
+ child.kill();
102
+ } catch {
103
+ }
104
+ throw err;
105
+ }
106
+ this.restartAttempts.delete(name);
107
+ this.setupCrashRecovery(name, child, dir, isVariant);
108
+ if (isVariant) {
109
+ setVariantRunning(baseName, variantName, true);
110
+ } else {
111
+ setAgentRunning(name, true);
112
+ }
113
+ console.error(`[daemon] started agent ${name} on port ${port}`);
114
+ }
115
+ setupCrashRecovery(name, child, dir, isVariant) {
116
+ child.on("exit", async (code) => {
117
+ this.agents.delete(name);
118
+ if (this.shuttingDown || this.stopping.has(name)) return;
119
+ console.error(`[daemon] agent ${name} exited with code ${code}`);
120
+ const wasRestart = isVariant ? false : await this.handleRestart(name, dir);
121
+ if (wasRestart) {
122
+ console.error(`[daemon] restarting ${name} immediately after merge`);
123
+ this.restartAttempts.delete(name);
124
+ this.startAgent(name).catch((err) => {
125
+ console.error(`[daemon] failed to restart ${name} after merge:`, err);
126
+ });
127
+ } else {
128
+ const attempts = this.restartAttempts.get(name) ?? 0;
129
+ if (attempts >= MAX_RESTART_ATTEMPTS) {
130
+ console.error(`[daemon] ${name} crashed ${attempts} times \u2014 giving up on restart`);
131
+ const [base, variant] = name.split("@", 2);
132
+ if (variant) {
133
+ setVariantRunning(base, variant, false);
134
+ } else {
135
+ setAgentRunning(name, false);
136
+ }
137
+ return;
138
+ }
139
+ const delay = Math.min(BASE_RESTART_DELAY * 2 ** attempts, MAX_RESTART_DELAY);
140
+ this.restartAttempts.set(name, attempts + 1);
141
+ console.error(
142
+ `[daemon] crash recovery for ${name} \u2014 attempt ${attempts + 1}/${MAX_RESTART_ATTEMPTS}, restarting in ${delay}ms`
143
+ );
144
+ setTimeout(() => {
145
+ if (this.shuttingDown) return;
146
+ this.startAgent(name).catch((err) => {
147
+ console.error(`[daemon] failed to restart ${name}:`, err);
148
+ });
149
+ }, delay);
150
+ }
151
+ });
152
+ }
153
+ async handleRestart(name, dir) {
154
+ const restartPath = resolve(dir, ".volute", "restart.json");
155
+ if (!existsSync(restartPath)) return false;
156
+ try {
157
+ const signal = JSON.parse(readFileSync(restartPath, "utf-8"));
158
+ unlinkSync(restartPath);
159
+ if (signal.action === "merge" && signal.name) {
160
+ const err = validateBranchName(signal.name);
161
+ if (err) {
162
+ console.error(`[daemon] invalid variant name in restart.json for ${name}: ${err}`);
163
+ return false;
164
+ }
165
+ console.error(`[daemon] merging variant for ${name}: ${signal.name}`);
166
+ const mergeArgs = ["merge", name, signal.name];
167
+ if (signal.summary) mergeArgs.push("--summary", signal.summary);
168
+ if (signal.justification) mergeArgs.push("--justification", signal.justification);
169
+ if (signal.memory) mergeArgs.push("--memory", signal.memory);
170
+ const { VOLUTE_DAEMON_TOKEN: _t, ...mergeEnv } = process.env;
171
+ await execFileAsync("volute", mergeArgs, {
172
+ cwd: dir,
173
+ env: { ...mergeEnv, VOLUTE_SUPERVISOR: "1" }
174
+ });
175
+ }
176
+ return true;
177
+ } catch (e) {
178
+ console.error(`[daemon] failed to handle restart for ${name}:`, e);
179
+ return false;
180
+ }
181
+ }
182
+ async stopAgent(name) {
183
+ const tracked = this.agents.get(name);
184
+ if (!tracked) return;
185
+ this.stopping.add(name);
186
+ const { child } = tracked;
187
+ this.agents.delete(name);
188
+ await new Promise((resolve2) => {
189
+ child.on("exit", () => resolve2());
190
+ try {
191
+ process.kill(-child.pid, "SIGTERM");
192
+ } catch {
193
+ resolve2();
194
+ }
195
+ setTimeout(() => {
196
+ try {
197
+ process.kill(-child.pid, "SIGKILL");
198
+ } catch {
199
+ }
200
+ resolve2();
201
+ }, 5e3);
202
+ });
203
+ this.stopping.delete(name);
204
+ this.restartAttempts.delete(name);
205
+ const [baseName, variantName] = name.split("@", 2);
206
+ if (variantName) {
207
+ setVariantRunning(baseName, variantName, false);
208
+ } else {
209
+ setAgentRunning(name, false);
210
+ }
211
+ console.error(`[daemon] stopped agent ${name}`);
212
+ }
213
+ async restartAgent(name) {
214
+ await this.stopAgent(name);
215
+ await this.startAgent(name);
216
+ }
217
+ async stopAll() {
218
+ this.shuttingDown = true;
219
+ const names = [...this.agents.keys()];
220
+ await Promise.all(names.map((name) => this.stopAgent(name)));
221
+ }
222
+ isRunning(name) {
223
+ return this.agents.has(name);
224
+ }
225
+ getRunningAgents() {
226
+ return [...this.agents.keys()];
227
+ }
228
+ };
229
+ async function killProcessOnPort(port) {
230
+ try {
231
+ const { stdout } = await execFileAsync("lsof", ["-ti", `:${port}`, "-sTCP:LISTEN"]);
232
+ const pids = /* @__PURE__ */ new Set();
233
+ for (const line of stdout.trim().split("\n").filter(Boolean)) {
234
+ const pid = parseInt(line, 10);
235
+ pids.add(pid);
236
+ try {
237
+ const { stdout: psOut } = await execFileAsync("ps", ["-p", String(pid), "-o", "pgid="]);
238
+ const pgid = parseInt(psOut.trim(), 10);
239
+ if (pgid > 1) pids.add(pgid);
240
+ } catch {
241
+ }
242
+ }
243
+ for (const pid of pids) {
244
+ try {
245
+ process.kill(-pid, "SIGTERM");
246
+ } catch {
247
+ }
248
+ try {
249
+ process.kill(pid, "SIGTERM");
250
+ } catch {
251
+ }
252
+ }
253
+ } catch {
254
+ }
255
+ }
256
+ var instance = null;
257
+ function initAgentManager() {
258
+ if (instance) throw new Error("AgentManager already initialized");
259
+ instance = new AgentManager();
260
+ return instance;
261
+ }
262
+ function getAgentManager() {
263
+ if (!instance) instance = new AgentManager();
264
+ return instance;
265
+ }
266
+
267
+ export {
268
+ AgentManager,
269
+ initAgentManager,
270
+ getAgentManager
271
+ };
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/lib/spawn-server.ts
4
4
  import { spawn } from "child_process";
5
+ import { closeSync, mkdirSync, openSync, readFileSync } from "fs";
5
6
  import { resolve } from "path";
6
7
  function tsxBin(cwd) {
7
8
  return resolve(cwd, "node_modules", ".bin", "tsx");
@@ -39,33 +40,39 @@ function spawnAttached(cwd, port) {
39
40
  });
40
41
  }
41
42
  function spawnDetached(cwd, port) {
43
+ const logsDir = resolve(cwd, ".volute", "logs");
44
+ mkdirSync(logsDir, { recursive: true });
45
+ const logPath = resolve(logsDir, "agent.log");
46
+ const logFd = openSync(logPath, "a");
42
47
  const child = spawn(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
43
48
  cwd,
44
- stdio: ["ignore", "pipe", "pipe"],
49
+ stdio: ["ignore", logFd, logFd],
45
50
  detached: true
46
51
  });
47
- return new Promise((resolve2) => {
48
- const timeout = setTimeout(() => resolve2(null), 3e4);
49
- function checkOutput(data) {
50
- const match = data.toString().match(/listening on :(\d+)/);
51
- if (match) {
52
- clearTimeout(timeout);
53
- child.stdout?.destroy();
54
- child.stderr?.destroy();
55
- child.unref();
56
- resolve2({ child, actualPort: parseInt(match[1], 10) });
57
- }
58
- }
59
- child.stdout?.on("data", checkOutput);
60
- child.stderr?.on("data", checkOutput);
61
- child.on("error", () => {
62
- clearTimeout(timeout);
63
- resolve2(null);
64
- });
65
- child.on("exit", () => {
52
+ child.unref();
53
+ closeSync(logFd);
54
+ return new Promise((res) => {
55
+ let done = false;
56
+ function finish(result) {
57
+ if (done) return;
58
+ done = true;
59
+ clearInterval(interval);
66
60
  clearTimeout(timeout);
67
- resolve2(null);
68
- });
61
+ res(result);
62
+ }
63
+ const interval = setInterval(() => {
64
+ try {
65
+ const content = readFileSync(logPath, "utf-8");
66
+ const match = content.match(/listening on :(\d+)/);
67
+ if (match) {
68
+ finish({ child, actualPort: parseInt(match[1], 10) });
69
+ }
70
+ } catch {
71
+ }
72
+ }, 100);
73
+ const timeout = setTimeout(() => finish(null), 3e4);
74
+ child.on("error", () => finish(null));
75
+ child.on("exit", () => finish(null));
69
76
  });
70
77
  }
71
78
 
@@ -45,6 +45,7 @@ var log = {
45
45
  var logger_default = log;
46
46
 
47
47
  // src/lib/ndjson.ts
48
+ var MAX_BUFFER_SIZE = 1e6;
48
49
  async function* readNdjson(body) {
49
50
  const reader = body.getReader();
50
51
  const decoder = new TextDecoder();
@@ -54,6 +55,11 @@ async function* readNdjson(body) {
54
55
  const { done, value } = await reader.read();
55
56
  if (done) break;
56
57
  buffer += decoder.decode(value, { stream: true });
58
+ if (buffer.length > MAX_BUFFER_SIZE) {
59
+ logger_default.warn("ndjson: buffer exceeded 1MB, resetting");
60
+ buffer = "";
61
+ continue;
62
+ }
57
63
  const lines = buffer.split("\n");
58
64
  buffer = lines.pop() || "";
59
65
  for (const line of lines) {