Skip to content

Commit 005e29f

Browse files
committed
feat: add remount command to recreate containers preserving writable layer
Adds a `remount` action that commits the container's current state into its image, removes the old container, then creates a fresh one from the updated image. The new container picks up any new mounts, env vars, or network config from createContainer() while preserving installed packages and configs from the writable layer. Useful when createContainer() gains new volume mounts (like /board) and existing creatures need to pick them up without losing their self-installed tools (supervisord, python packages, etc). - supervisor: remount() method (docker commit + rm + spawnCreature) - orchestrator: POST /api/creatures/:name/remount endpoint - dashboard: remount button with confirmation dialog Made-with: Cursor
1 parent c2ef122 commit 005e29f

3 files changed

Lines changed: 54 additions & 0 deletions

File tree

dashboard/src/components/CreatureDetail.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ export function CreatureDetail() {
7474
<button className="bg-white border border-[#d0d0d0] text-text-secondary px-1.5 py-0.5 rounded text-[11px] cursor-pointer hover:bg-[#f5f5f5] hover:text-text-primary transition-colors" onClick={() => api.creatureAction(name, 'wake')}>wake</button>
7575
<button className="bg-white border border-[#d0d0d0] text-text-secondary px-1.5 py-0.5 rounded text-[11px] cursor-pointer hover:bg-[#f5f5f5] hover:text-text-primary transition-colors disabled:opacity-40 disabled:cursor-not-allowed" disabled={degraded} title={degraded ? 'Orchestrator degraded' : undefined} onClick={() => { api.creatureAction(name, 'restart'); refresh(); }}>restart</button>
7676
<button className="bg-white border border-warn text-warn-light px-1.5 py-0.5 rounded text-[11px] cursor-pointer hover:bg-[#fffbf5] transition-colors disabled:opacity-40 disabled:cursor-not-allowed" disabled={degraded} title={degraded ? 'Orchestrator degraded' : undefined} onClick={() => { api.creatureAction(name, 'rebuild'); refresh(); }}>rebuild</button>
77+
<button className="bg-white border border-[#d0d0d0] text-text-secondary px-1.5 py-0.5 rounded text-[11px] cursor-pointer hover:bg-[#f5f5f5] hover:text-text-primary transition-colors disabled:opacity-40 disabled:cursor-not-allowed" disabled={degraded} title={degraded ? 'Orchestrator degraded' : 'Recreate container preserving installed packages'} onClick={() => {
78+
if (confirm(`Remount "${name}"? Preserves installed packages but restarts the container.`)) {
79+
api.creatureAction(name, 'remount'); refresh();
80+
}
81+
}}>remount</button>
7782
<button className="bg-white border border-warn text-warn-light px-1.5 py-0.5 rounded text-[11px] cursor-pointer hover:bg-[#fffbf5] transition-colors" onClick={() => {
7883
if (confirm(`Archive creature "${name}"? It will be stopped and moved to the archive.`)) {
7984
api.creatureAction(name, 'archive').then(() => { refresh(); selectCreature(null); });

src/host/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,12 @@ export class Orchestrator {
320320
await supervisor.restart();
321321
}
322322

323+
async remountCreature(name: string): Promise<void> {
324+
const supervisor = this.supervisors.get(name);
325+
if (!supervisor) throw new Error(`creature "${name}" is not running`);
326+
await supervisor.remount();
327+
}
328+
323329
async spawnCreature(name: string, _dir: string, purpose?: string, genome = 'dreamer', model?: string): Promise<void> {
324330
console.log(`[orchestrator] spawning "${name}"...`);
325331
const result = await spawnCreature({ name, purpose, genome, model });
@@ -800,6 +806,21 @@ export class Orchestrator {
800806
return;
801807
}
802808

809+
if (action === 'remount' && req.method === 'POST') {
810+
const health = getStatus();
811+
if (health.status !== 'healthy') {
812+
res.writeHead(503, { 'Content-Type': 'application/json' });
813+
res.end(JSON.stringify({ error: 'Orchestrator degraded — cannot remount creatures', dependencies: health.dependencies }));
814+
return;
815+
}
816+
try {
817+
console.log(`[${name}] developer-initiated remount`);
818+
await this.remountCreature(name);
819+
res.writeHead(200); res.end('ok');
820+
} catch (err: any) { res.writeHead(400); res.end(err.message); }
821+
return;
822+
}
823+
803824
if (action === 'wake' && req.method === 'POST') {
804825
try {
805826
const supervisor = this.supervisors.get(name);

src/host/supervisor.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,34 @@ export class CreatureSupervisor {
114114
this.status = 'starting';
115115
await this.spawnCreature();
116116
}
117+
// Recreate container preserving its writable layer (installed packages,
118+
// configs, etc.). Commits the container state into the image, removes the old
119+
// container, then creates a fresh one from the updated image — picking up any
120+
// new mounts, env vars, or network config. Running processes stop but their
121+
// installed artifacts survive.
122+
async remount(): Promise<void> {
123+
this.expectingExit = true;
124+
this.clearTimers();
125+
this.healthyAt = null;
126+
const cname = this.containerName();
127+
128+
console.log(`[${this.name}] remounting: committing container state to image`);
129+
try { execSync(`docker stop ${cname}`, { stdio: 'ignore', timeout: 15_000 }); } catch {}
130+
try {
131+
execSync(`docker commit ${cname} ${cname}`, { stdio: 'ignore', timeout: 60_000 });
132+
} catch (err) {
133+
console.error(`[${this.name}] remount: commit failed — container preserved, aborting`, err);
134+
this.expectingExit = false;
135+
throw err;
136+
}
137+
try { execSync(`docker rm -f ${cname}`, { stdio: 'ignore' }); } catch {}
138+
139+
this.creature = null;
140+
this.currentSHA = getCurrentSHA(this.dir);
141+
this.status = 'starting';
142+
await this.spawnCreature();
143+
}
144+
117145

118146
// Full rebuild: destroys container (writable layer lost). Developer-only.
119147
async rebuild(): Promise<void> {

0 commit comments

Comments
 (0)