mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 11:07:42 +00:00
* fix(worktree): gate agent activation on the live surface census, not renderer state (STA-5701)
* fix(worktree): seed a pane when the surface census cannot prove ownership (STA-5701)
Failing closed must not also fail silent. When the census is unverifiable
the sweep adopts nothing and mints nothing, yet the gate still reported
'adopted' — and both callers suppress their own seeding on any outcome but
'empty', so the workspace ended with zero surfaces. The sweep now reports
whether any live PTY holds a surface and the gate hands the caller its seed
when none does. Also folds equivalent workspace-path spellings in the census
index and in exact-surface binding, so a host row spelled differently is
neither dropped (mint a duplicate) nor unbindable (no pane).
* fix(worktree): name the live PTYs the surface census declined (STA-5701)
The adoption sweep can leave a live PTY without a surface — an unreadable
census, two host surfaces claiming one PTY, or a host-named leaf the
persisted layout does not have. The gate already stops reporting 'adopted'
in that case so the caller seeds a shell, but the decline itself was mute.
- adoptLiveWorkspacePtySurfaces now returns { surfaced, declinedPtyIds }
and the gate warns with the workspace and the PTY ids left unsurfaced.
- Pin the host-named-leaf decline, which had no test either way.
- Pin the superseded-inventory race in terminal.list: a concurrent refresh
makes hostScope.hostIds empty, which is what makes the renderer's
'unverifiable' verdict reachable on a plain local machine.
92 lines
3.6 KiB
TypeScript
92 lines
3.6 KiB
TypeScript
import { normalizeRuntimePathForComparison } from '../cross-platform-path'
|
|
import { WORKTREE_ID_SEPARATOR } from '../pty-session-id-format'
|
|
import type { Repo } from '../repo-types'
|
|
|
|
export { WORKTREE_ID_SEPARATOR } from '../pty-session-id-format'
|
|
|
|
export type ParsedWorktreeId = {
|
|
repoId: string
|
|
worktreePath: string
|
|
}
|
|
|
|
export const FOLDER_WORKSPACE_INSTANCE_SEPARATOR = '::workspace:'
|
|
const FOLDER_WORKSPACE_INSTANCE_SUFFIX = new RegExp(
|
|
`${FOLDER_WORKSPACE_INSTANCE_SEPARATOR.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[0-9a-f-]{36}$`
|
|
)
|
|
|
|
/**
|
|
* Worktree id of the repo's own checkout. A bare repo id is never a valid worktree id —
|
|
* runtimes reject it with `worktree_id_requires_full_path` (#16447).
|
|
*/
|
|
export function getRepoMainWorktreeId(repo: Pick<Repo, 'id' | 'path'>): string {
|
|
return `${repo.id}${WORKTREE_ID_SEPARATOR}${repo.path}`
|
|
}
|
|
|
|
export function getRepoIdFromWorktreeId(worktreeId: string): string {
|
|
const separatorIdx = worktreeId.indexOf(WORKTREE_ID_SEPARATOR)
|
|
return separatorIdx === -1 ? worktreeId : worktreeId.slice(0, separatorIdx)
|
|
}
|
|
|
|
/**
|
|
* Canonical comparison form of a worktree id: the repoId is compared EXACT and only the path folds,
|
|
* through the same `normalizeRuntimePathForComparison` a `path:` selector has always applied and
|
|
* byte-exact id matching denied the renderer (#16243). Null for a malformed id, so callers keep
|
|
* exact matching for it. Comparison only — never persist or return this key.
|
|
*/
|
|
export function worktreeIdComparisonKey(worktreeId: string): string | null {
|
|
const parsed = splitWorktreeId(worktreeId)
|
|
if (!parsed || !parsed.repoId || !parsed.worktreePath) {
|
|
return null
|
|
}
|
|
return `${parsed.repoId}${WORKTREE_ID_SEPARATOR}${normalizeRuntimePathForComparison(
|
|
parsed.worktreePath
|
|
)}`
|
|
}
|
|
|
|
/**
|
|
* Why: workspace identity is per *workspace*, not per checkout dir. Folder projects back several
|
|
* independent workspaces with one directory, separated only by the `::workspace:<uuid>` suffix that
|
|
* filesystem callers must strip; stripping it here instead lets one session steal a sibling's PTYs.
|
|
* Normalize only path spelling, so Windows/WSL/SSH ids still match themselves across hosts, and
|
|
* fall back to exact equality for a malformed id.
|
|
*/
|
|
export function worktreeIdsEqual(left: string, right: string): boolean {
|
|
const leftKey = worktreeIdComparisonKey(left)
|
|
return leftKey === null ? left === right : leftKey === worktreeIdComparisonKey(right)
|
|
}
|
|
|
|
export function splitWorktreeId(worktreeId: string): ParsedWorktreeId | null {
|
|
const separatorIdx = worktreeId.indexOf(WORKTREE_ID_SEPARATOR)
|
|
if (separatorIdx === -1) {
|
|
return null
|
|
}
|
|
return {
|
|
repoId: worktreeId.slice(0, separatorIdx),
|
|
worktreePath: worktreeId.slice(separatorIdx + WORKTREE_ID_SEPARATOR.length)
|
|
}
|
|
}
|
|
|
|
export function splitWorktreeIdForFilesystem(worktreeId: string): ParsedWorktreeId | null {
|
|
const parsed = splitWorktreeId(worktreeId)
|
|
if (!parsed) {
|
|
return null
|
|
}
|
|
return {
|
|
repoId: parsed.repoId,
|
|
// Why: folder projects can have multiple workspace sessions backed by the
|
|
// same directory. Their IDs carry a UUID suffix, but filesystem callers
|
|
// still need the real folder path as cwd/root.
|
|
worktreePath: parsed.worktreePath.replace(FOLDER_WORKSPACE_INSTANCE_SUFFIX, '')
|
|
}
|
|
}
|
|
|
|
export function getWorktreePathBasenameFromId(worktreeId: string): string | null {
|
|
const parsed = splitWorktreeIdForFilesystem(worktreeId)
|
|
const normalizedPath = parsed?.worktreePath.trim().replace(/[\\/]+$/g, '') ?? ''
|
|
if (!normalizedPath) {
|
|
return null
|
|
}
|
|
const basename = normalizedPath.split(/[\\/]/).findLast(Boolean)?.trim()
|
|
return basename || null
|
|
}
|