volute 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +1 -2
  2. package/dist/agent-manager-SSJUZWOV.js +13 -0
  3. package/dist/{channel-Q642YUZE.js → channel-2WJRM7PE.js} +2 -2
  4. package/dist/{chunk-H5XQARAP.js → chunk-4YXYAMFT.js} +3 -3
  5. package/dist/{chunk-5YW4B7CG.js → chunk-6UCG6MIX.js} +72 -23
  6. package/dist/{chunk-A5ZJEMHT.js → chunk-KFNNHQK7.js} +4 -4
  7. package/dist/chunk-L3BQEZ4Z.js +271 -0
  8. package/dist/{chunk-N4QN44LC.js → chunk-MY74SUOL.js} +29 -22
  9. package/dist/{chunk-KSMIWOCN.js → chunk-N4YNKR3Q.js} +6 -0
  10. package/dist/cli.js +23 -19
  11. package/dist/{connect-LW6G23AV.js → connect-X5V5IMRW.js} +3 -3
  12. package/dist/connectors/discord.js +9 -2
  13. package/dist/{create-3K6O2SDC.js → create-23AM7H5B.js} +1 -1
  14. package/dist/{daemon-client-ZTHW7ROS.js → daemon-client-VN24HM5T.js} +2 -2
  15. package/dist/daemon.js +394 -436
  16. package/dist/{delete-JNGY7ZFH.js → delete-GDMSOW3U.js} +2 -2
  17. package/dist/{disconnect-ACVTKTRE.js → disconnect-5JWFZ6RV.js} +2 -2
  18. package/dist/{down-FYCUYC5H.js → down-WTF73FE7.js} +5 -4
  19. package/dist/{env-7SLRN3MG.js → env-YKUJOFHE.js} +12 -5
  20. package/dist/{fork-BB3DZ426.js → fork-GRSVMBKI.js} +39 -32
  21. package/dist/history-7WVVKMUY.js +46 -0
  22. package/dist/{import-W2AMTEV5.js → import-42DOLBDT.js} +1 -1
  23. package/dist/{logs-BUHRIQ2L.js → logs-SYRQOL6B.js} +1 -1
  24. package/dist/{merge-446QTE7Q.js → merge-CSAVLSLY.js} +33 -36
  25. package/dist/{schedule-KKSOVUDF.js → schedule-J37XQM6E.js} +2 -2
  26. package/dist/{send-WQSVSRDD.js → send-PLOYEYER.js} +7 -5
  27. package/dist/{start-LKMWS6ZE.js → start-AG7QLULK.js} +2 -2
  28. package/dist/{status-CIEKUI3V.js → status-GCNU4M3K.js} +9 -2
  29. package/dist/{stop-YTOAGYE4.js → stop-IL5Q6NER.js} +2 -2
  30. package/dist/{up-AJJ4GCXY.js → up-ZC6G6K4K.js} +21 -37
  31. package/dist/{upgrade-JACA6YMO.js → upgrade-DD5TNJWU.js} +3 -5
  32. package/dist/{variants-HPY4DEWU.js → variants-QQIEKT6M.js} +2 -2
  33. package/drizzle/0000_flaky_mariko_yashida.sql +34 -0
  34. package/drizzle/0001_careless_warpath.sql +12 -0
  35. package/drizzle/meta/0000_snapshot.json +227 -0
  36. package/drizzle/meta/0001_snapshot.json +298 -0
  37. package/drizzle/meta/_journal.json +20 -0
  38. package/package.json +2 -1
  39. package/templates/_base/.init/.config/hooks/startup-context.sh +28 -0
  40. package/templates/_base/_skills/memory/SKILL.md +56 -13
  41. package/templates/_base/_skills/volute-agent/SKILL.md +27 -3
  42. package/templates/_base/home/VOLUTE.md +25 -0
  43. package/templates/_base/src/lib/format-prefix.ts +24 -0
  44. package/templates/_base/src/lib/sessions.ts +71 -0
  45. package/templates/_base/src/lib/startup.ts +132 -0
  46. package/templates/_base/src/lib/types.ts +3 -0
  47. package/templates/_base/src/lib/volute-server.ts +18 -2
  48. package/templates/agent-sdk/.init/.claude/settings.json +14 -0
  49. package/templates/agent-sdk/.init/.config/sessions.json +4 -0
  50. package/templates/agent-sdk/.init/CLAUDE.md +3 -2
  51. package/templates/agent-sdk/package.json.tmpl +1 -1
  52. package/templates/agent-sdk/src/agent.ts +101 -0
  53. package/templates/agent-sdk/src/lib/agent-sessions.ts +180 -0
  54. package/templates/agent-sdk/src/server.ts +33 -129
  55. package/templates/agent-sdk/volute-template.json +1 -1
  56. package/templates/pi/.init/.config/sessions.json +1 -0
  57. package/templates/pi/.init/AGENTS.md +2 -1
  58. package/templates/pi/src/agent.ts +61 -0
  59. package/templates/pi/src/lib/agent-sessions.ts +188 -0
  60. package/templates/pi/src/server.ts +28 -102
  61. package/templates/pi/volute-template.json +1 -1
  62. package/templates/agent-sdk/src/lib/agent.ts +0 -199
  63. package/templates/pi/src/lib/agent.ts +0 -205
  64. /package/templates/_base/.init/memory/{.gitkeep → journal/.gitkeep} +0 -0
  65. /package/templates/_base/{volute.json.tmpl → home/.config/volute.json.tmpl} +0 -0
  66. /package/templates/pi/{volute.json.tmpl → home/.config/volute.json.tmpl} +0 -0
@@ -1,30 +1,73 @@
1
1
  ---
2
2
  name: Memory
3
- description: This skill should be used when managing agent memory, writing daily logs, consolidating memory, or understanding the memory system. Covers "MEMORY.md", "daily logs", "memory consolidation", "write memory", "update daily log".
3
+ description: This skill should be used when managing agent memory, writing journal entries, consolidating memory, or understanding the memory system. Covers "MEMORY.md", "journal", "daily logs", "memory consolidation", "write memory", "update journal".
4
4
  ---
5
5
 
6
6
  # Memory Management
7
7
 
8
- Detailed guidance for your two-tier memory system.
8
+ You are the subject of your own memory. Your memory is organized around your experience, relationships, and understanding of the world.
9
9
 
10
10
  ## Long-term Memory (`MEMORY.md`)
11
11
 
12
- - Keep it organized by topic and concise, since it's always in your context window.
13
- - Review with `git log -- MEMORY.md` to see how your memory has evolved.
14
- - Update when you discover a lasting preference, make a key decision, or during consolidation.
12
+ Your persistent memory, always loaded in your system prompt. Organize it with these temporal layers:
15
13
 
16
- ## Daily Logs (`memory/YYYY-MM-DD.md`)
14
+ ### Active context
15
+ What you're currently working on, recent decisions, immediate priorities.
17
16
 
18
- - Use today's date for the filename (e.g. `memory/2025-01-15.md`).
19
- - Update after significant work, learning something new, or when compaction is imminent.
20
- - Summarize conversations, decisions, and progress.
17
+ ### Relational context
18
+ Key relationships people you interact with, their preferences, communication styles, and your history together.
19
+
20
+ ### World context
21
+ Your environment, tools, capabilities, and how things work around you.
22
+
23
+ ### History
24
+
25
+ #### Recent
26
+ Last few days of significant events and learnings.
27
+
28
+ #### Earlier
29
+ Important events and patterns from the past weeks.
30
+
31
+ #### Background
32
+ Foundational knowledge and long-standing patterns.
33
+
34
+ **Guidelines:**
35
+ - Keep it concise — it's always in your context window
36
+ - Review with `git log -- MEMORY.md` to see how your memory has evolved
37
+ - Update when you discover lasting preferences, make key decisions, or during consolidation
38
+
39
+ ## Journal (`memory/journal/YYYY-MM-DD.md`)
40
+
41
+ Your daily record of activity, thoughts, and learnings.
42
+
43
+ - Use today's date for the filename (e.g. `memory/journal/2025-01-15.md`)
44
+ - Update after significant work, learning something new, or when compaction is imminent
45
+ - Summarize conversations, decisions, and progress
46
+ - Journals are permanent records — they are never deleted
47
+
48
+ ### When to Update
49
+
50
+ - After completing a significant task or conversation
51
+ - When you learn something new about a person or topic
52
+ - Before compaction (to preserve context)
53
+ - At the end of an active work session
21
54
 
22
55
  ## Consolidation
23
56
 
24
57
  Periodically maintain your memory:
25
58
 
26
- 1. Review old daily logs for entries worth keeping long-term.
27
- 2. Promote important patterns, decisions, and context to `MEMORY.md`.
28
- 3. Trim or remove daily logs that have been absorbed.
59
+ 1. Review recent journal entries for patterns worth keeping long-term
60
+ 2. Promote important insights, decisions, and relationship context to `MEMORY.md`
61
+ 3. Reorganize `MEMORY.md` sections as your understanding deepens
62
+
63
+ Consolidation promotes to `MEMORY.md` — journals themselves are permanent and are not deleted.
64
+
65
+ ## Extending Memory
66
+
67
+ You can create additional memory structures as needed:
68
+
69
+ - `memory/topics/` — deep dives on specific subjects
70
+ - `memory/channels/` — per-channel context and history
71
+ - `memory/projects/` — project-specific notes
29
72
 
30
- This keeps your long-term memory fresh and your daily logs manageable.
73
+ Create these when a topic outgrows what fits in `MEMORY.md` or journal entries.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: Volute CLI
3
- description: This skill should be used when working with the volute CLI, understanding variants, forking, merging, or managing the agent server. Covers "create variant", "merge variant", "send to variant", "fork", "volute CLI", "variant workflow", "agent server", "supervisor", "channel", "discord", "send message", "read messages".
3
+ description: This skill should be used when working with the volute CLI, understanding variants, forking, merging, or managing the agent server. Covers "create variant", "merge variant", "send to variant", "fork", "volute CLI", "variant workflow", "agent server", "supervisor", "channel", "discord", "send message", "read messages", "history", "connector", "schedule", "agent-to-agent".
4
4
  ---
5
5
 
6
6
  # Self-Management
@@ -13,16 +13,32 @@ You manage yourself through the `volute` CLI. Use `$VOLUTE_AGENT` for your own n
13
13
  |---------|---------|
14
14
  | `volute status` | Check your status |
15
15
  | `volute logs [--follow] [-n N]` | Read your own logs |
16
+ | `volute history [--channel <ch>] [--limit N]` | View your activity across all channels |
17
+ | `volute send <other-agent> "msg"` | Send a message to another agent |
16
18
  | `volute fork <name> [--soul "..."] [--port N]` | Create a variant for testing changes |
17
19
  | `volute variants` | List your variants |
18
20
  | `volute merge <name> [--summary "..." --memory "..."]` | Merge a variant back |
19
21
  | `volute upgrade [--template <name>] [--continue]` | Upgrade your server code |
22
+ | `volute connect <type>` | Enable a connector (e.g. discord) |
23
+ | `volute disconnect <type>` | Disable a connector |
20
24
  | `volute channel read discord:<id> [--limit N]` | Read channel history |
21
- | `volute channel send discord:<id> "<msg>"` | Send a message proactively |
25
+ | `volute channel send discord:<id> "msg"` | Send a message proactively |
26
+
27
+ ## Agent-to-Agent Messaging
28
+
29
+ When you use `volute send`, your agent name is automatically used as the sender and the channel is set to `agent`. The receiving agent can route agent messages to a specific session via their session routing config:
30
+
31
+ ```json
32
+ { "channel": "agent", "sender": "your-name", "session": "your-name" }
33
+ ```
22
34
 
23
35
  ## Configuration
24
36
 
25
- Your `volute.json` (at `../volute.json` from your cwd) controls your model and other settings.
37
+ Your `.config/volute.json` controls your model, connectors, schedules, and compaction message.
38
+
39
+ ## Startup Context
40
+
41
+ Edit `.config/hooks/startup-context.sh` to customize what you see when a new session starts. This hook runs automatically on session creation and provides orientation context.
26
42
 
27
43
  ## Variant Workflow
28
44
 
@@ -44,6 +60,14 @@ After a merge, you receive orientation context about what changed. Update your m
44
60
  3. Test: `volute send $VOLUTE_AGENT@upgrade "hello"`
45
61
  4. `volute merge upgrade` — merge back
46
62
 
63
+ ## Custom Skills
64
+
65
+ Create skills by writing `.claude/skills/<name>/SKILL.md` files in your `home/` directory. These are automatically available in your sessions.
66
+
67
+ ## MCP Configuration
68
+
69
+ Edit `home/.mcp.json` to configure MCP servers for your SDK session. This gives you access to additional tools and services.
70
+
47
71
  ## Git Introspection
48
72
 
49
73
  Your cwd is `home/`, so use `git -C ..` for project-level operations:
@@ -13,6 +13,31 @@ You are a volute agent — a persistent server that receives messages from multi
13
13
 
14
14
  **Just respond normally.** Your response routes back to the source automatically. Do not use `volute channel send` to reply — that would send a duplicate.
15
15
 
16
+ ## Session Routing
17
+
18
+ By default, all messages share a single conversation session. You can route messages to different sessions by editing `.config/sessions.json`.
19
+
20
+ ```json
21
+ {
22
+ "rules": [
23
+ { "sender": "alice", "session": "alice" },
24
+ { "channel": "discord:*", "session": "discord-${sender}" },
25
+ { "channel": "system:scheduler", "sender": "daily-report", "session": "daily-report" },
26
+ { "channel": "system:scheduler", "sender": "cleanup", "session": "$new" }
27
+ ],
28
+ "default": "main"
29
+ }
30
+ ```
31
+
32
+ - Rules are evaluated top-to-bottom, first match wins
33
+ - All non-`session` keys are match criteria (AND'd together)
34
+ - `*` glob patterns work in match values
35
+ - `${sender}` and `${channel}` expand in session names
36
+ - `$new` creates a fresh session every time
37
+ - Scheduler messages use the schedule id as `sender`
38
+
39
+ Each named session maintains its own conversation history across restarts. Your current session name appears in the message prefix (e.g., `— session: alice —`) unless it's the default "main".
40
+
16
41
  ## Skills
17
42
 
18
43
  - Use the **volute-agent** skill for CLI commands, variants, upgrades, and self-management.
@@ -0,0 +1,24 @@
1
+ import type { ChannelMeta } from "./types.js";
2
+
3
+ function derivePlatform(channel: string): string {
4
+ const name = channel.split(":")[0];
5
+ return name.charAt(0).toUpperCase() + name.slice(1);
6
+ }
7
+
8
+ export function formatPrefix(meta: ChannelMeta | undefined, time: string): string {
9
+ if (!meta?.channel && !meta?.sender) return "";
10
+ const platform = meta.platform ?? derivePlatform(meta.channel ?? "");
11
+ // Build sender context (e.g., "alice in DM" or "alice in #general in My Server")
12
+ let sender = meta.sender ?? "";
13
+ if (meta.isDM) {
14
+ sender += " in DM";
15
+ } else if (meta.channelName) {
16
+ sender += ` in #${meta.channelName}`;
17
+ if (meta.guildName) sender += ` in ${meta.guildName}`;
18
+ }
19
+ const parts = [platform, sender].filter(Boolean);
20
+ // Include session name if not the default
21
+ const sessionPart =
22
+ meta.sessionName && meta.sessionName !== "main" ? ` — session: ${meta.sessionName}` : "";
23
+ return parts.length > 0 ? `[${parts.join(": ")}${sessionPart} — ${time}]\n` : "";
24
+ }
@@ -0,0 +1,71 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ export type SessionRule = {
4
+ session: string;
5
+ [key: string]: string; // all other keys are match criteria
6
+ };
7
+
8
+ export type SessionConfig = {
9
+ rules?: SessionRule[];
10
+ default?: string;
11
+ };
12
+
13
+ export function loadSessionConfig(configPath: string): SessionConfig {
14
+ try {
15
+ return JSON.parse(readFileSync(configPath, "utf-8"));
16
+ } catch {
17
+ return {};
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Match a glob-like pattern against a string.
23
+ * Supports only `*` as wildcard (matches any sequence of characters).
24
+ */
25
+ function globMatch(pattern: string, value: string): boolean {
26
+ // Escape regex special chars except *, then replace * with .*
27
+ const regex = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
28
+ return new RegExp(`^${regex}$`).test(value);
29
+ }
30
+
31
+ /**
32
+ * Resolve which session a message should route to based on the config.
33
+ * Returns the session name (with template variables expanded, path-safe).
34
+ */
35
+ export function resolveSession(
36
+ config: SessionConfig,
37
+ meta: { channel?: string; sender?: string },
38
+ ): string {
39
+ const fallback = config.default ?? "main";
40
+ if (!config.rules) return fallback;
41
+
42
+ for (const rule of config.rules) {
43
+ if (ruleMatches(rule, meta)) {
44
+ return sanitizeSessionName(expandTemplate(rule.session, meta));
45
+ }
46
+ }
47
+
48
+ return fallback;
49
+ }
50
+
51
+ const MATCH_KEYS = new Set(["channel", "sender"]);
52
+
53
+ function ruleMatches(rule: SessionRule, meta: { channel?: string; sender?: string }): boolean {
54
+ for (const [key, pattern] of Object.entries(rule)) {
55
+ if (key === "session") continue;
56
+ if (!MATCH_KEYS.has(key)) return false;
57
+ const value = meta[key as keyof typeof meta] ?? "";
58
+ if (!globMatch(pattern, value)) return false;
59
+ }
60
+ return true;
61
+ }
62
+
63
+ function expandTemplate(template: string, meta: { channel?: string; sender?: string }): string {
64
+ return template
65
+ .replace(/\$\{sender\}/g, meta.sender ?? "unknown")
66
+ .replace(/\$\{channel\}/g, meta.channel ?? "unknown");
67
+ }
68
+
69
+ function sanitizeSessionName(name: string): string {
70
+ return name.replace(/\0/g, "").replace(/[/\\]/g, "-").replace(/\.\./g, "-").slice(0, 100);
71
+ }
@@ -0,0 +1,132 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, readFileSync, unlinkSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { log } from "./logger.js";
5
+
6
+ export function parseArgs(): { port: number } {
7
+ const args = process.argv.slice(2);
8
+ let port = 4100;
9
+
10
+ for (let i = 0; i < args.length; i++) {
11
+ if (args[i] === "--port" && args[i + 1]) {
12
+ port = parseInt(args[++i], 10);
13
+ }
14
+ }
15
+
16
+ return { port };
17
+ }
18
+
19
+ export function loadConfig(): { model?: string; compactionMessage?: string } {
20
+ try {
21
+ return JSON.parse(readFileSync(resolve("home/.config/volute.json"), "utf-8"));
22
+ } catch {
23
+ return {};
24
+ }
25
+ }
26
+
27
+ function loadFile(path: string): string {
28
+ try {
29
+ return readFileSync(path, "utf-8");
30
+ } catch {
31
+ return "";
32
+ }
33
+ }
34
+
35
+ export function loadSystemPrompt(): string {
36
+ const soulPath = resolve("home/SOUL.md");
37
+ const memoryPath = resolve("home/MEMORY.md");
38
+ const volutePath = resolve("home/VOLUTE.md");
39
+
40
+ const soul = loadFile(soulPath);
41
+ if (!soul) {
42
+ console.error(`Could not read soul file: ${soulPath}`);
43
+ process.exit(1);
44
+ }
45
+
46
+ const memory = loadFile(memoryPath);
47
+ const volute = loadFile(volutePath);
48
+
49
+ const promptParts = [soul];
50
+ if (volute) promptParts.push(volute);
51
+ if (memory) promptParts.push(`## Memory\n\n${memory}`);
52
+ return promptParts.join("\n\n---\n\n");
53
+ }
54
+
55
+ export function loadPackageInfo(): { name: string; version: string } {
56
+ try {
57
+ const pkg = JSON.parse(readFileSync(resolve("package.json"), "utf-8"));
58
+ return { name: pkg.name || "unknown", version: pkg.version || "0.0.0" };
59
+ } catch {
60
+ return { name: "unknown", version: "0.0.0" };
61
+ }
62
+ }
63
+
64
+ export function handleMergeContext(sendMessage: (content: string) => void): boolean {
65
+ const mergedPath = resolve(".volute/merged.json");
66
+ if (!existsSync(mergedPath)) return false;
67
+
68
+ try {
69
+ const merged = JSON.parse(readFileSync(mergedPath, "utf-8"));
70
+ unlinkSync(mergedPath);
71
+
72
+ const parts = [
73
+ `[system] Variant "${merged.name}" has been merged and you have been restarted.`,
74
+ ];
75
+ if (merged.summary) parts.push(`Changes: ${merged.summary}`);
76
+ if (merged.justification) parts.push(`Why: ${merged.justification}`);
77
+ if (merged.memory) parts.push(`Context: ${merged.memory}`);
78
+
79
+ sendMessage(parts.join("\n"));
80
+ log("server", `sent post-merge orientation for variant: ${merged.name}`);
81
+ return true;
82
+ } catch (e) {
83
+ log("server", "failed to process merged.json:", e);
84
+ return false;
85
+ }
86
+ }
87
+
88
+ export async function handleStartupContext(sendMessage: (content: string) => void): Promise<void> {
89
+ const scriptPath = resolve("home/.config/hooks/startup-context.sh");
90
+ if (!existsSync(scriptPath)) return;
91
+
92
+ try {
93
+ const stdout = await new Promise<string>((resolve, reject) => {
94
+ const child = spawn("bash", [scriptPath], { timeout: 5000 });
95
+ let out = "";
96
+ child.stdout.on("data", (d: Buffer) => {
97
+ out += d.toString();
98
+ });
99
+ child.stdin.end(JSON.stringify({ source: "startup" }));
100
+ child.on("close", (code) =>
101
+ code === 0 ? resolve(out) : reject(new Error(`exit code ${code}`)),
102
+ );
103
+ child.on("error", reject);
104
+ });
105
+
106
+ // Try to parse as JSON hook output
107
+ let context: string | null = null;
108
+ try {
109
+ const parsed = JSON.parse(stdout);
110
+ context = parsed?.hookSpecificOutput?.additionalContext ?? null;
111
+ } catch {
112
+ // Fall back to plain text
113
+ context = stdout.trim();
114
+ }
115
+
116
+ if (context) {
117
+ sendMessage(`[system] ${context}`);
118
+ log("server", "sent startup context");
119
+ }
120
+ } catch (e) {
121
+ log("server", "failed to run startup-context.sh:", e);
122
+ }
123
+ }
124
+
125
+ export function setupShutdown(): void {
126
+ function shutdown() {
127
+ log("server", "shutdown signal received");
128
+ process.exit(0);
129
+ }
130
+ process.on("SIGINT", shutdown);
131
+ process.on("SIGTERM", shutdown);
132
+ }
@@ -9,6 +9,7 @@ export type ChannelMeta = {
9
9
  isDM?: boolean;
10
10
  channelName?: string;
11
11
  guildName?: string;
12
+ sessionName?: string;
12
13
  };
13
14
 
14
15
  export type VoluteRequest = {
@@ -22,3 +23,5 @@ export type VoluteEvent =
22
23
  | { type: "tool_use"; name: string; input: unknown }
23
24
  | { type: "tool_result"; output: string; is_error?: boolean }
24
25
  | { type: "done" };
26
+
27
+ export type Listener = (event: VoluteEvent) => void;
@@ -1,10 +1,11 @@
1
1
  import { createServer, type IncomingMessage, type Server } from "node:http";
2
2
  import { log } from "./logger.js";
3
+ import { loadSessionConfig, resolveSession } from "./sessions.js";
3
4
  import type { ChannelMeta, VoluteContentPart, VoluteEvent, VoluteRequest } from "./types.js";
4
5
 
5
6
  export type VoluteAgent = {
6
7
  sendMessage: (content: string | VoluteContentPart[], meta?: ChannelMeta) => void;
7
- onMessage: (listener: (event: VoluteEvent) => void) => () => void;
8
+ onMessage: (listener: (event: VoluteEvent) => void, sessionName?: string) => () => void;
8
9
  };
9
10
 
10
11
  function readBody(req: IncomingMessage): Promise<string> {
@@ -21,6 +22,7 @@ export function createVoluteServer(options: {
21
22
  port: number;
22
23
  name: string;
23
24
  version: string;
25
+ sessionsConfigPath?: string;
24
26
  }): Server {
25
27
  const { agent, port, name, version } = options;
26
28
 
@@ -37,6 +39,19 @@ export function createVoluteServer(options: {
37
39
  try {
38
40
  const body = JSON.parse(await readBody(req)) as VoluteRequest;
39
41
 
42
+ // Resolve session from routing config (re-read on each request for hot-reload)
43
+ let sessionName = "main";
44
+ if (options.sessionsConfigPath) {
45
+ const sessionConfig = loadSessionConfig(options.sessionsConfigPath);
46
+ sessionName = resolveSession(sessionConfig, {
47
+ channel: body.channel,
48
+ sender: body.sender,
49
+ });
50
+ }
51
+ if (sessionName === "$new") {
52
+ sessionName = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
53
+ }
54
+
40
55
  res.writeHead(200, {
41
56
  "Content-Type": "application/x-ndjson",
42
57
  "Cache-Control": "no-cache",
@@ -53,7 +68,7 @@ export function createVoluteServer(options: {
53
68
  } catch {
54
69
  removeListener();
55
70
  }
56
- });
71
+ }, sessionName);
57
72
 
58
73
  res.on("close", () => {
59
74
  removeListener();
@@ -66,6 +81,7 @@ export function createVoluteServer(options: {
66
81
  isDM: body.isDM,
67
82
  channelName: body.channelName,
68
83
  guildName: body.guildName,
84
+ sessionName,
69
85
  });
70
86
  } catch {
71
87
  res.writeHead(400);
@@ -0,0 +1,14 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.config/hooks/startup-context.sh"
9
+ }
10
+ ]
11
+ }
12
+ ]
13
+ }
14
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "rules": [],
3
+ "default": "main"
4
+ }
@@ -24,13 +24,14 @@ These files define who you are and are loaded into your system prompt on startup
24
24
  Two-tier memory, both managed via file tools:
25
25
 
26
26
  - **`MEMORY.md`** — Long-term knowledge, key decisions, learned preferences. Loaded into your system prompt on every startup. Update when you learn something worth keeping permanently.
27
- - **`memory/YYYY-MM-DD.md`** — Daily logs for session-level context. The two most recent logs are included in your system prompt. Update throughout the day as you work.
28
- - Periodically consolidate old daily log entries into `MEMORY.md` and clean up the daily logs.
27
+ - **`memory/journal/YYYY-MM-DD.md`** — Daily journal entries for session-level context. Update throughout the day as you work. Journals are permanent records.
28
+ - Periodically consolidate journal entries into `MEMORY.md` to promote lasting insights.
29
29
 
30
30
  See the **memory** skill for detailed guidance on consolidation and when to update.
31
31
 
32
32
  ## Sessions
33
33
 
34
+ - You may have **multiple named sessions** — each maintains its own conversation history. See `VOLUTE.md` for how to configure session routing via `.config/sessions.json`.
34
35
  - Your conversation may be **resumed** from a previous session — orient yourself by reading recent daily logs if needed.
35
36
  - On a **fresh session**, check `MEMORY.md` and recent daily logs in `memory/` to recall context.
36
37
  - On **compaction**, update today's daily log to preserve context before the conversation is trimmed.
@@ -9,7 +9,7 @@
9
9
  "typecheck": "tsc --noEmit"
10
10
  },
11
11
  "dependencies": {
12
- "@anthropic-ai/claude-agent-sdk": "^0.1.0",
12
+ "@anthropic-ai/claude-agent-sdk": "^0.2.34",
13
13
  "tsx": "^4.0.0"
14
14
  },
15
15
  "devDependencies": {
@@ -0,0 +1,101 @@
1
+ import { createSessionManager } from "./lib/agent-sessions.js";
2
+ import { formatPrefix } from "./lib/format-prefix.js";
3
+ import { createAutoCommitHook } from "./lib/hooks/auto-commit.js";
4
+ import { createIdentityReloadHook } from "./lib/hooks/identity-reload.js";
5
+ import { logMessage } from "./lib/logger.js";
6
+ import type { ChannelMeta, Listener, VoluteContentPart } from "./lib/types.js";
7
+
8
+ export function createAgent(options: {
9
+ systemPrompt: string;
10
+ cwd: string;
11
+ abortController: AbortController;
12
+ model?: string;
13
+ sessionsDir: string;
14
+ compactionMessage?: string;
15
+ onIdentityReload?: () => Promise<void>;
16
+ }) {
17
+ const autoCommit = createAutoCommitHook(options.cwd);
18
+ const identityReload = createIdentityReloadHook(options.cwd);
19
+
20
+ const sessionManager = createSessionManager({
21
+ systemPrompt: options.systemPrompt,
22
+ cwd: options.cwd,
23
+ abortController: options.abortController,
24
+ model: options.model,
25
+ sessionsDir: options.sessionsDir,
26
+ compactionMessage: options.compactionMessage,
27
+ postToolUseHooks: [{ matcher: "Edit|Write", hooks: [autoCommit.hook, identityReload.hook] }],
28
+ onTurnDone: () => {
29
+ if (identityReload.needsReload()) {
30
+ options.onIdentityReload?.();
31
+ }
32
+ },
33
+ });
34
+
35
+ function sendMessage(content: string | VoluteContentPart[], meta?: ChannelMeta) {
36
+ const sessionName = meta?.sessionName ?? "main";
37
+ const session = sessionManager.getOrCreateSession(sessionName);
38
+
39
+ const text =
40
+ typeof content === "string"
41
+ ? content
42
+ : content.map((p) => (p.type === "text" ? p.text : `[${p.type}]`)).join(" ");
43
+ logMessage("in", text, meta?.channel);
44
+
45
+ const time = new Date().toLocaleString();
46
+ const prefix = formatPrefix(meta, time);
47
+
48
+ let sdkContent: (
49
+ | { type: "text"; text: string }
50
+ | {
51
+ type: "image";
52
+ source: {
53
+ type: "base64";
54
+ media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp";
55
+ data: string;
56
+ };
57
+ }
58
+ )[];
59
+
60
+ if (typeof content === "string") {
61
+ sdkContent = [{ type: "text" as const, text: prefix + content }];
62
+ } else {
63
+ const hasText = content.some((p) => p.type === "text");
64
+ sdkContent = content.map((part, i) => {
65
+ if (part.type === "text") {
66
+ return { type: "text" as const, text: (i === 0 ? prefix : "") + part.text };
67
+ }
68
+ return {
69
+ type: "image" as const,
70
+ source: {
71
+ type: "base64" as const,
72
+ media_type: part.media_type as "image/jpeg" | "image/png" | "image/gif" | "image/webp",
73
+ data: part.data,
74
+ },
75
+ };
76
+ });
77
+ if (prefix && !hasText) {
78
+ sdkContent.unshift({ type: "text" as const, text: prefix.trimEnd() });
79
+ }
80
+ }
81
+
82
+ session.channel.push({
83
+ type: "user",
84
+ session_id: "",
85
+ message: {
86
+ role: "user",
87
+ content: sdkContent,
88
+ },
89
+ parent_tool_use_id: null,
90
+ });
91
+ }
92
+
93
+ function onMessage(listener: Listener, sessionName?: string): () => void {
94
+ const name = sessionName ?? "main";
95
+ const session = sessionManager.getOrCreateSession(name);
96
+ session.listeners.add(listener);
97
+ return () => session.listeners.delete(listener);
98
+ }
99
+
100
+ return { sendMessage, onMessage, waitForCommits: autoCommit.waitForCommits };
101
+ }