Support live toggle of agent status hooks with WSL relay gating (#13361)
* fix(agent-hooks): gate WSL relay reattach on agentStatusHooksEnabled Spawn only ensures the guest relay distro when agent status hooks are enabled, but reattach called ensureForDistro unconditionally — so a disabled setting reinstalled guest hooks on every local WSL reattach. Pass the same isAgentStatusHooksEnabled gate through all three reattach call sites as a required argument so a new site cannot skip it. Co-authored-by: Orca <help@stably.ai> * Gate WSL relay at manager level for live toggle support - Move agentStatusHooksEnabled check from reattach call sites to centralized isWslHookRelayAllowed gate - Add non-permanent dispose mode so manager can revive relays when setting is re-enabled - Watch setting changes and dispose live relays when agent status hooks are disabled mid-session * Restore WSL relays when re-enabling agent status hooks Extract guest install logic to `wsl-hook-relay-guest-install.ts` for modularity and add `resumeStoppedRelays()` to restart relays when hooks are re-enabled. Track distros stopped during a hooks-off teardown, but skip resuming those the user has shut down (which would unwantedly boot a stopped distro). Strengthen the disposed check with state identity to prevent respawning untracked relays. Abandon in-flight launches when hooks are switched off so no relay exists after opting out. --------- Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
d3cb02f6a9
commit
2dc172f666
|
|
@ -364,8 +364,9 @@ async function main() {
|
|||
|
||||
const { runtime, getController } = createRuntimeStub()
|
||||
// Why hooks off: fresh WSL spawn still runs buildPtyHostEnv, which calls ensureForDistro when
|
||||
// hooks are on. This bench isolates the reattach call site (ensureWslHookRelayForReattach),
|
||||
// which does not gate on agentStatusHooksEnabled.
|
||||
// hooks are on. This bench isolates the reattach call site (ensureWslHookRelayForReattach);
|
||||
// the manager's own hooks gate lives behind the ensureForDistro patch above, so it stays out
|
||||
// of the measurement — `manager` below resolves its own (enabled) managed-hook settings.
|
||||
ptyIpc.registerPtyHandlers(
|
||||
createRendererWindowStub(),
|
||||
runtime,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ type RemoveOptions = {
|
|||
}
|
||||
|
||||
export function isAgentStatusHooksEnabled(
|
||||
settings: Pick<GlobalSettings, 'agentStatusHooksEnabled'> | null | undefined
|
||||
settings: Partial<Pick<GlobalSettings, 'agentStatusHooksEnabled'>> | null | undefined
|
||||
): boolean {
|
||||
return settings?.agentStatusHooksEnabled !== false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import type { GlobalSettings } from '../../shared/types'
|
|||
import type { TuiAgentDetectionCommand } from '../ipc/tui-agent-detection-commands'
|
||||
|
||||
export type ManagedHookDetectionSettings = Partial<
|
||||
Pick<GlobalSettings, 'agentCmdOverrides' | 'disabledTuiAgents'>
|
||||
Pick<GlobalSettings, 'agentCmdOverrides' | 'disabledTuiAgents' | 'agentStatusHooksEnabled'>
|
||||
> | null
|
||||
|
||||
export function buildManagedHookDetectionCommands(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { createHash } from 'node:crypto'
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
import { isAgentStatusHooksEnabled } from './managed-agent-hook-controls'
|
||||
import { agentHookServer } from './server'
|
||||
import type { ManagedHookDetectionSettings } from './managed-hook-detection-commands'
|
||||
import { installRemoteManagedAgentHooks } from './remote-managed-hook-installers'
|
||||
|
|
@ -67,6 +68,16 @@ export type WslHookRelayManagerDeps = {
|
|||
transientRetryDelayMs: number
|
||||
}
|
||||
|
||||
/** Every relay start — spawn, PTY reattach, crash recovery — funnels through this gate,
|
||||
* so the user's agent-status-hooks switch is read live instead of at each call site. */
|
||||
export function isWslHookRelayAllowed(deps: WslHookRelayManagerDeps): boolean {
|
||||
return (
|
||||
deps.platform() === 'win32' &&
|
||||
deps.remoteHooksEnabled() &&
|
||||
isAgentStatusHooksEnabled(deps.managedHookSettings())
|
||||
)
|
||||
}
|
||||
|
||||
export const defaultWslHookRelayDeps: WslHookRelayManagerDeps = {
|
||||
platform: () => process.platform,
|
||||
remoteHooksEnabled: () => isRemoteAgentHooksEnabled(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
// The install pass a connected WSL relay runs inside its guest: the managed
|
||||
// hook installers, the OpenCode plugin overlay, and the interval policy that
|
||||
// decides when a still-running relay may install again. Kept out of the
|
||||
// manager so that file stays about relay lifecycle.
|
||||
import type { ManagedHookDetectionSettings } from './managed-hook-detection-commands'
|
||||
import type { installRemoteManagedAgentHooks } from './remote-managed-hook-installers'
|
||||
import { requestGuestOpenCodeOverlayDir } from './wsl-guest-plugin-install'
|
||||
import { installWslGuestHooks } from './wsl-hook-fs-adapter'
|
||||
import { REINSTALL_MIN_INTERVAL_MS } from './wsl-hook-relay-deps'
|
||||
import type { SshChannelMultiplexer } from '../ssh/ssh-channel-multiplexer'
|
||||
import type { PluginSources } from '../../relay/plugin-overlay'
|
||||
|
||||
/** Structural slice of WslHookRelayManagerDeps — only what an install pass uses. */
|
||||
type GuestInstallDeps = {
|
||||
installHooks: typeof installRemoteManagedAgentHooks
|
||||
managedHookSettings: () => ManagedHookDetectionSettings
|
||||
pluginSources: () => PluginSources
|
||||
warn: (message: string) => void
|
||||
}
|
||||
|
||||
/** Structural slice of the manager's DistroState this pass reads and writes. */
|
||||
type GuestInstallState = {
|
||||
distro: string
|
||||
mux?: SshChannelMultiplexer
|
||||
guestHome?: string
|
||||
opencodeOverlayDir?: string
|
||||
lastInstallAt?: number
|
||||
}
|
||||
|
||||
export async function runWslRelayGuestInstall(
|
||||
deps: GuestInstallDeps,
|
||||
state: GuestInstallState,
|
||||
mux: SshChannelMultiplexer,
|
||||
guestHome: string
|
||||
): Promise<void> {
|
||||
state.lastInstallAt = Date.now()
|
||||
await installWslGuestHooks({
|
||||
mux,
|
||||
guestHome,
|
||||
distro: state.distro,
|
||||
installHooks: deps.installHooks,
|
||||
settings: deps.managedHookSettings(),
|
||||
warn: deps.warn
|
||||
})
|
||||
// Why: ship OpenCode's status plugin and record the guest overlay dir the
|
||||
// PTY env points OPENCODE_CONFIG_DIR at; identity-guarded against teardown.
|
||||
const overlay = await requestGuestOpenCodeOverlayDir(mux, deps, state.distro)
|
||||
if (state.mux === mux && overlay.kind !== 'unavailable') {
|
||||
// Clearing on 'none' matters: a rebuild that failed after wiping leaves the dir
|
||||
// present but plugin-less, and advertising it would hide the user's own config.
|
||||
state.opencodeOverlayDir = overlay.kind === 'dir' ? overlay.dir : undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** Rate-limited repeat of the (byte-equality idempotent) install pass on a live relay. */
|
||||
export async function maybeRerunWslRelayGuestInstall(
|
||||
deps: GuestInstallDeps,
|
||||
state: GuestInstallState
|
||||
): Promise<void> {
|
||||
const mux = state.mux
|
||||
const guestHome = state.guestHome
|
||||
if (
|
||||
!mux ||
|
||||
!guestHome ||
|
||||
mux.isDisposed() ||
|
||||
Date.now() - (state.lastInstallAt ?? 0) < REINSTALL_MIN_INTERVAL_MS
|
||||
) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
// Why: the pass also re-ships the plugin source, so a mid-session Orca upgrade refreshes it.
|
||||
await runWslRelayGuestInstall(deps, state, mux, guestHome)
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err)
|
||||
deps.warn(`[agent-hooks] WSL hook reinstall for '${state.distro}' failed: ${detail}`)
|
||||
}
|
||||
}
|
||||
|
|
@ -264,6 +264,11 @@ export async function launchWslRelayWithInstall(options: {
|
|||
await options.connect(transport, child)
|
||||
return
|
||||
} catch (err) {
|
||||
// Why before the failure triage: once disposed, the guest install and retries below are
|
||||
// work the caller no longer wants — the "failure" is usually our own teardown kill.
|
||||
if (options.isDisposed()) {
|
||||
return
|
||||
}
|
||||
const failure = (err as { startup?: WslRelayStartupFailure }).startup
|
||||
if (!failure) {
|
||||
throw err
|
||||
|
|
|
|||
|
|
@ -374,14 +374,92 @@ describe('WslHookRelayManager', () => {
|
|||
manager.disposeAll()
|
||||
})
|
||||
|
||||
it('is inert off-Windows and when remote hooks are disabled', async () => {
|
||||
it('is inert off-Windows, when remote hooks are disabled, and when agent status hooks are off', async () => {
|
||||
const offPlatform = createManager({ platform: () => 'darwin' })
|
||||
offPlatform.manager.ensureForDistro('Ubuntu')
|
||||
const disabled = createManager({ remoteHooksEnabled: () => false })
|
||||
disabled.manager.ensureForDistro('Ubuntu')
|
||||
const hooksOff = createManager({
|
||||
managedHookSettings: () => ({ agentStatusHooksEnabled: false })
|
||||
})
|
||||
hooksOff.manager.ensureForDistro('Ubuntu')
|
||||
await new Promise((resolve) => setTimeout(resolve, 20))
|
||||
expect(offPlatform.deps.spawnRelay).not.toHaveBeenCalled()
|
||||
expect(disabled.deps.spawnRelay).not.toHaveBeenCalled()
|
||||
expect(hooksOff.deps.spawnRelay).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('stops live relays and refuses to revive them once agent status hooks are switched off', async () => {
|
||||
const settings = { agentStatusHooksEnabled: true }
|
||||
const { manager, deps } = createManager({ managedHookSettings: () => settings })
|
||||
manager.ensureForDistro('Ubuntu')
|
||||
await vi.waitFor(() => expect(deps.installHooks).toHaveBeenCalledTimes(1))
|
||||
|
||||
settings.agentStatusHooksEnabled = false
|
||||
manager.disposeAll({ permanent: false })
|
||||
// Reattach and crash recovery both re-enter ensureForDistro; neither may reinstall guest hooks now.
|
||||
manager.ensureForDistro('Ubuntu')
|
||||
await new Promise((resolve) => setTimeout(resolve, 20))
|
||||
|
||||
expect(deps.spawnRelay).toHaveBeenCalledTimes(1)
|
||||
expect(deps.installHooks).toHaveBeenCalledTimes(1)
|
||||
expect(manager.getGuestEndpointFilePath('Ubuntu')).toBeNull()
|
||||
|
||||
// Re-enabling puts the relay back without waiting for the next WSL spawn.
|
||||
settings.agentStatusHooksEnabled = true
|
||||
manager.resumeStoppedRelays()
|
||||
await vi.waitFor(() => expect(deps.spawnRelay).toHaveBeenCalledTimes(2))
|
||||
manager.disposeAll()
|
||||
})
|
||||
|
||||
it('does not resume a relay whose distro the user shut down while hooks were off', async () => {
|
||||
const settings = { agentStatusHooksEnabled: true }
|
||||
const isDistroRunning = vi.fn(async () => true)
|
||||
const { manager, deps } = createManager({
|
||||
isDistroRunning,
|
||||
managedHookSettings: () => settings
|
||||
})
|
||||
manager.ensureForDistro('Ubuntu')
|
||||
await vi.waitFor(() => expect(deps.spawnRelay).toHaveBeenCalledTimes(1))
|
||||
|
||||
settings.agentStatusHooksEnabled = false
|
||||
manager.disposeAll({ permanent: false })
|
||||
settings.agentStatusHooksEnabled = true
|
||||
// Why: resuming through `wsl -d` would boot the VM the user shut down, and no agent inside it
|
||||
// is waiting on status — the next WSL terminal re-ensures anyway.
|
||||
isDistroRunning.mockResolvedValue(false)
|
||||
manager.resumeStoppedRelays()
|
||||
await new Promise((resolve) => setTimeout(resolve, 20))
|
||||
|
||||
expect(deps.spawnRelay).toHaveBeenCalledTimes(1)
|
||||
// A second resume must not retry a distro already consumed by the first.
|
||||
isDistroRunning.mockResolvedValue(true)
|
||||
manager.resumeStoppedRelays()
|
||||
await new Promise((resolve) => setTimeout(resolve, 20))
|
||||
expect(deps.spawnRelay).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('abandons a launch that was still in flight when hooks were switched off', async () => {
|
||||
let failSentinel: ((error: unknown) => void) | undefined
|
||||
const { manager, deps } = createManager({
|
||||
waitForSentinel: vi.fn(
|
||||
() =>
|
||||
new Promise<MultiplexerTransport>((_resolve, reject) => {
|
||||
failSentinel = reject
|
||||
})
|
||||
)
|
||||
})
|
||||
manager.ensureForDistro('Ubuntu')
|
||||
await vi.waitFor(() => expect(deps.spawnRelay).toHaveBeenCalledTimes(1))
|
||||
|
||||
manager.disposeAll({ permanent: false })
|
||||
// The teardown's child kill reaches the in-flight launch as a startup failure; its retry and
|
||||
// guest-install paths must not run, or the user would get an untracked relay after opting out.
|
||||
failSentinel?.(startupError(1))
|
||||
await new Promise((resolve) => setTimeout(resolve, 20))
|
||||
|
||||
expect(deps.spawnRelay).toHaveBeenCalledTimes(1)
|
||||
expect(deps.runInstall).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('requires WSL fs-bridge home coordinates before exposing an endpoint path', () => {
|
||||
|
|
|
|||
|
|
@ -3,14 +3,17 @@
|
|||
// spawn, forwarding envelopes into ingestRemote and installing guest hooks.
|
||||
import type { ChildProcessWithoutNullStreams } from 'node:child_process'
|
||||
|
||||
import { installWslGuestHooks } from './wsl-hook-fs-adapter'
|
||||
import {
|
||||
runWslRelayGuestInstall,
|
||||
maybeRerunWslRelayGuestInstall
|
||||
} from './wsl-hook-relay-guest-install'
|
||||
import { buildWslRelaySpawnEnv, launchWslRelayWithInstall } from './wsl-hook-relay-launch'
|
||||
import {
|
||||
defaultWslHookRelayDeps,
|
||||
isWslHookRelayAllowed,
|
||||
FAILURE_COOLDOWN_BASE_MS,
|
||||
FAILURE_COOLDOWN_MAX_MS,
|
||||
NO_NODE_COOLDOWN_MS,
|
||||
REINSTALL_MIN_INTERVAL_MS,
|
||||
REINSTALL_ONE_SHOT_DELAY_MS,
|
||||
RUNNING_TEARDOWN_COOLDOWN_MS,
|
||||
STABLE_UPTIME_MS,
|
||||
|
|
@ -19,7 +22,6 @@ import {
|
|||
import { wireWslRelayLink } from './wsl-hook-relay-link'
|
||||
import { WslRelayRecovery } from './wsl-hook-relay-recovery'
|
||||
import { wslHookRelayStateKey } from './wsl-hook-relay-state-key'
|
||||
import { requestGuestOpenCodeOverlayDir } from './wsl-guest-plugin-install'
|
||||
import { SshChannelMultiplexer, type MultiplexerTransport } from '../ssh/ssh-channel-multiplexer'
|
||||
import { AGENT_HOOK_REQUEST_REPLAY_METHOD } from '../../shared/agent-hook-relay'
|
||||
import {
|
||||
|
|
@ -49,6 +51,8 @@ export class WslHookRelayManager {
|
|||
private deps: WslHookRelayManagerDeps
|
||||
private recovery: WslRelayRecovery
|
||||
private states = new Map<string, DistroState>()
|
||||
/** Distros a hooks-off teardown stopped, so re-enabling can put them back. */
|
||||
private stoppedByHooksOff = new Set<string>()
|
||||
private defaultDistro: string | null = null
|
||||
private disposed = false
|
||||
private warnedBundleMissing = false
|
||||
|
|
@ -78,13 +82,12 @@ export class WslHookRelayManager {
|
|||
|
||||
/** Fire-and-forget from every WSL PTY spawn-env build; errors breadcrumb. */
|
||||
ensureForDistro(distro: string | null): void {
|
||||
if (this.disposed || this.deps.platform() !== 'win32' || !this.deps.remoteHooksEnabled()) {
|
||||
if (this.disposed || !isWslHookRelayAllowed(this.deps)) {
|
||||
return
|
||||
}
|
||||
void this.ensureInternal(distro).catch((err) => {
|
||||
this.deps.warn(
|
||||
`[agent-hooks] WSL hook relay ensure failed: ${err instanceof Error ? err.message : String(err)}`
|
||||
)
|
||||
const detail = err instanceof Error ? err.message : String(err)
|
||||
this.deps.warn(`[agent-hooks] WSL hook relay ensure failed: ${detail}`)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -106,16 +109,38 @@ export class WslHookRelayManager {
|
|||
return this.stateFor(distro)?.opencodeOverlayDir ?? null
|
||||
}
|
||||
|
||||
disposeAll(): void {
|
||||
this.disposed = true
|
||||
/** Kills every live relay. Non-permanent (hooks switched off mid-session) leaves the
|
||||
* manager reusable, so re-enabling hooks can start relays again without an app restart. */
|
||||
disposeAll({ permanent = true }: { permanent?: boolean } = {}): void {
|
||||
this.disposed ||= permanent
|
||||
for (const state of this.states.values()) {
|
||||
this.recovery.clearTimers(state)
|
||||
state.mux?.dispose()
|
||||
state.child?.kill()
|
||||
if (!permanent) {
|
||||
this.stoppedByHooksOff.add(state.distro)
|
||||
}
|
||||
}
|
||||
this.states.clear()
|
||||
}
|
||||
|
||||
/** Restarts what a hooks-off teardown stopped. Skips distros the user has since shut
|
||||
* down: `wsl -d` BOOTS a stopped distro, and nothing in it is waiting on status. */
|
||||
resumeStoppedRelays(): void {
|
||||
const distros = [...this.stoppedByHooksOff]
|
||||
this.stoppedByHooksOff.clear()
|
||||
for (const distro of distros) {
|
||||
void this.deps
|
||||
.isDistroRunning(distro)
|
||||
.then((running) => {
|
||||
if (running) {
|
||||
this.ensureForDistro(distro)
|
||||
}
|
||||
})
|
||||
.catch(() => undefined)
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureInternal(requestedDistro: string | null): Promise<void> {
|
||||
const distro = requestedDistro ?? (await this.resolveDefaultDistro())
|
||||
if (!distro || this.disposed) {
|
||||
|
|
@ -125,7 +150,7 @@ export class WslHookRelayManager {
|
|||
const existing = this.states.get(key)
|
||||
if (existing) {
|
||||
if (existing.phase === 'running') {
|
||||
void this.maybeReinstallHooks(existing)
|
||||
void maybeRerunWslRelayGuestInstall(this.deps, existing)
|
||||
return
|
||||
}
|
||||
if (existing.phase !== 'failed' || Date.now() < existing.cooldownUntil) {
|
||||
|
|
@ -172,7 +197,9 @@ export class WslHookRelayManager {
|
|||
bundleJsPath: bundle.jsPath,
|
||||
version: bundle.version,
|
||||
io: this.deps,
|
||||
isDisposed: () => this.disposed,
|
||||
// Why the identity half: a hooks-off teardown drops this state and kills its child, but
|
||||
// that kill reads as a startup failure and the retry loop would respawn an untracked relay.
|
||||
isDisposed: () => this.disposed || this.states.get(key) !== state,
|
||||
onChild: (child) => {
|
||||
state.child = child
|
||||
},
|
||||
|
|
@ -252,7 +279,7 @@ export class WslHookRelayManager {
|
|||
}
|
||||
state.guestHome = homeResult.home
|
||||
state.guestEndpointFilePath = wslHookRelayEndpointFilePath(homeResult.home, instanceKey)
|
||||
await this.runInstallers(state, mux, homeResult.home)
|
||||
await runWslRelayGuestInstall(this.deps, state, mux, homeResult.home)
|
||||
|
||||
if (state.phase === 'failed' || state.mux !== mux) {
|
||||
// Child died while installing — already recorded; don't revive.
|
||||
|
|
@ -263,58 +290,13 @@ export class WslHookRelayManager {
|
|||
// Why: one-shot catch-up so a single-spawn session (no later ensure)
|
||||
// still writes Codex's deferred trust after the launch path seeds config.toml.
|
||||
this.recovery.scheduleOneShotReinstall(state, REINSTALL_ONE_SHOT_DELAY_MS, () => {
|
||||
void this.maybeReinstallHooks(state)
|
||||
void maybeRerunWslRelayGuestInstall(this.deps, state)
|
||||
})
|
||||
void mux.request(AGENT_HOOK_REQUEST_REPLAY_METHOD).catch(() => {
|
||||
// Fresh relays have nothing to replay; tolerate.
|
||||
})
|
||||
}
|
||||
|
||||
private async runInstallers(
|
||||
state: DistroState,
|
||||
mux: SshChannelMultiplexer,
|
||||
guestHome: string
|
||||
): Promise<void> {
|
||||
state.lastInstallAt = Date.now()
|
||||
await installWslGuestHooks({
|
||||
mux,
|
||||
guestHome,
|
||||
distro: state.distro,
|
||||
installHooks: this.deps.installHooks,
|
||||
settings: this.deps.managedHookSettings(),
|
||||
warn: this.deps.warn
|
||||
})
|
||||
// Why: ship OpenCode's status plugin and record the guest overlay dir the
|
||||
// PTY env points OPENCODE_CONFIG_DIR at; identity-guarded against teardown.
|
||||
const overlay = await requestGuestOpenCodeOverlayDir(mux, this.deps, state.distro)
|
||||
if (state.mux === mux && overlay.kind !== 'unavailable') {
|
||||
// Clearing on 'none' matters: a rebuild that failed after wiping leaves the dir
|
||||
// present but plugin-less, and advertising it would hide the user's own config.
|
||||
state.opencodeOverlayDir = overlay.kind === 'dir' ? overlay.dir : undefined
|
||||
}
|
||||
}
|
||||
|
||||
private async maybeReinstallHooks(state: DistroState): Promise<void> {
|
||||
const mux = state.mux
|
||||
const guestHome = state.guestHome
|
||||
if (
|
||||
!mux ||
|
||||
!guestHome ||
|
||||
mux.isDisposed() ||
|
||||
Date.now() - (state.lastInstallAt ?? 0) < REINSTALL_MIN_INTERVAL_MS
|
||||
) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
// Why: runInstallers also re-ships the plugin source so a mid-session Orca upgrade refreshes it.
|
||||
await this.runInstallers(state, mux, guestHome)
|
||||
} catch (err) {
|
||||
this.deps.warn(
|
||||
`[agent-hooks] WSL hook reinstall for '${state.distro}' failed: ${err instanceof Error ? err.message : String(err)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Records + breadcrumbs the failure and always arms the restart timer —
|
||||
* one failed relaunch must not end self-recovery; the timer's
|
||||
* distro-running probe keeps this from booting stopped distros. */
|
||||
|
|
|
|||
|
|
@ -2136,6 +2136,16 @@ void app.whenReady().then(async () => {
|
|||
// Why: Store is the mutation authority for all settings writes, so every macOS toggle updates the native item live.
|
||||
syncMacMenuBarIcon(settings.showMenuBarIcon !== false)
|
||||
}
|
||||
if ('agentStatusHooksEnabled' in updates) {
|
||||
// Why both directions: the ensure gate only blocks NEW relays, so off must stop the running
|
||||
// guest process and timers, and on must restart them — otherwise open WSL panes report no
|
||||
// status until their next spawn.
|
||||
if (isAgentStatusHooksEnabled(settings)) {
|
||||
wslHookRelayManager.resumeStoppedRelays()
|
||||
} else {
|
||||
wslHookRelayManager.disposeAll({ permanent: false })
|
||||
}
|
||||
}
|
||||
})
|
||||
// Why: run before ClaudeRuntimeAuthService's constructor sync — a surviving daemon Claude CLI holds the single-use refresh token; early refresh rotates it out mid-session.
|
||||
attachClaudeLivePtyPersistence(store)
|
||||
|
|
|
|||
|
|
@ -17124,6 +17124,33 @@ describe('registerPtyHandlers', () => {
|
|||
])
|
||||
})
|
||||
|
||||
it('refreshes the WSL hook relay for the distro a reattached pane already owns', async () => {
|
||||
// Why here and not only in the helper's unit test: nothing else catches pty.ts dropping the
|
||||
// reattach call — the manager owns the hooks/platform gating this spy stands in for.
|
||||
const ensureForDistro = vi
|
||||
.spyOn(wslHookRelayManager, 'ensureForDistro')
|
||||
.mockImplementation(() => {})
|
||||
setLocalPtyProvider({
|
||||
spawn: vi.fn(async () => ({ id: 'pty-wsl', isReattach: true, wslDistro: 'Ubuntu-24.04' })),
|
||||
write: vi.fn(),
|
||||
resize: vi.fn(),
|
||||
kill: vi.fn(),
|
||||
shutdown: vi.fn(),
|
||||
onData: vi.fn(() => vi.fn()),
|
||||
onExit: vi.fn(() => vi.fn()),
|
||||
listProcesses: vi.fn(async () => []),
|
||||
getForegroundProcess: vi.fn(async () => null)
|
||||
} as never)
|
||||
registerPtyHandlers(mainWindow as never)
|
||||
|
||||
try {
|
||||
await handlers.get('pty:spawn')!(null, { cols: 80, rows: 24, sessionId: 'pty-wsl' })
|
||||
expect(ensureForDistro).toHaveBeenCalledWith('Ubuntu-24.04')
|
||||
} finally {
|
||||
ensureForDistro.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
posixOnlyIt(
|
||||
'does not guess route provenance for a pane-local shell startup CODEX_HOME',
|
||||
async () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue