fix(daemon): skip legacy drain after discovery failure

If legacy session discovery fails after startup probing already found live work,
do not let the post-discovery sweep reinterpret an empty routing map as drained.
Keep that legacy daemon alive for this app session and let the next startup
re-probe it instead.
This commit is contained in:
Jinwoo-H
2026-05-01 17:34:10 -04:00
parent 588306c032
commit 3df2be65e9
2 changed files with 34 additions and 5 deletions
+25 -4
View File
@@ -11,7 +11,8 @@ type AdapterMock = DaemonPtyAdapter & {
function createAdapter(
label: string,
sessions: string[] = [],
reconcileResult?: { alive: string[]; killed: string[] }
reconcileResult?: { alive: string[]; killed: string[] },
opts: { listProcessesRejects?: boolean } = {}
): AdapterMock {
const writes: { id: string; data: string }[] = []
const dataListeners: ((payload: { id: string; data: string }) => void)[] = []
@@ -22,13 +23,16 @@ function createAdapter(
sessions.push(id)
return { id }
}),
listProcesses: vi.fn(async () =>
sessions.map((id) => ({
listProcesses: vi.fn(async () => {
if (opts.listProcessesRejects) {
throw new Error('discovery failed')
}
return sessions.map((id) => ({
id,
cwd: '',
title: label
}))
),
}),
write: vi.fn((id: string, data: string) => {
writes.push({ id, data })
}),
@@ -218,6 +222,23 @@ describe('DaemonPtyRouter', () => {
expect(onLegacyDrained).toHaveBeenCalledWith(legacy)
})
it('does not drain an adapter when legacy discovery fails', async () => {
const current = createAdapter('current')
const legacy = createAdapter('legacy', ['legacy-session'], undefined, {
listProcessesRejects: true
})
const onLegacyDrained = vi.fn()
const router = new DaemonPtyRouter({ current, legacy: [legacy], onLegacyDrained })
await router.discoverLegacySessions()
router.sweepDrainedLegacyAdapters()
legacy.emitExit('legacy-session', 0)
expect(onLegacyDrained).not.toHaveBeenCalled()
await router.listProcesses()
expect(legacy.listProcesses).toHaveBeenCalledTimes(2)
})
it('merges startup reconciliation and updates route mappings', async () => {
const current = createAdapter('current', [], {
alive: ['current-alive'],
+9 -1
View File
@@ -6,6 +6,7 @@ export class DaemonPtyRouter implements IPtyProvider {
private legacy: DaemonPtyAdapter[]
private sessionAdapters = new Map<string, DaemonPtyAdapter>()
private unsubscribersByAdapter = new Map<DaemonPtyAdapter, (() => void)[]>()
private discoveryFailedAdapters = new Set<DaemonPtyAdapter>()
private onLegacyDrained?: (adapter: DaemonPtyAdapter) => void
private dataListeners: ((payload: { id: string; data: string }) => void)[] = []
private exitListeners: ((payload: { id: string; code: number }) => void)[] = []
@@ -42,10 +43,12 @@ export class DaemonPtyRouter implements IPtyProvider {
for (const adapter of this.legacy) {
try {
const sessions = await adapter.listProcesses()
this.discoveryFailedAdapters.delete(adapter)
for (const session of sessions) {
this.sessionAdapters.set(session.id, adapter)
}
} catch (error) {
this.discoveryFailedAdapters.add(adapter)
console.warn('[daemon] Failed to discover legacy daemon sessions', error)
}
}
@@ -204,10 +207,15 @@ export class DaemonPtyRouter implements IPtyProvider {
private drainLegacyAdapterIfEmpty(adapter: DaemonPtyAdapter): void {
// Why: only previous-version adapters are cleaned up here. The current
// daemon lifecycle must remain byte-identical when its sessions exit.
if (!this.legacy.includes(adapter) || this.hasActiveSessionsOn(adapter)) {
if (
!this.legacy.includes(adapter) ||
this.discoveryFailedAdapters.has(adapter) ||
this.hasActiveSessionsOn(adapter)
) {
return
}
this.legacy = this.legacy.filter((a) => a !== adapter)
this.discoveryFailedAdapters.delete(adapter)
this.unsubscribeAdapter(adapter)
this.onLegacyDrained?.(adapter)
}