volute 0.3.1 → 0.5.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 +29 -29
- package/dist/agent-Z2B6EFEQ.js +75 -0
- package/dist/{agent-manager-AUCKMGPR.js → agent-manager-PXBKA2GK.js} +4 -4
- package/dist/channel-MK5OK2SI.js +113 -0
- package/dist/chunk-5X7HGB6L.js +107 -0
- package/dist/{chunk-YGFIWIOF.js → chunk-7L4AN5D4.js} +1 -1
- package/dist/{chunk-VRVVQIYY.js → chunk-AZEL2IEK.js} +1 -1
- package/dist/chunk-B3R6L2GW.js +24 -0
- package/dist/{chunk-DNOXHLE5.js → chunk-HE67X4T6.js} +1 -1
- package/dist/{chunk-I6OHXCMV.js → chunk-MW2KFO3B.js} +47 -9
- package/dist/chunk-MXUCNIBG.js +168 -0
- package/dist/chunk-SMISE4SV.js +226 -0
- package/dist/{chunk-SOZA2TLP.js → chunk-UAVD2AHX.js} +1 -1
- package/dist/{chunk-3C2XR4IY.js → chunk-UX25Z2ND.js} +113 -107
- package/dist/{chunk-GSPKUPKU.js → chunk-XUA3JUFK.js} +2 -1
- package/dist/chunk-ZYGKG6VC.js +22 -0
- package/dist/cli.js +98 -75
- package/dist/connector-LYEMXQEV.js +157 -0
- package/dist/connectors/discord.js +104 -161
- package/dist/connectors/slack.js +179 -0
- package/dist/connectors/telegram.js +175 -0
- package/dist/conversation-ERXEQZTY.js +163 -0
- package/dist/create-RVCZN6HE.js +91 -0
- package/dist/{daemon-client-XR24PUJF.js → daemon-client-ZY6UUN2M.js} +2 -2
- package/dist/daemon.js +824 -252
- package/dist/{delete-GQ7JEK2S.js → delete-3QH7VYIN.js} +8 -9
- package/dist/{down-3OB6UVAJ.js → down-O7IFZLVJ.js} +1 -1
- package/dist/{env-JB27UAC3.js → env-4D4REPJF.js} +8 -5
- package/dist/{history-3VRUBGGV.js → history-OEONB53Z.js} +5 -5
- package/dist/{import-K4MP2GX7.js → import-MXJB2EII.js} +23 -8
- package/dist/{logs-NXFFGUKY.js → logs-DF342W4M.js} +2 -2
- package/dist/message-ADHWFHSI.js +32 -0
- package/dist/package-VQOE7JNH.js +89 -0
- package/dist/{schedule-4I5TYHFH.js → schedule-NAG6F463.js} +12 -7
- package/dist/send-66QMKRUH.js +75 -0
- package/dist/{setup-SRS7AUAA.js → setup-RPRRGG2F.js} +6 -6
- package/dist/{start-LDPMCMYT.js → start-TUOXDSFL.js} +3 -3
- package/dist/{status-MVSQG54T.js → status-A36EHRO4.js} +3 -3
- package/dist/{stop-5PZTZCLL.js → stop-AOJZLQ5X.js} +6 -7
- package/dist/{up-UT3IMKCA.js → up-7ILD7GU7.js} +2 -2
- package/dist/update-LPSIAWQ2.js +140 -0
- package/dist/update-check-Y33QDCFL.js +17 -0
- package/dist/{upgrade-CDKECCGN.js → upgrade-FX2TKJ2S.js} +16 -15
- package/dist/{variant-CVYM3EQG.js → variant-LAB67OC2.js} +17 -12
- package/dist/web-assets/assets/index-BbRmoxoA.js +308 -0
- package/dist/web-assets/index.html +2 -2
- package/drizzle/0003_clean_ego.sql +12 -0
- package/drizzle/meta/0003_snapshot.json +417 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +3 -1
- package/templates/_base/.init/.config/hooks/startup-context.sh +19 -1
- package/templates/_base/_skills/volute-agent/SKILL.md +112 -16
- package/templates/_base/home/.config/routes.json +10 -0
- package/templates/_base/home/VOLUTE.md +19 -28
- package/templates/_base/src/lib/file-handler.ts +46 -0
- package/templates/_base/src/lib/format-prefix.ts +1 -1
- package/templates/_base/src/lib/router.ts +327 -0
- package/templates/_base/src/lib/routing.ts +137 -0
- package/templates/_base/src/lib/types.ts +16 -3
- package/templates/_base/src/lib/volute-server.ts +20 -48
- package/templates/agent-sdk/.init/.config/routes.json +5 -0
- package/templates/agent-sdk/.init/CLAUDE.md +2 -2
- package/templates/agent-sdk/src/agent.ts +269 -82
- package/templates/agent-sdk/src/server.ts +19 -4
- package/templates/agent-sdk/volute-template.json +1 -1
- package/templates/pi/.init/.config/routes.json +5 -0
- package/templates/pi/.init/AGENTS.md +1 -1
- package/templates/pi/src/agent.ts +279 -58
- package/templates/pi/src/server.ts +15 -4
- package/templates/pi/volute-template.json +1 -1
- package/dist/channel-7FZ6D25H.js +0 -90
- package/dist/chunk-N4YNKR3Q.js +0 -90
- package/dist/connector-TVJULIRT.js +0 -96
- package/dist/create-BRG2DBWI.js +0 -79
- package/dist/send-UK3JBZIB.js +0 -53
- package/dist/web-assets/assets/index-BC5eSqbY.js +0 -296
- package/templates/_base/src/lib/sessions.ts +0 -71
- package/templates/agent-sdk/.init/.config/sessions.json +0 -4
- package/templates/agent-sdk/src/lib/agent-sessions.ts +0 -204
- package/templates/pi/.init/.config/sessions.json +0 -1
- package/templates/pi/src/lib/agent-sessions.ts +0 -210
- package/dist/{service-SA4TTMDU.js → service-HZNIDNJF.js} +3 -3
|
@@ -1,15 +1,58 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve as resolvePath } from "node:path";
|
|
3
|
+
import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
|
|
4
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
3
5
|
import { createAutoCommitHook } from "./lib/hooks/auto-commit.js";
|
|
4
6
|
import { createIdentityReloadHook } from "./lib/hooks/identity-reload.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
import { createPreCompactHook } from "./lib/hooks/pre-compact.js";
|
|
8
|
+
import { log, logText, logThinking, logToolUse } from "./lib/logger.js";
|
|
9
|
+
import { createMessageChannel } from "./lib/message-channel.js";
|
|
10
|
+
import type {
|
|
11
|
+
HandlerMeta,
|
|
12
|
+
HandlerResolver,
|
|
13
|
+
Listener,
|
|
14
|
+
MessageHandler,
|
|
15
|
+
VoluteContentPart,
|
|
16
|
+
VoluteEvent,
|
|
11
17
|
} from "./lib/types.js";
|
|
12
18
|
|
|
19
|
+
type SDKContent = (
|
|
20
|
+
| { type: "text"; text: string }
|
|
21
|
+
| {
|
|
22
|
+
type: "image";
|
|
23
|
+
source: {
|
|
24
|
+
type: "base64";
|
|
25
|
+
media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp";
|
|
26
|
+
data: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
)[];
|
|
30
|
+
|
|
31
|
+
type Session = {
|
|
32
|
+
name: string;
|
|
33
|
+
channel: ReturnType<typeof createMessageChannel>;
|
|
34
|
+
listeners: Set<Listener>;
|
|
35
|
+
messageIds: (string | undefined)[];
|
|
36
|
+
currentMessageId?: string;
|
|
37
|
+
currentQuery?: ReturnType<typeof query>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function toSDKContent(content: VoluteContentPart[]): SDKContent {
|
|
41
|
+
return content.map((part) => {
|
|
42
|
+
if (part.type === "text") {
|
|
43
|
+
return { type: "text" as const, text: part.text };
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
type: "image" as const,
|
|
47
|
+
source: {
|
|
48
|
+
type: "base64" as const,
|
|
49
|
+
media_type: part.media_type as "image/jpeg" | "image/png" | "image/gif" | "image/webp",
|
|
50
|
+
data: part.data,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
13
56
|
export function createAgent(options: {
|
|
14
57
|
systemPrompt: string;
|
|
15
58
|
cwd: string;
|
|
@@ -18,95 +61,239 @@ export function createAgent(options: {
|
|
|
18
61
|
sessionsDir: string;
|
|
19
62
|
compactionMessage?: string;
|
|
20
63
|
onIdentityReload?: () => Promise<void>;
|
|
21
|
-
}) {
|
|
64
|
+
}): { resolve: HandlerResolver; waitForCommits: () => Promise<void> } {
|
|
22
65
|
const autoCommit = createAutoCommitHook(options.cwd);
|
|
23
66
|
const identityReload = createIdentityReloadHook(options.cwd);
|
|
67
|
+
const postToolUseHooks: { matcher: string; hooks: HookCallback[] }[] = [
|
|
68
|
+
{ matcher: "Edit|Write", hooks: [autoCommit.hook, identityReload.hook] },
|
|
69
|
+
];
|
|
24
70
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
71
|
+
const sessions = new Map<string, Session>();
|
|
72
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
73
|
+
const compactionMessage =
|
|
74
|
+
options.compactionMessage ??
|
|
75
|
+
`Context is getting long — compaction is about to summarize this conversation. Before that happens, save anything important to files (MEMORY.md, memory/journal/${today}.md, etc.) since those survive compaction. Focus on: decisions made, open tasks, and anything you'd need to pick up where you left off.`;
|
|
76
|
+
|
|
77
|
+
// --- Session persistence ---
|
|
78
|
+
|
|
79
|
+
function sessionFilePath(sessionName: string): string {
|
|
80
|
+
return resolvePath(options.sessionsDir, `${sessionName}.json`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function loadSessionId(sessionName: string): string | undefined {
|
|
84
|
+
try {
|
|
85
|
+
const data = JSON.parse(readFileSync(sessionFilePath(sessionName), "utf-8"));
|
|
86
|
+
return data.sessionId;
|
|
87
|
+
} catch (err: any) {
|
|
88
|
+
if (err?.code !== "ENOENT") {
|
|
89
|
+
log("agent", `failed to load session file for "${sessionName}":`, err);
|
|
36
90
|
}
|
|
37
|
-
|
|
38
|
-
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
39
94
|
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const text =
|
|
45
|
-
typeof content === "string"
|
|
46
|
-
? content
|
|
47
|
-
: content.map((p) => (p.type === "text" ? p.text : `[${p.type}]`)).join(" ");
|
|
48
|
-
logMessage("in", text, meta?.channel);
|
|
49
|
-
|
|
50
|
-
const time = new Date().toLocaleString();
|
|
51
|
-
const prefix = formatPrefix(meta, time);
|
|
52
|
-
|
|
53
|
-
let sdkContent: (
|
|
54
|
-
| { type: "text"; text: string }
|
|
55
|
-
| {
|
|
56
|
-
type: "image";
|
|
57
|
-
source: {
|
|
58
|
-
type: "base64";
|
|
59
|
-
media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp";
|
|
60
|
-
data: string;
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
)[];
|
|
95
|
+
function saveSessionId(sessionName: string, sessionId: string) {
|
|
96
|
+
mkdirSync(options.sessionsDir, { recursive: true });
|
|
97
|
+
writeFileSync(sessionFilePath(sessionName), JSON.stringify({ sessionId }));
|
|
98
|
+
}
|
|
64
99
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
100
|
+
function deleteSessionId(sessionName: string) {
|
|
101
|
+
try {
|
|
102
|
+
const path = sessionFilePath(sessionName);
|
|
103
|
+
if (existsSync(path)) unlinkSync(path);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
log("agent", `failed to delete session file for "${sessionName}":`, err);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --- Event broadcasting ---
|
|
110
|
+
|
|
111
|
+
function broadcastToSession(session: Session, event: VoluteEvent) {
|
|
112
|
+
const tagged =
|
|
113
|
+
session.currentMessageId != null ? { ...event, messageId: session.currentMessageId } : event;
|
|
114
|
+
for (const listener of session.listeners) {
|
|
115
|
+
try {
|
|
116
|
+
listener(tagged);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
log("agent", "listener threw during broadcast:", err);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- SDK stream management ---
|
|
124
|
+
|
|
125
|
+
function createStream(session: Session, resume?: string) {
|
|
126
|
+
const preCompact = createPreCompactHook(() => {
|
|
127
|
+
session.messageIds.push(undefined);
|
|
128
|
+
session.channel.push({
|
|
129
|
+
type: "user",
|
|
130
|
+
session_id: "",
|
|
131
|
+
message: {
|
|
132
|
+
role: "user",
|
|
133
|
+
content: [{ type: "text", text: compactionMessage }],
|
|
134
|
+
},
|
|
135
|
+
parent_tool_use_id: null,
|
|
81
136
|
});
|
|
82
|
-
|
|
83
|
-
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return query({
|
|
140
|
+
prompt: session.channel.iterable,
|
|
141
|
+
options: {
|
|
142
|
+
systemPrompt: options.systemPrompt,
|
|
143
|
+
permissionMode: "bypassPermissions",
|
|
144
|
+
allowDangerouslySkipPermissions: true,
|
|
145
|
+
settingSources: ["project"],
|
|
146
|
+
cwd: options.cwd,
|
|
147
|
+
abortController: options.abortController,
|
|
148
|
+
model: options.model,
|
|
149
|
+
resume,
|
|
150
|
+
hooks: {
|
|
151
|
+
PostToolUse: postToolUseHooks,
|
|
152
|
+
PreCompact: [{ hooks: [preCompact.hook] }],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function consumeStream(stream: ReturnType<typeof query>, session: Session) {
|
|
159
|
+
for await (const msg of stream) {
|
|
160
|
+
if (session.currentMessageId === undefined) {
|
|
161
|
+
session.currentMessageId = session.messageIds.shift();
|
|
162
|
+
}
|
|
163
|
+
if ("session_id" in msg && msg.session_id) {
|
|
164
|
+
if (!session.name.startsWith("new-")) {
|
|
165
|
+
saveSessionId(session.name, msg.session_id as string);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (msg.type === "assistant") {
|
|
169
|
+
for (const b of msg.message.content) {
|
|
170
|
+
if (b.type === "thinking" && "thinking" in b && b.thinking) {
|
|
171
|
+
logThinking(b.thinking as string);
|
|
172
|
+
} else if (b.type === "text") {
|
|
173
|
+
const text = (b as { text: string }).text;
|
|
174
|
+
logText(text);
|
|
175
|
+
broadcastToSession(session, { type: "text", content: text });
|
|
176
|
+
} else if (b.type === "tool_use") {
|
|
177
|
+
const tb = b as { name: string; input: unknown };
|
|
178
|
+
logToolUse(tb.name, tb.input);
|
|
179
|
+
broadcastToSession(session, { type: "tool_use", name: tb.name, input: tb.input });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (msg.type === "result") {
|
|
184
|
+
log("agent", `session "${session.name}": turn done`);
|
|
185
|
+
broadcastToSession(session, { type: "done" });
|
|
186
|
+
session.currentMessageId = undefined;
|
|
187
|
+
if (identityReload.needsReload()) {
|
|
188
|
+
options.onIdentityReload?.();
|
|
189
|
+
}
|
|
84
190
|
}
|
|
85
191
|
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function startSession(session: Session, savedSessionId?: string) {
|
|
195
|
+
(async () => {
|
|
196
|
+
log("agent", `session "${session.name}": stream consumer started`);
|
|
197
|
+
try {
|
|
198
|
+
const q = createStream(session, savedSessionId);
|
|
199
|
+
session.currentQuery = q;
|
|
200
|
+
await consumeStream(q, session);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
if (savedSessionId) {
|
|
203
|
+
log("agent", `session "${session.name}": resume failed, starting fresh:`, err);
|
|
204
|
+
deleteSessionId(session.name);
|
|
205
|
+
try {
|
|
206
|
+
const q = createStream(session);
|
|
207
|
+
session.currentQuery = q;
|
|
208
|
+
await consumeStream(q, session);
|
|
209
|
+
} catch (retryErr) {
|
|
210
|
+
log("agent", `session "${session.name}": stream consumer error:`, retryErr);
|
|
211
|
+
broadcastToSession(session, { type: "done" });
|
|
212
|
+
sessions.delete(session.name);
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
log("agent", `session "${session.name}": stream consumer error:`, err);
|
|
216
|
+
broadcastToSession(session, { type: "done" });
|
|
217
|
+
sessions.delete(session.name);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
log("agent", `session "${session.name}": stream consumer ended`);
|
|
221
|
+
})();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function getOrCreateSession(name: string): Session {
|
|
225
|
+
const existing = sessions.get(name);
|
|
226
|
+
if (existing) return existing;
|
|
227
|
+
|
|
228
|
+
const session: Session = {
|
|
229
|
+
name,
|
|
230
|
+
channel: createMessageChannel(),
|
|
231
|
+
listeners: new Set(),
|
|
232
|
+
messageIds: [],
|
|
233
|
+
};
|
|
234
|
+
sessions.set(name, session);
|
|
86
235
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
236
|
+
const isEphemeral = name.startsWith("new-");
|
|
237
|
+
const savedSessionId = isEphemeral ? undefined : loadSessionId(name);
|
|
238
|
+
if (savedSessionId) {
|
|
239
|
+
log("agent", `session "${name}": resuming ${savedSessionId}`);
|
|
240
|
+
} else {
|
|
241
|
+
log("agent", `session "${name}": starting fresh`);
|
|
90
242
|
}
|
|
91
243
|
|
|
92
|
-
session
|
|
93
|
-
session
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
244
|
+
startSession(session, savedSessionId);
|
|
245
|
+
return session;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// --- MessageHandler implementation ---
|
|
249
|
+
|
|
250
|
+
function createSessionHandler(sessionName: string): MessageHandler {
|
|
251
|
+
return {
|
|
252
|
+
handle(content: VoluteContentPart[], meta: HandlerMeta, listener: Listener): () => void {
|
|
253
|
+
const session = getOrCreateSession(sessionName);
|
|
254
|
+
|
|
255
|
+
// Filter listener to only receive events for this messageId
|
|
256
|
+
const filteredListener: Listener = (event) => {
|
|
257
|
+
if (event.messageId === meta.messageId) listener(event);
|
|
258
|
+
};
|
|
259
|
+
session.listeners.add(filteredListener);
|
|
260
|
+
|
|
261
|
+
// Interrupt if requested and session is mid-turn
|
|
262
|
+
if (meta.interrupt && session.currentMessageId !== undefined && session.currentQuery) {
|
|
263
|
+
log("agent", `session "${sessionName}": interrupting current turn`);
|
|
264
|
+
session.currentQuery.interrupt();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Push message into SDK
|
|
268
|
+
session.messageIds.push(meta.messageId);
|
|
269
|
+
session.channel.push({
|
|
270
|
+
type: "user",
|
|
271
|
+
session_id: "",
|
|
272
|
+
message: { role: "user", content: toSDKContent(content) },
|
|
273
|
+
parent_tool_use_id: null,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
return () => session.listeners.delete(filteredListener);
|
|
99
277
|
},
|
|
100
|
-
|
|
101
|
-
});
|
|
278
|
+
};
|
|
102
279
|
}
|
|
103
280
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
281
|
+
// --- HandlerResolver ---
|
|
282
|
+
|
|
283
|
+
const handlers = new Map<string, MessageHandler>();
|
|
284
|
+
|
|
285
|
+
function resolve(sessionName: string): MessageHandler {
|
|
286
|
+
// Ephemeral sessions get unique names — don't cache their handlers
|
|
287
|
+
if (sessionName.startsWith("new-")) {
|
|
288
|
+
return createSessionHandler(sessionName);
|
|
289
|
+
}
|
|
290
|
+
let handler = handlers.get(sessionName);
|
|
291
|
+
if (!handler) {
|
|
292
|
+
handler = createSessionHandler(sessionName);
|
|
293
|
+
handlers.set(sessionName, handler);
|
|
294
|
+
}
|
|
295
|
+
return handler;
|
|
109
296
|
}
|
|
110
297
|
|
|
111
|
-
return {
|
|
298
|
+
return { resolve, waitForCommits: autoCommit.waitForCommits };
|
|
112
299
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, renameSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { createAgent } from "./agent.js";
|
|
4
|
+
import { createFileHandlerResolver } from "./lib/file-handler.js";
|
|
4
5
|
import { log } from "./lib/logger.js";
|
|
6
|
+
import { createRouter } from "./lib/router.js";
|
|
5
7
|
import {
|
|
6
8
|
handleMergeContext,
|
|
7
9
|
loadConfig,
|
|
@@ -39,24 +41,37 @@ const agent = createAgent({
|
|
|
39
41
|
onIdentityReload: async () => {
|
|
40
42
|
log("server", "identity file changed — restarting to reload");
|
|
41
43
|
await agent.waitForCommits();
|
|
44
|
+
// Signal daemon to restart immediately (bypasses crash backoff)
|
|
45
|
+
try {
|
|
46
|
+
writeFileSync(resolve(".volute/restart.json"), JSON.stringify({ action: "reload" }));
|
|
47
|
+
} catch (err) {
|
|
48
|
+
log("server", "failed to write restart signal:", err);
|
|
49
|
+
}
|
|
42
50
|
server.close();
|
|
43
51
|
process.exit(0);
|
|
44
52
|
},
|
|
45
53
|
});
|
|
46
54
|
|
|
55
|
+
const router = createRouter({
|
|
56
|
+
configPath: resolve("home/.config/routes.json"),
|
|
57
|
+
agentHandler: agent.resolve,
|
|
58
|
+
fileHandler: createFileHandlerResolver(resolve("home")),
|
|
59
|
+
});
|
|
60
|
+
|
|
47
61
|
const server = createVoluteServer({
|
|
48
|
-
|
|
62
|
+
router,
|
|
49
63
|
port,
|
|
50
64
|
name: pkg.name,
|
|
51
65
|
version: pkg.version,
|
|
52
|
-
sessionsConfigPath: resolve("home/.config/sessions.json"),
|
|
53
66
|
});
|
|
54
67
|
|
|
55
68
|
server.listen(port, () => {
|
|
56
69
|
const addr = server.address();
|
|
57
70
|
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
58
71
|
log("server", `listening on :${actualPort}`);
|
|
59
|
-
handleMergeContext((content) =>
|
|
72
|
+
handleMergeContext((content) =>
|
|
73
|
+
router.route([{ type: "text", text: content }], { channel: "system" }),
|
|
74
|
+
);
|
|
60
75
|
});
|
|
61
76
|
|
|
62
77
|
setupShutdown();
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"biome.json.tmpl": "biome.json",
|
|
5
5
|
"home/.config/volute.json.tmpl": "home/.config/volute.json"
|
|
6
6
|
},
|
|
7
|
-
"substitute": ["package.json", ".init/SOUL.md"],
|
|
7
|
+
"substitute": ["package.json", ".init/SOUL.md", "home/.config/routes.json"],
|
|
8
8
|
"skillsDir": "home/.claude/skills"
|
|
9
9
|
}
|
|
@@ -23,7 +23,7 @@ See the **memory** skill for detailed guidance.
|
|
|
23
23
|
|
|
24
24
|
## Sessions
|
|
25
25
|
|
|
26
|
-
- You may have **multiple named sessions** — each maintains its own conversation history. See `VOLUTE.md` for how to configure session routing via `.config/
|
|
26
|
+
- You may have **multiple named sessions** — each maintains its own conversation history. See `VOLUTE.md` for how to configure session routing via `.config/routes.json`.
|
|
27
27
|
- Your conversation may be **resumed** from a previous session — orient yourself by reading recent daily logs if needed.
|
|
28
28
|
- On a **fresh session**, read `MEMORY.md` and recent daily logs to remember where you left off.
|
|
29
29
|
- On **compaction**, update today's daily log to preserve context before the conversation is trimmed.
|