mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 00:03:08 +00:00
fix(daemon): self-heal write routing for undiscovered legacy sessions
DaemonPtyRouter fans out reads (onData/hasPty/listProcesses) across every adapter, but routes writes and resizes via a sessionAdapters map that is populated only by the one-shot discoverLegacySessions()/reconcile/spawn boot passes. When a surviving previous-protocol daemon owns a session that boot discovery missed (e.g. a Windows named-pipe/startup-order race, widened by the .perf startup reordering), adapterFor() fell back to `this.current` — so output kept streaming (reads fan out) while every keystroke was silently dispatched to the wrong daemon and dropped. Symptom: adopted terminals stream live output but accept no input after a protocol-bumping update. Make adapterFor() self-heal: on a routing-map miss, ask each adapter whether it owns the pty (synchronous Set lookup), cache the winner, and route there; current-first precedence keeps the safe default. This mirrors the self-healing providerFor() already used by DegradedDaemonPtyProvider. Adds regression tests covering the undiscovered-legacy-session route and the no-owner fallback.
This commit is contained in:
@@ -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'])
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
Reference in New Issue
Block a user