Files
orca/src/shared/runtime-worktree-contracts.ts
T
Neil 64021b0f47 fix(mobile): guard catalog snapshots against late replies and trim the snapshot paths (#36902)
* fix(mobile): guard catalog snapshots against late replies and trim the snapshot paths

A slow unchanged or delta reply that predates a client switch (A→B→A)
could re-point the paired-server snapshot token at rows it no longer
matched; the next row delta then rebuilt onto the wrong base. Pending
reads now carry the token they asked with, and admit drops any non-full
reply whose token is no longer held.

Single-flight reads with one trailing join move into
WorktreeCatalogSnapshotClient.read(), so paired-server rows get the same
protection home had; home's own copy goes away. Server rows use one read
path for old and new desktops and drop snapshots for servers that leave.

The host screen asks for a first page only while nothing is shown and
the full read is not failing, and drops it on an invalid reply. The
shell no longer seeds the page with a status proven on a connection
that has since dropped.

Cleanup: drop exactActivity (unreleased; home only ranks by coarsened
activity and its exact rows defeated delta reuse), fold the legacy opt-in
check into the snapshot resolvers, remove unused worktree.ps result
types and a duplicate page subscription, and re-record the affected
RPC goldens.

* fix(mobile): keep server-row snapshots per client and skip first pages for loaded empty hosts

A shared per-host snapshot let a slow read on a replaced client reclaim
ownership from the new one, discarding the new client's reply. Snapshots
are now kept per client, and a delta rebuild retry runs only for the
current owner. An empty host that already loaded no longer asks for a
first page on every poll.
2026-10-31 22:43:13 -07:00

215 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { SmartTier } from './sidebar-list/sort-stamps'
import type { AgentStatusState, AgentType, AgentWorkingMode } from './agent-status-types'
import type { AgentMainAgentStatus } from './main-agent-status'
import type { ProgramStatusFacet } from './program-status/facet'
import type { CheckStatus } from './github/pull-request-types'
import type { AgentChildWorkView } from './agent-status-child-work-view'
import type { AgentSubagentSnapshot } from './agent-status-subagent-snapshot'
import type { RuntimeTerminalInteractiveWait } from './runtime-terminal-contracts'
import type { BaseRefSearchResult, Repo } from './repo-types'
import type { CreateWorktreeResult, RemoveWorktreeResult } from './worktree/create-types'
import type {
WorkspaceLineage,
WorktreeLineage,
WorktreeLineageWarning
} from './worktree/lineage-types'
import type { RuntimeListingHostScope } from './runtime-listing-host-scope'
import type { GitWorktreeInfo, Worktree } from './worktree/types'
import type { WorkspaceStatusWarning } from './workspace-status-warning'
import type { WorktreeStatus } from './worktree-status-rollup'
export type RuntimeWorktreeSetResult = {
worktree: RuntimeWorktreeRecord
/** Older hosts omit warnings; metadata writes still succeed. */
warnings?: WorkspaceStatusWarning[]
}
export type RuntimeWorktreeAgentRow = {
paneKey: string
parentPaneKey: string | null
state: AgentStatusState
workingMode?: AgentWorkingMode
agentType: AgentType | null
prompt: string
taskTitle: string | null
displayName: string | null
/** The name desktop's compact row leads with (rename, chat or live title). Old hosts omit it. */
conversationName?: string
lastAssistantMessage: string | null
toolName: string | null
toolInput: string | null
interrupted: boolean
/** Main-agent state/verdict/clock even while subagents keep the row working; old hosts omit it, falling back to interrupted. */
mainAgent?: AgentMainAgentStatus
/** Same as `terminal show`, only when requested: null is "looked, no wait"; absent is not evaluated (structured workers, old hosts). */
agentWait?: RuntimeTerminalInteractiveWait | null
stateStartedAt: number
updatedAt: number
/** Native canonical views win over the CLI roster, including an explicit empty list. */
children?: AgentChildWorkView[]
subagents?: AgentSubagentSnapshot[]
/** The host's freshness verdict; child clocks cannot be compared with the phone's clock. */
subagentObservation?: 'live' | 'unverifiable'
restoredUnconfirmed?: boolean
/** A structured host still owns the provider child, so age alone cannot stale it. Old hosts omit this. */
structuredHostOwned?: true
/** A connected PTY backs the row, so a stale row reads "No update in …" rather than idle. Old hosts omit this. */
hasLivePty?: true
/** The program's own OSC 7501 report; never decides `state`. Optional: old hosts never send it. */
programStatus?: ProgramStatusFacet
}
export type RuntimeWorktreePsSummary = {
workspaceKind?: 'git' | 'folder-workspace'
worktreeId: string
repoId: string
hostId?: Worktree['hostId']
terminalPlatform?: NodeJS.Platform
repo: string
path: string
priorWorktreeIds?: string[]
branch: string
isArchived: boolean
isMainWorktree: boolean
hasHostSidebarActivity: boolean
worktreeInstanceId?: string
lineageWorktreeInstanceId?: string
parentWorktreeInstanceId?: string
parentWorktreeId: string | null
childWorktreeIds: string[]
displayName: string
workspaceStatus: string
sortOrder: number
manualOrder?: number
lastActivityAt?: number
/** The owning host's Smart stamps (sidebar-list/sort-stamps.ts); absent tier is Rest. */
smartTier?: SmartTier
smartAt?: number
/** Set by a host that writes sort stamps: clients sort Recent and Smart by them alone. */
sortedByStamps?: true
createdAt?: number
creatorProvenance?: Worktree['creatorProvenance']
linkedIssue: number | null
/** The cached PR the desktop's PR lanes read, else a linked number with state UNCACHED_LINKED_PR_STATE. Old hosts omit title/url/checks. */
linkedPR: {
number: number
state: string
title?: string
url?: string
checksStatus?: CheckStatus
} | null
linkedLinearIssue: string | null
linkedGitLabMR: number | null
linkedGitLabIssue: number | null
linkedItems?: Worktree['linkedItems']
comment: string
isPinned: boolean
isActive: boolean
unread: boolean
liveTerminalCount: number
hasAttachedPty: boolean
/** Lost-contact terminals are neither liveTerminalCount nor exited; current hosts send even zero, old hosts omit. */
unverifiableTerminalCount?: number
lastOutputAt: number | null
preview: string
status: RuntimeWorktreeStatus
/** Optional discriminator for a working workspace; older clients fall back to ordinary working. */
workingMode?: AgentWorkingMode
/** The desktop sidebar dot, including failed/interrupted/unconfirmed; older hosts omit it. */
sidebarStatus?: WorktreeStatus
agents: RuntimeWorktreeAgentRow[]
/** See `Worktree.removing`; sent only to clients that advertise background removal. */
removing?: true
/** See `GitWorktreeInfo.removalError`. */
removalError?: string
/** Host-derived workspace kinds. Missing defaultBranch retains reader guesses; other missing flags show the row.
* Older hosts omit these facts. */
defaultBranch?: boolean
automationCreated?: true
/** The automation's name when it made the row; older hosts send only `automationCreated`. */
automationName?: string
cliCreated?: true
detachedHead?: true
/** Other-client classification for this host’s own phone, which shares its filter setting. */
desktopOtherClient?: true
}
export type RuntimeGitLocalBranches = {
current: string | null
branches: string[]
}
export type RuntimeSpeechModelSummary = {
id: string
label: string
provider: 'local' | 'openai'
sizeBytes: number | null
recommended: boolean
status: 'ready' | 'not-downloaded' | 'downloading' | 'extracting' | 'error'
progress: number | null
}
export type RuntimeSpeechSetupState = {
enabled: boolean
selectedModelId: string
dictationMode: 'toggle' | 'hold'
models: RuntimeSpeechModelSummary[]
}
export type RuntimeGitCheckoutResult = {
ok: true
branch: string
}
/** A linked PR the host has no cached state for: it shows a badge but, like desktop, sits in the In progress lane. */
export const UNCACHED_LINKED_PR_STATE = 'unknown'
export type RuntimeWorktreeStatus = 'active' | 'working' | 'permission' | 'done' | 'inactive'
export type RuntimeWorktreeRecord = Worktree & {
parentWorktreeId: string | null
childWorktreeIds: string[]
lineage: WorktreeLineage | null
workspaceLineage?: WorkspaceLineage | null
git: GitWorktreeInfo
}
export type RuntimeWorktreeCreateResult = {
worktree: RuntimeWorktreeRecord
lineage: WorktreeLineage | null
workspaceLineage?: WorkspaceLineage | null
warnings: WorktreeLineageWarning[]
warning?: string
startupTerminal?: CreateWorktreeResult['startupTerminal']
agentTerminalHandle?: string
}
export type RuntimeWorktreeRemoveResult = RemoveWorktreeResult & {
removed: boolean
warning?: string
}
export type RuntimeWorktreePsResult = {
worktrees: RuntimeWorktreePsSummary[]
totalCount: number
truncated: boolean
/** Absent from hosts that predate the field; treat that scope as unverifiable. */
hostScope?: RuntimeListingHostScope
}
export type RuntimeRepoList = { repos: Repo[] }
export type RuntimeRepoSearchRefs = {
refs: string[]
refDetails?: BaseRefSearchResult[]
truncated: boolean
}
export type RuntimeWorktreeListResult = {
worktrees: RuntimeWorktreeRecord[]
totalCount: number
truncated: boolean
/** Absent from hosts that predate the field; treat that scope as unverifiable. */
hostScope?: RuntimeListingHostScope
}