fix(runtime): join the resize repaint's seed without per-PTY seed state

Replaces the rendererSeedsByPtyId map with a check over existing state. If
an on-data seed was already considered before the subscribe's hydrate, the
subscribe awaits the model's write chain and reads the outcome from the
hydration state, the installed model and the provider-preferred flag.

Claude-Session: lane-host-hydrate
This commit is contained in:
Jinwoo-H
2026-10-06 00:56:33 -04:00
parent 944b57b8d8
commit 67701cf87d
4 changed files with 20 additions and 14 deletions
@@ -130,8 +130,6 @@ export class OrcaRuntimeWithFitOverrideListeners extends OrcaRuntimeWithStopRequ
// Absent → hydration has not been considered yet for this PTY.
// See docs/mobile-prefer-renderer-scrollback.md.
protected headlessHydrationState = new Map<string, 'pending' | 'done'>()
/** The model's renderer seed, resolving true once written; lives as long as the model. */
protected rendererSeedsByPtyId = new Map<string, Promise<boolean>>()
// Why: mobile-fit overrides are keyed by ptyId (not terminal handle) because
// handles can be reissued while the PTY identity is stable. In-memory only —
@@ -110,19 +110,32 @@ export class OrcaRuntimeWithMaybeHydrateHeadlessFromRenderer extends OrcaRuntime
}
}
})
const seed = state.writeChain.then(() => seeded)
this.rendererSeedsByPtyId.set(ptyId, seed)
return seed
return state.writeChain.then(() => seeded)
}
/** Public: a phone fit resizes the PTY, but an idle TUI sends no byte to hydrate on. Resolves
* true once the model holds the pane's screen reflowed onto the PTY's current grid. */
hydrateHeadlessTerminalFromRenderer(ptyId: string): Promise<boolean> {
// Why join: a resize repaint can run the on-data seed first; its result answers this caller too.
const priorHydration = this.headlessHydrationState.get(ptyId)
const seed = this.maybeHydrateHeadlessFromRenderer(ptyId)
if (seed) {
return seed
}
// Why: a resize repaint can run the on-data seed before this call; join its outcome instead.
return priorHydration ? this.readRendererSeedOutcome(ptyId) : Promise.resolve(false)
}
private async readRendererSeedOutcome(ptyId: string): Promise<boolean> {
const state = this.headlessTerminals.get(ptyId)
if (!state) {
return false
}
await state.writeChain
// Why: a failed seed also ends 'done'; treated as seeded, since that model is on the PTY grid.
return (
this.maybeHydrateHeadlessFromRenderer(ptyId) ??
this.rendererSeedsByPtyId.get(ptyId) ??
Promise.resolve(false)
this.headlessHydrationState.get(ptyId) === 'done' &&
this.headlessTerminals.get(ptyId) === state &&
!this.providerSnapshotPreferredPtys.has(ptyId)
)
}
@@ -75,7 +75,6 @@ export class OrcaRuntimeWithSerializeHeadlessTerminalBuffer extends OrcaRuntimeW
protected disposeHeadlessTerminal(ptyId: string): void {
this.headlessHydrationState.delete(ptyId)
this.rendererSeedsByPtyId.delete(ptyId)
const state = this.headlessTerminals.get(ptyId)
if (!state) {
return
@@ -41,10 +41,6 @@ const CLEARED_BY_REAPER_HELPER: Record<string, { helper: string; module: string
agentPromptPermissionSequenceByPtyId: {
helper: 'advancePtyLifecycleGeneration',
module: 'src/main/runtime/orca-runtime-record-agent-prompt-lifecycle-state.ts'
},
rendererSeedsByPtyId: {
helper: 'disposeHeadlessTerminal',
module: 'src/main/runtime/orca-runtime-serialize-headless-terminal-buffer.ts'
}
}