diff --git a/src/main/daemon/daemon-pty-router.test.ts b/src/main/daemon/daemon-pty-router.test.ts index 47d44aa807c..3522811b30e 100644 --- a/src/main/daemon/daemon-pty-router.test.ts +++ b/src/main/daemon/daemon-pty-router.test.ts @@ -118,6 +118,34 @@ describe('DaemonPtyRouter', () => { expect(current.write).toHaveBeenCalledWith(fresh.id, 'new\n') }) + it('routes an undiscovered legacy session to its owning daemon, not current', () => { + // Reproduces the frozen-input bug: the old daemon still owns the session + // (hasPty true, output streaming), but the one-shot discoverLegacySessions() + // boot pass never mapped it — so the routing map is empty for this id. + const current = createAdapter('current') + const legacy = createAdapter('legacy', ['orphan-session']) + const router = new DaemonPtyRouter({ current, legacy: [legacy] }) + + // Deliberately NO discoverLegacySessions() / reconcile / spawn call. + router.write('orphan-session', 'input\n') + router.resize('orphan-session', 80, 24) + + expect(legacy.write).toHaveBeenCalledWith('orphan-session', 'input\n') + expect(legacy.resize).toHaveBeenCalledWith('orphan-session', 80, 24) + expect(current.write).not.toHaveBeenCalled() + }) + + it('falls back to the current adapter when no adapter owns the session', () => { + const current = createAdapter('current') + const legacy = createAdapter('legacy') + const router = new DaemonPtyRouter({ current, legacy: [legacy] }) + + router.write('unknown-session', 'input\n') + + expect(current.write).toHaveBeenCalledWith('unknown-session', 'input\n') + expect(legacy.write).not.toHaveBeenCalled() + }) + it('drops a legacy mapping after the routed session exits', async () => { const current = createAdapter('current') const legacy = createAdapter('legacy', ['legacy-session']) diff --git a/src/main/daemon/daemon-pty-router.ts b/src/main/daemon/daemon-pty-router.ts index 1f80f770cd9..a1d6a0344cb 100644 --- a/src/main/daemon/daemon-pty-router.ts +++ b/src/main/daemon/daemon-pty-router.ts @@ -250,7 +250,20 @@ export class DaemonPtyRouter implements IPtyProvider { } private adapterFor(sessionId: string): DaemonPtyAdapter { - return this.sessionAdapters.get(sessionId) ?? this.current + const routed = this.sessionAdapters.get(sessionId) + if (routed) { + return routed + } + // Why: reads fan out across all adapters, but this routing map is filled only + // by one-shot boot discovery. A legacy session it missed had its writes silently + // dropped onto `current`; self-heal by asking who actually owns the pty, then cache. + for (const adapter of this.allAdapters()) { + if (adapter.hasPty(sessionId)) { + this.sessionAdapters.set(sessionId, adapter) + return adapter + } + } + return this.current } private allAdapters(): DaemonPtyAdapter[] {