From 24eefda23be2b424c2a891d648d12e36d3f1c32d Mon Sep 17 00:00:00 2001 From: Priyanshu Choudhary <57816400+Priyanchew@users.noreply.github.com> Date: Sat, 25 Apr 2026 11:35:25 +0530 Subject: [PATCH] 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) --- packages/core/src/session-manager.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/core/src/session-manager.ts b/packages/core/src/session-manager.ts index 539f415aa..95010f0e5 100644 --- a/packages/core/src/session-manager.ts +++ b/packages/core/src/session-manager.ts @@ -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();