Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Bridges are **system-wide**, not per-mind. Config lives in `~/.volute/system/bri

### Centralized state directory

Volute per-mind system state (logs, env, bridge PIDs) lives in `~/.volute/state/<name>/`, separate from mind directories. This keeps mind projects portable — they contain only mind-owned state (sessions, cursors). The `stateDir(name)` helper in `packages/daemon/src/lib/mind/registry.ts` resolves state paths. On daemon startup, `migrateDotVoluteDir()` renames any legacy `<mindDir>/.volute/` to `<mindDir>/.mind/`, then `migrateMindState()` copies `env.json` and `logs/` from the mind's `.mind/` to the centralized state dir.
Volute per-mind system state (logs, env, bridge PIDs) lives in `~/.volute/state/<name>/`, separate from mind directories. This keeps mind projects portable — they contain only mind-owned state (sessions, cursors). The `stateDir(name)` helper in `packages/daemon/src/lib/mind/registry.ts` resolves state paths.

Minds receive `VOLUTE_MIND`, `VOLUTE_STATE_DIR`, `VOLUTE_MIND_DIR`, `VOLUTE_MIND_PORT`, `VOLUTE_DAEMON_PORT`, and `VOLUTE_MIND_TOKEN` env vars from the daemon. The mind env is built from an allowlist (benign system vars, outbound proxy / custom-CA vars, + `VOLUTE_*`), not a full `process.env` spread, so ambient host secrets are withheld. `VOLUTE_MIND_TOKEN` is a per-mind, non-admin token — distinct from the daemon's own admin `VOLUTE_DAEMON_TOKEN`, which is never handed to minds. Instead of file-based IPC (restart.json, merged.json), minds call the daemon's REST API via `daemonRestart()` and `daemonSend()` from `templates/_base/src/lib/daemon-client.ts`. The daemon delivers post-restart context (merge info) to minds via HTTP POST to the mind's `/message` endpoint.

Expand Down Expand Up @@ -220,7 +220,7 @@ Extensions add functionality to Volute — custom UI sections, API routes, datab
| `volute service status` | Show service status |
| `volute update` | Check for updates |

Mind-scoped commands (`chat`, `clock`, `skill`) use `--mind <name>` or `VOLUTE_MIND` env var. Legacy aliases (`volute mind seed/sprout/sleep/wake`, `volute variant ...`) forward to the current nouns. Full flags: `volute <cmd> --help`.
Mind-scoped commands (`chat`, `clock`, `skill`) use `--mind <name>` or `VOLUTE_MIND` env var. Full flags: `volute <cmd> --help`.

## Directory guide

Expand Down
17 changes: 16 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions packages/cli/src/cli-remote.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Stripped-down CLI entry point for remote use (no local daemon required).
// Connects to a daemon via VOLUTE_DAEMON_URL or stored session URL.
export {};

process.noDeprecation = true;

const command = process.argv[2];
Expand All @@ -24,9 +25,6 @@ switch (command) {
case "chat":
await import("./commands/chat.js").then((m) => m.run(args));
break;
case "variant":
await import("./commands/variant.js").then((m) => m.run(args));
break;
case "clock":
await import("./commands/clock.js").then((m) => m.run(args));
break;
Expand Down
16 changes: 0 additions & 16 deletions packages/cli/src/commands/mind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,22 +72,6 @@ const cmd = subcommands({
description: "Join a variant back into its parent",
run: (args) => import("./join.js").then((m) => m.run(args)),
},
sleep: {
description: "(legacy) Use 'volute clock sleep' instead",
run: (args) => import("./mind-sleep.js").then((m) => m.run(args)),
},
wake: {
description: "(legacy) Use 'volute clock wake' instead",
run: (args) => import("./mind-wake.js").then((m) => m.run(args)),
},
seed: {
description: "(legacy) Use 'volute seed create' instead",
run: (args) => import("./seed.js").then((m) => m.run(args)),
},
sprout: {
description: "(legacy) Use 'volute seed sprout' instead",
run: (args) => import("./sprout.js").then((m) => m.run(args)),
},
},
footer: "Mind name can be omitted (where applicable) if VOLUTE_MIND is set.",
});
Expand Down
5 changes: 0 additions & 5 deletions packages/cli/src/commands/seed.ts

This file was deleted.

21 changes: 0 additions & 21 deletions packages/cli/src/commands/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,27 +88,6 @@ const cmd = subcommands({
description: "Check service status",
run: async () => status(),
},
install: {
description: "(deprecated) Use 'volute setup' instead",
run: async () => {
console.log("'volute service install' has been replaced by 'volute setup'.");
console.log("Run `volute setup` to configure your installation.");
},
},
uninstall: {
description: "(deprecated) Use 'volute setup' instead",
run: async () => {
console.log("'volute service uninstall' has been replaced by 'volute setup'.");
console.log("To uninstall the service, remove the service file manually:");
if (process.platform === "darwin") {
console.log(" launchctl unload ~/Library/LaunchAgents/com.volute.daemon.plist");
console.log(" rm ~/Library/LaunchAgents/com.volute.daemon.plist");
} else {
console.log(" systemctl --user disable --now volute");
console.log(" rm ~/.config/systemd/user/volute.service");
}
},
},
},
});

Expand Down
5 changes: 0 additions & 5 deletions packages/cli/src/commands/sprout.ts

This file was deleted.

54 changes: 0 additions & 54 deletions packages/cli/src/commands/variant.ts

This file was deleted.

45 changes: 0 additions & 45 deletions packages/daemon/src/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,6 @@ export async function startDaemon(opts: {
mkdirSync(home, { recursive: true });
ensureSystemDir();

// Split provider credentials into secrets.json and relax config.json to 0644 so
// non-root host CLI commands can read it (v0.41.1 regression). Runs as root.
const { migrateConfigSecrets, migrateSetupCompleted } = await import("./lib/config/setup.js");
migrateConfigSecrets();

// Migrate pre-existing installations (setup field without setupCompleted)
migrateSetupCompleted();

// Migrate bare model ids to provider-qualified form before the spirit starts,
// so syncSpiritTemplate reads a qualified spiritModel.
const { migrateAiModelQualification } = await import("./lib/ai-service.js");
migrateAiModelQualification();

// Initialize database (runs drizzle migrations + creates raw connection)
await (await import("./lib/db.js")).getDb();

Expand All @@ -146,38 +133,6 @@ export async function startDaemon(opts: {
log.warn("avatar size migration failed", log.errorData(err));
}

// Rename legacy "session" keys to "thread" in each mind's routes.json and
// volute.json (#493) — a leftover `session` rule key makes the whole rule
// unmatchable, which gates the channel's messages. Non-fatal per file.
try {
const { migrateThreadConfigs } = await import("./lib/mind/migrate-thread-config.js");
await migrateThreadConfigs();
} catch (err) {
log.warn("session→thread config migration failed", log.errorData(err));
}

// Substitute the leftover `{{name}}` placeholder in each mind's routes.json —
// template .init/ files shipped it unsubstituted, leaving every mind with a
// channel batch trigger ("@{{name}}") that can never match. Non-fatal per mind.
try {
const { migrateNamePlaceholders } = await import("./lib/mind/migrate-name-placeholder.js");
await migrateNamePlaceholders();
} catch (err) {
log.warn("{{name}} placeholder migration failed", log.errorData(err));
}

// Add `.init/` infrastructure files (hooks, bin shims) minds never received —
// the upgrade's `.init/` exclusion protects identity files but also blocked
// these, leaving minds created before a hook existed permanently without it.
// Notably the notices drain hook, the sole reader of next-turn system events
// (#808). Adds only what's missing, never overwrites. Non-fatal per mind.
try {
const { migrateInitInfrastructure } = await import("./lib/mind/migrate-init-infrastructure.js");
await migrateInitInfrastructure();
} catch (err) {
log.warn("`.init/` infrastructure backfill failed", log.errorData(err));
}

// Initialize sandbox runtime for mind process isolation
const { initSandbox } = await import("./lib/mind/sandbox.js");
await initSandbox();
Expand Down
57 changes: 0 additions & 57 deletions packages/daemon/src/lib/ai-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,63 +284,6 @@ export function setEnabledModels(modelIds: string[]): void {
writeGlobalConfig({ ...config, ai });
}

/**
* One-time migration: convert bare model ids in `ai.models`, `spiritModel`, and
* `ai.utilityModel` to provider-qualified `provider:model` form. A bare id is
* ambiguous (many providers serve the same id), so this pins each to the
* explicitly-configured provider(s) that serve it: the enabled list expands to
* one entry per configured provider, and the single-value defaults qualify to
* the first configured provider that serves them. Idempotent — a no-op once
* every stored id already contains a ":".
*/
export function migrateAiModelQualification(): void {
const config = readGlobalConfig();
const ai = config.ai;
const isBare = (id?: string): id is string => id != null && !id.includes(":");

const hasBareInList = (ai?.models ?? []).some(isBare);
if (!hasBareInList && !isBare(config.spiritModel) && !isBare(ai?.utilityModel)) return;

// Only providers the admin explicitly added (with credentials) — not ambient
// env/OAuth providers — are candidates, matching what the user configured.
const providers = ai ? Object.keys(ai.providers) : [];
const customModels = ai?.customModels ?? [];
const providersServing = (bareId: string): string[] =>
providers.filter(
(p) =>
getBuiltinModel(p as never, bareId as never) != null ||
customModels.some((cm) => cm.provider === p && cm.id === bareId),
);

let changed = false;

if (ai?.models) {
const expanded: string[] = [];
for (const id of ai.models) {
if (!isBare(id)) {
expanded.push(id);
continue;
}
changed = true;
for (const p of providersServing(id)) expanded.push(`${p}:${id}`);
}
ai.models = expanded.length > 0 ? [...new Set(expanded)] : undefined;
if (!ai.models) delete ai.models;
}

const qualifyOne = (id: string | undefined): string | undefined => {
if (!isBare(id)) return id;
const [provider] = providersServing(id);
if (!provider) return id; // no configured provider serves it — leave as-is
changed = true;
return `${provider}:${id}`;
};
config.spiritModel = qualifyOne(config.spiritModel);
if (ai) ai.utilityModel = qualifyOne(ai.utilityModel);

if (changed) writeGlobalConfig(config);
}

/** Get the admin-defined custom models (not in pi-ai's built-in catalog). */
export function getCustomModels(): CustomModel[] {
return getAiConfig()?.customModels ?? [];
Expand Down
Loading
Loading