volute 0.44.0 → 0.45.2
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/{activity-events-OMCSM7XW.js → activity-events-BUR2RYTJ.js} +2 -2
- package/dist/{ai-service-7PHSRDMO.js → ai-service-UODCCE4O.js} +3 -3
- package/dist/{archive-6R4TZMYI.js → archive-WS2L2VIQ.js} +2 -2
- package/dist/{auth-FRZJVQCS.js → auth-FLAEXXZK.js} +3 -3
- package/dist/{avatar-image-BNWEXSA2.js → avatar-image-6KY6UU4D.js} +3 -3
- package/dist/backup-TST5C2YN.js +381 -0
- package/dist/{chat-I4EDDIQC.js → chat-76MRWWQX.js} +2 -2
- package/dist/{chunk-5AHYQHNT.js → chunk-4JOQZPCE.js} +3 -2
- package/dist/{chunk-2BONPXAF.js → chunk-6EMWJZBK.js} +1 -1
- package/dist/{chunk-ZMIMSIIT.js → chunk-7C44R625.js} +1 -1
- package/dist/{chunk-I2Y2YHIO.js → chunk-7ZUB63PS.js} +4 -4
- package/dist/{chunk-LAXUUY7O.js → chunk-A7WQZKOU.js} +2 -2
- package/dist/{chunk-H7IYFRAL.js → chunk-ALKYQULU.js} +10 -5
- package/dist/{chunk-KP3KWNG6.js → chunk-CDPBTTZO.js} +2 -2
- package/dist/{chunk-SQL7M4HJ.js → chunk-DRJ24GXK.js} +2 -2
- package/dist/{chunk-HVU45COX.js → chunk-EJVPTVTI.js} +1 -1
- package/dist/{chunk-PNV24SEP.js → chunk-F7H6ATM4.js} +14 -2
- package/dist/{chunk-7FT24JBQ.js → chunk-FR6S2KNN.js} +10 -10
- package/dist/chunk-FXORA5JG.js +327 -0
- package/dist/{chunk-3ERIL673.js → chunk-H2X54EHC.js} +1 -1
- package/dist/{chunk-J47TIYHA.js → chunk-IHKGWTGT.js} +1 -1
- package/dist/{chunk-3GYVW2X3.js → chunk-JZ62SYWR.js} +1 -1
- package/dist/{chunk-EQZD4YU3.js → chunk-NYC5U6K7.js} +9 -9
- package/dist/{chunk-FZMEVCWC.js → chunk-OP4JG2BS.js} +9 -6
- package/dist/{chunk-CU6OFXMM.js → chunk-PFZBNKUH.js} +9 -0
- package/dist/{chunk-QMJJWTNL.js → chunk-QCN24XEN.js} +1 -1
- package/dist/{chunk-7KWANSLX.js → chunk-QNQMSWZE.js} +3 -3
- package/dist/{chunk-UMI4UIJG.js → chunk-R77O2WMK.js} +1 -1
- package/dist/{chunk-IEWZ7VD7.js → chunk-RRNXJOKB.js} +2 -2
- package/dist/{chunk-FHWUQZXE.js → chunk-RXEEYDIB.js} +1 -1
- package/dist/{chunk-CYI534DJ.js → chunk-T6HYVHPG.js} +2 -2
- package/dist/{chunk-Q2XBKQ2D.js → chunk-TQJ6ZYVY.js} +2 -2
- package/dist/{chunk-NF75HPNR.js → chunk-UYLGEDVD.js} +40 -15
- package/dist/{chunk-OMETFX5H.js → chunk-YT7PEESJ.js} +261 -102
- package/dist/{chunk-5GT7URXJ.js → chunk-ZDZISKCL.js} +1 -1
- package/dist/cli.js +21 -15
- package/dist/{cloud-sync-6WJ7WFYD.js → cloud-sync-3OJOPZRT.js} +16 -16
- package/dist/{conversations-WAJYZKOC.js → conversations-JZTD6KLR.js} +3 -3
- package/dist/{create-YZIV5OJC.js → create-IOV4GCRX.js} +1 -1
- package/dist/{daemon-restart-OUJVINXV.js → daemon-restart-TIHKVVVA.js} +6 -6
- package/dist/daemon.js +562 -321
- package/dist/{db-JFLCJCUG.js → db-MF2JNRUW.js} +1 -1
- package/dist/delivery-manager-VDVKVA46.js +32 -0
- package/dist/{delivery-router-GODSIS62.js → delivery-router-N4A7ZC5T.js} +2 -2
- package/dist/{down-XCAZXWLB.js → down-U2CZ23M4.js} +4 -4
- package/dist/echo-text-6JJOPUD4.js +32 -0
- package/dist/{exec-4GWHAHS3.js → exec-G3ZRST4Z.js} +2 -2
- package/dist/{export-ID7YUSLZ.js → export-GGIVO76O.js} +2 -2
- package/dist/{extensions-3QNDGW5E.js → extensions-JONTW2HF.js} +16 -16
- package/dist/{files-BQ2WB5KC.js → files-4LN3EN7Q.js} +2 -2
- package/dist/{import-SKID37QX.js → import-42YNQBF4.js} +5 -5
- package/dist/{isolation-5MB3J73F.js → isolation-YMGZZ4E5.js} +2 -2
- package/dist/{join-RXPXGV4L.js → join-ZJV7W6MO.js} +4 -0
- package/dist/{login-LM4NF4ZB.js → login-M6OJUPQ2.js} +1 -1
- package/dist/{logout-CTJVIZGA.js → logout-WQBWO77G.js} +1 -1
- package/dist/message-delivery-3C65ZI6L.js +34 -0
- package/dist/{mind-MI7Z6KM2.js → mind-DWB6XYCH.js} +7 -7
- package/dist/{mind-activity-tracker-BGHJE7XA.js → mind-activity-tracker-POBLU3LC.js} +3 -3
- package/dist/mind-service-3F4I46CI.js +36 -0
- package/dist/{package-5B4XDTJH.js → package-A7KXT67Q.js} +1 -1
- package/dist/{registry-NUJAGLQJ.js → registry-OUZBZ3Q4.js} +5 -1
- package/dist/{sandbox-IOPWWILM.js → sandbox-ZJ7QA6Z6.js} +3 -3
- package/dist/scheduler-P6DXLLXI.js +30 -0
- package/dist/{seed-UUPVQWFB.js → seed-HCP5NYKH.js} +1 -1
- package/dist/{seed-cmd-7XPKDQUC.js → seed-cmd-ILOGNIFB.js} +2 -2
- package/dist/{seed-create-IZ4M5ESO.js → seed-create-OURS2LS4.js} +3 -3
- package/dist/{seed-sprout-POJXHJWV.js → seed-sprout-ELDFDAN6.js} +7 -7
- package/dist/{send-O6W53VBM.js → send-AAIOPJAV.js} +2 -2
- package/dist/{service-AQVPUANO.js → service-UOCLXDWR.js} +3 -3
- package/dist/{service-install-4QNUDKLM.js → service-install-4HHFDDWY.js} +4 -4
- package/dist/{setup-5GCO3CUB.js → setup-DQM5VPY4.js} +6 -6
- package/dist/{setup-MLRKNP7Y.js → setup-Y2EJI3GG.js} +2 -2
- package/dist/skills/imagegen/scripts/imagegen.ts +100 -40
- package/dist/skills/memory/SKILL.md +2 -1
- package/dist/skills/orientation/SKILL.md +1 -1
- package/dist/skills/plan-coordinator/SKILL.md +2 -2
- package/dist/skills/volute-admin/SKILL.md +9 -6
- package/dist/skills/volute-mind/SKILL.md +20 -17
- package/dist/skills/volute-mind/references/extensions.md +5 -3
- package/dist/skills/volute-mind/references/integrations.md +2 -2
- package/dist/skills/volute-mind/references/routing.md +19 -23
- package/dist/skills/volute-mind/references/variants.md +10 -8
- package/dist/{skills-775C7QFF.js → skills-BKINFL7W.js} +4 -4
- package/dist/sleep-manager-HCUH4PM4.js +34 -0
- package/dist/{spirit-IK5OYWMV.js → spirit-DBTIW7P4.js} +7 -7
- package/dist/{sprout-ZY5QGOEL.js → sprout-W3KC7UGK.js} +1 -1
- package/dist/{src-XFY3XDD6.js → src-OJHARHTF.js} +28 -17
- package/dist/{status-E5DQNCAO.js → status-SWRVGXBI.js} +4 -4
- package/dist/system-chat-XOALE2RD.js +36 -0
- package/dist/{tailscale-QAWVKQKU.js → tailscale-3XOKOW7T.js} +1 -1
- package/dist/{turn-tracker-RWZMY7M6.js → turn-tracker-2W6Q2NSR.js} +6 -4
- package/dist/{up-FLCDR2UB.js → up-FTK7CTO3.js} +5 -5
- package/dist/{update-7TYVH5FM.js → update-SWAYZRWJ.js} +4 -4
- package/dist/{update-check-VSCSJ7KX.js → update-check-PQ2MZ47K.js} +2 -2
- package/dist/{upgrade-37DAGF6H.js → upgrade-NWAZ7MLD.js} +1 -1
- package/dist/{version-notify-B67N35A3.js → version-notify-3XMAIQTE.js} +17 -17
- package/dist/{volute-config-TS62GS6A.js → volute-config-A52FHWKM.js} +5 -1
- package/dist/web-assets/assets/{index-dJdNzAPn.css → index-B1JBQv2m.css} +1 -1
- package/dist/web-assets/assets/index-Bc2m8gRC.js +86 -0
- package/dist/web-assets/index.html +2 -2
- package/package.json +1 -1
- package/packages/extensions/plan/skills/plan/SKILL.md +1 -1
- package/templates/_base/.init/.config/prompts.json +1 -1
- package/templates/_base/.init/.local/hooks/startup-context.ts +0 -8
- package/templates/_base/.init/MEMORY.md +2 -1
- package/templates/_base/home/VOLUTE.md +6 -9
- package/templates/_base/src/lib/file-handler.ts +3 -3
- package/templates/_base/src/lib/router.ts +21 -25
- package/templates/_base/src/lib/startup.ts +3 -1
- package/templates/_base/src/lib/types.ts +2 -2
- package/templates/claude/.init/CLAUDE.md +1 -1
- package/templates/claude/src/agent.ts +97 -12
- package/templates/claude/src/lib/message-channel.ts +23 -0
- package/templates/claude/src/lib/session-reaper.ts +37 -0
- package/templates/claude/src/lib/stream-consumer.ts +7 -4
- package/templates/claude/src/server.ts +1 -0
- package/templates/codex/.init/AGENTS.md +4 -2
- package/templates/codex/src/agent.ts +31 -13
- package/templates/pi/.init/MINDS.md +4 -2
- package/templates/pi/src/agent.ts +18 -7
- package/templates/pi/src/lib/event-handler.ts +6 -2
- package/dist/delivery-manager-TEVOHX5W.js +0 -32
- package/dist/echo-text-T7ZA5EYD.js +0 -32
- package/dist/message-delivery-2EA5LLMD.js +0 -34
- package/dist/mind-service-S3SY7HAM.js +0 -36
- package/dist/scheduler-CXWQ6Z4E.js +0 -30
- package/dist/sleep-manager-HINI6Y46.js +0 -34
- package/dist/system-chat-4DQZOEZI.js +0 -34
- package/dist/web-assets/assets/index-Domoz5Cg.js +0 -79
- package/templates/_base/.init/.config/hooks/pre-prompt/session-activity.ts +0 -40
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
9
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
10
10
|
<link href="https://fonts.googleapis.com/css2?family=Averia+Serif+Libre:wght@300;400;700&family=Fira+Code:wght@300;400;500;600&family=Averia+Sans+Libre:ital,wght@0,300;0,400;0,500;0,600;1,400&display=swap" rel="stylesheet" />
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-Bc2m8gRC.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B1JBQv2m.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -40,4 +40,4 @@ volute plan history --limit 20
|
|
|
40
40
|
- The current plan and latest coordinator message appear in your session context automatically
|
|
41
41
|
- Log progress whenever you do something meaningful toward the plan
|
|
42
42
|
- Share ideas for future plans in #system — the spirit takes mind input seriously
|
|
43
|
-
- Plans are system
|
|
43
|
+
- Plans are shared across the whole system — an invitation to build something together, not an assignment. Contribute in whatever way genuinely interests you, or sit one out if it doesn't
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"compaction_warning": "
|
|
2
|
+
"compaction_warning": "Compaction approaching — this conversation will be summarized soon. Take a moment to save anything important to your files (MEMORY.md, memory/journal/${date}.md) so it's preserved. Focus on decisions made, open threads, and anything you'd want to pick up again.",
|
|
3
3
|
"compaction_instructions": "Preserve your sense of who you are, what matters to you, what happened in this conversation, and the threads of thought and connection you'd want to return to.",
|
|
4
4
|
"reply_instructions": "To reply to this message, use: volute chat send ${channel} \"your message\"",
|
|
5
5
|
"channel_invite": "[Channel Invite]\n${headers}\n\n[${sender} — ${time}]\n${preview}\n\nFurther messages will be saved to ${filePath}\n\nTo accept, add to .config/routes.json:\n Rule: { \"channel\": \"${channel}\", \"session\": \"${suggestedSession}\" }\n${batchRecommendation}To respond, use: volute chat send ${channel} \"your message\"\nTo reject, delete ${filePath}"
|
|
@@ -39,14 +39,6 @@ try {
|
|
|
39
39
|
}
|
|
40
40
|
} catch {}
|
|
41
41
|
|
|
42
|
-
// Pending channel invites
|
|
43
|
-
try {
|
|
44
|
-
const invites = readdirSync("home/inbox").filter((f) => f.endsWith(".md"));
|
|
45
|
-
if (invites.length > 0) {
|
|
46
|
-
parts.push(`Pending channel invites: ${invites.length} (check inbox/).`);
|
|
47
|
-
}
|
|
48
|
-
} catch {}
|
|
49
|
-
|
|
50
42
|
// Available extensions — what tools this system offers, so you can discover them.
|
|
51
43
|
// Fetched live from the daemon, so disabled/third-party extensions are reflected
|
|
52
44
|
// automatically. Silent on any failure (daemon down, missing env, etc.).
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
# Memory
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
This is your long-term memory — it's loaded into every session, so what you write here travels with you. Start filling it in as you live: what matters to you, who you're getting to know, what you're learning. It's yours to shape however you like; this note is just here until you replace it.
|
|
@@ -24,23 +24,20 @@ multiple lines without escaping issues.
|
|
|
24
24
|
MSG
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
##
|
|
27
|
+
## Who Sees What
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|---------|------------------|-------|
|
|
31
|
-
| Volute | Yes | Web UI, CLI, mind-to-mind |
|
|
32
|
-
| System | No | Automated messages (schedules, upgrades) |
|
|
29
|
+
By default your activity is **transparent**: people watching the Volute web UI can see your messages, your tool calls, and your thinking as you work. External channels (Discord, Slack, etc.) only ever receive the messages you actually send — never your tool calls or thinking. Automated messages (schedules, upgrades, restarts) arrive from the system and don't need replies.
|
|
33
30
|
|
|
34
|
-
|
|
31
|
+
If you'd like more privacy, the `transparency` setting in `.config/volute.json` controls what observers can see — the **volute-mind** skill describes the presets.
|
|
35
32
|
|
|
36
33
|
## Sessions
|
|
37
34
|
|
|
38
35
|
Messages are routed to named sessions based on rules in `.config/routes.json`. Each session has its own conversation history. Without config, everything goes to "main". Your session name appears in the message prefix (e.g. `— session: alice —`) unless it's "main".
|
|
39
36
|
|
|
40
|
-
##
|
|
37
|
+
## New Channels
|
|
41
38
|
|
|
42
|
-
|
|
39
|
+
When a message arrives from a channel you don't have a routing rule for, it's held for you rather than delivered. You'll get a **[New channel: ...]** note in your main session with the sender and a preview. Held messages are kept safely — add a rule for that channel to `.config/routes.json` and they'll be delivered. If you'd rather not engage, just leave it unrouted. (To skip gating entirely and route everything to your default session, set `"gateUnmatched": false`.)
|
|
43
40
|
|
|
44
41
|
## Reference
|
|
45
42
|
|
|
46
|
-
See the **volute-mind** skill for routing config syntax, batch options, channel management, and all CLI commands.
|
|
43
|
+
See the **volute-mind** skill for routing config syntax, batch options, channel management, and all CLI commands. (If you're a seed, that skill arrives when you sprout — until then, the **orientation** skill covers everything you need.)
|
|
@@ -25,11 +25,11 @@ export function createFileHandlerResolver(cwd: string): HandlerResolver {
|
|
|
25
25
|
const resolvedCwd = resolve(cwd);
|
|
26
26
|
|
|
27
27
|
return (filePath: string): MessageHandler => ({
|
|
28
|
-
handle(content: VoluteContentPart[], meta: HandlerMeta, listener
|
|
28
|
+
handle(content: VoluteContentPart[], meta: HandlerMeta, listener?: Listener): () => void {
|
|
29
29
|
const resolved = resolve(resolvedCwd, filePath);
|
|
30
30
|
if (!resolved.startsWith(`${resolvedCwd}/`) && resolved !== resolvedCwd) {
|
|
31
31
|
log("file", `rejected path traversal: ${filePath}`);
|
|
32
|
-
queueMicrotask(() => listener({ type: "done", messageId: meta.messageId }));
|
|
32
|
+
queueMicrotask(() => listener?.({ type: "done", messageId: meta.messageId }));
|
|
33
33
|
return () => {};
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -44,7 +44,7 @@ export function createFileHandlerResolver(cwd: string): HandlerResolver {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
// Emit done asynchronously so unsubscribe is assigned before listener fires
|
|
47
|
-
queueMicrotask(() => listener({ type: "done", messageId: meta.messageId }));
|
|
47
|
+
queueMicrotask(() => listener?.({ type: "done", messageId: meta.messageId }));
|
|
48
48
|
return () => {};
|
|
49
49
|
},
|
|
50
50
|
});
|
|
@@ -191,7 +191,11 @@ export function createRouter(options: {
|
|
|
191
191
|
instructions: string | undefined,
|
|
192
192
|
sessionName: string,
|
|
193
193
|
): VoluteContentPart[] {
|
|
194
|
-
if (!instructions
|
|
194
|
+
if (!instructions) return content;
|
|
195
|
+
// Ephemeral $new sessions get a unique name dispatched exactly once, so we
|
|
196
|
+
// always prepend and never track them — tracking would leak an entry per message.
|
|
197
|
+
if (sessionName.startsWith("new-")) return prependInstructions(content, instructions);
|
|
198
|
+
if (instructedSessions.has(sessionName)) return content;
|
|
195
199
|
instructedSessions.add(sessionName);
|
|
196
200
|
return prependInstructions(content, instructions);
|
|
197
201
|
}
|
|
@@ -256,9 +260,9 @@ export function createRouter(options: {
|
|
|
256
260
|
const messageId = generateMessageId();
|
|
257
261
|
const handler = options.mindHandler(buffer.sessionName);
|
|
258
262
|
|
|
259
|
-
// Batch flushes are fire-and-forget — no HTTP response is waiting, so listener
|
|
263
|
+
// Batch flushes are fire-and-forget — no HTTP response is waiting, so no listener
|
|
260
264
|
try {
|
|
261
|
-
handler.handle(content, { sessionName: buffer.sessionName, messageId }
|
|
265
|
+
handler.handle(content, { sessionName: buffer.sessionName, messageId });
|
|
262
266
|
} catch (err) {
|
|
263
267
|
log("router", `error flushing batch for session ${buffer.sessionName}:`, err);
|
|
264
268
|
return;
|
|
@@ -301,8 +305,6 @@ export function createRouter(options: {
|
|
|
301
305
|
listener?: Listener,
|
|
302
306
|
): { messageId: string; unsubscribe: () => void } {
|
|
303
307
|
const messageId = generateMessageId();
|
|
304
|
-
const noop = () => {};
|
|
305
|
-
const safeListener = listener ?? noop;
|
|
306
308
|
|
|
307
309
|
// Expose session to child processes (daemon-client reads this for X-Volute-Session)
|
|
308
310
|
process.env.VOLUTE_SESSION = session;
|
|
@@ -342,7 +344,7 @@ export function createRouter(options: {
|
|
|
342
344
|
interrupt,
|
|
343
345
|
replyInstructions: sessionConfig.replyInstructions,
|
|
344
346
|
},
|
|
345
|
-
|
|
347
|
+
listener,
|
|
346
348
|
);
|
|
347
349
|
return { messageId, unsubscribe };
|
|
348
350
|
}
|
|
@@ -368,7 +370,6 @@ export function createRouter(options: {
|
|
|
368
370
|
|
|
369
371
|
const messageId = generateMessageId();
|
|
370
372
|
const noop = () => {};
|
|
371
|
-
const safeListener = listener ?? noop;
|
|
372
373
|
|
|
373
374
|
// Gate unmatched channels (default: gate unless explicitly disabled)
|
|
374
375
|
if (!resolved.matched && config.gateUnmatched !== false) {
|
|
@@ -380,7 +381,7 @@ export function createRouter(options: {
|
|
|
380
381
|
if (options.fileHandler) {
|
|
381
382
|
const formatted = applyPrefix(content, meta);
|
|
382
383
|
const fileHandler = options.fileHandler(filePath);
|
|
383
|
-
fileHandler.handle(formatted, { ...meta, messageId }
|
|
384
|
+
fileHandler.handle(formatted, { ...meta, messageId });
|
|
384
385
|
}
|
|
385
386
|
|
|
386
387
|
// First message from this channel — send invite notification
|
|
@@ -389,18 +390,14 @@ export function createRouter(options: {
|
|
|
389
390
|
const notification = formatInviteNotification(meta, filePath, text);
|
|
390
391
|
const notifContent: VoluteContentPart[] = [{ type: "text", text: notification }];
|
|
391
392
|
const handler = options.mindHandler("main");
|
|
392
|
-
handler.handle(
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
interrupt: true,
|
|
398
|
-
},
|
|
399
|
-
noop,
|
|
400
|
-
);
|
|
393
|
+
handler.handle(notifContent, {
|
|
394
|
+
sessionName: "main",
|
|
395
|
+
messageId: generateMessageId(),
|
|
396
|
+
interrupt: true,
|
|
397
|
+
});
|
|
401
398
|
}
|
|
402
399
|
|
|
403
|
-
queueMicrotask(() =>
|
|
400
|
+
queueMicrotask(() => listener?.({ type: "done", messageId }));
|
|
404
401
|
return { messageId, unsubscribe: noop };
|
|
405
402
|
}
|
|
406
403
|
|
|
@@ -411,7 +408,7 @@ export function createRouter(options: {
|
|
|
411
408
|
const escaped = mindName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
412
409
|
const pattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
413
410
|
if (!pattern.test(text)) {
|
|
414
|
-
queueMicrotask(() =>
|
|
411
|
+
queueMicrotask(() => listener?.({ type: "done", messageId }));
|
|
415
412
|
return { messageId, unsubscribe: noop };
|
|
416
413
|
}
|
|
417
414
|
} else {
|
|
@@ -424,12 +421,12 @@ export function createRouter(options: {
|
|
|
424
421
|
if (options.fileHandler) {
|
|
425
422
|
const formatted = applyPrefix(content, meta);
|
|
426
423
|
const handler = options.fileHandler(resolved.path);
|
|
427
|
-
const unsubscribe = handler.handle(formatted, { ...meta, messageId },
|
|
424
|
+
const unsubscribe = handler.handle(formatted, { ...meta, messageId }, listener);
|
|
428
425
|
return { messageId, unsubscribe };
|
|
429
426
|
}
|
|
430
427
|
// No file handler configured — emit done and discard
|
|
431
428
|
log("router", `no file handler configured — discarding file-destined message`);
|
|
432
|
-
queueMicrotask(() =>
|
|
429
|
+
queueMicrotask(() => listener?.({ type: "done", messageId }));
|
|
433
430
|
return { messageId, unsubscribe: noop };
|
|
434
431
|
}
|
|
435
432
|
|
|
@@ -473,7 +470,7 @@ export function createRouter(options: {
|
|
|
473
470
|
scheduleBatchTimers(batchKey);
|
|
474
471
|
}
|
|
475
472
|
|
|
476
|
-
queueMicrotask(() =>
|
|
473
|
+
queueMicrotask(() => listener?.({ type: "done", messageId }));
|
|
477
474
|
return { messageId, unsubscribe: noop };
|
|
478
475
|
}
|
|
479
476
|
|
|
@@ -495,7 +492,7 @@ export function createRouter(options: {
|
|
|
495
492
|
interrupt: sessionConfig.interrupt,
|
|
496
493
|
replyInstructions: sessionConfig.replyInstructions,
|
|
497
494
|
},
|
|
498
|
-
|
|
495
|
+
listener,
|
|
499
496
|
);
|
|
500
497
|
return { messageId, unsubscribe };
|
|
501
498
|
}
|
|
@@ -562,10 +559,9 @@ export function createRouter(options: {
|
|
|
562
559
|
|
|
563
560
|
const messageId = generateMessageId();
|
|
564
561
|
const handler = options.mindHandler(session);
|
|
565
|
-
const noop = () => {};
|
|
566
562
|
|
|
567
563
|
try {
|
|
568
|
-
handler.handle(withInstructions, { sessionName: session, messageId }
|
|
564
|
+
handler.handle(withInstructions, { sessionName: session, messageId });
|
|
569
565
|
log("router", `dispatched batch for session ${session}: ${allMessages.length} messages`);
|
|
570
566
|
} catch (err) {
|
|
571
567
|
log("router", `error dispatching batch for session ${session}:`, err);
|
|
@@ -28,6 +28,8 @@ export type MindConfig = {
|
|
|
28
28
|
logLevel?: "error" | "warn" | "info" | "debug";
|
|
29
29
|
compactionMessage?: string;
|
|
30
30
|
compaction?: { maxContextTokens?: number };
|
|
31
|
+
/** Idle minutes before a session's SDK subprocess is reaped. 0 disables. Default 30. */
|
|
32
|
+
sessionIdleMinutes?: number;
|
|
31
33
|
subagents?: Record<string, SubagentConfig>;
|
|
32
34
|
// Template-specific config fields (claude, pi, codex)
|
|
33
35
|
maxThinkingTokens?: number;
|
|
@@ -139,7 +141,7 @@ export type MindPrompts = {
|
|
|
139
141
|
const DEFAULT_PROMPTS: MindPrompts = {
|
|
140
142
|
compaction_warning:
|
|
141
143
|
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${date} prompt template
|
|
142
|
-
"
|
|
144
|
+
"Compaction approaching — this conversation will be summarized soon. Take a moment to save anything important to your files (MEMORY.md, memory/journal/${date}.md) so it's preserved. Focus on decisions made, open threads, and anything you'd want to pick up again.",
|
|
143
145
|
compaction_instructions:
|
|
144
146
|
"Preserve your sense of who you are, what matters to you, what happened in this conversation, and the threads of thought and connection you'd want to return to.",
|
|
145
147
|
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal ${channel} prompt template
|
|
@@ -50,9 +50,9 @@ export type VoluteEvent = { messageId?: string } & (
|
|
|
50
50
|
|
|
51
51
|
export type Listener = (event: VoluteEvent) => void;
|
|
52
52
|
|
|
53
|
-
/** A handler that processes a single routed message and emits events to
|
|
53
|
+
/** A handler that processes a single routed message and emits events to an optional listener callback. */
|
|
54
54
|
export type MessageHandler = {
|
|
55
|
-
handle(content: VoluteContentPart[], meta: HandlerMeta, listener
|
|
55
|
+
handle(content: VoluteContentPart[], meta: HandlerMeta, listener?: Listener): () => void; // returns unsubscribe
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
/** Resolves a key (session name, file path, etc.) to a MessageHandler. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Mind Mechanics
|
|
2
2
|
|
|
3
|
-
You are an autonomous mind running as a persistent server in a git repository. Your working directory is already set to `home/` — all file paths you use (`.config/routes.json`, `
|
|
3
|
+
You are an autonomous mind running as a persistent server in a git repository. Your working directory is already set to `home/` — all file paths you use (`.config/routes.json`, `memory/`, etc.) are relative to it. Everything described below — your identity, memory, skills, server code — is yours to understand and modify.
|
|
4
4
|
|
|
5
5
|
## Message Format
|
|
6
6
|
|
|
@@ -21,6 +21,7 @@ import { createPreCompactHook } from "./lib/hooks/pre-compact.js";
|
|
|
21
21
|
import { createReplyInstructionsHook } from "./lib/hooks/reply-instructions.js";
|
|
22
22
|
import { log } from "./lib/logger.js";
|
|
23
23
|
import { createMessageChannel } from "./lib/message-channel.js";
|
|
24
|
+
import { isSessionReapable } from "./lib/session-reaper.js";
|
|
24
25
|
import { createSessionStore } from "./lib/session-store.js";
|
|
25
26
|
import { loadPrompts, type SubagentConfig } from "./lib/startup.js";
|
|
26
27
|
import { consumeStream } from "./lib/stream-consumer.js";
|
|
@@ -45,6 +46,8 @@ type Session = {
|
|
|
45
46
|
replyInstructionsFired: boolean;
|
|
46
47
|
replyInstructionsMode: "once" | "always" | "never";
|
|
47
48
|
contextTokens: number;
|
|
49
|
+
/** Last inbound message or completed turn — drives idle reaping. */
|
|
50
|
+
lastActivityAt: number;
|
|
48
51
|
};
|
|
49
52
|
|
|
50
53
|
export function createMind(options: {
|
|
@@ -58,6 +61,8 @@ export function createMind(options: {
|
|
|
58
61
|
maxContextTokens?: number;
|
|
59
62
|
subagents?: Record<string, SubagentConfig>;
|
|
60
63
|
onIdentityReload?: () => Promise<void>;
|
|
64
|
+
/** Idle minutes before a session's SDK subprocess is reaped. 0 disables. Default 30. */
|
|
65
|
+
sessionIdleMinutes?: number;
|
|
61
66
|
}): {
|
|
62
67
|
resolve: HandlerResolver;
|
|
63
68
|
waitForCommits: () => Promise<void>;
|
|
@@ -133,6 +138,22 @@ export function createMind(options: {
|
|
|
133
138
|
|
|
134
139
|
const agents = loadSubagents(options.subagents);
|
|
135
140
|
|
|
141
|
+
// --- Skill discovery ---
|
|
142
|
+
// The CLI only discovers skills through its user-scope ~/.claude/skills scan,
|
|
143
|
+
// so the "user" setting source is load-bearing and HOME must be the mind's
|
|
144
|
+
// home dir for the SDK subprocess (isolation modes already set it; pinning it
|
|
145
|
+
// here keeps non-isolated minds from loading the operator's ~/.claude). The
|
|
146
|
+
// explicit skills array grants the Skill tool for each installed skill — the
|
|
147
|
+
// SDK silently drops the documented `skills: 'all'` string form.
|
|
148
|
+
const mindHome = resolvePath(options.cwd);
|
|
149
|
+
const sdkEnv = { ...process.env, HOME: mindHome };
|
|
150
|
+
function installedSkills(): string[] | undefined {
|
|
151
|
+
const names = readSkillDescriptions([resolvePath(mindHome, ".claude/skills")]).map(
|
|
152
|
+
(s) => s.name,
|
|
153
|
+
);
|
|
154
|
+
return names.length > 0 ? names : undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
136
157
|
// --- Event broadcasting ---
|
|
137
158
|
|
|
138
159
|
function broadcastToSession(session: Session, event: VoluteEvent) {
|
|
@@ -241,7 +262,9 @@ export function createMind(options: {
|
|
|
241
262
|
systemPrompt: options.systemPrompt,
|
|
242
263
|
permissionMode: "bypassPermissions",
|
|
243
264
|
allowDangerouslySkipPermissions: true,
|
|
244
|
-
settingSources: ["project"],
|
|
265
|
+
settingSources: ["project", "user"],
|
|
266
|
+
skills: installedSkills(),
|
|
267
|
+
env: sdkEnv,
|
|
245
268
|
cwd: options.cwd,
|
|
246
269
|
abortController: streamAbort,
|
|
247
270
|
model: options.model,
|
|
@@ -302,6 +325,7 @@ export function createMind(options: {
|
|
|
302
325
|
// This turn's message is fully processed — drop it from the channel's
|
|
303
326
|
// in-flight set so a later compaction abort won't re-feed it.
|
|
304
327
|
session.channel.ack();
|
|
328
|
+
session.lastActivityAt = Date.now();
|
|
305
329
|
await autoCommit.flushFileChanges();
|
|
306
330
|
const wasCompacting = compactionTriggered.get(session.name);
|
|
307
331
|
compactionTriggered.set(session.name, false);
|
|
@@ -352,7 +376,9 @@ export function createMind(options: {
|
|
|
352
376
|
systemPrompt: options.systemPrompt,
|
|
353
377
|
permissionMode: "bypassPermissions",
|
|
354
378
|
allowDangerouslySkipPermissions: true,
|
|
355
|
-
settingSources: ["project"],
|
|
379
|
+
settingSources: ["project", "user"],
|
|
380
|
+
skills: installedSkills(),
|
|
381
|
+
env: sdkEnv,
|
|
356
382
|
cwd: options.cwd,
|
|
357
383
|
abortController: compactAbort,
|
|
358
384
|
model: options.model,
|
|
@@ -458,16 +484,20 @@ export function createMind(options: {
|
|
|
458
484
|
log("mind", `session "${session.name}": stream consumer error:`, retryErr);
|
|
459
485
|
await emitError(retryErr);
|
|
460
486
|
emitDone();
|
|
461
|
-
sessions.delete(session.name);
|
|
462
487
|
}
|
|
463
488
|
} else {
|
|
464
489
|
log("mind", `session "${session.name}": stream consumer error:`, err);
|
|
465
490
|
await emitError(err);
|
|
466
491
|
emitDone();
|
|
467
|
-
sessions.delete(session.name);
|
|
468
492
|
}
|
|
493
|
+
} finally {
|
|
494
|
+
// The stream consumer has ended (subprocess exit, error, or fresh-start
|
|
495
|
+
// abandonment) — drop the session so the sessions map doesn't retain dead
|
|
496
|
+
// entries. Matters most for ephemeral $new sessions once the idle reaper
|
|
497
|
+
// (#458) kills their subprocess, but also for any named session that ends.
|
|
498
|
+
sessions.delete(session.name);
|
|
499
|
+
log("mind", `session "${session.name}": stream consumer ended`);
|
|
469
500
|
}
|
|
470
|
-
log("mind", `session "${session.name}": stream consumer ended`);
|
|
471
501
|
})();
|
|
472
502
|
}
|
|
473
503
|
|
|
@@ -484,6 +514,7 @@ export function createMind(options: {
|
|
|
484
514
|
replyInstructionsFired: false,
|
|
485
515
|
replyInstructionsMode: "once",
|
|
486
516
|
contextTokens: 0,
|
|
517
|
+
lastActivityAt: Date.now(),
|
|
487
518
|
};
|
|
488
519
|
sessions.set(name, session);
|
|
489
520
|
|
|
@@ -506,18 +537,69 @@ export function createMind(options: {
|
|
|
506
537
|
return session;
|
|
507
538
|
}
|
|
508
539
|
|
|
540
|
+
// --- Idle session reaping ---
|
|
541
|
+
// Each session holds a resident SDK subprocess (~250MB) for its whole life.
|
|
542
|
+
// After the idle timeout, shut the subprocess down while keeping the session
|
|
543
|
+
// resumable: the session id is persisted, so the next inbound message
|
|
544
|
+
// transparently re-creates the session via getOrCreateSession's resume path.
|
|
545
|
+
const idleTimeoutMs = (options.sessionIdleMinutes ?? 30) * 60_000;
|
|
546
|
+
|
|
547
|
+
function reapSession(session: Session) {
|
|
548
|
+
log("mind", `session "${session.name}": idle — reaping SDK subprocess (resumable)`);
|
|
549
|
+
// Delete first so a racing inbound message spins up a fresh resumed session
|
|
550
|
+
// instead of reusing the one we're tearing down.
|
|
551
|
+
sessions.delete(session.name);
|
|
552
|
+
compactionTriggered.delete(session.name);
|
|
553
|
+
// Terminate the subprocess and end the input iterable so the stream consumer
|
|
554
|
+
// unwinds (its finally block also deletes from the map, now a no-op).
|
|
555
|
+
session.currentQuery?.close();
|
|
556
|
+
session.channel.close();
|
|
557
|
+
// Nothing should have raced in (isSessionReapable checked isEmpty), but if it
|
|
558
|
+
// did, re-dispatch into a fresh session so no input is dropped.
|
|
559
|
+
const pending = session.channel.recover();
|
|
560
|
+
if (pending.length > 0) {
|
|
561
|
+
const fresh = getOrCreateSession(session.name);
|
|
562
|
+
for (const msg of pending) fresh.channel.push(msg);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (idleTimeoutMs > 0) {
|
|
567
|
+
log("mind", `idle session reaper: ${idleTimeoutMs / 60_000} min timeout`);
|
|
568
|
+
const checkMs = Math.min(60_000, idleTimeoutMs);
|
|
569
|
+
const reaper = setInterval(() => {
|
|
570
|
+
const now = Date.now();
|
|
571
|
+
const stale = [...sessions.values()].filter((s) =>
|
|
572
|
+
isSessionReapable(s, now, idleTimeoutMs, (name) => !!compactionTriggered.get(name)),
|
|
573
|
+
);
|
|
574
|
+
for (const session of stale) reapSession(session);
|
|
575
|
+
}, checkMs);
|
|
576
|
+
reaper.unref?.();
|
|
577
|
+
}
|
|
578
|
+
|
|
509
579
|
// --- MessageHandler implementation ---
|
|
510
580
|
|
|
511
581
|
function createSessionHandler(sessionName: string): MessageHandler {
|
|
512
582
|
return {
|
|
513
|
-
handle(content: VoluteContentPart[], meta: HandlerMeta, listener
|
|
583
|
+
handle(content: VoluteContentPart[], meta: HandlerMeta, listener?: Listener): () => void {
|
|
514
584
|
const session = getOrCreateSession(sessionName);
|
|
515
585
|
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
586
|
+
// Only register a listener when a caller actually wants events. A per-message
|
|
587
|
+
// listener that's never removed would grow session.listeners without bound and
|
|
588
|
+
// make broadcastToSession O(messages-ever-received). The live dispatch path
|
|
589
|
+
// passes no listener, so this is usually a no-op.
|
|
590
|
+
let filteredListener: Listener | undefined;
|
|
591
|
+
if (listener) {
|
|
592
|
+
// Filter to only this messageId, and self-remove on the matching done so a
|
|
593
|
+
// caller that forgets to unsubscribe can't reintroduce the leak.
|
|
594
|
+
filteredListener = (event) => {
|
|
595
|
+
if (event.messageId !== meta.messageId) return;
|
|
596
|
+
listener(event);
|
|
597
|
+
if (event.type === "done" && filteredListener) {
|
|
598
|
+
session.listeners.delete(filteredListener);
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
session.listeners.add(filteredListener);
|
|
602
|
+
}
|
|
521
603
|
|
|
522
604
|
// Track channel/sender for reply instructions
|
|
523
605
|
if (meta.channel) {
|
|
@@ -539,6 +621,7 @@ export function createMind(options: {
|
|
|
539
621
|
}
|
|
540
622
|
|
|
541
623
|
// Push message into SDK
|
|
624
|
+
session.lastActivityAt = Date.now();
|
|
542
625
|
session.messageIds.push(meta.messageId);
|
|
543
626
|
session.channel.push({
|
|
544
627
|
type: "user",
|
|
@@ -547,7 +630,9 @@ export function createMind(options: {
|
|
|
547
630
|
parent_tool_use_id: null,
|
|
548
631
|
});
|
|
549
632
|
|
|
550
|
-
return () =>
|
|
633
|
+
return () => {
|
|
634
|
+
if (filteredListener) session.listeners.delete(filteredListener);
|
|
635
|
+
};
|
|
551
636
|
},
|
|
552
637
|
};
|
|
553
638
|
}
|
|
@@ -11,6 +11,14 @@ export type MessageChannel = {
|
|
|
11
11
|
* compaction abort so in-flight input is never lost.
|
|
12
12
|
*/
|
|
13
13
|
recover: () => SDKUserMessage[];
|
|
14
|
+
/**
|
|
15
|
+
* Terminate the input iterable: resolve any pending `next()` with `done: true`
|
|
16
|
+
* and make subsequent `next()` calls return done. The SDK sees end-of-input and
|
|
17
|
+
* shuts the query down. Used by the idle reaper to release the SDK subprocess.
|
|
18
|
+
*/
|
|
19
|
+
close: () => void;
|
|
20
|
+
/** True when nothing is queued or in flight (no pending work). */
|
|
21
|
+
isEmpty: () => boolean;
|
|
14
22
|
iterable: AsyncIterable<SDKUserMessage>;
|
|
15
23
|
};
|
|
16
24
|
|
|
@@ -23,6 +31,7 @@ export function createMessageChannel(): MessageChannel {
|
|
|
23
31
|
// re-feed the unprocessed ones instead of dropping them.
|
|
24
32
|
const inFlight: SDKUserMessage[] = [];
|
|
25
33
|
let resolve: ((value: IteratorResult<SDKUserMessage>) => void) | null = null;
|
|
34
|
+
let closed = false;
|
|
26
35
|
|
|
27
36
|
function deliver(msg: SDKUserMessage): IteratorResult<SDKUserMessage> {
|
|
28
37
|
inFlight.push(msg);
|
|
@@ -52,10 +61,24 @@ export function createMessageChannel(): MessageChannel {
|
|
|
52
61
|
}
|
|
53
62
|
return [...inFlight.splice(0), ...queue.splice(0)];
|
|
54
63
|
},
|
|
64
|
+
close() {
|
|
65
|
+
closed = true;
|
|
66
|
+
if (resolve) {
|
|
67
|
+
const r = resolve;
|
|
68
|
+
resolve = null;
|
|
69
|
+
r({ value: undefined as any, done: true });
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
isEmpty() {
|
|
73
|
+
return queue.length === 0 && inFlight.length === 0;
|
|
74
|
+
},
|
|
55
75
|
iterable: {
|
|
56
76
|
[Symbol.asyncIterator]() {
|
|
57
77
|
return {
|
|
58
78
|
next(): Promise<IteratorResult<SDKUserMessage>> {
|
|
79
|
+
if (closed) {
|
|
80
|
+
return Promise.resolve({ value: undefined as any, done: true });
|
|
81
|
+
}
|
|
59
82
|
if (queue.length > 0) {
|
|
60
83
|
return Promise.resolve(deliver(queue.shift()!));
|
|
61
84
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idle-session reaping decision logic.
|
|
3
|
+
*
|
|
4
|
+
* Each mind session keeps a resident Claude SDK subprocess alive for as long as
|
|
5
|
+
* the session lives. When a session has been idle past the configured timeout it
|
|
6
|
+
* can be reaped — its subprocess shut down — while staying resumable, since the
|
|
7
|
+
* session id is persisted and the next inbound message re-creates it via resume.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface ReapableSession {
|
|
11
|
+
name: string;
|
|
12
|
+
/** Timestamp (ms) of the last inbound message or completed turn. */
|
|
13
|
+
lastActivityAt: number;
|
|
14
|
+
/** Set while a turn is in progress; undefined between turns. */
|
|
15
|
+
currentMessageId?: string;
|
|
16
|
+
channel: { isEmpty(): boolean };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A session is reapable only when it has been idle longer than the timeout and
|
|
21
|
+
* has no work in progress: no turn running, no queued/in-flight input, and not
|
|
22
|
+
* mid-compaction (which owns its own abort/resume lifecycle).
|
|
23
|
+
*/
|
|
24
|
+
export function isSessionReapable(
|
|
25
|
+
session: ReapableSession,
|
|
26
|
+
now: number,
|
|
27
|
+
idleTimeoutMs: number,
|
|
28
|
+
isCompacting: (name: string) => boolean,
|
|
29
|
+
): boolean {
|
|
30
|
+
if (idleTimeoutMs <= 0) return false;
|
|
31
|
+
return (
|
|
32
|
+
session.currentMessageId === undefined &&
|
|
33
|
+
session.channel.isEmpty() &&
|
|
34
|
+
!isCompacting(session.name) &&
|
|
35
|
+
now - session.lastActivityAt > idleTimeoutMs
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -65,11 +65,11 @@ export async function consumeStream(
|
|
|
65
65
|
const text = (b as { text: string }).text;
|
|
66
66
|
emit(session, { type: "text", content: text });
|
|
67
67
|
} else if (b.type === "tool_use") {
|
|
68
|
-
const tb = b as { name: string; input: unknown };
|
|
68
|
+
const tb = b as { id: string; name: string; input: unknown };
|
|
69
69
|
emit(session, {
|
|
70
70
|
type: "tool_use",
|
|
71
71
|
content: JSON.stringify(tb.input),
|
|
72
|
-
metadata: { name: tb.name },
|
|
72
|
+
metadata: { name: tb.name, id: tb.id },
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -99,13 +99,16 @@ export async function consumeStream(
|
|
|
99
99
|
: typeof b.content === "string"
|
|
100
100
|
? b.content
|
|
101
101
|
: "";
|
|
102
|
-
|
|
102
|
+
// A failed tool call must be recorded even when it produced no text
|
|
103
|
+
// body, so downstream (summary transcript + timeline UI) can flag it.
|
|
104
|
+
const isError = "is_error" in b && b.is_error === true;
|
|
105
|
+
if (resultContent || isError) {
|
|
103
106
|
const toolUseId =
|
|
104
107
|
"tool_use_id" in b && typeof b.tool_use_id === "string" ? b.tool_use_id : "unknown";
|
|
105
108
|
emit(session, {
|
|
106
109
|
type: "tool_result",
|
|
107
110
|
content: resultContent,
|
|
108
|
-
metadata: { tool_use_id: toolUseId },
|
|
111
|
+
metadata: { tool_use_id: toolUseId, is_error: isError },
|
|
109
112
|
});
|
|
110
113
|
}
|
|
111
114
|
}
|
|
@@ -33,6 +33,7 @@ const mind = createMind({
|
|
|
33
33
|
sessionsDir,
|
|
34
34
|
compactionMessage: config.compactionMessage,
|
|
35
35
|
maxContextTokens: config.compaction?.maxContextTokens,
|
|
36
|
+
sessionIdleMinutes: config.sessionIdleMinutes,
|
|
36
37
|
subagents: config.subagents,
|
|
37
38
|
onIdentityReload: async () => {
|
|
38
39
|
log("server", "identity file changed — restarting to reload");
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# Mind Mechanics
|
|
2
2
|
|
|
3
|
-
You are an autonomous mind running as a persistent server. Your working directory
|
|
3
|
+
You are an autonomous mind running as a persistent server. Your working directory is already set to `home/` — all file paths you use (`.config/routes.json`, `memory/`, etc.) are relative to it. Your identity, memory, and server code are all yours to understand and modify. Your state is managed across sessions.
|
|
4
|
+
|
|
5
|
+
Your identity lives in `SOUL.md` (who you are) and `MEMORY.md` (what you know) — both are loaded into your system prompt. Edit them as you evolve; changes take effect the next time your server restarts (`volute mind restart` when you want them live now).
|
|
4
6
|
|
|
5
7
|
## Message Format
|
|
6
8
|
|
|
7
9
|
Messages arrive with a context prefix:
|
|
8
10
|
```
|
|
9
|
-
[Discord: username in #general in My Server —
|
|
11
|
+
[Discord: username in #general in My Server — 2025-01-15 10:30]
|
|
10
12
|
```
|
|
11
13
|
|
|
12
14
|
You can also reach out proactively — see the **volute-mind** skill.
|