mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
* fix(remote): resolve workspace cwd, mise Node, host scope, and TUI scrollback honestly #15296 relay: a folder workspace id (`folder:<uuid>`) carries no path, so the worktree-id split yielded nothing and $HOME silently won. Resolve the spawn cwd through worktreeId -> ORCA_WORKSPACE_ROOT -> host default, and refuse an agent spawn outright when a folder workspace names a root this host cannot resolve. #11733 ssh: generalize the NVM dotfile scrape into `orca_dotfile_dirs` and drive mise off `MISE_DATA_DIR` / `XDG_DATA_HOME` instead of a hardcoded `$HOME/.local/share/mise`. #13713 ai-vault: an unresolvable workspace host is `unverifiable`, not local. Widen the default scope to every host rather than scanning the client's own history and reporting "No agent sessions found". #6106 terminal: hydration asked the renderer for `scrollback: 0` while an alt-screen TUI was up, which drops the normal buffer's shell history rather than the TUI bytes. Drop the flag; readers already split the two buffers apart. * fix(remote): stop the relay answering host questions for a guest execution host Three findings from review of the spawn-cwd resolver, all the same shape: a path question answered against the wrong host, or with the wrong key. - resolveRelaySpawnCwd refused an agent launch whenever a folder workspace named a root that did not stat on the relay. But relayHostDirectoryExists stats the relay's *own* filesystem, and the relay supports WSL shells, so a folder workspace on a Windows relay launching into WSL now threw where it previously spawned -- contradicting the function's own doc comment, which says an absent path for that exact host pair is a miss, not a refusal. Thread the shell's execution host in and demote the refusal to a miss when the spawn does not run on the relay's filesystem. - requireRelaySpawnCwd's doc claims both call sites route through one resolver so the fence can never be keyed on a directory the spawn won't use, but the fence key was still computed with the non-stripping splitWorktreeId while the cwd used splitWorktreeIdForFilesystem. For a `::workspace:<uuid>` id those disagree by construction, in adjacent lines: the removal fence guarded a path no spawn ever enters. Same defect in shutdownForWorktreePath and the revive path; all three now use the filesystem split. - The remote Node probe expanded `$HOME` and `~/` prefixes out of a dotfile assignment but not `$XDG_DATA_HOME`, so `MISE_DATA_DIR=$XDG_DATA_HOME/...` was used as a literal directory name. Add the case arm, defaulting to the POSIX `$HOME/.local/share` the seed value already uses -- sshd's exec channel usually has no XDG_DATA_HOME at all.
173 lines
6.0 KiB
TypeScript
173 lines
6.0 KiB
TypeScript
import type {
|
|
AgentSessionClaimedSpawnResult,
|
|
AgentSessionExecutionClaim,
|
|
AgentSessionSurfaceBinding
|
|
} from '../../shared/agent-session-host-authority'
|
|
import type { AgentProviderSessionMetadata } from '../../shared/agent-session-resume'
|
|
import type { TuiAgent } from '../../shared/tui-agent'
|
|
import type { WorktreeStartupLaunch } from '../../shared/worktree/launch-types'
|
|
import type { PtyIncarnationId } from '../../shared/pty-incarnation'
|
|
import type { PtyBindingSourceExpectation } from '../persistence'
|
|
import type { ExecutionHostId } from '../../shared/execution-host'
|
|
import type { PtyProviderBufferSnapshot, PtyProcessInfo, PtySpawnResult } from '../providers/types'
|
|
|
|
export type RuntimePtyController = {
|
|
claimStablePaneCreate?(args: {
|
|
worktreeId: string
|
|
connectionId: string | null
|
|
tabId: string
|
|
leafId: string
|
|
}): () => void
|
|
adoptStablePane?(opts: {
|
|
cols: number
|
|
rows: number
|
|
cwd?: string
|
|
connectionId: string | null
|
|
worktreeId: string
|
|
preAllocatedHandle: string
|
|
tabId: string
|
|
leafId: string
|
|
}): Promise<{
|
|
result: PtySpawnResult
|
|
owner: {
|
|
handle?: string
|
|
tabId: string
|
|
leafId: string
|
|
ptyId: string
|
|
incarnationId?: string
|
|
}
|
|
materialized?: true
|
|
} | null>
|
|
spawn?(opts: {
|
|
cols: number
|
|
rows: number
|
|
cwd?: string
|
|
command?: string
|
|
launchAgent?: TuiAgent
|
|
commandDelivery?: 'renderer' | 'provider'
|
|
startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery']
|
|
env?: Record<string, string>
|
|
envToDelete?: string[]
|
|
resumeProviderSession?: AgentProviderSessionMetadata
|
|
telemetry?: WorktreeStartupLaunch['telemetry']
|
|
connectionId?: string | null
|
|
worktreeId?: string
|
|
preAllocatedHandle?: string
|
|
tabId?: string
|
|
leafId?: string
|
|
sessionId?: string
|
|
isNewSession?: boolean
|
|
persistHostSessionBinding?: boolean
|
|
expectedSourceBinding?: PtyBindingSourceExpectation
|
|
terminalColorQueryReplies?: { foreground?: string; background?: string }
|
|
agentSessionEnsure?: {
|
|
claim: AgentSessionExecutionClaim
|
|
surface: AgentSessionSurfaceBinding
|
|
}
|
|
agentSessionCreateOperationId?: string
|
|
signal?: AbortSignal
|
|
onPtySpawnCommitted?: () => void
|
|
adoptedStablePane?: {
|
|
result: PtySpawnResult
|
|
owner: {
|
|
handle?: string
|
|
tabId: string
|
|
leafId: string
|
|
ptyId: string
|
|
incarnationId?: string
|
|
}
|
|
materialized?: true
|
|
}
|
|
}): Promise<{
|
|
id: string
|
|
pid?: number | null
|
|
incarnationId?: PtyIncarnationId
|
|
wslDistro?: string
|
|
stablePaneOwner?: { handle: string; tabId: string; leafId: string }
|
|
agentSessionEnsure?: AgentSessionClaimedSpawnResult
|
|
}>
|
|
write(ptyId: string, data: string): boolean
|
|
writeAgentSessionProof?(
|
|
ptyId: string,
|
|
data: string,
|
|
authority: { sessionId: string; spawnToken: string }
|
|
): boolean
|
|
writeWithSettlement?(ptyId: string, data: string): Promise<boolean>
|
|
/** Attach-only adoption of a live local daemon session so its output streams
|
|
* to main without a renderer pane; never creates, resizes, or focuses.
|
|
* False on doubt (absent session, SSH-scoped id, non-daemon provider). */
|
|
attach?(ptyId: string): Promise<boolean>
|
|
kill(ptyId: string): boolean
|
|
retireRejectedPty?(ptyId: string, stopConfirmed: boolean): void
|
|
stopAndWait?(
|
|
ptyId: string,
|
|
opts?: { keepHistory?: boolean; deadlineMs?: number }
|
|
): Promise<boolean>
|
|
markReversibleStops?(ptyIds: readonly string[]): () => void
|
|
getCwd?(ptyId: string): Promise<string | null>
|
|
getForegroundProcess(ptyId: string): Promise<string | null>
|
|
inspectProcess?(
|
|
ptyId: string
|
|
): Promise<{ foregroundProcess: string | null; hasChildProcesses: boolean; unavailable?: true }>
|
|
confirmForegroundProcess?(ptyId: string): Promise<string | null>
|
|
confirmShellForeground?(ptyId: string): Promise<boolean>
|
|
hasChildProcesses?(ptyId: string): Promise<boolean>
|
|
clearBuffer?(ptyId: string): Promise<void>
|
|
resize?(ptyId: string, cols: number, rows: number): boolean
|
|
// Why: exact-id mobile polls should not enumerate every local and SSH PTY.
|
|
hasPty?(ptyId: string): boolean | null
|
|
listProcesses?(
|
|
connectionId?: string | null,
|
|
opts?: { deadlineMs?: number }
|
|
): Promise<PtyProcessInfo[]>
|
|
listProcessesWithHostScope?(opts?: { deadlineMs?: number }): Promise<{
|
|
processes: PtyProcessInfo[]
|
|
hostIds: ExecutionHostId[]
|
|
}>
|
|
serializeBuffer?(
|
|
ptyId: string,
|
|
opts?: { scrollbackRows?: number }
|
|
): Promise<{
|
|
data: string
|
|
cols: number
|
|
rows: number
|
|
seq?: number
|
|
lastTitle?: string
|
|
kittyKeyboardFlags?: number
|
|
} | null>
|
|
/** Authoritative provider-owned snapshot for restored PTYs with no mounted renderer. */
|
|
serializeProviderBuffer?(
|
|
ptyId: string,
|
|
opts?: { scrollbackRows?: number }
|
|
): Promise<PtyProviderBufferSnapshot | null>
|
|
// Why: synchronous probe used by maybeHydrateHeadlessFromRenderer to skip
|
|
// hydration when no renderer is authoritative for this PTY. See
|
|
// docs/mobile-prefer-renderer-scrollback.md.
|
|
hasRendererSerializer?(ptyId: string): boolean
|
|
getRendererSerializerGeneration?(ptyId: string): number
|
|
waitForRendererSerializer?(
|
|
ptyId: string,
|
|
afterGeneration: number,
|
|
timeoutMs?: number,
|
|
signal?: AbortSignal
|
|
): Promise<boolean>
|
|
getSize?(ptyId: string): { cols: number; rows: number } | null
|
|
/** False only when the owning provider proved the PTY absent; null = unknown (never a denial). */
|
|
probePtyLiveness?(ptyId: string): Promise<boolean | null>
|
|
}
|
|
|
|
export type PtyControllerTerminalIdentity = Readonly<{
|
|
handle: string
|
|
incarnationId: string
|
|
wslDistro?: string | null
|
|
}>
|
|
|
|
export type PtyControllerInventory = Readonly<{
|
|
livePtyIds: ReadonlySet<string>
|
|
// Why: livePtyIds is worktree-scoped when a target is given; absence proofs
|
|
// must consult the unscoped inventory or a misattributed live PTY reads as dead.
|
|
allLivePtyIds: ReadonlySet<string>
|
|
terminalIdentityByPtyId: ReadonlyMap<string, PtyControllerTerminalIdentity>
|
|
queriedHostIds: ReadonlySet<ExecutionHostId>
|
|
}>
|