mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 10:08:13 +00:00
* fix(runtime): answer startup terminal queries for background-created terminals A runtime-created terminal (orchestration worker-start, `orca terminal create`) has no renderer pane until the user opens its tab. Main only answers terminal queries for PTYs the renderer marked hidden, so on a fresh app sitting on the landing screen nothing answered the agent's startup cursor-position query. Muse waits ~2s per unanswered CPR and then exits 0 with no output, which surfaced as `agent_readiness: timeout`. Background runtime spawns now carry initiallyHidden, mirroring the renderer's hidden-at-spawn path: fresh daemon sessions are marked before byte zero, the committed id is marked and paced as backgrounded, and the mark is released on failure, reattach, or adoption. A pane that later mounts visible unmarks and restores from the model snapshot as before. * fix(runtime): keep background PTYs hidden and paced across renderer reloads A runtime background spawn has no renderer pane to report visibility or re-mark it hidden, so it synced as foregrounded (no backpressure thinning) and a reload/crash gate reset cleared its hidden mark, leaving startup queries unanswered. Track runtime-owned hidden marks: they survive renderer-scoped resets, count as known-hidden for backgrounded pacing until a visible report, and are released by a renderer unmark or PTY teardown. * fix(runtime): don't re-hide a background PTY whose view mounted visible during spawn
186 lines
6.7 KiB
TypeScript
186 lines
6.7 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'
|
|
import type { PtyProcessInspection } from '../providers/pty-process-inspection'
|
|
import type { WriteSettlement } from '../../shared/pty-write-settlement'
|
|
|
|
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
|
|
/** Windows shell to spawn AS this PTY, instead of the host default. */
|
|
shellOverride?: string
|
|
isNewSession?: boolean
|
|
/** No renderer view exists at spawn; main owns delivery and query replies until one mounts. */
|
|
initiallyHidden?: boolean
|
|
persistHostSessionBinding?: boolean
|
|
expectedSourceBinding?: PtyBindingSourceExpectation
|
|
terminalKittyKeyboardProtocol?: boolean
|
|
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
|
|
/** Three-valued settlement; local providers settle synchronously. */
|
|
writeWithSettlement?(ptyId: string, data: string): WriteSettlement | Promise<WriteSettlement>
|
|
/** 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,
|
|
options?: { expectedIncarnationId?: PtyIncarnationId; scanChildProcesses?: boolean }
|
|
): Promise<PtyProcessInspection>
|
|
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; includeForegroundProcessEvidence?: boolean }
|
|
): Promise<PtyProcessInfo[]>
|
|
listProcessesWithHostScope?(opts?: {
|
|
deadlineMs?: number
|
|
includeForegroundProcessEvidence?: boolean
|
|
}): Promise<{
|
|
processes: PtyProcessInfo[]
|
|
hostIds: ExecutionHostId[]
|
|
}>
|
|
supportsForegroundProcessEvidence?(connectionId?: string | null): Promise<boolean>
|
|
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>
|
|
}>
|