Files
orca/src/main/ipc/pty/runtime/spawn-execute.ts
T
Brennan Benson 657a47c25c fix(pty): report agent start failures the same way from every launch path (#28724)
* feat(telemetry): count agent start failures from the host too

The host spawn path (CLI, phone, orchestration workers, headless automations,
worktree startup) now records agent_error when starting an agent fails, as the
window path already did. Removes C5a's TEMPORARY recordsAgentError input; the
host already threads each launch's telemetry, so failures get real kinds.

* fix(telemetry): count a claimed start that several requests joined once

Concurrent resumes of one agent session join one claimed start. When it
fails, every joined request ran the host catch and recorded agent_error, so
one failed start counted once per request. Only the request that ran the
start records it now; the others still clean up.

* fix(telemetry): only skip agent_error for a request that joined another start

The host marked a request as joined before calling ensure, so a claim that
refused the request on its own (conflict, capacity, unreachable owner) recorded
no agent_error. ensure now reports the join itself, right before awaiting the
other request's start.

* chore: re-run CI after main's localization fix (#28810)
2026-10-14 12:51:39 -07:00

216 lines
9.0 KiB
TypeScript

import type { PtySpawnResult } from '../../../providers/pty-provider-contract'
import { ptyIncarnationById } from '../provider/ownership-state'
import { tryGetProviderForPty } from '../provider/registry'
import { ensureWslHookRelayForReattach } from '../../../agent-hooks/wsl-hook-relay-reattach'
import { noteWslDistrosRunning } from '../../../wsl-background-distro-gate'
import {
agentSessionOwners,
assertSpawnReplyWasLive,
reconcileAgentSessionOwnerListings
} from '../pane/agent-session-owners'
import { spawnForStablePane, resolveStablePaneOwner } from '../pane/stable-owner'
import { clearProviderPtyState } from '../provider/state-cleanup'
import { isProviderAgentSessionOwnerLive } from '../provider/liveness'
import { failPtySpawnExecute } from '../spawn-execute-failure'
import type { RuntimePtySpawnState } from './spawn-state'
import { markRuntimeSpawnHiddenBeforeSpawn } from './spawn-hidden-delivery'
export async function executeRuntimePtySpawn(ctx: RuntimePtySpawnState): Promise<void> {
const args = ctx.args
const runtime = ctx.deps.runtime
const acquireWorktreeSpawn = runtime?.acquireWorktreeTerminalSpawn
ctx.releaseWorktreeSpawn = acquireWorktreeSpawn
? await acquireWorktreeSpawn.call(runtime, args.worktreeId, {
refuseSleptWorktree: args.refuseSleptWorktree
})
: undefined
let awaitingAnotherStart = false
try {
if (args.preAllocatedHandle) {
ctx.deps.trustedTerminalHandleEnv.add(args.preAllocatedHandle)
}
const stablePaneOwnerCandidate = ctx.preAdoptedStablePane
? ctx.preAdoptedStablePane.owner
: args.agentSessionEnsure
? null
: resolveStablePaneOwner(
ctx.deps.runtime,
ctx.deps.store,
ctx.spawnIdentityPaneKey,
args.worktreeId,
args.connectionId
)
const expectedPtyId =
stablePaneOwnerCandidate?.ptyId ?? ctx.effectiveSessionAppId ?? ctx.sessionId
if (!stablePaneOwnerCandidate) {
markRuntimeSpawnHiddenBeforeSpawn(ctx)
}
if (expectedPtyId) {
ctx.deps.runtime?.beginPtyRegistration?.(expectedPtyId)
ctx.pendingRegistrationPtyId = expectedPtyId
}
if (ctx.isDaemonHostSpawn && expectedPtyId) {
ctx.preparedProvisionalExecutionContext =
ctx.deps.runtime?.preparePtyExecutionContext?.(expectedPtyId, ctx.expectedWslDistro, {
resetIncarnation: ctx.isNewDaemonSession && !stablePaneOwnerCandidate,
preserveExisting: !ctx.isNewDaemonSession || Boolean(stablePaneOwnerCandidate)
}) ?? false
}
const sequenceBeforeProviderSpawn = expectedPtyId
? (ctx.deps.runtime?.getPtyOutputSequence?.(expectedPtyId) ?? 0)
: 0
const assertClientStillConnected = (): void => {
if (args.signal?.aborted) {
throw new Error('client_disconnected')
}
}
if (args.agentSessionEnsure && !ctx.preAdoptedStablePane) {
// Why: daemon-backed claims can outlive this controller; import all
// proven owners before deciding that an identity is absent.
await reconcileAgentSessionOwnerListings()
const recoveredOwner = agentSessionOwners.find(args.agentSessionEnsure.claim)
if (recoveredOwner && ctx.pendingRegistrationPtyId !== recoveredOwner.ptyId) {
if (ctx.pendingRegistrationPtyId) {
ctx.deps.runtime?.cancelPendingPtyRegistration?.(ctx.pendingRegistrationPtyId)
}
ctx.deps.runtime?.beginPtyRegistration?.(
recoveredOwner.ptyId,
ptyIncarnationById.get(recoveredOwner.ptyId)
)
ctx.pendingRegistrationPtyId = recoveredOwner.ptyId
}
let providerResult: PtySpawnResult | null = null
const ensured = await agentSessionOwners.ensure({
claim: args.agentSessionEnsure.claim,
surface: args.agentSessionEnsure.surface,
spawn: async () => {
assertClientStillConnected()
providerResult = await ctx.provider.spawn(ctx.spawnOptions)
ctx.rejectedRegistrationCandidate = providerResult
// Why: a successful lower-owner return proves physical work committed even if admission sees an early exit.
ctx.reportPtySpawnCommitted()
assertSpawnReplyWasLive(providerResult)
ctx.deps.runtime?.assertPtyRegistrationAllowed?.(
providerResult.id,
providerResult.incarnationId
)
if (providerResult.incarnationId) {
// Why: local providers cannot serialize controller claims, so liveness proof
// needs the exact incarnation before the registry promotes the new owner.
ptyIncarnationById.set(providerResult.id, providerResult.incarnationId)
}
const providerEnsure = providerResult.agentSessionEnsure
return {
ptyId: providerResult.id,
...(providerEnsure
? {
owner: providerEnsure.owner,
disposition: providerEnsure.disposition
}
: {})
}
},
isLive: async (owner) => {
const ownerProvider = tryGetProviderForPty(owner.ptyId)
if (!ownerProvider) {
// Why: an owner on a host this process cannot reach is unknown, never absent.
throw new Error('execution_owner_unavailable')
}
return await isProviderAgentSessionOwnerLive(ownerProvider, owner)
},
onJoin: () => {
awaitingAnotherStart = true
}
})
awaitingAnotherStart = false
ctx.result = providerResult ?? {
id: ensured.owner.ptyId,
isReattach: true,
// Why: adoption from an authoritative listing must preserve the
// incarnation proof used to reject a delayed exit from an older process.
incarnationId: ptyIncarnationById.get(ensured.owner.ptyId)
}
ctx.result.agentSessionEnsure = ensured
} else {
assertClientStillConnected()
const stablePaneSpawn = ctx.preAdoptedStablePane
? ctx.preAdoptedStablePane
: await spawnForStablePane({
runtime: ctx.deps.runtime,
store: ctx.deps.store,
provider: ctx.provider,
spawnOptions: ctx.spawnOptions,
owner: stablePaneOwnerCandidate,
worktreeId: args.worktreeId,
connectionId: args.connectionId,
resolveOwner: () =>
resolveStablePaneOwner(
ctx.deps.runtime,
ctx.deps.store,
ctx.spawnIdentityPaneKey,
args.worktreeId,
args.connectionId
),
onFreshSpawn: ctx.reportPtySpawnCommitted
})
ctx.result = stablePaneSpawn.result
ctx.stablePaneOwner = stablePaneSpawn.owner
if (
ctx.stablePaneOwner &&
ctx.isNewDaemonSession &&
ctx.effectiveSessionAppId &&
ctx.effectiveSessionAppId !== ctx.result.id
) {
clearProviderPtyState(ctx.effectiveSessionAppId)
}
ctx.rejectedRegistrationCandidate = ctx.result
assertSpawnReplyWasLive(ctx.result)
}
ctx.rejectedRegistrationCandidate ??= ctx.result
if (ctx.pendingRegistrationPtyId !== ctx.result.id) {
if (ctx.pendingRegistrationPtyId) {
ctx.deps.runtime?.cancelPendingPtyRegistration?.(ctx.pendingRegistrationPtyId)
}
ctx.deps.runtime?.beginPtyRegistration?.(ctx.result.id, ctx.result.incarnationId)
ctx.pendingRegistrationPtyId = ctx.result.id
}
// Why: admission precedes sequence/context state and every durable publication below.
ctx.deps.runtime?.assertPtyRegistrationAllowed?.(ctx.result.id, ctx.result.incarnationId)
if (ctx.result.providerSequence) {
const runtimeSequenceBeforeReconcile =
ctx.deps.runtime?.getPtyOutputSequence?.(ctx.result.id) ?? 0
// Why kept: this is the reattach boundary in the RENDERER's sequence
// domain, and the daemon snapshot's kitty flags mean nothing without
// the boundary they were proven at.
ctx.reconciledSnapshotSeq =
ctx.deps.runtime?.synchronizePtyOutputSequenceFromProvider?.(
ctx.result.id,
ctx.result.providerSequence,
sequenceBeforeProviderSpawn
) ?? null
if (runtimeSequenceBeforeReconcile > sequenceBeforeProviderSpawn) {
ctx.snapshotKittyFlagsCoverReconciledSeq = false
}
}
ensureWslHookRelayForReattach(ctx.result)
const spawnedWslDistro =
ctx.result.wslDistro === undefined ? ctx.expectedWslDistro : ctx.result.wslDistro
if (spawnedWslDistro && !args.connectionId) {
// A local WSL terminal is user-initiated, so background work held for this distro may run.
noteWslDistrosRunning([spawnedWslDistro])
}
ctx.deps.runtime?.preparePtyExecutionContext?.(ctx.result.id, spawnedWslDistro)
} catch (err) {
throw failPtySpawnExecute(ctx, err, {
sessionId: ctx.sessionId,
ownsNewSession: ctx.isNewDaemonSession,
telemetry: args.telemetry,
joinedAnotherStart: awaitingAnotherStart
})
} finally {
if (args.preAllocatedHandle) {
ctx.deps.trustedTerminalHandleEnv.delete(args.preAllocatedHandle)
}
}
}