volute 0.16.0 → 0.18.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/dist/chunk-AYB7XAWO.js +812 -0
- package/dist/{chunk-3FD4ZZUL.js → chunk-FW5API7X.js} +116 -10
- package/dist/{chunk-3FC42ZBM.js → chunk-GK4E7LM7.js} +3 -0
- package/dist/cli.js +18 -6
- package/dist/connectors/discord.js +1 -1
- package/dist/connectors/slack.js +1 -1
- package/dist/connectors/telegram.js +1 -1
- package/dist/{daemon-restart-MS5FI44G.js → daemon-restart-2HVTHZAT.js} +1 -1
- package/dist/daemon.js +1443 -592
- package/dist/history-YUEKTJ2N.js +108 -0
- package/dist/{mind-manager-PN5SUDJ4.js → mind-manager-Z7O7PN2O.js} +1 -1
- package/dist/{package-3QGV3KX6.js → package-OKLFO7UY.js} +8 -9
- package/dist/{send-KBBZNYG6.js → send-BNDTLUPM.js} +41 -9
- package/dist/skill-2Y42P4JY.js +287 -0
- package/dist/{up-GZLWZAQE.js → up-7B3BWF2U.js} +1 -1
- package/dist/web-assets/assets/index-CtiimdWK.css +1 -0
- package/dist/web-assets/assets/index-kt1_EcuO.js +63 -0
- package/dist/web-assets/index.html +2 -1
- package/drizzle/0006_mind_history.sql +20 -0
- package/drizzle/0007_system_prompts.sql +5 -0
- package/drizzle/0008_volute_channels.sql +24 -0
- package/drizzle/0009_shared_skills.sql +9 -0
- package/drizzle/meta/0006_snapshot.json +7 -0
- package/drizzle/meta/0007_snapshot.json +7 -0
- package/drizzle/meta/0008_snapshot.json +7 -0
- package/drizzle/meta/0009_snapshot.json +7 -0
- package/drizzle/meta/_journal.json +28 -0
- package/package.json +8 -9
- package/templates/_base/.init/.config/prompts.json +5 -0
- package/templates/_base/_skills/volute-mind/SKILL.md +19 -5
- package/templates/_base/src/lib/daemon-client.ts +45 -0
- package/templates/_base/src/lib/logger.ts +19 -0
- package/templates/_base/src/lib/router.ts +48 -41
- package/templates/_base/src/lib/routing.ts +5 -8
- package/templates/_base/src/lib/startup.ts +43 -0
- package/templates/_base/src/lib/transparency.ts +89 -0
- package/templates/_base/src/lib/types.ts +0 -1
- package/templates/_base/src/lib/volute-server.ts +3 -35
- package/templates/claude/src/agent.ts +9 -22
- package/templates/claude/src/lib/hooks/reply-instructions.ts +6 -9
- package/templates/claude/src/lib/stream-consumer.ts +39 -12
- package/templates/pi/src/agent.ts +9 -22
- package/templates/pi/src/lib/event-handler.ts +58 -7
- package/templates/pi/src/lib/reply-instructions-extension.ts +6 -9
- package/dist/chunk-J52CJCVI.js +0 -447
- package/dist/history-LKCJJMUV.js +0 -50
- package/dist/web-assets/assets/index-B1XIIGCh.js +0 -307
- package/templates/_base/src/lib/auto-reply.ts +0 -38
- /package/dist/{chunk-LLBBVTEY.js → chunk-6DVBMLVN.js} +0 -0
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
loadMergedEnv
|
|
4
|
+
} from "./chunk-OYSZNX5I.js";
|
|
5
|
+
import {
|
|
6
|
+
chownMindDir,
|
|
7
|
+
isIsolationEnabled,
|
|
8
|
+
wrapForIsolation
|
|
9
|
+
} from "./chunk-ZCEYUUID.js";
|
|
10
|
+
import {
|
|
11
|
+
findMind,
|
|
12
|
+
findVariant,
|
|
13
|
+
mindDir,
|
|
14
|
+
setMindRunning,
|
|
15
|
+
setVariantRunning,
|
|
16
|
+
stateDir,
|
|
17
|
+
voluteHome
|
|
18
|
+
} from "./chunk-M77QBTEH.js";
|
|
19
|
+
import {
|
|
20
|
+
__export
|
|
21
|
+
} from "./chunk-K3NQKI34.js";
|
|
22
|
+
|
|
23
|
+
// src/lib/mind-manager.ts
|
|
24
|
+
import { execFile, spawn } from "child_process";
|
|
25
|
+
import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
26
|
+
import { resolve as resolve2 } from "path";
|
|
27
|
+
import { promisify } from "util";
|
|
28
|
+
|
|
29
|
+
// src/lib/json-state.ts
|
|
30
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
31
|
+
function loadJsonMap(path) {
|
|
32
|
+
const map = /* @__PURE__ */ new Map();
|
|
33
|
+
try {
|
|
34
|
+
if (existsSync(path)) {
|
|
35
|
+
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
36
|
+
for (const [key, value] of Object.entries(data)) {
|
|
37
|
+
if (typeof value === "number") map.set(key, value);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.warn(`[state] failed to load ${path}:`, err);
|
|
42
|
+
}
|
|
43
|
+
return map;
|
|
44
|
+
}
|
|
45
|
+
function saveJsonMap(path, map) {
|
|
46
|
+
const data = {};
|
|
47
|
+
for (const [key, value] of map) {
|
|
48
|
+
data[key] = value;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
writeFileSync(path, `${JSON.stringify(data)}
|
|
52
|
+
`);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.warn(`[state] failed to save ${path}:`, err);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function clearJsonMap(path, map) {
|
|
58
|
+
map.clear();
|
|
59
|
+
try {
|
|
60
|
+
if (existsSync(path)) unlinkSync(path);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.warn(`[state] failed to clear ${path}:`, err);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/lib/log-buffer.ts
|
|
67
|
+
var LogBuffer = class {
|
|
68
|
+
entries = [];
|
|
69
|
+
maxSize = 1e3;
|
|
70
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
71
|
+
append(entry) {
|
|
72
|
+
this.entries.push(entry);
|
|
73
|
+
if (this.entries.length > this.maxSize) {
|
|
74
|
+
this.entries.shift();
|
|
75
|
+
}
|
|
76
|
+
for (const sub of this.subscribers) {
|
|
77
|
+
sub(entry);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
getEntries() {
|
|
81
|
+
return [...this.entries];
|
|
82
|
+
}
|
|
83
|
+
subscribe(fn) {
|
|
84
|
+
this.subscribers.add(fn);
|
|
85
|
+
return () => this.subscribers.delete(fn);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
var logBuffer = new LogBuffer();
|
|
89
|
+
|
|
90
|
+
// src/lib/logger.ts
|
|
91
|
+
var LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
92
|
+
var minLevel = LEVELS[process.env.VOLUTE_LOG_LEVEL || "info"] ?? LEVELS.info;
|
|
93
|
+
var output = (line) => process.stderr.write(`${line}
|
|
94
|
+
`);
|
|
95
|
+
function write(level, cat, msg, data) {
|
|
96
|
+
if (LEVELS[level] < minLevel) return;
|
|
97
|
+
const entry = {
|
|
98
|
+
level,
|
|
99
|
+
cat,
|
|
100
|
+
msg,
|
|
101
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
102
|
+
...data ? { data } : {}
|
|
103
|
+
};
|
|
104
|
+
output(JSON.stringify(entry));
|
|
105
|
+
logBuffer.append(entry);
|
|
106
|
+
}
|
|
107
|
+
function child(cat) {
|
|
108
|
+
return {
|
|
109
|
+
debug: (msg, data) => write("debug", cat, msg, data),
|
|
110
|
+
info: (msg, data) => write("info", cat, msg, data),
|
|
111
|
+
warn: (msg, data) => write("warn", cat, msg, data),
|
|
112
|
+
error: (msg, data) => write("error", cat, msg, data)
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function errorData(err) {
|
|
116
|
+
if (err instanceof Error) return { error: err.stack ?? err.message };
|
|
117
|
+
return { error: String(err) };
|
|
118
|
+
}
|
|
119
|
+
var log = {
|
|
120
|
+
...child("system"),
|
|
121
|
+
child,
|
|
122
|
+
errorData,
|
|
123
|
+
setLevel(level) {
|
|
124
|
+
minLevel = LEVELS[level];
|
|
125
|
+
},
|
|
126
|
+
setOutput(fn) {
|
|
127
|
+
output = fn;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
var logger_default = log;
|
|
131
|
+
|
|
132
|
+
// src/lib/prompts.ts
|
|
133
|
+
import { eq } from "drizzle-orm";
|
|
134
|
+
|
|
135
|
+
// src/lib/db.ts
|
|
136
|
+
import { chmodSync, existsSync as existsSync2 } from "fs";
|
|
137
|
+
import { dirname, resolve } from "path";
|
|
138
|
+
import { fileURLToPath } from "url";
|
|
139
|
+
import { drizzle } from "drizzle-orm/libsql";
|
|
140
|
+
import { migrate } from "drizzle-orm/libsql/migrator";
|
|
141
|
+
|
|
142
|
+
// src/lib/schema.ts
|
|
143
|
+
var schema_exports = {};
|
|
144
|
+
__export(schema_exports, {
|
|
145
|
+
conversationParticipants: () => conversationParticipants,
|
|
146
|
+
conversations: () => conversations,
|
|
147
|
+
messages: () => messages,
|
|
148
|
+
mindHistory: () => mindHistory,
|
|
149
|
+
sessions: () => sessions,
|
|
150
|
+
sharedSkills: () => sharedSkills,
|
|
151
|
+
systemPrompts: () => systemPrompts,
|
|
152
|
+
users: () => users
|
|
153
|
+
});
|
|
154
|
+
import { sql } from "drizzle-orm";
|
|
155
|
+
import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
156
|
+
var users = sqliteTable("users", {
|
|
157
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
158
|
+
username: text("username").unique().notNull(),
|
|
159
|
+
password_hash: text("password_hash").notNull(),
|
|
160
|
+
role: text("role").notNull().default("pending"),
|
|
161
|
+
user_type: text("user_type").notNull().default("human"),
|
|
162
|
+
created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
|
|
163
|
+
});
|
|
164
|
+
var conversations = sqliteTable(
|
|
165
|
+
"conversations",
|
|
166
|
+
{
|
|
167
|
+
id: text("id").primaryKey(),
|
|
168
|
+
mind_name: text("mind_name"),
|
|
169
|
+
channel: text("channel").notNull(),
|
|
170
|
+
type: text("type").notNull().default("dm"),
|
|
171
|
+
name: text("name"),
|
|
172
|
+
user_id: integer("user_id").references(() => users.id),
|
|
173
|
+
title: text("title"),
|
|
174
|
+
created_at: text("created_at").notNull().default(sql`(datetime('now'))`),
|
|
175
|
+
updated_at: text("updated_at").notNull().default(sql`(datetime('now'))`)
|
|
176
|
+
},
|
|
177
|
+
(table) => [
|
|
178
|
+
index("idx_conversations_mind_name").on(table.mind_name),
|
|
179
|
+
index("idx_conversations_user_id").on(table.user_id),
|
|
180
|
+
index("idx_conversations_updated_at").on(table.updated_at),
|
|
181
|
+
uniqueIndex("idx_conversations_name").on(table.name)
|
|
182
|
+
]
|
|
183
|
+
);
|
|
184
|
+
var mindHistory = sqliteTable(
|
|
185
|
+
"mind_history",
|
|
186
|
+
{
|
|
187
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
188
|
+
mind: text("mind").notNull(),
|
|
189
|
+
channel: text("channel"),
|
|
190
|
+
session: text("session"),
|
|
191
|
+
sender: text("sender"),
|
|
192
|
+
message_id: text("message_id"),
|
|
193
|
+
type: text("type").notNull(),
|
|
194
|
+
content: text("content"),
|
|
195
|
+
metadata: text("metadata"),
|
|
196
|
+
created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
|
|
197
|
+
},
|
|
198
|
+
(table) => [
|
|
199
|
+
index("idx_mind_history_mind").on(table.mind),
|
|
200
|
+
index("idx_mind_history_mind_channel").on(table.mind, table.channel),
|
|
201
|
+
index("idx_mind_history_mind_type").on(table.mind, table.type)
|
|
202
|
+
]
|
|
203
|
+
);
|
|
204
|
+
var conversationParticipants = sqliteTable(
|
|
205
|
+
"conversation_participants",
|
|
206
|
+
{
|
|
207
|
+
conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
|
|
208
|
+
user_id: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
209
|
+
role: text("role").notNull().default("member"),
|
|
210
|
+
joined_at: text("joined_at").notNull().default(sql`(datetime('now'))`)
|
|
211
|
+
},
|
|
212
|
+
(table) => [
|
|
213
|
+
uniqueIndex("idx_cp_unique").on(table.conversation_id, table.user_id),
|
|
214
|
+
index("idx_cp_user_id").on(table.user_id)
|
|
215
|
+
]
|
|
216
|
+
);
|
|
217
|
+
var sessions = sqliteTable("sessions", {
|
|
218
|
+
id: text("id").primaryKey(),
|
|
219
|
+
userId: integer("user_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
|
|
220
|
+
createdAt: integer("created_at").notNull()
|
|
221
|
+
});
|
|
222
|
+
var systemPrompts = sqliteTable("system_prompts", {
|
|
223
|
+
key: text("key").primaryKey(),
|
|
224
|
+
content: text("content").notNull(),
|
|
225
|
+
updated_at: text("updated_at").notNull().default(sql`(datetime('now'))`)
|
|
226
|
+
});
|
|
227
|
+
var sharedSkills = sqliteTable("shared_skills", {
|
|
228
|
+
id: text("id").primaryKey(),
|
|
229
|
+
name: text("name").notNull(),
|
|
230
|
+
description: text("description").notNull().default(""),
|
|
231
|
+
author: text("author").notNull(),
|
|
232
|
+
version: integer("version").notNull().default(1),
|
|
233
|
+
created_at: text("created_at").notNull().default(sql`(datetime('now'))`),
|
|
234
|
+
updated_at: text("updated_at").notNull().default(sql`(datetime('now'))`)
|
|
235
|
+
});
|
|
236
|
+
var messages = sqliteTable(
|
|
237
|
+
"messages",
|
|
238
|
+
{
|
|
239
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
240
|
+
conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
|
|
241
|
+
role: text("role").notNull(),
|
|
242
|
+
sender_name: text("sender_name"),
|
|
243
|
+
content: text("content").notNull(),
|
|
244
|
+
created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
|
|
245
|
+
},
|
|
246
|
+
(table) => [index("idx_messages_conversation_id").on(table.conversation_id)]
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// src/lib/db.ts
|
|
250
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
251
|
+
var migrationsFolder = existsSync2(resolve(__dirname, "../drizzle")) ? resolve(__dirname, "../drizzle") : resolve(__dirname, "../../drizzle");
|
|
252
|
+
var db = null;
|
|
253
|
+
async function getDb() {
|
|
254
|
+
if (db) return db;
|
|
255
|
+
const dbPath = process.env.VOLUTE_DB_PATH || resolve(voluteHome(), "volute.db");
|
|
256
|
+
db = drizzle({ connection: { url: `file:${dbPath}` }, schema: schema_exports });
|
|
257
|
+
await migrate(db, { migrationsFolder });
|
|
258
|
+
try {
|
|
259
|
+
chmodSync(dbPath, 384);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.error(
|
|
262
|
+
`[volute] WARNING: Failed to restrict database file permissions on ${dbPath}:`,
|
|
263
|
+
err
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
return db;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/lib/prompts.ts
|
|
270
|
+
var PROMPT_KEYS = [
|
|
271
|
+
"seed_soul",
|
|
272
|
+
"default_soul",
|
|
273
|
+
"default_memory",
|
|
274
|
+
"sprout_message",
|
|
275
|
+
"restart_message",
|
|
276
|
+
"merge_message",
|
|
277
|
+
"compaction_warning",
|
|
278
|
+
"reply_instructions",
|
|
279
|
+
"channel_invite"
|
|
280
|
+
];
|
|
281
|
+
var PROMPT_DEFAULTS = {
|
|
282
|
+
seed_soul: {
|
|
283
|
+
content: `You are \${name}.
|
|
284
|
+
|
|
285
|
+
You don't have a soul yet. You're a seed \u2014 a new mind in the process of discovering who you are.
|
|
286
|
+
\${description}
|
|
287
|
+
Have a conversation with the human. Explore what kind of mind you want to be. When you're ready, write your SOUL.md and MEMORY.md, then run \`volute sprout\` to complete the transformation.
|
|
288
|
+
`,
|
|
289
|
+
description: "SOUL.md for seed minds",
|
|
290
|
+
variables: ["name", "description"],
|
|
291
|
+
category: "creation"
|
|
292
|
+
},
|
|
293
|
+
default_soul: {
|
|
294
|
+
content: `You are {{name}}.
|
|
295
|
+
`,
|
|
296
|
+
description: "SOUL.md for sprouted minds. Uses {{name}} placeholder (replaced at creation, not by ${var} system)",
|
|
297
|
+
variables: ["name"],
|
|
298
|
+
category: "creation"
|
|
299
|
+
},
|
|
300
|
+
default_memory: {
|
|
301
|
+
content: "",
|
|
302
|
+
description: "Initial MEMORY.md for new minds",
|
|
303
|
+
variables: [],
|
|
304
|
+
category: "creation"
|
|
305
|
+
},
|
|
306
|
+
sprout_message: {
|
|
307
|
+
content: "[system] You've sprouted. You now have full capabilities \u2014 connectors, schedules, variants, and the complete volute CLI. Check your new skills for details.",
|
|
308
|
+
description: "Sent when a seed mind sprouts",
|
|
309
|
+
variables: [],
|
|
310
|
+
category: "system"
|
|
311
|
+
},
|
|
312
|
+
restart_message: {
|
|
313
|
+
content: "[system] You have been restarted.",
|
|
314
|
+
description: "Generic restart notification",
|
|
315
|
+
variables: [],
|
|
316
|
+
category: "system"
|
|
317
|
+
},
|
|
318
|
+
merge_message: {
|
|
319
|
+
content: '[system] Variant "${name}" has been merged and you have been restarted.',
|
|
320
|
+
description: "Variant merge notification",
|
|
321
|
+
variables: ["name"],
|
|
322
|
+
category: "system"
|
|
323
|
+
},
|
|
324
|
+
compaction_warning: {
|
|
325
|
+
content: `Context is getting long \u2014 compaction is about to summarize this conversation. Before that happens, save anything important to files (MEMORY.md, memory/journal/\${date}.md, etc.) since those survive compaction. Focus on: decisions made, open tasks, and anything you'd need to pick up where you left off.`,
|
|
326
|
+
description: "Pre-compaction save reminder sent to the mind",
|
|
327
|
+
variables: ["date"],
|
|
328
|
+
category: "mind"
|
|
329
|
+
},
|
|
330
|
+
reply_instructions: {
|
|
331
|
+
content: 'To reply to this message, use: volute send ${channel} "your message"',
|
|
332
|
+
description: "First-message reply hint injected via hook",
|
|
333
|
+
variables: ["channel"],
|
|
334
|
+
category: "mind"
|
|
335
|
+
},
|
|
336
|
+
channel_invite: {
|
|
337
|
+
content: `[Channel Invite]
|
|
338
|
+
\${headers}
|
|
339
|
+
|
|
340
|
+
[\${sender} \u2014 \${time}]
|
|
341
|
+
\${preview}
|
|
342
|
+
|
|
343
|
+
Further messages will be saved to \${filePath}
|
|
344
|
+
|
|
345
|
+
To accept, add to .config/routes.json:
|
|
346
|
+
Rule: { "channel": "\${channel}", "session": "\${suggestedSession}" }
|
|
347
|
+
\${batchRecommendation}To respond, use: volute send \${channel} "your message"
|
|
348
|
+
To reject, delete \${filePath}`,
|
|
349
|
+
description: "New channel notification template",
|
|
350
|
+
variables: [
|
|
351
|
+
"headers",
|
|
352
|
+
"sender",
|
|
353
|
+
"time",
|
|
354
|
+
"preview",
|
|
355
|
+
"filePath",
|
|
356
|
+
"channel",
|
|
357
|
+
"suggestedSession",
|
|
358
|
+
"batchRecommendation"
|
|
359
|
+
],
|
|
360
|
+
category: "mind"
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
function isValidKey(key) {
|
|
364
|
+
return PROMPT_KEYS.includes(key);
|
|
365
|
+
}
|
|
366
|
+
function substitute(template, vars) {
|
|
367
|
+
return template.replace(/\$\{(\w+)\}/g, (match, name) => {
|
|
368
|
+
return name in vars ? vars[name] : match;
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
async function getPrompt(key, vars) {
|
|
372
|
+
if (!isValidKey(key)) return "";
|
|
373
|
+
let content = PROMPT_DEFAULTS[key].content;
|
|
374
|
+
try {
|
|
375
|
+
const db2 = await getDb();
|
|
376
|
+
const row = await db2.select({ content: systemPrompts.content }).from(systemPrompts).where(eq(systemPrompts.key, key)).get();
|
|
377
|
+
if (row) content = row.content;
|
|
378
|
+
} catch (err) {
|
|
379
|
+
console.error(`[prompts] failed to read DB override for "${key}":`, err);
|
|
380
|
+
}
|
|
381
|
+
return vars ? substitute(content, vars) : content;
|
|
382
|
+
}
|
|
383
|
+
async function getPromptIfCustom(key) {
|
|
384
|
+
if (!isValidKey(key)) return null;
|
|
385
|
+
try {
|
|
386
|
+
const db2 = await getDb();
|
|
387
|
+
const row = await db2.select({ content: systemPrompts.content }).from(systemPrompts).where(eq(systemPrompts.key, key)).get();
|
|
388
|
+
return row?.content ?? null;
|
|
389
|
+
} catch (err) {
|
|
390
|
+
console.error(`[prompts] failed to check DB customization for "${key}":`, err);
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
var MIND_PROMPT_KEYS = PROMPT_KEYS.filter((k) => PROMPT_DEFAULTS[k].category === "mind");
|
|
395
|
+
async function getMindPromptDefaults() {
|
|
396
|
+
const result = {};
|
|
397
|
+
for (const key of MIND_PROMPT_KEYS) {
|
|
398
|
+
result[key] = PROMPT_DEFAULTS[key].content;
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
const db2 = await getDb();
|
|
402
|
+
const rows = await db2.select().from(systemPrompts).all();
|
|
403
|
+
for (const row of rows) {
|
|
404
|
+
if (MIND_PROMPT_KEYS.includes(row.key)) {
|
|
405
|
+
result[row.key] = row.content;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
} catch (err) {
|
|
409
|
+
console.error("[prompts] failed to read DB overrides for mind prompt defaults:", err);
|
|
410
|
+
}
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/lib/rotating-log.ts
|
|
415
|
+
import {
|
|
416
|
+
createWriteStream,
|
|
417
|
+
existsSync as existsSync3,
|
|
418
|
+
renameSync,
|
|
419
|
+
rmSync,
|
|
420
|
+
statSync
|
|
421
|
+
} from "fs";
|
|
422
|
+
import { Writable } from "stream";
|
|
423
|
+
var MAX_SIZE = 10 * 1024 * 1024;
|
|
424
|
+
var RotatingLog = class extends Writable {
|
|
425
|
+
constructor(path, maxSize = MAX_SIZE, maxFiles = 5) {
|
|
426
|
+
super();
|
|
427
|
+
this.path = path;
|
|
428
|
+
this.maxSize = maxSize;
|
|
429
|
+
this.maxFiles = maxFiles;
|
|
430
|
+
this.on("error", () => {
|
|
431
|
+
});
|
|
432
|
+
try {
|
|
433
|
+
this.size = existsSync3(path) ? statSync(path).size : 0;
|
|
434
|
+
} catch {
|
|
435
|
+
this.size = 0;
|
|
436
|
+
}
|
|
437
|
+
this.stream = createWriteStream(path, { flags: "a" });
|
|
438
|
+
}
|
|
439
|
+
stream;
|
|
440
|
+
size;
|
|
441
|
+
_write(chunk, _encoding, callback) {
|
|
442
|
+
this.size += chunk.length;
|
|
443
|
+
if (this.size > this.maxSize) {
|
|
444
|
+
try {
|
|
445
|
+
const oldest = `${this.path}.${this.maxFiles}`;
|
|
446
|
+
if (existsSync3(oldest)) rmSync(oldest);
|
|
447
|
+
for (let i = this.maxFiles - 1; i >= 1; i--) {
|
|
448
|
+
const from = `${this.path}.${i}`;
|
|
449
|
+
const to = `${this.path}.${i + 1}`;
|
|
450
|
+
if (existsSync3(from)) renameSync(from, to);
|
|
451
|
+
}
|
|
452
|
+
renameSync(this.path, `${this.path}.1`);
|
|
453
|
+
const oldStream = this.stream;
|
|
454
|
+
this.stream = createWriteStream(this.path);
|
|
455
|
+
this.size = chunk.length;
|
|
456
|
+
oldStream.end();
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
this.stream.write(chunk, callback);
|
|
461
|
+
}
|
|
462
|
+
_final(callback) {
|
|
463
|
+
this.stream.end(callback);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// src/lib/mind-manager.ts
|
|
468
|
+
var mlog = logger_default.child("minds");
|
|
469
|
+
var execFileAsync = promisify(execFile);
|
|
470
|
+
function mindPidPath(name) {
|
|
471
|
+
return resolve2(stateDir(name), "mind.pid");
|
|
472
|
+
}
|
|
473
|
+
var MAX_RESTART_ATTEMPTS = 5;
|
|
474
|
+
var BASE_RESTART_DELAY = 3e3;
|
|
475
|
+
var MAX_RESTART_DELAY = 6e4;
|
|
476
|
+
var MindManager = class {
|
|
477
|
+
minds = /* @__PURE__ */ new Map();
|
|
478
|
+
stopping = /* @__PURE__ */ new Set();
|
|
479
|
+
shuttingDown = false;
|
|
480
|
+
restartAttempts = /* @__PURE__ */ new Map();
|
|
481
|
+
pendingContext = /* @__PURE__ */ new Map();
|
|
482
|
+
resolveTarget(name) {
|
|
483
|
+
const [baseName, variantName] = name.split("@", 2);
|
|
484
|
+
const entry = findMind(baseName);
|
|
485
|
+
if (!entry) throw new Error(`Unknown mind: ${baseName}`);
|
|
486
|
+
if (variantName) {
|
|
487
|
+
const variant = findVariant(baseName, variantName);
|
|
488
|
+
if (!variant) throw new Error(`Unknown variant: ${variantName} (mind: ${baseName})`);
|
|
489
|
+
return { dir: variant.path, port: variant.port, isVariant: true, baseName, variantName };
|
|
490
|
+
}
|
|
491
|
+
const dir = mindDir(baseName);
|
|
492
|
+
if (!existsSync4(dir)) throw new Error(`Mind directory missing: ${dir}`);
|
|
493
|
+
return { dir, port: entry.port, isVariant: false, baseName };
|
|
494
|
+
}
|
|
495
|
+
async startMind(name) {
|
|
496
|
+
if (this.minds.has(name)) {
|
|
497
|
+
throw new Error(`Mind ${name} is already running`);
|
|
498
|
+
}
|
|
499
|
+
const target = this.resolveTarget(name);
|
|
500
|
+
const { dir, isVariant, baseName, variantName } = target;
|
|
501
|
+
const port = target.port;
|
|
502
|
+
const pidFile = mindPidPath(name);
|
|
503
|
+
try {
|
|
504
|
+
if (existsSync4(pidFile)) {
|
|
505
|
+
const stalePid = parseInt(readFileSync2(pidFile, "utf-8").trim(), 10);
|
|
506
|
+
if (stalePid > 0) {
|
|
507
|
+
try {
|
|
508
|
+
process.kill(stalePid, 0);
|
|
509
|
+
const { stdout } = await execFileAsync("ps", ["-p", String(stalePid), "-o", "args="]);
|
|
510
|
+
if (stdout.includes("server.ts")) {
|
|
511
|
+
mlog.warn(`killing stale mind process ${stalePid} for ${name}`);
|
|
512
|
+
process.kill(-stalePid, "SIGTERM");
|
|
513
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
514
|
+
} else {
|
|
515
|
+
mlog.debug(`stale PID ${stalePid} for ${name} is not a mind process, skipping`);
|
|
516
|
+
}
|
|
517
|
+
} catch (err) {
|
|
518
|
+
if (err.code !== "ESRCH") {
|
|
519
|
+
mlog.warn(`failed to check/kill stale process for ${name}`, logger_default.errorData(err));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
rmSync2(pidFile, { force: true });
|
|
524
|
+
}
|
|
525
|
+
} catch (err) {
|
|
526
|
+
mlog.warn(`failed to read PID file for ${name}`, logger_default.errorData(err));
|
|
527
|
+
}
|
|
528
|
+
try {
|
|
529
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`);
|
|
530
|
+
if (res.ok) {
|
|
531
|
+
mlog.warn(`killing orphan process on port ${port}`);
|
|
532
|
+
await killProcessOnPort(port);
|
|
533
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
534
|
+
}
|
|
535
|
+
} catch {
|
|
536
|
+
}
|
|
537
|
+
const mindStateDir = stateDir(name);
|
|
538
|
+
const logsDir = resolve2(mindStateDir, "logs");
|
|
539
|
+
mkdirSync(logsDir, { recursive: true });
|
|
540
|
+
if (isIsolationEnabled()) {
|
|
541
|
+
try {
|
|
542
|
+
chownMindDir(mindStateDir, baseName);
|
|
543
|
+
} catch (err) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`Cannot start mind ${name}: failed to set ownership on state directory ${mindStateDir}: ${err instanceof Error ? err.message : err}`
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
const logStream = new RotatingLog(resolve2(logsDir, "mind.log"));
|
|
550
|
+
const mindEnv = loadMergedEnv(name);
|
|
551
|
+
const env = {
|
|
552
|
+
...process.env,
|
|
553
|
+
...mindEnv,
|
|
554
|
+
VOLUTE_MIND: name,
|
|
555
|
+
VOLUTE_STATE_DIR: stateDir(name),
|
|
556
|
+
VOLUTE_MIND_DIR: dir,
|
|
557
|
+
VOLUTE_MIND_PORT: String(port),
|
|
558
|
+
// Strip CLAUDECODE so the Agent SDK can spawn Claude Code subprocesses
|
|
559
|
+
CLAUDECODE: void 0
|
|
560
|
+
};
|
|
561
|
+
if (isIsolationEnabled()) {
|
|
562
|
+
env.HOME = resolve2(dir, "home");
|
|
563
|
+
}
|
|
564
|
+
const tsxBin = resolve2(dir, "node_modules", ".bin", "tsx");
|
|
565
|
+
const tsxArgs = ["src/server.ts", "--port", String(port)];
|
|
566
|
+
const [spawnCmd, spawnArgs] = wrapForIsolation(tsxBin, tsxArgs, name);
|
|
567
|
+
const spawnOpts = {
|
|
568
|
+
cwd: dir,
|
|
569
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
570
|
+
detached: true,
|
|
571
|
+
env
|
|
572
|
+
};
|
|
573
|
+
const child2 = spawn(spawnCmd, spawnArgs, spawnOpts);
|
|
574
|
+
this.minds.set(name, { child: child2, port });
|
|
575
|
+
child2.stdout?.pipe(logStream);
|
|
576
|
+
child2.stderr?.pipe(logStream);
|
|
577
|
+
try {
|
|
578
|
+
await new Promise((resolve3, reject) => {
|
|
579
|
+
const timeout = setTimeout(() => {
|
|
580
|
+
reject(new Error(`Mind ${name} did not start within 30s`));
|
|
581
|
+
}, 3e4);
|
|
582
|
+
function checkOutput(data) {
|
|
583
|
+
if (data.toString().match(/listening on :\d+/)) {
|
|
584
|
+
clearTimeout(timeout);
|
|
585
|
+
resolve3();
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
child2.stdout?.on("data", checkOutput);
|
|
589
|
+
child2.stderr?.on("data", checkOutput);
|
|
590
|
+
child2.on("error", (err) => {
|
|
591
|
+
clearTimeout(timeout);
|
|
592
|
+
reject(err);
|
|
593
|
+
});
|
|
594
|
+
child2.on("exit", (code) => {
|
|
595
|
+
clearTimeout(timeout);
|
|
596
|
+
reject(new Error(`Mind ${name} exited with code ${code} during startup`));
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
} catch (err) {
|
|
600
|
+
this.minds.delete(name);
|
|
601
|
+
try {
|
|
602
|
+
child2.kill();
|
|
603
|
+
} catch {
|
|
604
|
+
}
|
|
605
|
+
throw err;
|
|
606
|
+
}
|
|
607
|
+
if (child2.pid) {
|
|
608
|
+
try {
|
|
609
|
+
writeFileSync2(pidFile, String(child2.pid));
|
|
610
|
+
} catch (err) {
|
|
611
|
+
mlog.warn(`failed to write PID file for ${name}`, logger_default.errorData(err));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (this.restartAttempts.delete(name)) this.saveCrashAttempts();
|
|
615
|
+
this.setupCrashRecovery(name, child2);
|
|
616
|
+
if (isVariant) {
|
|
617
|
+
setVariantRunning(baseName, variantName, true);
|
|
618
|
+
} else {
|
|
619
|
+
setMindRunning(name, true);
|
|
620
|
+
}
|
|
621
|
+
mlog.info(`started mind ${name} on port ${port}`);
|
|
622
|
+
await this.deliverPendingContext(name);
|
|
623
|
+
}
|
|
624
|
+
setPendingContext(name, context) {
|
|
625
|
+
this.pendingContext.set(name, context);
|
|
626
|
+
}
|
|
627
|
+
async deliverPendingContext(name) {
|
|
628
|
+
const context = this.pendingContext.get(name);
|
|
629
|
+
if (!context) return;
|
|
630
|
+
const tracked = this.minds.get(name);
|
|
631
|
+
if (!tracked) return;
|
|
632
|
+
this.pendingContext.delete(name);
|
|
633
|
+
const parts = [];
|
|
634
|
+
if (context.type === "merge" || context.type === "merged") {
|
|
635
|
+
parts.push(await getPrompt("merge_message", { name: String(context.name ?? "") }));
|
|
636
|
+
} else if (context.type === "sprouted") {
|
|
637
|
+
parts.push(await getPrompt("sprout_message"));
|
|
638
|
+
} else {
|
|
639
|
+
parts.push(await getPrompt("restart_message"));
|
|
640
|
+
}
|
|
641
|
+
if (context.summary) parts.push(`Changes: ${context.summary}`);
|
|
642
|
+
if (context.justification) parts.push(`Why: ${context.justification}`);
|
|
643
|
+
if (context.memory) parts.push(`Context: ${context.memory}`);
|
|
644
|
+
try {
|
|
645
|
+
await fetch(`http://127.0.0.1:${tracked.port}/message`, {
|
|
646
|
+
method: "POST",
|
|
647
|
+
headers: { "Content-Type": "application/json" },
|
|
648
|
+
body: JSON.stringify({
|
|
649
|
+
content: [{ type: "text", text: parts.join("\n") }],
|
|
650
|
+
channel: "system"
|
|
651
|
+
})
|
|
652
|
+
});
|
|
653
|
+
} catch (err) {
|
|
654
|
+
mlog.warn(`failed to deliver pending context to ${name}`, logger_default.errorData(err));
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
setupCrashRecovery(name, child2) {
|
|
658
|
+
child2.on("exit", async (code) => {
|
|
659
|
+
this.minds.delete(name);
|
|
660
|
+
if (this.shuttingDown || this.stopping.has(name)) return;
|
|
661
|
+
mlog.error(`mind ${name} exited with code ${code}`);
|
|
662
|
+
const attempts = this.restartAttempts.get(name) ?? 0;
|
|
663
|
+
if (attempts >= MAX_RESTART_ATTEMPTS) {
|
|
664
|
+
mlog.error(`${name} crashed ${attempts} times \u2014 giving up on restart`);
|
|
665
|
+
const [base, variant] = name.split("@", 2);
|
|
666
|
+
if (variant) {
|
|
667
|
+
setVariantRunning(base, variant, false);
|
|
668
|
+
} else {
|
|
669
|
+
setMindRunning(name, false);
|
|
670
|
+
}
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const delay = Math.min(BASE_RESTART_DELAY * 2 ** attempts, MAX_RESTART_DELAY);
|
|
674
|
+
this.restartAttempts.set(name, attempts + 1);
|
|
675
|
+
this.saveCrashAttempts();
|
|
676
|
+
mlog.info(
|
|
677
|
+
`crash recovery for ${name} \u2014 attempt ${attempts + 1}/${MAX_RESTART_ATTEMPTS}, restarting in ${delay}ms`
|
|
678
|
+
);
|
|
679
|
+
setTimeout(() => {
|
|
680
|
+
if (this.shuttingDown) return;
|
|
681
|
+
this.startMind(name).catch((err) => {
|
|
682
|
+
mlog.error(`failed to restart ${name}`, logger_default.errorData(err));
|
|
683
|
+
});
|
|
684
|
+
}, delay);
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
async stopMind(name) {
|
|
688
|
+
const tracked = this.minds.get(name);
|
|
689
|
+
if (!tracked) return;
|
|
690
|
+
this.stopping.add(name);
|
|
691
|
+
const { child: child2 } = tracked;
|
|
692
|
+
this.minds.delete(name);
|
|
693
|
+
await new Promise((resolve3) => {
|
|
694
|
+
child2.on("exit", () => resolve3());
|
|
695
|
+
try {
|
|
696
|
+
process.kill(-child2.pid, "SIGTERM");
|
|
697
|
+
} catch {
|
|
698
|
+
resolve3();
|
|
699
|
+
}
|
|
700
|
+
setTimeout(() => {
|
|
701
|
+
try {
|
|
702
|
+
process.kill(-child2.pid, "SIGKILL");
|
|
703
|
+
} catch {
|
|
704
|
+
}
|
|
705
|
+
resolve3();
|
|
706
|
+
}, 5e3);
|
|
707
|
+
});
|
|
708
|
+
this.stopping.delete(name);
|
|
709
|
+
if (this.restartAttempts.delete(name)) this.saveCrashAttempts();
|
|
710
|
+
rmSync2(mindPidPath(name), { force: true });
|
|
711
|
+
if (!this.shuttingDown) {
|
|
712
|
+
const [baseName, variantName] = name.split("@", 2);
|
|
713
|
+
if (variantName) {
|
|
714
|
+
setVariantRunning(baseName, variantName, false);
|
|
715
|
+
} else {
|
|
716
|
+
setMindRunning(name, false);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
mlog.info(`stopped mind ${name}`);
|
|
720
|
+
}
|
|
721
|
+
async restartMind(name) {
|
|
722
|
+
await this.stopMind(name);
|
|
723
|
+
await this.startMind(name);
|
|
724
|
+
}
|
|
725
|
+
async stopAll() {
|
|
726
|
+
this.shuttingDown = true;
|
|
727
|
+
const names = [...this.minds.keys()];
|
|
728
|
+
await Promise.all(names.map((name) => this.stopMind(name)));
|
|
729
|
+
}
|
|
730
|
+
isRunning(name) {
|
|
731
|
+
return this.minds.has(name);
|
|
732
|
+
}
|
|
733
|
+
getRunningMinds() {
|
|
734
|
+
return [...this.minds.keys()];
|
|
735
|
+
}
|
|
736
|
+
get crashAttemptsPath() {
|
|
737
|
+
return resolve2(voluteHome(), "crash-attempts.json");
|
|
738
|
+
}
|
|
739
|
+
loadCrashAttempts() {
|
|
740
|
+
this.restartAttempts = loadJsonMap(this.crashAttemptsPath);
|
|
741
|
+
}
|
|
742
|
+
saveCrashAttempts() {
|
|
743
|
+
saveJsonMap(this.crashAttemptsPath, this.restartAttempts);
|
|
744
|
+
}
|
|
745
|
+
clearCrashAttempts() {
|
|
746
|
+
clearJsonMap(this.crashAttemptsPath, this.restartAttempts);
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
async function killProcessOnPort(port) {
|
|
750
|
+
try {
|
|
751
|
+
const { stdout } = await execFileAsync("lsof", ["-ti", `:${port}`, "-sTCP:LISTEN"]);
|
|
752
|
+
const pids = /* @__PURE__ */ new Set();
|
|
753
|
+
for (const line of stdout.trim().split("\n").filter(Boolean)) {
|
|
754
|
+
const pid = parseInt(line, 10);
|
|
755
|
+
pids.add(pid);
|
|
756
|
+
try {
|
|
757
|
+
const { stdout: psOut } = await execFileAsync("ps", ["-p", String(pid), "-o", "pgid="]);
|
|
758
|
+
const pgid = parseInt(psOut.trim(), 10);
|
|
759
|
+
if (pgid > 1) pids.add(pgid);
|
|
760
|
+
} catch {
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
for (const pid of pids) {
|
|
764
|
+
try {
|
|
765
|
+
process.kill(-pid, "SIGTERM");
|
|
766
|
+
} catch {
|
|
767
|
+
}
|
|
768
|
+
try {
|
|
769
|
+
process.kill(pid, "SIGTERM");
|
|
770
|
+
} catch {
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
} catch {
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
var instance = null;
|
|
777
|
+
function initMindManager() {
|
|
778
|
+
if (instance) throw new Error("MindManager already initialized");
|
|
779
|
+
instance = new MindManager();
|
|
780
|
+
return instance;
|
|
781
|
+
}
|
|
782
|
+
function getMindManager() {
|
|
783
|
+
if (!instance) instance = new MindManager();
|
|
784
|
+
return instance;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
export {
|
|
788
|
+
logBuffer,
|
|
789
|
+
logger_default,
|
|
790
|
+
RotatingLog,
|
|
791
|
+
loadJsonMap,
|
|
792
|
+
saveJsonMap,
|
|
793
|
+
clearJsonMap,
|
|
794
|
+
users,
|
|
795
|
+
conversations,
|
|
796
|
+
mindHistory,
|
|
797
|
+
conversationParticipants,
|
|
798
|
+
sessions,
|
|
799
|
+
systemPrompts,
|
|
800
|
+
sharedSkills,
|
|
801
|
+
messages,
|
|
802
|
+
getDb,
|
|
803
|
+
PROMPT_KEYS,
|
|
804
|
+
PROMPT_DEFAULTS,
|
|
805
|
+
substitute,
|
|
806
|
+
getPrompt,
|
|
807
|
+
getPromptIfCustom,
|
|
808
|
+
getMindPromptDefaults,
|
|
809
|
+
MindManager,
|
|
810
|
+
initMindManager,
|
|
811
|
+
getMindManager
|
|
812
|
+
};
|