Merge pull request #414 from nwparker/cloud/ra11e-fallback-fix

fix(cloud): RA-11e resume backstop watches the relaunched terminal (25 s, ANSI-safe, logged)
This commit is contained in:
Neil
2026-11-11 03:13:35 -08:00
committed by GitHub
3 changed files with 68 additions and 13 deletions
@@ -26,6 +26,7 @@ import {
parseRemoteRuntimePtyId
} from '../../../../../shared/remote-runtime/remote-runtime-pty-id'
import { isOrcaCloudEnvironmentId } from '@/lib/orca-cloud/orca-cloud-environment-id'
import { ORCA_CLOUD_SESSION_ENV_REFRESH } from '@/lib/orca-cloud/cloud-restart-session-env'
import { buildFreshAccountRestartCommand } from '@/lib/agent-session/accounts/agent-account-restart-startup'
import type { AccountRestartAgent } from '@/store/terminals/terminal-contracts'
import type { usePaneAccountRestartStartup } from './use-pane-account-restart-startup'
@@ -80,6 +81,16 @@ export function useReadyPaneAccountRestarts({
type ReadyRestartPane = { id: number; leafId: string }
/** The fresh command; if it can't be built from settings, the agent's plain launch after the env refresh. */
function freshCommandFor(build: () => string, agent: AccountRestartAgent): string {
try {
return build()
} catch (err) {
console.warn('[orca-cloud] RA-11e fresh command fell back to the plain launch:', err)
return `${ORCA_CLOUD_SESSION_ENV_REFRESH}${agent}`
}
}
function worktreeIdForTab(tabId: string): string | null {
for (const [worktreeId, tabs] of Object.entries(useAppStore.getState().tabsByWorktree)) {
if (tabs.some((tab) => tab.id === tabId)) {
@@ -211,7 +222,10 @@ export function runReadyPaneAccountRestarts<
pane.leafId
] ?? null,
replacedPtyId: ptyId,
freshCommand: args.buildFreshCommand(agent, pane.leafId)
freshCommand: freshCommandFor(
() => args.buildFreshCommand(agent, pane.leafId),
agent
)
})
)
.then((startedFresh) => {
@@ -113,6 +113,21 @@ describe('freshIfResumeFoundNothing (RA-11e)', () => {
})
})
it('ANSI-coloured output and a late message (after 15 s) are still caught', async () => {
readTail = ['\u001b[31mNo conversation found with session ID: s1\u001b[39m', '$ ']
let reads = 0
const relaunched = await freshIfResumeFoundNothing({
// The pane binds its new terminal only after 15 s.
currentPtyId: () => (++reads > 15 ? 'remote:env-1@@term_new' : 'remote:env-1@@term_old'),
replacedPtyId: 'remote:env-1@@term_old',
freshCommand: 'claude',
sleep,
now
})
expect(relaunched).toBe(true)
expect(calls.filter((c) => c.method === 'terminal.send')).toHaveLength(1)
})
it('a resume that worked is left alone', async () => {
readTail = ['Claude Code v2.1.319', '❯ say hi 1']
const relaunched = await freshIfResumeFoundNothing({
@@ -92,19 +92,30 @@ export async function resumeRemotePaneAgent(args: {
/** What Claude prints when `--resume <id>` names a session that never had a conversation. */
const NO_CONVERSATION = /No conversation found with session ID/i
const RESUME_CHECK_MS = 10_000
// Claude's own start on a sandbox plus the pane's re-attach can take well over 10 s.
const RESUME_CHECK_MS = 25_000
const RESUME_POLL_MS = 1_000
// eslint-disable-next-line no-control-regex -- terminal output carries ANSI escapes.
const ANSI = /\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][^\u0007]*\u0007/g
/** The pane's terminal handle on its host, from the pane's PTY id (`remote:<env>@@<handle>`). */
function hostTerminalOf(ptyId: string | null): { environmentId: string; handle: string } | null {
const parsed = ptyId ? parseRemoteRuntimePtyId(ptyId) : null
return parsed?.environmentId
? { environmentId: parsed.environmentId, handle: parsed.handle }
: null
}
/**
* RA-11e backstop: a recorded session can have no conversation on disk (Claude started, never ran a
* turn). Its `--resume` prints "No conversation found…" and exits, leaving a bare shell. Watch the
* relaunched pane briefly; if that happened, start the agent fresh in the same shell, once.
* Returns whether it relaunched fresh.
* relaunched pane's terminal (its new handle) for up to 25 s; if that happened, start the agent fresh
* in the same shell, once. Returns whether it relaunched fresh.
*/
export async function freshIfResumeFoundNothing(args: {
/** The pane's PTY id now (the relaunched terminal), or null while it is still binding. */
currentPtyId: () => string | null
/** The PTY id the restart replaced; the check waits for a different one. */
/** The PTY id the restart replaced; only a terminal with a different handle is read. */
replacedPtyId: string
freshCommand: string
sleep?: (ms: number) => Promise<void>
@@ -113,31 +124,46 @@ export async function freshIfResumeFoundNothing(args: {
const sleep =
args.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)))
const now = args.now ?? Date.now
const replaced = hostTerminalOf(args.replacedPtyId)
const deadline = now() + RESUME_CHECK_MS
let lastSeen: string | null = null
while (now() < deadline) {
await sleep(RESUME_POLL_MS)
const ptyId = args.currentPtyId()
const parsed = ptyId && ptyId !== args.replacedPtyId ? parseRemoteRuntimePtyId(ptyId) : null
if (!parsed?.environmentId) {
const current = hostTerminalOf(args.currentPtyId())
// Key off the terminal handle, not the whole id: the host may reuse its PTY id on relaunch.
if (!current || current.handle === replaced?.handle) {
continue
}
const target = { kind: 'environment' as const, environmentId: parsed.environmentId }
if (lastSeen !== current.handle) {
lastSeen = current.handle
console.info('[orca-cloud] RA-11e watching relaunched terminal', current.handle)
}
const target = { kind: 'environment' as const, environmentId: current.environmentId }
const read = await callRuntimeRpc<{ terminal?: { tail?: string[] } }>(
target,
'terminal.read',
{ terminal: parsed.handle, limit: 40 },
{ terminal: current.handle, limit: 80 },
{ timeoutMs: 5_000 }
).catch(() => null)
if (!read?.terminal?.tail?.some((line) => NO_CONVERSATION.test(line))) {
).catch((err: unknown) => {
console.warn('[orca-cloud] RA-11e terminal.read failed:', err)
return null
})
const lines = (read?.terminal?.tail ?? []).map((line) => line.replace(ANSI, ''))
if (!lines.some((line) => NO_CONVERSATION.test(line))) {
continue
}
console.info(
'[orca-cloud] RA-11e resume found no conversation; starting fresh in',
current.handle
)
await callRuntimeRpc(
target,
'terminal.send',
{ terminal: parsed.handle, text: args.freshCommand, enter: true },
{ terminal: current.handle, text: args.freshCommand, enter: true },
{ timeoutMs: 5_000 }
)
return true
}
console.info('[orca-cloud] RA-11e watch ended without a "no conversation" line', lastSeen)
return false
}