Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/claude/claude-structured-acquisition-launch.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {
AgentSessionAcquisitionExitUnprovenError,
AgentSessionPreSpawnError,
stopAgentSessionProviderRoot,
type StructuredAgentSessionAcquireInput
} from '../native-chat/agent-session-wire/structured-agent-session-adapter'
import { stopAgentSessionProviderRoot } from '../native-chat/agent-session-wire/structured-agent-session-provider-exit-proof'
import { withAgentSessionCreatePhase } from '../observability/agent-session-instrumentation'
import type { ClaudeStructuredLaunch } from './claude-structured-launch-resolution'
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import type { AgentSessionRecord } from '../../../shared/agent-session-record'
import {
AgentSessionPreSpawnError,
isAgentSessionPreSpawnError,
rethrowAfterAgentSessionAcquisitionCleanup,
type StructuredAgentSessionProviderChildPhase
} from './structured-agent-session-adapter'
import { rethrowAfterAgentSessionAcquisitionCleanup } from './structured-agent-session-provider-exit-proof'
import { journalIdentityFor } from './structured-agent-session-attach'
import type { AttachFlowInput } from './structured-agent-session-attach-flow'
import { readNativeSessionOptions } from './structured-agent-session-option-restoration'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import {
AgentSessionAcquisitionExitProvenError,
AgentSessionAcquisitionExitUnprovenError,
AgentSessionAcquisitionRefusal,
AgentSessionAcquisitionRootExitObservedError,
rethrowAfterAgentSessionAcquisitionCleanup
AgentSessionAcquisitionRootExitObservedError
} from './structured-agent-session-adapter'
import { rethrowAfterAgentSessionAcquisitionCleanup } from './structured-agent-session-provider-exit-proof'

describe('failed agent-session acquisition cleanup', () => {
it('names a failure exit-proven after proven cleanup, keeping its diagnostic and cause', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,7 @@ import type {
AgentSessionSlashCommand,
AgentSessionThreadGoalChange
} from '../../../shared/agent-session-wire'
import {
isAgentSessionWireRefusalCode,
type AgentSessionRefusalReason
} from '../../../shared/agent-session-wire-refusals'
import type { AgentSessionRefusalReason } from '../../../shared/agent-session-wire-refusals'
import type {
ProviderDiagnostic,
SubmissionRejectionFact
Expand Down Expand Up @@ -397,57 +394,3 @@ export type StructuredAgentSessionAdapter = StructuredAgentSessionAdapterStop &
/** Host acknowledgement that the proven-dead child, lease and journal owner are released. */
acknowledgeSessionRelease?(sessionId: string): void
}

export async function rethrowAfterAgentSessionAcquisitionCleanup(
adapter: Pick<StructuredAgentSessionAdapter, 'releaseAcquisition'>,
sessionId: string,
cause: unknown
): Promise<never> {
let released: boolean
try {
released = (await adapter.releaseAcquisition?.({ sessionId })) === true
} catch (cleanupError) {
// A root exit the cleanup observed first-hand keeps its classification and its
// provider diagnostic; the failure that triggered cleanup rides along as cause.
throw cleanupError instanceof AgentSessionAcquisitionRootExitObservedError
? new AgentSessionAcquisitionRootExitObservedError(
new AggregateError([cause, cleanupError], cleanupError.message)
)
: new AgentSessionAcquisitionExitUnprovenError(
new AggregateError([cause, cleanupError], 'agent session acquisition cleanup failed')
)
}
if (released) {
throw provenExitAcquisitionFailure(cause)
}
throw new AgentSessionAcquisitionExitUnprovenError(cause)
}

/** A failure whose child cleanup proved gone. One that already names its own verdict — a
* refusal, a typed exit proof, or a host store code — keeps it. */
function provenExitAcquisitionFailure(cause: unknown): unknown {
const classified =
cause instanceof AgentSessionAcquisitionRefusal ||
cause instanceof AgentSessionAcquisitionRootExitObservedError ||
cause instanceof AgentSessionAcquisitionExitUnprovenError ||
isAgentSessionPreSpawnError(cause) ||
(cause instanceof Error && isAgentSessionWireRefusalCode(cause.message))
return classified ? cause : new AgentSessionAcquisitionExitProvenError(cause)
}

/** Whether a stop left the provider root gone. The lease follows the root, so a first-hand root
* exit or a processless child ends the session whatever its descendants did; any other
* failure still throws. */
export async function stopAgentSessionProviderRoot(stop: () => Promise<boolean>): Promise<boolean> {
try {
return (await stop()) === true
} catch (error) {
if (
error instanceof AgentSessionAcquisitionRootExitObservedError ||
isAgentSessionPreSpawnError(error)
) {
return true
}
throw error
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import type { AgentSessionRecord } from '../../../shared/agent-session-record'
import type { AttachFlowInput } from './structured-agent-session-attach-flow'
import {
AgentSessionAcquisitionExitUnprovenError,
AgentSessionAcquisitionRootExitObservedError,
rethrowAfterAgentSessionAcquisitionCleanup
AgentSessionAcquisitionRootExitObservedError
} from './structured-agent-session-adapter'
import { rethrowAfterAgentSessionAcquisitionCleanup } from './structured-agent-session-provider-exit-proof'

export async function settlePostAcquisitionAttachFailure(
input: AttachFlowInput,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { agentSessionFailureFact } from '../../../shared/agent-session-failure'
import {
stopAgentSessionProviderRoot,
type StructuredAgentSessionLifecycleEvent
} from './structured-agent-session-adapter'
import type { StructuredAgentSessionLifecycleEvent } from './structured-agent-session-adapter'
import { stopAgentSessionProviderRoot } from './structured-agent-session-provider-exit-proof'
import type {
StructuredAgentSessionHostDeps,
StructuredAgentSessionHostSession
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
// reach it; forgetting it anyway stranded the process forever and reported success. Leaving the
// session in place is what makes the next close a real retry instead of a no-op.

import {
stopAgentSessionProviderRoot,
type StructuredAgentSessionAdapter,
type StructuredAgentSessionStopCause
import type {
StructuredAgentSessionAdapter,
StructuredAgentSessionStopCause
} from './structured-agent-session-adapter'
import { stopAgentSessionProviderRoot } from './structured-agent-session-provider-exit-proof'
import type { DeferredStructuredAgentSessionEventSink } from './structured-agent-session-event-sink'
import type { StructuredAgentSessionStopVerdict } from './structured-agent-session-host-types'
import { withTimeout } from '../../../shared/promise-timeout-fallback'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// What a cleanup or a stop proved about a provider child's exit, read from the adapter's typed
// verdicts: a failed acquisition rethrows with the verdict its cleanup reached, and a stop answers
// whether the provider root is gone.

import { isAgentSessionWireRefusalCode } from '../../../shared/agent-session-wire-refusals'
import {
AgentSessionAcquisitionExitProvenError,
AgentSessionAcquisitionExitUnprovenError,
AgentSessionAcquisitionRefusal,
AgentSessionAcquisitionRootExitObservedError,
isAgentSessionPreSpawnError,
type StructuredAgentSessionAdapter
} from './structured-agent-session-adapter'

export async function rethrowAfterAgentSessionAcquisitionCleanup(
adapter: Pick<StructuredAgentSessionAdapter, 'releaseAcquisition'>,
sessionId: string,
cause: unknown
): Promise<never> {
let released: boolean
try {
released = (await adapter.releaseAcquisition?.({ sessionId })) === true
} catch (cleanupError) {
// A root exit the cleanup observed first-hand keeps its classification and its
// provider diagnostic; the failure that triggered cleanup rides along as cause.
throw cleanupError instanceof AgentSessionAcquisitionRootExitObservedError
? new AgentSessionAcquisitionRootExitObservedError(
new AggregateError([cause, cleanupError], cleanupError.message)
)
: new AgentSessionAcquisitionExitUnprovenError(
new AggregateError([cause, cleanupError], 'agent session acquisition cleanup failed')
)
}
if (released) {
throw provenExitAcquisitionFailure(cause)
}
throw new AgentSessionAcquisitionExitUnprovenError(cause)
}

/** A failure whose child cleanup proved gone. One that already names its own verdict — a
* refusal, a typed exit proof, or a host store code — keeps it. */
function provenExitAcquisitionFailure(cause: unknown): unknown {
const classified =
cause instanceof AgentSessionAcquisitionRefusal ||
cause instanceof AgentSessionAcquisitionRootExitObservedError ||
cause instanceof AgentSessionAcquisitionExitUnprovenError ||
isAgentSessionPreSpawnError(cause) ||
(cause instanceof Error && isAgentSessionWireRefusalCode(cause.message))
return classified ? cause : new AgentSessionAcquisitionExitProvenError(cause)
}

/** Whether a stop left the provider root gone. The lease follows the root, so a first-hand root
* exit or a processless child ends the session whatever its descendants did; any other
* failure still throws. */
export async function stopAgentSessionProviderRoot(stop: () => Promise<boolean>): Promise<boolean> {
try {
return (await stop()) === true
} catch (error) {
if (
error instanceof AgentSessionAcquisitionRootExitObservedError ||
isAgentSessionPreSpawnError(error)
) {
return true
}
throw error
}
}
Loading