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
2 changes: 1 addition & 1 deletion dashboard/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export async function fetchGenomes(): Promise<GenomeInfo[]> {
return res.json();
}

export async function creatureAction(name: string, action: 'start' | 'stop' | 'restart' | 'rebuild' | 'wake' | 'archive', method = 'POST'): Promise<void> {
export async function creatureAction(name: string, action: 'start' | 'stop' | 'restart' | 'rebuild' | 'remount' | 'wake' | 'archive', method = 'POST'): Promise<void> {
await fetch(`/api/creatures/${name}/${action}`, { method });
}

Expand Down
29 changes: 21 additions & 8 deletions dashboard/src/components/CreatureDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ export function CreatureDetail() {
const refresh = useStore(s => s.refresh);
const degraded = useStore(s => s.health.status !== 'healthy');
const eventsEndRef = useRef<HTMLDivElement>(null);
const [busy, setBusy] = useState<string | null>(null);

const doAction = async (action: string, needsConfirm?: string) => {
if (needsConfirm && !confirm(needsConfirm)) return;
setBusy(action);
try {
await api.creatureAction(name, action as any);
refresh();
} finally {
setBusy(null);
}
};

useEffect(() => {
if (tab === 'log') {
Expand All @@ -71,14 +83,15 @@ export function CreatureDetail() {
{c?.janeeVersion && <> · <span className="text-text-muted text-[11px]">janee {c.janeeVersion}</span></>}
</div>
<BudgetInfo />
<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>
<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>
<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>
<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={() => {
if (confirm(`Archive creature "${name}"? It will be stopped and moved to the archive.`)) {
api.creatureAction(name, 'archive').then(() => { refresh(); selectCreature(null); });
}
}}>archive</button>
<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={!!busy} onClick={() => doAction('wake')}>{busy === 'wake' ? 'waking...' : 'wake'}</button>
<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 || !!busy} title={degraded ? 'Orchestrator degraded' : undefined} onClick={() => doAction('restart')}>{busy === 'restart' ? 'restarting...' : 'restart'}</button>
<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 || !!busy} title={degraded ? 'Orchestrator degraded' : undefined} onClick={() => doAction('rebuild', `Rebuild "${name}"? This destroys the container writable layer.`)}>{busy === 'rebuild' ? 'rebuilding...' : 'rebuild'}</button>
<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 || !!busy} title={degraded ? 'Orchestrator degraded' : 'Recreate container preserving installed packages'} onClick={() => doAction('remount', `Remount "${name}"? Preserves installed packages but restarts the container.`)}>{busy === 'remount' ? 'remounting...' : 'remount'}</button>
<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={!!busy} onClick={async () => {
if (!confirm(`Archive creature "${name}"? It will be stopped and moved to the archive.`)) return;
setBusy('archive');
try { await api.creatureAction(name, 'archive'); refresh(); selectCreature(null); } finally { setBusy(null); }
}}>{busy === 'archive' ? 'archiving...' : 'archive'}</button>
</div>

{/* Tab bar */}
Expand Down
21 changes: 21 additions & 0 deletions src/host/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,12 @@ export class Orchestrator {
await supervisor.restart();
}

async remountCreature(name: string): Promise<void> {
const supervisor = this.supervisors.get(name);
if (!supervisor) throw new Error(`creature "${name}" is not running`);
await supervisor.remount();
}

async spawnCreature(name: string, _dir: string, purpose?: string, genome = 'dreamer', model?: string): Promise<void> {
console.log(`[orchestrator] spawning "${name}"...`);
const result = await spawnCreature({ name, purpose, genome, model });
Expand Down Expand Up @@ -800,6 +806,21 @@ export class Orchestrator {
return;
}

if (action === 'remount' && req.method === 'POST') {
const health = getStatus();
if (health.status !== 'healthy') {
res.writeHead(503, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Orchestrator degraded — cannot remount creatures', dependencies: health.dependencies }));
return;
}
try {
console.log(`[${name}] developer-initiated remount`);
await this.remountCreature(name);
res.writeHead(200); res.end('ok');
} catch (err: any) { res.writeHead(400); res.end(err.message); }
return;
}

if (action === 'wake' && req.method === 'POST') {
try {
const supervisor = this.supervisors.get(name);
Expand Down
28 changes: 28 additions & 0 deletions src/host/supervisor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,34 @@ export class CreatureSupervisor {
this.status = 'starting';
await this.spawnCreature();
}
// Recreate container preserving its writable layer (installed packages,
// configs, etc.). Commits the container state into the image, removes the old
// container, then creates a fresh one from the updated image — picking up any
// new mounts, env vars, or network config. Running processes stop but their
// installed artifacts survive.
async remount(): Promise<void> {
this.expectingExit = true;
this.clearTimers();
this.healthyAt = null;
const cname = this.containerName();

console.log(`[${this.name}] remounting: committing container state to image`);
try { execSync(`docker stop ${cname}`, { stdio: 'ignore', timeout: 15_000 }); } catch {}
try {
execSync(`docker commit ${cname} ${cname}`, { stdio: 'ignore', timeout: 60_000 });
} catch (err) {
console.error(`[${this.name}] remount: commit failed — container preserved, aborting`, err);
this.expectingExit = false;
throw err;
}
try { execSync(`docker rm -f ${cname}`, { stdio: 'ignore' }); } catch {}

this.creature = null;
this.currentSHA = getCurrentSHA(this.dir);
this.status = 'starting';
await this.spawnCreature();
}


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