fix(runtime): drop stale local agent rows from worktree.ps after tab close

attachAgentRowsToSummaries attached every hydrated hook row by worktreeId
with no check that the pane/tab still exists, so agents from closed tabs
(last-status.json hydrates for days) kept showing on mobile as current
activity. Local rows now require the tab in a session/runtime graph or a
connected PTY; remote rows are exempt since their tabs may only exist on
the remote host.

Fixes #6072
This commit is contained in:
Brennan Benson
2026-07-29 18:32:21 -07:00
parent f4e46383df
commit 8efda0bbb2
2 changed files with 196 additions and 4 deletions
+147
View File
@@ -31073,6 +31073,21 @@ describe('OrcaRuntimeService', () => {
}
]
})
// Why: local rows only project while their tab exists (#6072); freshness
// is what varies here, so keep the tab present in the runtime graph.
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-1',
worktreeId: TEST_WORKTREE_ID,
title: 'Codex',
activeLeafId: '33333333-3333-4333-8333-333333333333',
layout: null
}
],
leaves: []
})
const { worktrees } = await runtime.getWorktreePs()
@@ -31083,6 +31098,138 @@ describe('OrcaRuntimeService', () => {
}
)
it('drops a hydrated done hook row after its local tab is closed', async () => {
// Why (#6072): last-status.json hydrates hook rows for days; a closed tab's
// agent must not resurface on mobile as current worktree activity.
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession({
...getDefaultWorkspaceSession(),
tabsByWorktree: {}
})
const now = Date.now()
const runtime = new OrcaRuntimeService(runtimeStore as never, undefined, {
getAgentStatusSnapshot: () => [
{
paneKey: 'closed-tab:66666666-6666-4666-8666-666666666666',
worktreeId: TEST_WORKTREE_ID,
tabId: 'closed-tab',
state: 'done',
prompt: 'refactor the parser',
agentType: 'claude',
connectionId: null,
receivedAt: now,
stateStartedAt: now - 60_000
}
]
})
const { worktrees } = await runtime.getWorktreePs()
const summary = worktrees.find((worktree) => worktree.worktreeId === TEST_WORKTREE_ID)
expect(summary).toMatchObject({
liveTerminalCount: 0,
hasHostSidebarActivity: false,
status: 'inactive',
agents: []
})
})
it('keeps a local hook row while a connected PTY still backs its pane', async () => {
// Why: daemon-held terminals stay live across renderer graph gaps even when
// no session tab records them; a connected PTY is proof the pane exists.
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession({
...getDefaultWorkspaceSession(),
tabsByWorktree: {}
})
const paneKey = 'daemon-tab:77777777-7777-4777-8777-777777777777'
const now = Date.now()
const runtime = new OrcaRuntimeService(runtimeStore as never, undefined, {
getAgentStatusSnapshot: () => [
{
paneKey,
worktreeId: TEST_WORKTREE_ID,
tabId: 'daemon-tab',
state: 'working',
prompt: 'long-running daemon agent',
agentType: 'codex',
connectionId: null,
receivedAt: now,
stateStartedAt: now - 100
}
]
})
runtime['recordPtyWorktree']('daemon-pty', TEST_WORKTREE_ID, {
connected: true,
tabId: 'daemon-tab',
paneKey
})
const { worktrees } = await runtime.getWorktreePs()
const summary = worktrees.find((worktree) => worktree.worktreeId === TEST_WORKTREE_ID)
expect(summary?.agents).toEqual([
expect.objectContaining({ paneKey, state: 'working', prompt: 'long-running daemon agent' })
])
expect(summary).toMatchObject({ hasHostSidebarActivity: true, status: 'working' })
})
it('keeps remote hook rows whose tabs are only tracked on the remote host', async () => {
// Why: the local session partition for an SSH host can be empty while the
// remote host owns the terminals; absence there is not proof of a close.
const remoteRepo = {
id: 'repo-ssh-6072',
path: '/home/me/project',
displayName: 'remote-vm',
badgeColor: 'blue',
addedAt: 1,
connectionId: 'ssh-6072'
}
const remoteWorktree = {
path: '/home/me/project/.worktrees/feature-agents',
head: 'def',
branch: 'refs/heads/feature/agents',
isBare: false,
isMainWorktree: false
}
const remoteWorktreeId = `${remoteRepo.id}::${remoteWorktree.path}`
const metaById: Record<string, WorktreeMeta> = {
[remoteWorktreeId]: makeWorktreeMeta({ displayName: 'Remote agents' })
}
const runtimeStore = {
...store,
getRepos: () => [remoteRepo],
getRepo: (id: string) => (id === remoteRepo.id ? remoteRepo : undefined),
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
getWorkspaceSession: () => getDefaultWorkspaceSession()
}
registerSshGitProvider('ssh-6072', {
listWorktrees: vi.fn().mockResolvedValue([remoteWorktree])
} as never)
const now = Date.now()
const runtime = new OrcaRuntimeService(runtimeStore as never, undefined, {
getAgentStatusSnapshot: () => [
{
paneKey: 'remote-tab:88888888-8888-4888-8888-888888888888',
worktreeId: remoteWorktreeId,
tabId: 'remote-tab',
state: 'working',
prompt: 'remote agent without local tab records',
agentType: 'codex',
connectionId: 'ssh-6072',
receivedAt: now,
stateStartedAt: now - 100
}
]
})
const { worktrees } = await runtime.getWorktreePs()
const summary = worktrees.find((worktree) => worktree.worktreeId === remoteWorktreeId)
expect(summary?.agents).toEqual([
expect.objectContaining({ prompt: 'remote agent without local tab records' })
])
})
it('marks the desktop-active worktree as isActive', async () => {
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(
makeWorkspaceSessionWithHeadlessTerminal()
+49 -4
View File
@@ -16461,11 +16461,32 @@ export class OrcaRuntimeService {
}
}
// Why: a connected PTY proves a pane is still live even when its tab has
// already left every session record (daemon-held terminals, graph gaps).
const connectedPtyEvidence = {
tabIds: new Set<string>(),
paneKeys: new Set<string>(),
ptyIds: new Set<string>()
}
for (const pty of this.ptysById.values()) {
if (!pty.connected) {
continue
}
connectedPtyEvidence.ptyIds.add(pty.ptyId)
if (pty.tabId) {
connectedPtyEvidence.tabIds.add(pty.tabId)
}
if (pty.paneKey) {
connectedPtyEvidence.paneKeys.add(pty.paneKey)
}
}
this.attachAgentRowsToSummaries(
summaries,
runtimeWorktreeSummaryPathIndex,
missingRuntimeWorktreeIds,
mirroredWorktreeIdByTabId
mirroredWorktreeIdByTabId,
connectedPtyEvidence
)
const sorted = [...summaries.values()].sort(compareWorktreePs)
@@ -16484,7 +16505,12 @@ export class OrcaRuntimeService {
summaries: Map<string, RuntimeWorktreePsSummary>,
runtimeWorktreeSummaryPathIndex: RuntimeWorktreeSummaryPathIndex,
missingRuntimeWorktreeIds: Set<string>,
mirroredWorktreeIdByTabId: ReadonlyMap<string, string>
mirroredWorktreeIdByTabId: ReadonlyMap<string, string>,
connectedPtyEvidence: {
tabIds: ReadonlySet<string>
paneKeys: ReadonlySet<string>
ptyIds: ReadonlySet<string>
}
): void {
// Why: most agents report via hooks (agent-hooks/server), not OSC, so the
// hook snapshot is the primary source — same one the desktop sidebar reads.
@@ -16493,8 +16519,10 @@ export class OrcaRuntimeService {
string,
{
paneKey: string
ptyId?: string
tabId?: string
worktreeId?: string
connectionId: string | null
state: ParsedAgentStatusPayload['state']
agentType: string | null
prompt: string
@@ -16510,8 +16538,10 @@ export class OrcaRuntimeService {
const { payload } = snapshot
rowSources.set(snapshot.paneKey, {
paneKey: snapshot.paneKey,
ptyId: snapshot.ptyId,
tabId: snapshot.tabId,
worktreeId: snapshot.worktreeId,
connectionId: null,
state: payload.state,
agentType: payload.agentType ?? null,
prompt: payload.prompt,
@@ -16534,6 +16564,7 @@ export class OrcaRuntimeService {
paneKey: entry.paneKey,
tabId: entry.tabId,
worktreeId: entry.worktreeId,
connectionId: entry.connectionId,
state: entry.state,
agentType: entry.agentType ?? null,
prompt: entry.prompt,
@@ -16555,8 +16586,22 @@ export class OrcaRuntimeService {
// Why: hooks retain launch-time attribution across automatic workspace
// renames; the tab's current mirrored owner is authoritative when present.
const tabId = src.tabId ?? parsePaneKey(src.paneKey)?.tabId
const worktreeId =
(tabId ? mirroredWorktreeIdByTabId.get(tabId) : undefined) ?? src.worktreeId
const mirroredWorktreeId = tabId ? mirroredWorktreeIdByTabId.get(tabId) : undefined
if (
tabId !== undefined &&
mirroredWorktreeId === undefined &&
src.connectionId === null &&
!connectedPtyEvidence.tabIds.has(tabId) &&
!connectedPtyEvidence.paneKeys.has(src.paneKey) &&
(src.ptyId === undefined || !connectedPtyEvidence.ptyIds.has(src.ptyId))
) {
// Why: hook snapshots hydrate from last-status.json for days, so a local
// row whose tab left every session/graph and has no connected PTY is
// retained history — surfacing it resurrects closed agents on mobile
// (#6072). Remote rows are exempt: their tabs may only exist remotely.
continue
}
const worktreeId = mirroredWorktreeId ?? src.worktreeId
if (!worktreeId) {
continue
}