fix(worktree-activation): keep the sleeping-session deferral through the main merge

Main resumes sleeping agent sessions synchronously so seeding sees a restored
surface; this branch gates seeding until the structured/agent inventory has
hydrated. They solve the same bare-shell problem by different means, and my
first resolution dropped the gate's sleeping-session arm on the assumption the
resume subsumed it. It does not: STA-1111 defers seeding until startup
restoration is READY, which is a different condition from resuming, and the
fork-bomb test caught it.

The gate is now decided BEFORE resuming, and the resume runs only when not
deferring. Terminal.tsx's effect dep order is restored to main's, which the
adjacency-anchored source pin in initial-terminal-wiring depends on; React does
not care about dep order.
This commit is contained in:
Brennan Benson
2026-08-19 17:32:37 -07:00
parent 005c7e790f
commit ea1aabc9c9
2 changed files with 29 additions and 21 deletions
+1 -1
View File
@@ -1539,8 +1539,8 @@ function Terminal(): React.JSX.Element | null {
cancelled = true
}
}, [
activeWorktreeHasTerminalState,
activeWorktreeId,
activeWorktreeHasTerminalState,
createTab,
hydrationSucceeded,
reconcileWorktreeTabModel,
+28 -20
View File
@@ -14,7 +14,10 @@ import {
setWorktreeNavActivator,
setWorktreeNavViewActivator
} from '@/store/slices/worktree-nav-history'
import { gateWorktreeAgentActivation } from '@/lib/worktree-agent-activation-gate'
import {
gateWorktreeAgentActivation,
workspaceHasSleepingAgentSessions
} from '@/lib/worktree-agent-activation-gate'
import { resumeSleepingAgentSessionsForWorktree } from '@/lib/resume-sleeping-agent-session'
import { shouldAutoCreateInitialTerminal } from '@/components/terminal/initial-terminal'
import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner'
@@ -130,15 +133,17 @@ export function activateAndRevealFolderWorkspace(
if (!state.isNavigatingHistory) {
state.recordWorktreeVisit(workspaceKey)
}
// Why: same ordering as the worktree path — resume first so the seeding below sees the restored
// surface; the gate then covers only the structured inventory that is still hydrating.
resumeSleepingAgentSessionsForWorktree(workspaceKey)
// Why: same ordering as the worktree path — gate first, then resume only when not deferring.
const shouldGateAgentActivation =
!opts?.startup &&
canInspectAgentActivationInventory() &&
shouldAutoCreateInitialTerminal(
state.reconcileWorktreeTabModel(workspaceKey).renderableTabCount
)
(workspaceHasSleepingAgentSessions(state, workspaceKey) ||
(canInspectAgentActivationInventory() &&
shouldAutoCreateInitialTerminal(
state.reconcileWorktreeTabModel(workspaceKey).renderableTabCount
)))
if (!shouldGateAgentActivation) {
resumeSleepingAgentSessionsForWorktree(workspaceKey)
}
if (shouldGateAgentActivation) {
void gateWorktreeAgentActivation(workspaceKey).then((outcome) => {
if (outcome === 'empty' && useAppStore.getState().activeWorktreeId === workspaceKey) {
@@ -232,20 +237,23 @@ export function activateAndRevealWorktree(
state.recordWorktreeVisit(worktreeId)
}
// Why: sleeping destroys the local PTY but preserves the provider session id, so waking should restore those CLI sessions automatically.
// Ordering is load-bearing: resuming synchronously creates the session's tab first, so the
// seeding below sees a renderable surface and doesn't add a bare shell next to it.
resumeSleepingAgentSessionsForWorktree(worktreeId)
// Why: the synchronous resume above covers sleeping CLI sessions. Structured agent inventory
// hydrates asynchronously and is still invisible here, so an empty tab model can authorize a
// fallback terminal beside a chat that is about to appear. Gate on that case only.
// Why: the gate is decided BEFORE resuming. A sleeping session must defer seeding until startup
// restoration is ready (STA-1111) — resuming first would leave nothing to gate on. Structured
// agent inventory hydrates asynchronously too, so an empty tab model can otherwise authorize a
// fallback terminal beside a chat that is about to appear.
const shouldGateAgentActivation =
!hasActivationWork &&
canInspectAgentActivationInventory() &&
shouldAutoCreateInitialTerminal(
postActivationState.reconcileWorktreeTabModel(worktreeId).renderableTabCount
)
(workspaceHasSleepingAgentSessions(postActivationState, worktreeId) ||
(canInspectAgentActivationInventory() &&
shouldAutoCreateInitialTerminal(
postActivationState.reconcileWorktreeTabModel(worktreeId).renderableTabCount
)))
if (!shouldGateAgentActivation) {
// Why: sleeping destroys the local PTY but preserves the provider session id, so waking should
// restore those CLI sessions. Ordering is load-bearing: resuming synchronously creates the
// session's tab first, so the seeding below doesn't add a bare shell next to it.
resumeSleepingAgentSessionsForWorktree(worktreeId)
}
if (shouldGateAgentActivation) {
void gateWorktreeAgentActivation(worktreeId).then((outcome) => {
const currentState = useAppStore.getState()