fix(core): restore must rewrite statePayload.runtime.handle, not just top-level

Restore was writing the freshly-spawned runtime handle to the top-level
metadata `runtimeHandle` key, but the canonical lifecycle parser prefers
`statePayload.runtime.handle` and falls back to the top-level only when
statePayload is missing. The next lifecycle tick read the stale handle
from statePayload and rewrote both keys from it, silently undoing the
restore's update.

Symptom: a session restored after AO restart kept the old PID in
metadata. Lifecycle probe found that PID dead (it was from a previous
boot) and the dashboard rendered the orchestrator as exited/killed even
though a new process was actually running.

Fix: rebuild the canonical lifecycle with the new handle via
buildUpdatedLifecycle() and persist via lifecycleMetadataUpdates() so
statePayload and runtimeHandle stay in sync. Mirrors the pattern used
in kill/spawn paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Priyanshu Choudhary 2026-04-25 11:35:25 +05:30
parent d16d3c3f4d
commit 24eefda23b
1 changed files with 14 additions and 2 deletions

View File

@ -2876,11 +2876,23 @@ export function createSessionManager(deps: SessionManagerDeps): OpenCodeSessionM
},
});
// 9. Update metadata — merge updates, preserving existing fields
// 9. Update metadata — merge updates, preserving existing fields.
//
// We must update both the top-level `runtimeHandle` AND the nested
// `runtime.handle` inside `statePayload`. The lifecycle parser prefers
// the statePayload over top-level keys, so writing only the top-level
// key gets silently clobbered by the next lifecycle tick (which reads
// statePayload and rewrites the top-level field from it). Rebuilding
// the canonical lifecycle with the fresh handle keeps both in sync.
const now = new Date().toISOString();
const updatedLifecycle = buildUpdatedLifecycle(sessionId, raw, (lifecycle) => {
lifecycle.runtime.handle = handle;
lifecycle.runtime.tmuxName = tmuxName ?? lifecycle.runtime.tmuxName;
lifecycle.runtime.lastObservedAt = now;
});
updateMetadata(sessionsDir, sessionId, {
...lifecycleMetadataUpdates(raw, updatedLifecycle),
status: "spawning",
runtimeHandle: JSON.stringify(handle),
restoredAt: now,
});
invalidateCache();