mirror of
https://github.com/stablyai/orca.git
synced 2026-09-24 00:02:24 +00:00
fix(daemon): retire idle legacy generations after updates
This commit is contained in:
@@ -5,8 +5,14 @@ import {
|
||||
} from './daemon-launch-paths'
|
||||
import { parseDaemonPidFile } from './daemon-pid-file-parse'
|
||||
import { DaemonPtyAdapter } from './daemon-pty-adapter'
|
||||
import { DaemonClient } from './client'
|
||||
import { getDaemonPidPath, getDaemonSocketPath, getDaemonTokenPath } from './daemon-spawner'
|
||||
import { PREVIOUS_DAEMON_PROTOCOL_VERSIONS } from './types'
|
||||
import {
|
||||
CLEAN_DISCONNECT_PROTOCOL_VERSION,
|
||||
PREVIOUS_DAEMON_PROTOCOL_VERSIONS,
|
||||
type ListSessionsResult,
|
||||
type ShutdownIfIdleResult
|
||||
} from './types'
|
||||
|
||||
function legacyDaemonProcessMayBeAlive(runtimeDir: string, protocolVersion: number): boolean {
|
||||
try {
|
||||
@@ -23,6 +29,40 @@ function legacyDaemonProcessMayBeAlive(runtimeDir: string, protocolVersion: numb
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retire an old daemon that has no live PTYs. Without this, every protocol
|
||||
* generation remains resident after an update even when all of its sessions
|
||||
* have exited (#9138).
|
||||
*/
|
||||
async function retireIdleLegacyDaemon(
|
||||
socketPath: string,
|
||||
tokenPath: string,
|
||||
protocolVersion: number
|
||||
): Promise<boolean> {
|
||||
const client = new DaemonClient({ socketPath, tokenPath, protocolVersion })
|
||||
try {
|
||||
await client.ensureConnectedWithin(1_000)
|
||||
const { sessions } = await client.request<ListSessionsResult>('listSessions', undefined, 1_000)
|
||||
if (sessions.some((session) => session.isAlive)) {
|
||||
return false
|
||||
}
|
||||
if (protocolVersion >= CLEAN_DISCONNECT_PROTOCOL_VERSION) {
|
||||
const result = await client.request<ShutdownIfIdleResult>('shutdownIfIdle', undefined, 1_000)
|
||||
return result.retiring
|
||||
}
|
||||
// Older generations predate shutdownIfIdle; the inventory just proved that
|
||||
// killing this daemon cannot take a live PTY with it.
|
||||
await client.request('shutdown', { killSessions: true }, 1_000)
|
||||
return true
|
||||
} catch {
|
||||
// A failed inventory is not evidence of idleness; preserve the adapter so
|
||||
// its sessions remain adoptable on the next startup.
|
||||
return false
|
||||
} finally {
|
||||
client.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// Why: callers that own an isolated runtime namespace must keep discovery history out of app userData.
|
||||
export async function createLegacyDaemonAdapters(
|
||||
runtimeDir: string,
|
||||
@@ -48,6 +88,9 @@ export async function createLegacyDaemonAdapters(
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (await retireIdleLegacyDaemon(socketPath, tokenPath, protocolVersion)) {
|
||||
continue
|
||||
}
|
||||
// Keep old-protocol PTYs routed to their original daemon during upgrade; legacy adapters never respawn (new code would recreate stale env semantics).
|
||||
// historyPath is still needed for cleanup — without it a later v4 session reusing the same ID could false-restore stale scrollback.bin.
|
||||
adapters.push(
|
||||
|
||||
@@ -147,12 +147,16 @@ export function retireParkedTerminalTab(tabId: string): void {
|
||||
// strong scroll-intent keys here or every closed parked tab leaks one per
|
||||
// leaf for the renderer lifetime.
|
||||
for (const pane of capture.panes) {
|
||||
releaseTerminalScrollIntentKey(pane.leafId)
|
||||
releaseCapturedPaneScrollIntent(pane)
|
||||
}
|
||||
capturedPanesByTabId.delete(tabId)
|
||||
}
|
||||
}
|
||||
|
||||
function releaseCapturedPaneScrollIntent(pane: ParkedTerminalPaneCapture): void {
|
||||
releaseTerminalScrollIntentKey(pane.leafId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously disposes any parked watcher subscribed to these PTYs.
|
||||
* Shutdown transactionally suspends dispatcher sidecars before teardown, then
|
||||
@@ -225,6 +229,11 @@ export function pruneParkedTerminalWatchers(liveWorktreeIds: ReadonlySet<string>
|
||||
}
|
||||
for (const [tabId, capture] of capturedPanesByTabId) {
|
||||
if (!liveWorktreeIds.has(capture.worktreeId)) {
|
||||
for (const pane of capture.panes) {
|
||||
// Worktree removal can bypass closeTab while panes are parked; release
|
||||
// the same strong scroll-intent keys as explicit tab retirement.
|
||||
releaseCapturedPaneScrollIntent(pane)
|
||||
}
|
||||
capturedPanesByTabId.delete(tabId)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user