mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
`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.
202 lines
7.6 KiB
TypeScript
202 lines
7.6 KiB
TypeScript
import type { PublicKnownRuntimeEnvironment } from '../shared/runtime-environments'
|
|
import type {
|
|
RuntimeRepoList,
|
|
RuntimeRepoSearchRefs,
|
|
RuntimeWorktreeListResult,
|
|
RuntimeWorktreePsResult,
|
|
RuntimeWorktreeRecord
|
|
} from '../shared/runtime-types'
|
|
import type { MemorySnapshot, WorktreeMemory } from '../shared/process-stats-types'
|
|
import { formatListingHostScope, type WithAnnotatedHostScope } from './omitted-host-scope-selectors'
|
|
|
|
export function formatMemorySnapshot(snapshot: MemorySnapshot): string {
|
|
const topWorktrees = [...snapshot.worktrees].sort((a, b) => b.memory - a.memory).slice(0, 10)
|
|
const hostAvailable = snapshot.host.availableMemory ?? snapshot.host.freeMemory
|
|
const hostAvailableSource = snapshot.host.availableMemorySource ?? 'free-memory'
|
|
const lines = [
|
|
`collectedAt: ${new Date(snapshot.collectedAt).toISOString()}`,
|
|
`totalMemory: ${formatByteCount(snapshot.totalMemory)}`,
|
|
`processMemoryMetric: ${formatProcessMemoryMetric(snapshot.processMemoryMetric)}`,
|
|
...formatCommitLines(snapshot),
|
|
`totalCpu: ${formatCpu(snapshot.totalCpu)}`,
|
|
[
|
|
`hostUsed: ${formatByteCount(snapshot.host.usedMemory)}`,
|
|
`/ ${formatByteCount(snapshot.host.totalMemory)}`,
|
|
`(${snapshot.host.memoryUsagePercent.toFixed(1)}%)`
|
|
].join(' '),
|
|
[`hostAvailable: ${formatByteCount(hostAvailable)}`, `(${hostAvailableSource})`].join(' '),
|
|
[
|
|
`app: ${formatByteCount(snapshot.app.memory)}`,
|
|
`(main ${formatByteCount(snapshot.app.main.memory)},`,
|
|
`renderer ${formatByteCount(snapshot.app.renderer.memory)},`,
|
|
`other ${formatByteCount(snapshot.app.other.memory)})`
|
|
].join(' '),
|
|
`worktrees: ${snapshot.worktrees.length}`
|
|
]
|
|
|
|
if (topWorktrees.length === 0) {
|
|
lines.push('topWorktrees: none')
|
|
return lines.join('\n')
|
|
}
|
|
|
|
lines.push('', 'Top worktrees:')
|
|
for (const worktree of topWorktrees) {
|
|
lines.push(formatWorktreeMemoryLine(worktree))
|
|
}
|
|
if (snapshot.worktrees.length > topWorktrees.length) {
|
|
lines.push(`... ${snapshot.worktrees.length - topWorktrees.length} more worktrees`)
|
|
}
|
|
return lines.join('\n')
|
|
}
|
|
|
|
function formatWorktreeMemoryLine(worktree: WorktreeMemory): string {
|
|
return [
|
|
`- ${worktree.worktreeName}`,
|
|
`${formatByteCount(worktree.memory)}`,
|
|
...(worktree.privateMemory === undefined
|
|
? []
|
|
: [`${formatByteCount(worktree.privateMemory)} committed`]),
|
|
`${formatCpu(worktree.cpu)}`,
|
|
`${worktree.sessions.length} session${worktree.sessions.length === 1 ? '' : 's'}`
|
|
].join(' ')
|
|
}
|
|
|
|
// Why omitted rather than zeroed: a host that predates the field, or cannot read
|
|
// commit at all, must not be printed as agents committing nothing.
|
|
function formatCommitLines(snapshot: MemorySnapshot): string[] {
|
|
if (typeof snapshot.totalPrivateMemory !== 'number') {
|
|
return []
|
|
}
|
|
return [
|
|
`totalPrivateMemory: ${formatByteCount(snapshot.totalPrivateMemory)}`,
|
|
`processCommitMetric: ${formatProcessCommitMetric(snapshot.processCommitMetric)}`
|
|
]
|
|
}
|
|
|
|
function formatProcessCommitMetric(metric: MemorySnapshot['processCommitMetric']): string {
|
|
return metric === 'private-bytes'
|
|
? 'summed private bytes; committed memory, counted whether resident or paged out'
|
|
: `summed ${metric ?? 'unknown'}`
|
|
}
|
|
|
|
function formatCpu(cpu: number): string {
|
|
return `${cpu.toFixed(1)}%`
|
|
}
|
|
|
|
function formatProcessMemoryMetric(metric: MemorySnapshot['processMemoryMetric']): string {
|
|
return metric === 'working-set'
|
|
? 'summed working set; shared pages may repeat'
|
|
: 'summed RSS; shared or aliased pages may repeat'
|
|
}
|
|
|
|
function formatByteCount(bytes: number): string {
|
|
if (!Number.isFinite(bytes) || bytes <= 0) {
|
|
return '0 B'
|
|
}
|
|
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
let value = bytes
|
|
let unitIndex = 0
|
|
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
value /= 1024
|
|
unitIndex += 1
|
|
}
|
|
const formatted = value >= 10 || unitIndex === 0 ? value.toFixed(0) : value.toFixed(1)
|
|
return `${formatted} ${units[unitIndex]}`
|
|
}
|
|
|
|
export function formatEnvironmentList(result: {
|
|
environments: PublicKnownRuntimeEnvironment[]
|
|
}): string {
|
|
if (result.environments.length === 0) {
|
|
return 'No saved environments.'
|
|
}
|
|
return result.environments
|
|
.map(
|
|
(environment) =>
|
|
`${environment.id} ${environment.name} ${environment.endpoints[0]?.endpoint ?? 'no-endpoint'}`
|
|
)
|
|
.join('\n')
|
|
}
|
|
|
|
export function formatEnvironment(environment: PublicKnownRuntimeEnvironment): string {
|
|
return [
|
|
`id: ${environment.id}`,
|
|
`name: ${environment.name}`,
|
|
`runtimeId: ${environment.runtimeId ?? 'unknown'}`,
|
|
`lastUsedAt: ${environment.lastUsedAt ?? 'never'}`,
|
|
`preferredEndpointId: ${environment.preferredEndpointId}`,
|
|
...environment.endpoints.map(
|
|
(endpoint) => `endpoint: ${endpoint.id} ${endpoint.kind} ${endpoint.endpoint}`
|
|
)
|
|
].join('\n')
|
|
}
|
|
|
|
export function formatWorktreePs(result: WithAnnotatedHostScope<RuntimeWorktreePsResult>): string {
|
|
const scope = formatListingHostScope(result.hostScope)
|
|
if (result.worktrees.length === 0) {
|
|
return `No worktrees found.\n${scope}`
|
|
}
|
|
const body = result.worktrees
|
|
.map(
|
|
(worktree) =>
|
|
`${worktree.repo} ${worktree.branch} host=${worktree.hostId ?? 'unverifiable'} live:${worktree.liveTerminalCount} pty:${worktree.hasAttachedPty ? 'yes' : 'no'} unread:${worktree.unread ? 'yes' : 'no'}\n${worktree.path}${worktree.preview ? `\npreview: ${worktree.preview}` : ''}`
|
|
)
|
|
.join('\n\n')
|
|
const bodyWithScope = `${body}\n\n${scope}`
|
|
return result.truncated
|
|
? `${bodyWithScope}\ntruncated: showing ${result.worktrees.length} of ${result.totalCount}`
|
|
: bodyWithScope
|
|
}
|
|
|
|
export function formatRepoList(result: RuntimeRepoList): string {
|
|
if (result.repos.length === 0) {
|
|
return 'No repos found.'
|
|
}
|
|
return result.repos.map((repo) => `${repo.id} ${repo.displayName} ${repo.path}`).join('\n')
|
|
}
|
|
|
|
export function formatRepoShow(result: { repo: Record<string, unknown> }): string {
|
|
return Object.entries(result.repo)
|
|
.map(
|
|
([key, value]) =>
|
|
`${key}: ${typeof value === 'object' ? JSON.stringify(value) : String(value)}`
|
|
)
|
|
.join('\n')
|
|
}
|
|
|
|
export function formatRepoRefs(result: RuntimeRepoSearchRefs): string {
|
|
if (result.refs.length === 0) {
|
|
return 'No refs found.'
|
|
}
|
|
return result.truncated ? `${result.refs.join('\n')}\n\ntruncated: yes` : result.refs.join('\n')
|
|
}
|
|
|
|
export function formatWorktreeList(
|
|
result: WithAnnotatedHostScope<RuntimeWorktreeListResult>
|
|
): string {
|
|
const scope = formatListingHostScope(result.hostScope)
|
|
if (result.worktrees.length === 0) {
|
|
return `No worktrees found.\n${scope}`
|
|
}
|
|
const body = result.worktrees
|
|
.map((worktree) => {
|
|
const childCount = worktree.childWorktreeIds?.length ?? 0
|
|
return `${String(worktree.id)} ${String(worktree.branch)} host=${String(worktree.hostId ?? 'unverifiable')} ${String(worktree.path)}\ndisplayName: ${String(worktree.displayName ?? '')}\nparentWorktreeId: ${String(worktree.parentWorktreeId ?? 'null')}\nchildWorktreeIds: ${childCount > 0 ? worktree.childWorktreeIds.join(',') : '[]'}\nlinkedIssue: ${String(worktree.linkedIssue ?? 'null')}\ncomment: ${String(worktree.comment ?? '')}`
|
|
})
|
|
.join('\n\n')
|
|
const bodyWithScope = `${body}\n\n${scope}`
|
|
return result.truncated
|
|
? `${bodyWithScope}\ntruncated: showing ${result.worktrees.length} of ${result.totalCount}`
|
|
: bodyWithScope
|
|
}
|
|
|
|
export function formatWorktreeShow(result: { worktree: RuntimeWorktreeRecord }): string {
|
|
const worktree = result.worktree
|
|
return Object.entries(worktree)
|
|
.map(
|
|
([key, value]) =>
|
|
`${key}: ${typeof value === 'object' ? JSON.stringify(value) : String(value)}`
|
|
)
|
|
.join('\n')
|
|
}
|