mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 02:57:36 +00:00
* 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)
216 lines
9.0 KiB
TypeScript
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)
|
|
}
|
|
}
|
|
}
|