fix(cli): report which hosts a worktree listing covered, and stop the cap starving remote ones (#18417)

`orca worktree list` returned zero of 24 SSH worktrees at the default limit
(#18104). Rows are resolved repo by repo, so every SSH repo's rows land
contiguously at the end of the fleet order — the 24 remote rows sat at indices
496-520 of 521 and a plain `slice(0, 200)` never reached them.

The omission was not fully silent: text output printed `truncated: showing 200
of 521` and JSON carried `totalCount` / `truncated`. What was missing is that
the omission was *categorically every remote host* — no host column, no
`hostScope`, nothing to distinguish "200 of 521" from "one host is entirely
absent". Per docs/reference/ssh-execution-boundary.md, a listing that does not
name its scope reads as absolute.

Adopt the mechanism `terminal list` already has rather than inventing a second
one:

- `RuntimeTerminalListHostScope` becomes an alias of a shared
  `RuntimeListingHostScope`, now also carried (optional, so old hosts are
  unaffected) on `worktree.list` and `worktree.ps` results.
- `src/shared/host-balanced-listing-page.ts` round-robins the row cap across
  hosts and returns the survivors in the caller's original relative order, so
  the page stays a subsequence of the unbounded listing and nothing downstream
  re-sorts. An uncapped listing is returned unchanged.
- `worktree list` / `worktree ps` text output gains a `host=` column and the
  same trailing `scope:` line `terminal list` prints.

Third defect, same mechanism: `hostScope.omittedHostIds` is built from the
runtime's own bookkeeping, so it names `runtime:` ids for servers that are no
longer paired — 6 of 9 in the recorded QA run hard-error when queried. Since
`hostScope` is *the* documented way to complete a partial listing, that makes
the mechanism unreliable for its intended use.

Annotate rather than filter. Dropping an id would shrink what the listing
admits it did not cover, and the boundary doc requires a listing to name its
gaps — the gap is real whether or not this machine can name the host that owns
it. `src/cli/omitted-host-scope-selectors.ts` resolves each omitted id against
this machine's pairing store and the runtime's SSH-target registry and attaches
the exact flag that reaches it, or `null` marked "not selectable from this
machine". This is a client-side annotation: nothing new goes over the wire, it
answers "can I select it" and never "is it up", and the SSH round trip is only
paid when an `ssh:` host was actually omitted.

No `--host` filter was added; the host column plus scope line covers the
reported need without a new selector axis.
This commit is contained in:
Neil
2026-09-03 14:43:09 -07:00
committed by GitHub
parent 9bed758e36
commit 95eed52801
22 changed files with 895 additions and 65 deletions
+49
View File
@@ -0,0 +1,49 @@
/**
* Chooses which rows survive a listing's row cap so that no execution host is starved by it.
*
* Worktree rows are resolved repo by repo, so every SSH repo's rows land contiguously at the end
* of the fleet order — 24 remote worktrees sat at indices 496-520 of 521 and a 200-row cap
* returned zero of them (#18104). A per-host round robin gives each host a share of the cap.
*
* Chosen rows keep the caller's original relative order, so the page stays a subsequence of the
* unbounded listing and nothing downstream has to re-sort. An uncapped listing is returned as-is.
*/
export function selectHostBalancedPage<TRow>(
rows: readonly TRow[],
limit: number,
getHostId: (row: TRow) => string | null | undefined
): TRow[] {
if (rows.length <= limit) {
return [...rows]
}
// Insertion order is first-appearance order per host, so the round robin is deterministic.
const indicesByHost = new Map<string, number[]>()
rows.forEach((row, index) => {
const hostId = getHostId(row) ?? ''
const bucket = indicesByHost.get(hostId)
if (bucket) {
bucket.push(index)
} else {
indicesByHost.set(hostId, [index])
}
})
const buckets = [...indicesByHost.values()]
const cursors = buckets.map(() => 0)
const chosen: number[] = []
while (chosen.length < limit) {
let advanced = false
for (let bucket = 0; bucket < buckets.length && chosen.length < limit; bucket += 1) {
const cursor = cursors[bucket] ?? 0
const index = buckets[bucket]?.[cursor]
if (index !== undefined) {
chosen.push(index)
cursors[bucket] = cursor + 1
advanced = true
}
}
if (!advanced) {
break
}
}
return chosen.sort((left, right) => left - right).map((index) => rows[index] as TRow)
}
+12
View File
@@ -0,0 +1,12 @@
import type { ExecutionHostId } from './execution-host'
/**
* What a bounded listing did and did not cover, by execution host. An absent scope means the
* host is too old to report one — not that it covered everything. See
* `docs/reference/ssh-execution-boundary.md`: a listing is only evidence about the hosts it
* actually covered, so an empty answer for a host that is missing here proves nothing.
*/
export type RuntimeListingHostScope = {
hostIds: ExecutionHostId[]
omittedHostIds: ExecutionHostId[]
}
+3 -4
View File
@@ -6,6 +6,7 @@ import type {
import type { StartupCommandDelivery } from './codex-startup-delivery'
import type { ExecutionHostId } from './execution-host'
import type { PtyIncarnationId } from './pty-incarnation'
import type { RuntimeListingHostScope } from './runtime-listing-host-scope'
import type { RuntimeMobileSessionTabsResult } from './runtime-session-contracts'
import type { TabGroupLayoutNode } from './tab-types'
import type { TerminalExitCause } from './terminal-exit-cause'
@@ -83,10 +84,8 @@ export type RuntimeTerminalVisualLayout = {
root: RuntimeTerminalVisualLayoutNode
}
export type RuntimeTerminalListHostScope = {
hostIds: ExecutionHostId[]
omittedHostIds: ExecutionHostId[]
}
/** The shared listing-scope shape, kept under its incumbent name for existing consumers. */
export type RuntimeTerminalListHostScope = RuntimeListingHostScope
export type RuntimeTerminalListResult = {
terminals: RuntimeTerminalSummary[]
+5
View File
@@ -6,6 +6,7 @@ import type {
WorktreeLineage,
WorktreeLineageWarning
} from './worktree/lineage-types'
import type { RuntimeListingHostScope } from './runtime-listing-host-scope'
import type { GitWorktreeInfo, Worktree } from './worktree/types'
export type RuntimeWorktreeAgentRow = {
@@ -125,6 +126,8 @@ 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 RuntimeWorktreePsSnapshotResult = RuntimeWorktreePsResult & { snapshotId: string }
@@ -150,4 +153,6 @@ export type RuntimeWorktreeListResult = {
worktrees: RuntimeWorktreeRecord[]
totalCount: number
truncated: boolean
/** Absent from hosts that predate the field; treat that scope as unverifiable. */
hostScope?: RuntimeListingHostScope
}