mirror of
https://github.com/stablyai/orca.git
synced 2026-09-28 16:02:45 +00:00
* fix(runtime): agent-neutral wait-blocked reasons and non-Gemini Antigravity readiness Reported by a user via the in-app help menu (report "not captured", 1.4.198). The trust/interactive/update/cwd prompt matchers are agent-agnostic - they match on dialog wording and never inspect the pane's agent - yet emitted hardcoded codex-* reasons. Those reached users verbatim in worker receipts (local-worker-start, federation), two automation surfaces, and raw CLI output, so an Antigravity user was told they had a Codex problem. findAntigravityReadyPromptIndex also required the model line to start with the literal "gemini". Antigravity CLI is not Gemini-only, so a non-Gemini session never registered as ready, stale trust text was never superseded, and the pane stayed blocked - which is why dispatch --inject answered agent_prompt_blocked. Add agent-neutral reasons additively (codex-* members kept on the wire per docs/reference/remote-wire-compatibility.md, with a legacy alias for older hosts) and decide Antigravity readiness structurally: header, then model/account rows, then the prompt caret. codex-model-migration-prompt and codex-hooks-review-prompt stay Codex-named - both key on Codex's own wording. * fix(runtime): finish the agent-neutral rename, revert the Antigravity readiness rewrite Review follow-up on this branch. Splits the two halves of the original commit: the reason rename lands, the Antigravity readiness detector goes back to merge-base until someone captures a real transcript. Rename half: - 'hooks need review' + 'press enter to confirm' inspects no agent, so it now publishes agent-hooks-review-prompt. That was the last agent-agnostic codex-* emission left, and it is the one the original report was about: a Claude Code user hitting a hooks dialog still read "codex-hooks-review-prompt". - The legacy alias is applied at all three surfaces that render a raw reason, not just the CLI. describeTerminalWaitBlockedReason() is the single formatter; the worker and federation "Agent startup blocked:" receipts use it too. Kept one-directional: nothing consumes agent-* -> codex-*, since an old client renders with its own shipped code. - Restores the compat note deleted at the permission-choices site. The Rule 1 citation is correct - remote-wire-compatibility.md names this enum by name. Antigravity half, reverted: findAntigravityReadyPromptIndex goes back to merge-base (header + a 'gemini' model line + a lone '>' caret) and antigravity-ready-prompt-index.ts is removed. Executing both builds against constructed tails, the rewrite read a live startup dialog as ready. Adding the account row from this repo's own ready-screen fixture to five silent startup dialogs (sign-in, model picker, theme picker, privacy notice, update banner) flipped all five from unready to ready; so did any narration line containing an email address, with no account row at all. Readiness is what gates typing the task prompt into the pane, so that path types a task prompt into a live authentication dialog. Merge-base returns unready for all ten. The rewrite also did not reliably fix the wedge it targeted: with no account row and a non-Gemini model - a personal or API-key user - it still returns unready. No real Antigravity transcript exists in this repo. The cursor-agent rules are derived from captures under src/main/runtime/__fixtures__; Antigravity has no equivalent, and every attempt so far has been tuned against a hand-written 5-line fixture. A false negative (the agent waits) is safer than a false positive (we type into an auth dialog), so this ships the known behaviour. Reverting restores a pre-existing gap, not a regression: a non-Gemini Antigravity session wedges on merge-base too. Closing it needs a captured ready screen and a captured dismissed-dialog screen, for a personal/API-key account as well as a Business one. Tests: - Ten ratchet fixtures pin the shapes any replacement detector must refuse - the five silent dialogs with an account row, and each with a narrated email. All ten fail against the reverted rewrite. - Vacuous tests rewritten so they fail without the code they cover: the CLI alias tests asserted only the absence of a suffix, and the worker receipt test asserted the raw token. Tests that are characterization rather than a guard now say so on the line above. --------- Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local> Co-authored-by: Neil <neil@stably.ai>
360 lines
10 KiB
TypeScript
360 lines
10 KiB
TypeScript
import type { AgentSessionPtyWriteRefusal } from './agent-session-pty-write-admission'
|
|
import type {
|
|
AgentProviderSessionMetadata,
|
|
SleepingAgentLaunchConfig
|
|
} from './agent-session-resume'
|
|
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'
|
|
import type { TerminalPaneLayoutNode } from './terminal-tab-types'
|
|
import type { TuiAgent } from './tui-agent'
|
|
|
|
export type RuntimeTerminalSummary = {
|
|
handle: string
|
|
ptyId: string | null
|
|
incarnationId?: string | null
|
|
orphaned?: boolean
|
|
worktreeId: string
|
|
worktreePath: string
|
|
branch: string
|
|
tabId: string
|
|
leafId: string
|
|
title: string | null
|
|
connected: boolean
|
|
writable: boolean
|
|
lastOutputAt: number | null
|
|
preview: string
|
|
/** Host-resolved agent identity for action consumers; absent when unknown or unsupported. */
|
|
agentIdentity?: TuiAgent
|
|
/** Absent while running or when the host predates the field; never infer a clean finish. */
|
|
exitCause?: TerminalExitCause
|
|
/** Absent when the host predates the field or could not name the execution host. */
|
|
executionHostId?: ExecutionHostId
|
|
}
|
|
|
|
export type RuntimeTerminalVisualTerminalNode = {
|
|
type: 'terminal'
|
|
handle: string
|
|
tabId: string
|
|
leafId: string
|
|
title: string | null
|
|
connected: boolean
|
|
active: boolean
|
|
}
|
|
|
|
export type RuntimeTerminalVisualPaneNode =
|
|
| RuntimeTerminalVisualTerminalNode
|
|
| {
|
|
type: 'pane-split'
|
|
direction: Extract<TerminalPaneLayoutNode, { type: 'split' }>['direction']
|
|
first: RuntimeTerminalVisualPaneNode
|
|
second: RuntimeTerminalVisualPaneNode
|
|
}
|
|
|
|
export type RuntimeTerminalVisualTab = {
|
|
tabId: string
|
|
title: string | null
|
|
activeLeafId: string | null
|
|
panes: RuntimeTerminalVisualPaneNode
|
|
}
|
|
|
|
export type RuntimeTerminalVisualGroupNode = {
|
|
type: 'group'
|
|
groupId: string | null
|
|
activeTabId: string | null
|
|
tabs: RuntimeTerminalVisualTab[]
|
|
}
|
|
|
|
export type RuntimeTerminalVisualLayoutNode =
|
|
| RuntimeTerminalVisualGroupNode
|
|
| {
|
|
type: 'split'
|
|
direction: Extract<TabGroupLayoutNode, { type: 'split' }>['direction']
|
|
first: RuntimeTerminalVisualLayoutNode
|
|
second: RuntimeTerminalVisualLayoutNode
|
|
}
|
|
|
|
export type RuntimeTerminalVisualLayout = {
|
|
worktreeId: string
|
|
worktreePath: string
|
|
root: RuntimeTerminalVisualLayoutNode
|
|
}
|
|
|
|
/** The shared listing-scope shape, kept under its incumbent name for existing consumers. */
|
|
export type RuntimeTerminalListHostScope = RuntimeListingHostScope
|
|
|
|
export type RuntimeTerminalListResult = {
|
|
terminals: RuntimeTerminalSummary[]
|
|
visualLayouts?: RuntimeTerminalVisualLayout[]
|
|
topologyRevisions?: Record<string, number>
|
|
totalCount: number
|
|
truncated: boolean
|
|
/** Absent from hosts that predate the field; treat that scope as unverifiable. */
|
|
hostScope?: RuntimeTerminalListHostScope
|
|
}
|
|
|
|
export type RuntimeTerminalOrphanAdoptionClaim = {
|
|
terminal: string
|
|
ptyId: string
|
|
incarnationId: PtyIncarnationId
|
|
tabId: string
|
|
leafId: string
|
|
}
|
|
|
|
export type RuntimeTerminalOrphanTopologyTab = {
|
|
tabId: string
|
|
root: TerminalPaneLayoutNode
|
|
activeLeafId: string
|
|
expandedLeafId: string | null
|
|
}
|
|
|
|
export type RuntimeTerminalOrphanTopologyGroup = {
|
|
id: string
|
|
activeTabId: string
|
|
tabOrder: string[]
|
|
recentTabIds?: string[]
|
|
}
|
|
|
|
export type RuntimeTerminalOrphanTopology = {
|
|
tabs: RuntimeTerminalOrphanTopologyTab[]
|
|
groups: RuntimeTerminalOrphanTopologyGroup[]
|
|
groupLayout?: TabGroupLayoutNode
|
|
}
|
|
|
|
export type RuntimeTerminalOrphanAdoptionRequest = {
|
|
worktree: string
|
|
expectedTopologyRevision: number
|
|
claims: RuntimeTerminalOrphanAdoptionClaim[]
|
|
activeTabId?: string
|
|
activeGroupId?: string
|
|
topology?: RuntimeTerminalOrphanTopology
|
|
}
|
|
|
|
export type RuntimeTerminalOrphanAdoptionResult = {
|
|
adopted: boolean
|
|
topologyRevision: number
|
|
snapshot: RuntimeMobileSessionTabsResult
|
|
}
|
|
|
|
export type RuntimeWorktreeTerminalSleepResult = {
|
|
stopped: number
|
|
stoppedPtyIds: string[]
|
|
livePtyIds: string[]
|
|
} & (
|
|
| { postStopVerified: true; postStopFailure?: never; remainingLivePtyIds?: never }
|
|
| {
|
|
postStopVerified: false
|
|
postStopFailure: 'terminal_liveness_unavailable'
|
|
remainingLivePtyIds?: never
|
|
}
|
|
| {
|
|
postStopVerified: false
|
|
postStopFailure: 'terminal_worktree_sleep_still_live'
|
|
remainingLivePtyIds: string[]
|
|
}
|
|
)
|
|
|
|
export type RuntimeWorktreeTerminalCloseResult = {
|
|
closed: number
|
|
stopped: number
|
|
retiredSurfaces: true
|
|
ptyStopVerdict?: 'live' | 'unverifiable'
|
|
ptyStopReason?: string
|
|
}
|
|
|
|
export type RuntimeTerminalInteractiveWaitSource = 'hook' | 'prompt-text' | 'title'
|
|
|
|
export type RuntimeTerminalInteractiveWait = {
|
|
source: RuntimeTerminalInteractiveWaitSource
|
|
reason?: RuntimeTerminalWaitBlockedReason
|
|
since?: number
|
|
}
|
|
|
|
export type RuntimeTerminalShow = RuntimeTerminalSummary & {
|
|
paneRuntimeId: number
|
|
ptyId: string | null
|
|
rendererGraphEpoch: number
|
|
/** Null means evaluated with no wait; absent means not evaluated. */
|
|
agentWait?: RuntimeTerminalInteractiveWait | null
|
|
}
|
|
|
|
export type RuntimeTerminalState = 'running' | 'exited' | 'unknown'
|
|
|
|
export type RuntimeTerminalRead = {
|
|
handle: string
|
|
status: RuntimeTerminalState
|
|
tail: string[]
|
|
truncated: boolean
|
|
limited?: boolean
|
|
oldestCursor?: string
|
|
nextCursor: string | null
|
|
latestCursor?: string
|
|
returnedLineCount?: number
|
|
source?: 'stream' | 'screen' | 'screen-unavailable'
|
|
/** UI-only composer text, excluded from `tail`. */
|
|
draft?: string
|
|
}
|
|
|
|
export type RuntimeTerminalRename = {
|
|
handle: string
|
|
tabId: string
|
|
title: string | null
|
|
}
|
|
|
|
export type RuntimeTerminalSend = {
|
|
handle: string
|
|
accepted: boolean
|
|
bytesWritten: number
|
|
refusedReason?: 'no-agent' | 'permission'
|
|
/**
|
|
* Present only when a durable agent-session lease refused the write. Additive and optional: an
|
|
* old client sees the `accepted: false` it already handles and ignores this field.
|
|
*/
|
|
agentSessionRefusal?: AgentSessionPtyWriteRefusal
|
|
prompt?: RuntimeTerminalPromptDelivery
|
|
}
|
|
|
|
export type RuntimeTerminalPromptStage = 'input_accepted' | 'turn_started'
|
|
|
|
export type RuntimeTerminalPromptDelivery = {
|
|
requestId: string
|
|
stages: RuntimeTerminalPromptStage[]
|
|
provider: 'claude' | 'codex' | 'unsupported' | 'old-host'
|
|
observation: 'supported' | 'unsupported' | 'incarnation_replaced' | 'permission'
|
|
processIncarnation: string
|
|
generation: number
|
|
baselineWorkingSequence: number
|
|
/** Hook turn-start timestamp before this prompt was accepted. */
|
|
baselineExplicitWorkingStartedAt?: number | null
|
|
/** Permission observations seen before this prompt was accepted. */
|
|
baselinePermissionSequence?: number
|
|
}
|
|
|
|
export type RuntimeTerminalAgentStatusState = 'working' | 'permission' | 'idle' | null
|
|
|
|
export type RuntimeTerminalAgentStatus = {
|
|
handle: string
|
|
isRunningAgent: boolean
|
|
status: RuntimeTerminalAgentStatusState
|
|
}
|
|
|
|
export type RuntimeTerminalPresentation = 'background' | 'focused'
|
|
|
|
type RuntimeTerminalCreateBaseRequestPayload = {
|
|
requestId: string
|
|
worktreeId?: string
|
|
afterTabId?: string
|
|
targetGroupId?: string
|
|
command?: string
|
|
cwd?: string
|
|
env?: Record<string, string>
|
|
envToDelete?: string[]
|
|
launchConfig?: SleepingAgentLaunchConfig
|
|
resumeProviderSession?: AgentProviderSessionMetadata
|
|
launchToken?: string
|
|
launchAgent?: TuiAgent
|
|
viewMode?: 'terminal' | 'chat'
|
|
startupCommandDelivery?: StartupCommandDelivery
|
|
title?: string
|
|
activate?: boolean
|
|
presentation?: RuntimeTerminalPresentation
|
|
surfaceOwner?: false
|
|
}
|
|
|
|
export type RuntimeTerminalCreateRequestPayload =
|
|
| (RuntimeTerminalCreateBaseRequestPayload & { source?: undefined })
|
|
| (RuntimeTerminalCreateBaseRequestPayload & {
|
|
worktreeId: string
|
|
source: 'runtime-session'
|
|
})
|
|
|
|
export type RuntimeTerminalCreate = {
|
|
handle: string
|
|
/** Host-owned PTY incarnation used to fence remote identity observations. */
|
|
incarnationId?: string | null
|
|
tabId?: string
|
|
paneKey?: string | null
|
|
ptyId?: string | null
|
|
worktreeId: string
|
|
title: string | null
|
|
executionHostId?: ExecutionHostId
|
|
hostPlatform?: NodeJS.Platform
|
|
surface?: 'background' | 'visible'
|
|
warning?: string
|
|
agentSessionDisposition?: 'created' | 'adopted'
|
|
isReattach?: true
|
|
/** Spawn process identity for host-internal ownership proof. */
|
|
processId?: number
|
|
}
|
|
|
|
export type RuntimeTerminalSplit = {
|
|
handle: string
|
|
tabId: string
|
|
paneRuntimeId: number
|
|
// Why: paired callers need the host-created leaf identity to focus the exact pane.
|
|
leafId?: string
|
|
}
|
|
|
|
export type RuntimeTerminalResolvePane = {
|
|
handle: string
|
|
/** Host-owned PTY incarnation used to fence remote identity observations. */
|
|
incarnationId?: string | null
|
|
tabId: string
|
|
leafId: string
|
|
ptyId: string | null
|
|
connected?: boolean
|
|
worktreeId?: string
|
|
executionHostId?: ExecutionHostId
|
|
hostPlatform?: NodeJS.Platform
|
|
}
|
|
|
|
export type RuntimeTerminalFocus = {
|
|
handle: string
|
|
tabId: string
|
|
worktreeId: string
|
|
navigated?: boolean
|
|
}
|
|
|
|
export type RuntimeTerminalClose = {
|
|
handle: string
|
|
tabId: string
|
|
closeMode?: 'tab'
|
|
ptyKilled: boolean
|
|
ptyStopVerdict?: 'live' | 'unverifiable'
|
|
ptyStopReason?: string
|
|
}
|
|
|
|
export type RuntimeTerminalWaitCondition = 'exit' | 'tui-idle'
|
|
|
|
// Why both spellings: the codex-* members were published by every host before the agent-neutral
|
|
// rename, so they are permanent — a client still has to read them off an older host. This build
|
|
// keeps a codex-* reason only where the matched wording is plausibly Codex's own; every matcher
|
|
// that inspects no agent publishes the agent-* spelling.
|
|
export type RuntimeTerminalWaitBlockedReason =
|
|
| 'codex-update-prompt'
|
|
| 'codex-trust-workspace'
|
|
| 'codex-cwd-prompt'
|
|
| 'codex-model-migration-prompt'
|
|
| 'codex-hooks-review-prompt'
|
|
| 'codex-interactive-prompt'
|
|
| 'agent-update-prompt'
|
|
| 'agent-trust-workspace'
|
|
| 'agent-cwd-prompt'
|
|
| 'agent-hooks-review-prompt'
|
|
| 'agent-interactive-prompt'
|
|
| 'agent-approval-prompt'
|
|
|
|
export type RuntimeTerminalWait = {
|
|
handle: string
|
|
condition: RuntimeTerminalWaitCondition
|
|
satisfied: boolean
|
|
status: RuntimeTerminalState
|
|
exitCode: number | null
|
|
exitCause?: TerminalExitCause
|
|
blockedReason?: RuntimeTerminalWaitBlockedReason
|
|
}
|