fix(ssh): a managed server restart no longer marks still-running agents done (#29604)

* fix(runtime): keep a restored agent turn unverifiable instead of done after a server restart

* fix(runtime): retire restored turns by the synced leaf's title clock; no Stop for unverifiable mobile turns

* fix(runtime): capability-gate restored agent rows so older clients keep the legacy done projection

* fix(runtime): retire restored turns only on shell titles; restored rows never pierce live client status

* test(web): the browser client advertises the restored agent status capability
This commit is contained in:
Neil
2026-10-16 04:00:24 -07:00
committed by GitHub
parent 827482c3b8
commit 98064b2608
15 changed files with 350 additions and 21 deletions
@@ -56,8 +56,11 @@ export function useMobileNativeChatActiveResolution(args: {
const activeChatStructured =
activeChatResolution != null && activeSessionTab?.type === 'agent-session'
const activeTabStatus = activeSessionTab?.agentStatus
// Why: a turn restored across a host restart is unverifiable, so it must not offer Stop.
const activeTabAgentWorking =
activeTabStatus?.state === 'working' && activeTabStatus.workingMode !== 'monitoring'
activeTabStatus?.state === 'working' &&
activeTabStatus.workingMode !== 'monitoring' &&
activeTabStatus.restoredUnconfirmed !== true
const nativeChatStatus = activeChatResolution && !activeChatStructured ? activeTabStatus : null
const routeKey = `${hostId}\0${worktreeId}\0${activeSessionTabId ?? ''}`
const streamIdentity = `${routeKey}\0${activeChatSessionId ?? ''}\0${activeHandleRef.current ?? ''}`
@@ -1,5 +1,6 @@
import {
AGENT_SESSION_PENDING_SEND_RESULT_RUNTIME_CAPABILITY,
AGENT_STATUS_RESTORED_UNCONFIRMED_RUNTIME_CAPABILITY,
AGENT_SESSION_TURN_ITEM_CAPABILITY,
CLAUDE_STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY,
PI_STRUCTURED_DIALOGS_RUNTIME_CAPABILITY,
@@ -37,7 +38,9 @@ export const MOBILE_RUNTIME_CLIENT_CAPABILITIES = remoteRuntimeClientCapabilitie
// Reads a listed launch tab with no terminal yet as not started, so the host may show it early.
AGENT_LAUNCH_UNSTARTED_TAB_CLIENT_CAPABILITY,
// Reads `agent_launch_tab_closed` (a user closed its tab, which stopped it) as a definite answer.
AGENT_LAUNCH_TAB_CLOSED_CLIENT_CAPABILITY
AGENT_LAUNCH_TAB_CLOSED_CLIENT_CAPABILITY,
// Native chat offers no Stop for a turn the host restored but could not confirm.
AGENT_STATUS_RESTORED_UNCONFIRMED_RUNTIME_CAPABILITY
])
export const MOBILE_RUNTIME_CLIENT_CAPABILITY_UPDATE_METHOD =
@@ -229,13 +229,55 @@ describe('headless hook agent-status projection (#11761)', () => {
// #12346: a row hydrated from last-status.json may describe a turn that ended while
// no receiver was up, so its recent `receivedAt` proves nothing. Publishing it would
// resurrect the question card on every restart, with no agent left to answer it.
it('refuses a hydrated unconfirmed row while keeping its resume identity', async () => {
it('refuses a hydrated unconfirmed row as live while keeping its resume identity', async () => {
const agentStatus = await projectAgentStatus([hookRow({ restoredUnconfirmed: true })])
expect(agentStatus).toEqual(
expect.objectContaining({ state: 'done', prompt: '', providerSession: PROVIDER_SESSION })
expect.objectContaining({
state: 'waiting',
restoredUnconfirmed: true,
providerSession: PROVIDER_SESSION
})
)
expect(agentStatus).not.toHaveProperty('interactivePrompt')
expect(agentStatus).not.toHaveProperty('toolName')
})
// A managed server restart re-hydrates last-status.json while the daemon keeps the agent
// running: the turn is unverifiable, so it must not be published as finished.
it('keeps a restored working turn unverifiable instead of done', async () => {
const receivedAt = Date.now() - 60_000
const agentStatus = await projectAgentStatus([
hookRow({
state: 'working',
prompt: 'refactor the parser',
toolName: undefined,
interactivePrompt: undefined,
receivedAt,
stateStartedAt: receivedAt - 5_000,
restoredUnconfirmed: true
})
])
expect(agentStatus).toEqual(
expect.objectContaining({
state: 'working',
prompt: 'refactor the parser',
restoredUnconfirmed: true,
updatedAt: receivedAt,
stateStartedAt: receivedAt - 5_000
})
)
})
it('lets a shell title retire a restored turn as done', async () => {
const agentStatus = await projectAgentStatus(
[hookRow({ state: 'working', interactivePrompt: undefined, restoredUnconfirmed: true })],
(runtime) => observePaneTitle(runtime, 'bash')
)
expect(agentStatus === undefined || agentStatus.state === 'done').toBe(true)
expect(agentStatus).not.toHaveProperty('restoredUnconfirmed')
})
// #7970: a retained OSC 9999 row is the pane's own report and keeps precedence.
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import {
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY,
AGENT_STATUS_RESTORED_UNCONFIRMED_RUNTIME_CAPABILITY,
CLAUDE_STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY,
STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY
} from '../../../../shared/protocol-version'
@@ -41,7 +42,56 @@ function makeSnapshot(sessionBoundary: boolean): RuntimeMobileSessionTabsSnapsho
}
}
function makeRestoredSnapshot(): RuntimeMobileSessionTabsSnapshot {
const snapshot = makeSnapshot(false)
const tab = snapshot.tabs[0]!
return {
...snapshot,
tabs: [
tab.type === 'terminal'
? {
...tab,
agentStatus: {
...tab.agentStatus!,
state: 'working',
prompt: 'refactor the parser',
restoredUnconfirmed: true
}
}
: tab
]
}
}
describe('projectSessionTabAgentStatus', () => {
// A client that cannot read `restoredUnconfirmed` would take the last known state as live.
it.each(['mobile', 'runtime'] as const)(
'keeps the legacy done projection for a %s client without the restored capability',
(clientKind) => {
const projected = projectSessionTabAgentStatus(makeRestoredSnapshot(), clientKind, [])
const tab = projected.tabs[0]
expect(tab?.type === 'terminal' ? tab.agentStatus : null).toEqual(
expect.objectContaining({ state: 'done', prompt: '' })
)
expect(tab?.type === 'terminal' ? tab.agentStatus : null).not.toHaveProperty(
'restoredUnconfirmed'
)
}
)
it('publishes a restored turn as unverifiable to a client that reads it', () => {
const projected = projectSessionTabAgentStatus(makeRestoredSnapshot(), 'runtime', [
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY,
AGENT_STATUS_RESTORED_UNCONFIRMED_RUNTIME_CAPABILITY
])
const tab = projected.tabs[0]
expect(tab?.type === 'terminal' ? tab.agentStatus : null).toEqual(
expect.objectContaining({ state: 'working', restoredUnconfirmed: true })
)
})
it('projects structured tabs and dangling group focus out of old clients', () => {
const snapshot: RuntimeMobileSessionTabsSnapshot = {
...makeSnapshot(false),
@@ -1,5 +1,6 @@
import {
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY,
AGENT_STATUS_RESTORED_UNCONFIRMED_RUNTIME_CAPABILITY,
type RuntimeCapability
} from '../../../../shared/protocol-version'
import type {
@@ -72,6 +73,12 @@ export function projectSessionTabAgentStatus<TPayload extends SessionTabsPayload
)
}
}
if (
clientKind !== undefined &&
!clientCapabilities?.includes(AGENT_STATUS_RESTORED_UNCONFIRMED_RUNTIME_CAPABILITY)
) {
projected = projectRestoredAgentStatusAsLegacyDone(projected)
}
// Why: only paired runtimes have legacy `done` completion side effects; mobile must keep its row without changing the exact v2 auth shape.
if (
clientKind !== 'runtime' ||
@@ -92,6 +99,23 @@ export function projectSessionTabAgentStatus<TPayload extends SessionTabsPayload
return changed ? ({ ...projected, tabs } as TPayload) : projected
}
/** What hosts published for a restored turn before clients could read it as unverifiable. */
function projectRestoredAgentStatusAsLegacyDone<TPayload extends SessionTabsPayload>(
payload: TPayload
): TPayload {
let changed = false
const tabs = payload.tabs.map((tab) => {
if (tab.type !== 'terminal' || tab.agentStatus?.restoredUnconfirmed !== true) {
return tab
}
changed = true
const { restoredUnconfirmed: _restored, ...legacyStatus } = tab.agentStatus
return { ...tab, agentStatus: { ...legacyStatus, state: 'done' as const, prompt: '' } }
})
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only terminal tabs' agentStatus changed; every other TPayload field is spread through unchanged.
return changed ? ({ ...payload, tabs } as TPayload) : payload
}
function projectUnsupportedAgentSessionTabTitles<TPayload extends SessionTabsPayload>(
payload: TPayload,
args: {
@@ -1,7 +1,9 @@
import { describe, expect, it } from 'vitest'
import type { AgentStatusIpcPayload } from '../../shared/agent-status-types'
import type { RuntimeMobileSessionTerminalTab } from '../../shared/runtime-types'
import type { RuntimeAgentRowSnapshot } from './runtime-hook-agent-row-selection'
import { buildRuntimeMobileAgentStatus } from './runtime-mobile-agent-status-builder'
import type { RuntimeLeafRecord } from './runtime-terminal-state-records'
const PROVIDER_SESSION = { key: 'session_id' as const, id: 'session-1' }
const TAB: RuntimeMobileSessionTerminalTab = {
@@ -37,3 +39,111 @@ describe('mobile agent status builder', () => {
)
})
})
// A synced leaf projects with no PTY record of its own, so a shell title it painted after a
// restored turn must still retire that turn through the leaf's PTY title clock.
const TAB_ID = 'tab-1'
const LEAF_ID = '11111111-1111-4111-8111-111111111111'
const PANE_KEY = `${TAB_ID}:${LEAF_ID}`
const PTY_ID = 'pty-1'
const LEAF_TAB: RuntimeMobileSessionTerminalTab = {
type: 'terminal',
id: `${TAB_ID}::${LEAF_ID}`,
title: 'Terminal 1',
parentTabId: TAB_ID,
leafId: LEAF_ID,
isActive: true
}
function leafWithTitle(lastOscTitle: string | null): RuntimeLeafRecord {
return {
tabId: TAB_ID,
worktreeId: 'wt-1',
leafId: LEAF_ID,
paneRuntimeId: 1,
ptyId: PTY_ID,
paneTitle: null,
title: null,
ptyGeneration: 1,
connected: true,
writable: true,
lastOutputAt: null,
lastExitCode: null,
lastExitCause: null,
lastAgentStatus: null,
lastAgentStatusObservedLive: false,
lastOscTitle,
lastOscTitleAt: lastOscTitle ? 1 : null,
paneTitleUpdatedAt: null,
tailBuffer: [],
tailTranscriptBuffer: [],
tailTranscriptChars: 0,
tailPartialLine: '',
tailPendingAnsi: '',
tailRedrawCursor: null,
tailTruncated: false,
tailLinesTotal: 0,
preview: '',
waitBlockedAt: null
}
}
function restoredRow(receivedAt: number): AgentStatusIpcPayload {
return {
paneKey: PANE_KEY,
state: 'working',
prompt: 'refactor the parser',
agentType: 'claude',
connectionId: null,
receivedAt,
stateStartedAt: receivedAt,
tabId: TAB_ID,
worktreeId: 'wt-1',
restoredUnconfirmed: true
}
}
function project(leaf: RuntimeLeafRecord, titleEpochMs: number | null, receivedAt: number) {
return buildRuntimeMobileAgentStatus(
null,
LEAF_TAB,
null,
null,
() => [restoredRow(receivedAt)],
{
getPaneKey: () => PANE_KEY,
getLeaf: () => leaf,
getTrackedTitle: () => null,
getPtyTitleObservedAtEpochMs: (ptyId) => (ptyId === PTY_ID ? titleEpochMs : null)
}
)
}
describe('restored turns on a synced leaf', () => {
it('stays unverifiable when only a seeded (unclocked) shell title is known', () => {
const receivedAt = Date.now() - 60_000
const result = project(leafWithTitle('bash'), null, receivedAt)
expect(result.agentStatus).toEqual(
expect.objectContaining({ state: 'working', restoredUnconfirmed: true })
)
})
it('stays unverifiable under a newer title that is not a shell', () => {
const receivedAt = Date.now() - 60_000
const result = project(leafWithTitle('vim'), receivedAt + 1_000, receivedAt)
expect(result.agentStatus).toEqual(
expect.objectContaining({ state: 'working', restoredUnconfirmed: true })
)
})
it('retires to done once the leaf PTY paints a newer shell title', () => {
const receivedAt = Date.now() - 60_000
const result = project(leafWithTitle('bash'), receivedAt + 1_000, receivedAt)
expect(result.agentStatus?.state).toBe('done')
expect(result.agentStatus).not.toHaveProperty('restoredUnconfirmed')
})
})
@@ -15,13 +15,16 @@ import type { RuntimeAgentRowSnapshot } from './runtime-worktree-agent-rows'
import {
classifyAgentTitle,
getLatestAgentCandidateTitle,
getLatestPtyTitle
getLatestPtyTitle,
terminalTitleBlocksExplicitAgentStatus
} from './runtime-worktree-status-projection'
type RuntimeMobileAgentStatusHost = {
getPaneKey(tab: RuntimeMobileSessionTerminalTab): string
getLeaf(tab: RuntimeMobileSessionTerminalTab): RuntimeLeafRecord | null
getTrackedTitle(ptyId: string | null): string | null
/** Wall clock of the PTY's latest live OSC title; seeded restore titles leave it unset. */
getPtyTitleObservedAtEpochMs?(ptyId: string | null): number | null
}
export function buildRuntimeMobileAgentStatus(
@@ -127,6 +130,41 @@ export function buildRuntimeMobileAgentStatus(
return { agentStatus: renewedStatus }
}
}
// Why: a turn restored across a server restart is unverifiable, not finished; a synthetic
// `done` told clients a running agent had stopped. Keep its own date (so it cannot outrank a
// client's row) and drop the question fields (#12346). Only a later shell title retires it.
const shellTitleRetiredRestored =
hookRow.restored !== null &&
terminalTitleBlocksExplicitAgentStatus(ptyTitle) &&
(pty?.lastOscTitleEpochMs ?? host.getPtyTitleObservedAtEpochMs?.(leaf?.ptyId ?? null) ?? 0) >=
hookRow.restored.updatedAt
const restored = !pty?.lastAgentStatus && !shellTitleRetiredRestored ? hookRow.restored : null
if (restored) {
const restoredAgentType = ownerAgent ?? restored.payload.agentType
return {
agentStatus: normalizeCompatibleAgentStatusEntryForOwner(
{
state: restored.payload.state,
prompt: restored.payload.prompt,
updatedAt: restored.updatedAt,
stateStartedAt: restored.stateStartedAt,
restoredUnconfirmed: true,
paneKey,
...(terminalHandle ? { terminalHandle } : {}),
...(restoredAgentType ? { agentType: restoredAgentType } : {}),
...((pty?.worktreeId ?? restored.worktreeId)
? { worktreeId: pty?.worktreeId ?? restored.worktreeId }
: {}),
tabId: tab.parentTabId,
terminalTitle,
stateHistory: [],
...providerSession
},
ownerAgent,
ownerOptions
)
}
}
// Last resort: the pane's hook evidence is identity only (resume rows, stale
// rows, or a row the freshness gate rejected). `done` is the honest
// projection — and it is what retires the card once the agent exits.
@@ -120,6 +120,8 @@ export type RuntimeHookAgentRowLookup = {
agentType: string | null
agentIsLive: boolean
live: HookLiveAgentRow | null
/** Latest fresh nonterminal row hydrated across a host restart: unverifiable, never live. */
restored: HookLiveAgentRow | null
}
/** Resume identity stays valid until relaunch; ownership and live status remain freshness-bounded. */
@@ -129,6 +131,7 @@ export function selectRuntimeHookAgentRowForPane(
let session: AgentStatusIpcPayload | null = null
let agent: AgentStatusIpcPayload | null = null
let live: AgentStatusIpcPayload | null = null
let restored: AgentStatusIpcPayload | null = null
const freshAfter = Date.now() - AGENT_STATUS_STALE_AFTER_MS
for (const entry of rows) {
if (entry.providerSession && (!session || entry.receivedAt > session.receivedAt)) {
@@ -152,6 +155,19 @@ export function selectRuntimeHookAgentRowForPane(
) {
live = entry
}
if (
entry.providerSessionOnly !== true &&
entry.restoredUnconfirmed === true &&
entry.state !== 'done' &&
(entry.evidenceObservedAt ?? entry.receivedAt) >= freshAfter &&
(!restored || entry.receivedAt > restored.receivedAt)
) {
restored = entry
}
}
// A confirmed row at least as new as the restored one supersedes it.
if (restored && live && live.receivedAt >= restored.receivedAt) {
restored = null
}
return {
providerSession: session?.providerSession ?? null,
@@ -159,17 +175,18 @@ export function selectRuntimeHookAgentRowForPane(
providerSessionReceivedAt: session?.receivedAt ?? null,
agentType: agent?.agentType ?? null,
agentIsLive: agent != null && agent.state !== 'done',
live: live
? {
payload: pickParsedAgentStatusPayload(live),
updatedAt: live.receivedAt,
...(live.evidenceObservedAt !== undefined
? { evidenceObservedAt: live.evidenceObservedAt }
: {}),
stateStartedAt: live.stateStartedAt ?? live.receivedAt,
...(live.worktreeId ? { worktreeId: live.worktreeId } : {})
}
: null
live: live ? toHookLiveAgentRow(live) : null,
restored: restored ? toHookLiveAgentRow(restored) : null
}
}
function toHookLiveAgentRow(row: AgentStatusIpcPayload): HookLiveAgentRow {
return {
payload: pickParsedAgentStatusPayload(row),
updatedAt: row.receivedAt,
...(row.evidenceObservedAt !== undefined ? { evidenceObservedAt: row.evidenceObservedAt } : {}),
stateStartedAt: row.stateStartedAt ?? row.receivedAt,
...(row.worktreeId ? { worktreeId: row.worktreeId } : {})
}
}
@@ -148,7 +148,9 @@ export abstract class OrcaRuntimeWithPruneMobileSessionTabGroupLayout extends Or
const leaf = this.leaves.get(this.getLeafKey(candidate.parentTabId, candidate.leafId))
return leaf ? this.getLeafDisplayRecord(leaf) : null
},
getTrackedTitle: (ptyId) => this.getUnpersistedTrackedTitleForPty(ptyId)
getTrackedTitle: (ptyId) => this.getUnpersistedTrackedTitleForPty(ptyId),
getPtyTitleObservedAtEpochMs: (ptyId) =>
ptyId ? (this.ptysById.get(ptyId)?.lastOscTitleEpochMs ?? null) : null
}
)
}
@@ -513,6 +513,30 @@ describe('remote-paired pane: host snapshot mirror vs client byte-derived status
expect(entry?.interactivePrompt).toBe('Allow Bash(rm -rf)?')
})
it('keeps live client status over a restored, unconfirmed host block', () => {
const store = seedPairedClientStore()
replayClientOscWorking(store, T0 + 500)
// Host clock ahead of the client: only the unconfirmed flag can keep this row out.
const restoredAt = T0 + 60_000
applyHostSnapshot(
store,
makeHostSnapshot({
snapshotVersion: 2,
hostNow: restoredAt,
includeAgentStatus: true,
agentStatusOverrides: {
state: 'blocked',
prompt: 'Allow Bash(rm -rf)?',
restoredUnconfirmed: true
}
}),
T0 + 1_000
)
expect(store.getState().agentStatusByPaneKey[MIRROR_PANE_KEY]?.state).toBe('working')
})
it('adopts host monitoring metadata without replacing client-owned byte status', () => {
const store = seedPairedClientStore()
applyHostSnapshot(
@@ -68,7 +68,11 @@ export function isMirroredAgentPaneKeyForTabs(
* interactive question cards reach the host over its HTTP agent hook, never
* through PTY bytes, so they must pierce the client-authority fence. */
export function hostAgentStatusPiercesClientAuthority(entry: AgentStatusEntry): boolean {
return entry.state === 'blocked' || entry.interactivePrompt != null
// Why: a row restored across a host restart is unverifiable, so it never outranks live bytes.
return (
entry.restoredUnconfirmed !== true &&
(entry.state === 'blocked' || entry.interactivePrompt != null)
)
}
/** True while this renderer's own byte-derived status owns the pane: it claimed
@@ -1,6 +1,7 @@
import {
AGENT_SESSION_BACKGROUND_TASK_ROW_STOP_CAPABILITY,
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY,
AGENT_STATUS_RESTORED_UNCONFIRMED_RUNTIME_CAPABILITY,
REPO_SEARCH_QUALIFIED_REFS_RUNTIME_CAPABILITY,
AGENT_SESSION_TURN_ITEM_CAPABILITY,
SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY,
@@ -28,5 +29,7 @@ export const WEB_RUNTIME_CLIENT_CAPABILITIES = [
WORKTREE_VISIBILITY_DEFAULTS_RUNTIME_CAPABILITY,
WORKTREE_VISIBILITY_SOURCE_DEFAULTS_RUNTIME_CAPABILITY,
WORKTREE_BACKGROUND_REMOVAL_RUNTIME_CAPABILITY,
TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY
TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY,
// Same renderer as the desktop: a restored, unconfirmed agent row shows as identity only.
AGENT_STATUS_RESTORED_UNCONFIRMED_RUNTIME_CAPABILITY
] as const
@@ -18,6 +18,7 @@ import {
AGENT_SESSION_BACKGROUND_TASK_ROW_STOP_CAPABILITY,
AGENT_SESSION_TURN_ITEM_CAPABILITY,
AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY,
AGENT_STATUS_RESTORED_UNCONFIRMED_RUNTIME_CAPABILITY,
REPO_SEARCH_QUALIFIED_REFS_RUNTIME_CAPABILITY,
SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY,
SESSION_TABS_SPLIT_GROUP_PLACEMENT_RUNTIME_CAPABILITY,
@@ -138,7 +139,8 @@ describe('WebRuntimeClient', () => {
WORKTREE_VISIBILITY_DEFAULTS_RUNTIME_CAPABILITY,
WORKTREE_VISIBILITY_SOURCE_DEFAULTS_RUNTIME_CAPABILITY,
WORKTREE_BACKGROUND_REMOVAL_RUNTIME_CAPABILITY,
TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY
TERMINAL_QUICK_COMMAND_LONG_PROMPTS_RUNTIME_CAPABILITY,
AGENT_STATUS_RESTORED_UNCONFIRMED_RUNTIME_CAPABILITY
]
})
@@ -2,6 +2,7 @@ import { TERMINAL_IMAGE_VIEWER_CAPABILITY } from './terminal-image-viewer-capabi
import {
AGENT_SESSION_ACCEPTED_SEND_RUNTIME_CAPABILITY,
AGENT_SESSION_PENDING_SEND_RESULT_RUNTIME_CAPABILITY,
AGENT_STATUS_RESTORED_UNCONFIRMED_RUNTIME_CAPABILITY,
BROWSER_CLIENT_HOST_RUNTIME_CAPABILITY,
BROWSER_CLIENT_PAGE_METADATA_RUNTIME_CAPABILITY,
CLAUDE_STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY,
@@ -41,5 +42,7 @@ export const ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES = [
// Main restores every tab-delta frame before the window or any other reader sees it.
SESSION_TABS_TAB_DELTA_RUNTIME_CAPABILITY,
// Only the renderer shows Deleting for a `removing` row; CLI and mobile get those rows omitted.
WORKTREE_BACKGROUND_REMOVAL_RUNTIME_CAPABILITY
WORKTREE_BACKGROUND_REMOVAL_RUNTIME_CAPABILITY,
// The renderer shows a restored, unconfirmed agent row as identity only.
AGENT_STATUS_RESTORED_UNCONFIRMED_RUNTIME_CAPABILITY
] as const
@@ -97,6 +97,10 @@ export const SESSION_TABS_RETIREMENT_PROOF_DELTA_RUNTIME_CAPABILITY =
// Why: a client advertising this rebuilds each `updated` frame's tab list from the previous frame
// on the same stream, so one tab's status change no longer re-sends every tab in the worktree.
export const SESSION_TABS_TAB_DELTA_RUNTIME_CAPABILITY = 'session-tabs.tab-delta.v1' as const
// Why: a client advertising this reads `restoredUnconfirmed` agent rows as unverifiable; older
// clients would read their last known state as live, so they keep the legacy `done` projection.
export const AGENT_STATUS_RESTORED_UNCONFIRMED_RUNTIME_CAPABILITY =
'agent-status.restored-unconfirmed.v1' as const
export const AGENT_SESSION_BOUNDARY_RUNTIME_CAPABILITY =
'agent-session.session-boundary.v1' as const
export const AGENT_SESSION_HOST_AUTHORITY_RUNTIME_CAPABILITY =