blob: 511132f297fbc409677d30a18e45b8f739512525 [file]
/**
* SessionManager — the public Runtime API.
*
* Ties together:
* SessionStore (storage) — SQLite persistence
* AgentBackend (AiSdkBackend etc) — SDK adapter
* ExecutionBoundary — session sandbox authority
*
* `SessionStore` comes from `@maka/storage`; its public interface owns
* persistence and same-session serialization semantics.
*/
import { createHash } from 'node:crypto';
import { isDeepStrictEqual } from 'node:util';
import { setTimeout as delay } from 'node:timers/promises';
import type {
ActiveInteractionRequestEvent,
SessionEvent,
CompleteEvent,
TextDeltaEvent,
ErrorEvent,
AbortEvent,
PermissionDecisionAckEvent,
PermissionRequestEvent,
QueueEnqueueOutcome,
ShellRunUpdate,
MessageContent,
} from '@maka/core';
import { messageContentsEqual, normalizeMessageContent } from '@maka/core';
import type {
SessionHeader,
SessionBlockedReason,
SessionStatus,
SessionSummary,
StoredMessage,
SubagentSessionParent,
TurnRecord,
UserMessage,
PermissionDecisionMessage,
SystemNoteMessage,
BackendKind,
} from '@maka/core';
import type {
AgentSpec,
ChildAgentTurnInput,
CreateSessionInput,
BranchFromTurnInput,
RegenerateTurnInput,
ReviseBeforeTurnInput,
UserMessageInput,
SessionListFilter,
} from '@maka/core';
import type { SandboxBoundaryResponse } from '@maka/core';
import type { UserQuestionResponse } from '@maka/core';
import type { PermissionMode } from '@maka/core';
import { DEFAULT_TOOL_MODE } from '@maka/core';
import type {
CreateSandboxBoundaryRequest,
ExecutionBoundary,
SandboxBoundaryRequest,
SandboxBoundarySettlement,
SettleSandboxBoundaryRequest,
} from '@maka/core';
import type { CollaborationMode } from '@maka/core';
import type { OrchestrationMode } from '@maka/core';
import {
PLAN_USER_ABANDON_REASON,
PLAN_USER_CANCEL_REASON,
PlanConflictError,
type ApprovePlanProposalInput,
type PlanMutationResult,
type PlanSessionState,
type PlanStore,
} from '@maka/core';
import {
DEFAULT_SESSION_NAME,
DEEP_RESEARCH_SESSION_LABEL,
SIDE_CONVERSATION_SESSION_LABEL,
SUBAGENT_SESSION_RUNTIME_SCHEMA_VERSION,
SUBAGENT_SESSION_SPAWN_SCHEMA_VERSION,
childSessionsForParent,
decodeAgentGraphIntentClaim,
executionBoundaryContains,
failureClassFromCompleteStopReason,
deriveTurnRecords,
isActiveShellRunStatus,
isDeepResearchSession,
isSessionInlineRun,
isTerminalRuntimeEvent,
subagentSessionRuntimeSummary,
} from '@maka/core';
import type {
AgentGraphIntentClaim,
AgentGraphIntentClaimStore,
AgentGraphOperatorProvisionRequest,
AgentGraphOperatorProvisionResult,
AgentGraphProvisionedEdge,
AgentGraphScheduleUpdateSource,
AgentRunEvent,
AgentRunHeader,
AgentRunStore,
ArtifactRecord,
ContinuationClaimV1,
ContinuationClaimStateV1,
RootExecutionDescriptor,
RuntimeEvent,
RuntimeEventStore,
RuntimeContinuationAuthorityStore,
ToolBoundaryProtocol,
SubagentWorkspaceBinding,
SubagentWorktreeExecutor,
SubagentPreset,
} from '@maka/core';
import { AGENT_GRAPH_OPERATOR_PROVISION_SCHEMA_VERSION } from '@maka/core';
import {
classifyRuntimeEventTerminalFact,
type RuntimeEventTerminalFact,
} from './runtime-event-read-model.js';
import {
RuntimeReadModel,
RuntimeReadModelError,
type RuntimeReadModelProjectionCache,
type RuntimeReadModelSessionView,
} from './runtime-read-model.js';
import { inspectAgentRunReadModel, type AgentRunInspectModel } from './agent-run-inspect.js';
import {
cloneConversationRuntimeLedger as cloneConversationLedger,
createConversationCopySlice,
prepareConversationRuntimeLedgerCopy,
type ConversationRuntimeLedgerCopyPlan,
} from './conversation-copy.js';
import { firstRuntimeRepairRunId, RuntimeLedgerRepair } from './runtime-ledger-repair.js';
import {
buildRecoveredTerminalRuntimeEvent,
classifyTerminalRuntimeLedger,
commitTerminalRunWithRuntimeFact,
effectiveRunHeaderFromTerminalFact,
terminalRunHeaderMatchesFact,
terminalRunStatusFromRuntimeEvent,
} from './terminal-run-commit.js';
import type { AgentBackend, BackendStopMode } from '@maka/core';
import type { MakaTool } from './tool-runtime.js';
import type { RunTraceRecorder } from './run-trace.js';
import type { ModelCallAttempt } from '@maka/core';
import type {
ProviderRequestAttemptRecord,
ProviderRequestCaptureLedgerRecord,
} from './provider-request-telemetry.js';
import { readLatestContextDiagnostics, type ContextDiagnostics } from './context-diagnostics.js';
import type { ShellRunProcessManager } from './shell-run-manager.js';
import type { ActiveFullCompactBlock } from './active-full-compact.js';
import type { SemanticCompactBlock } from './semantic-compact.js';
import type { HistoryCompactCheckpoint } from './history-compact-checkpoint.js';
import type { AgentRunLineage, RuntimeContinuationFailpoint } from './agent-run.js';
import type { RuntimeCommitResult, RuntimeCommitSink } from './runtime-commit-sink.js';
import {
attributeSandboxBoundaryRestartClosure,
classifyAgentRunRecovery,
type AgentRunRecoveryDecision,
} from './agent-run-recovery.js';
import { buildInterruptedCodeModeOutcomeCommits } from './recovery-resolver.js';
import type { InvocationResult, InvocationSource } from './invocation-context.js';
import {
isRuntimeHostedRootAuthority,
RuntimeMessageAuthorityInvariantError,
type RuntimeHostedRootExecutionInput,
type RuntimeMessageAuthority,
type RuntimeMessageRunIdentity,
} from './message-authority.js';
import {
RuntimeInteractionInvariantError,
type CanonicalPermissionOutcomeReader,
type RuntimeInteractionAuthority,
} from './interaction-authority.js';
import {
RuntimeKernel,
SessionQuiescentMutationBusyError,
type BackendActivationBoundary,
type RuntimeExecutionClaim,
type RuntimeKernelLike,
type ResumeContinuationOptions,
type TurnStartOptions,
} from './runtime-kernel.js';
import { fallbackSessionTitle, sessionTitleSource } from './session-title.js';
import type { HistoryCompactCleanupRequest } from './history-compact-checkpoint-coordinator.js';
import { fingerprintAgentGraphRunnableIntent } from './stream-graph-admission.js';
import type { AgentGraphRunnableIntent } from './stream-graph-readiness.js';
import { projectAgentGraphRecords } from './stream-graph-projection.js';
import {
buildStatusPatch,
buildTurnStateMessage,
turnHasRetainedOutput as messagesHaveRetainedOutput,
} from './session-projection-helpers.js';
import {
assertAgentDefinitionRunnable,
buildToolsForAgentDefinition,
getBuiltinAgentDefinition,
listBuiltinAgentDefinitions,
requireBuiltinAgentDefinition,
requireBuiltinAgentDefinitionByProfile,
AGENT_WORKSPACE_WORKTREE,
type AgentProfile,
type AgentDefinition,
type AgentDefinitionListItem,
type SubagentPresetListItem,
} from './agent-catalog.js';
import { buildRuntimeEventModelReplayPlan } from './model-history.js';
import { stableHash } from './request-shape.js';
import type { SubagentExecutionRef } from './subagent-execution.js';
import {
buildResumePlanFromRuntimeEvents,
RuntimeContinuationPlanner,
type RuntimeContinuation,
type RuntimeContinuationPlannerInput,
type RuntimeContinuationSafetyObservation,
type SafeBoundaryContinuationPlan,
} from './runtime-resume.js';
function runtimeContinuationAuthority(
store: RuntimeEventStore | undefined,
): RuntimeContinuationAuthorityStore | undefined {
const candidate = store as Partial<RuntimeContinuationAuthorityStore> | undefined;
return candidate?.continuationAuthorityCapability === 'runtime_continuation_authority_v1' &&
typeof candidate.readImmutableRuntimePrefix === 'function' &&
typeof candidate.readImmutableRuntimeEvents === 'function' &&
typeof candidate.claimContinuation === 'function' &&
typeof candidate.readContinuationClaimByBoundary === 'function' &&
typeof candidate.readContinuationClaimStateByBoundary === 'function' &&
typeof candidate.listContinuationClaimsForRecovery === 'function' &&
typeof candidate.commitContinuationStart === 'function' &&
typeof candidate.commitContinuationRepairStart === 'function'
? (candidate as RuntimeContinuationAuthorityStore)
: undefined;
}
function runtimeCommitSinkFromEventStore(
store: RuntimeEventStore | undefined,
): RuntimeCommitSink | undefined {
const candidate = store as Partial<RuntimeCommitSink> | undefined;
return typeof candidate?.commitToolPrepared === 'function' &&
typeof candidate.commitToolOutcome === 'function'
? (candidate as RuntimeCommitSink)
: undefined;
}
export interface StopSessionInput {
source?: 'stop_button' | 'benchmark_deadline' | 'graph_supervisor';
mode?: BackendStopMode;
}
interface CompactSessionOptions {
/**
* Override the configured recent-turn tail. Supervisor overflow recovery
* uses zero because the failed wake turn itself can contain the oversized
* tool result that must be folded.
*/
minRecentTurns?: number;
}
export type CompactSessionInput =
| (CompactSessionOptions & {
turnId?: string;
hostedRoot?: never;
})
| (CompactSessionOptions & {
turnId: string;
hostedRoot: {
runId: string;
onRunStarted?: () => void | Promise<void>;
};
});
export type PlanSafeBoundaryContinuationInput = Omit<RuntimeContinuationPlannerInput, 'sessionId'>;
export interface PlanAuthoritativeSafeBoundaryContinuationInput {
sourceRunId: string;
expectedRuntimeEventHighWater?: number;
}
export interface SpawnChildAgentInput {
parentRunId: string;
turnId?: string;
spec: AgentSpec;
prompt: string;
abortSignal?: AbortSignal;
onReady?: (input: { turnId: string; agentId: string; agentName: string }) => void | Promise<void>;
/** Presentation-only observer for projecting child activity into a parent surface. */
onEvent?: (event: SessionEvent) => void;
}
export interface SpawnChildSessionInput {
spawnedBy: SubagentSessionParent['spawnedBy'];
agentProfile: AgentProfile;
/** User-approved catalog selector. The runtime resolves its frozen model target. */
subagentId?: string;
prompt: string;
name?: string;
turnId?: string;
runId?: string;
swarm?: SubagentSessionParent['swarm'];
abortSignal?: AbortSignal;
onReady?: (input: {
childSessionId: string;
turnId: string;
runId: string;
agentId: string;
agentName: string;
permissionMode: SessionHeader['permissionMode'];
}) => void | Promise<void>;
/** Presentation-only observer for projecting child activity into a parent surface. */
onEvent?: (event: SessionEvent) => void;
}
type ResolvedSpawnChildSessionInput = SpawnChildSessionInput & {
resolvedPreset?: SubagentPreset;
};
export interface SpawnChildSessionResult extends SpawnChildAgentResult {
childSessionId: string;
runId: string;
profile: string;
}
export interface ProvisionAgentGraphOperatorInput {
graphId: string;
workId: string;
agentId?: string;
subagentId?: string;
operatorId: string;
source: AgentGraphScheduleUpdateSource;
edges: AgentGraphProvisionedEdge[];
expectedScheduleRevision: number;
}
export interface ProvisionAgentGraphOperatorResult extends AgentGraphOperatorProvisionResult {
header: SessionHeader;
}
export interface RunClaimedAgentGraphIntentInput {
/**
* Embedded graph claim authority and stream-graph admission dependency.
* Hosted execution treats this as a non-authoritative caller reference and
* re-reads the claim through its trusted composition capability.
*/
claimStore: AgentGraphIntentClaimStore;
/** Complete control-plane input used to verify the durable claim fingerprint. */
intent: AgentGraphRunnableIntent;
graphId: string;
intentId: string;
prompt: string;
/**
* Optional control-plane gate evaluated after Session serialization and
* immediately before a new Runtime turn is admitted. Existing durable runs
* bypass the gate and remain recoverable.
*/
admitExecution?: () => Promise<'executing' | 'cancelled'>;
abortSignal?: AbortSignal;
onReady?: (input: {
claimId: string;
graphId: string;
intentId: string;
operatorId: string;
childSessionId: string;
turnId: string;
runId: string;
agentId: string;
agentName: string;
}) => void | Promise<void>;
/** Presentation-only observer for the newly started runtime stream. */
onEvent?: (event: SessionEvent) => void;
}
export interface ClaimedAgentGraphIntentResult extends SpawnChildSessionResult {
claimId: string;
graphId: string;
intentId: string;
operatorId: string;
}
type ResolvedClaimedAgentGraphIntentInput = Omit<
RunClaimedAgentGraphIntentInput,
'claimStore' | 'graphId' | 'intentId'
> & {
claim: AgentGraphIntentClaim;
hostedGraphExecution?: RuntimeHostedAgentGraphExecutionCapability;
};
export interface PrepareChildAgentResumeResult {
sourceRunId: string;
execution: SubagentExecutionRef;
agentId: string;
agentName: string;
profile: string;
}
export interface ResumeChildAgentInput {
parentRunId: string;
sourceRunId: string;
turnId?: string;
prompt: string;
abortSignal?: AbortSignal;
onReady?: (input: {
childSessionId?: string;
turnId: string;
runId?: string;
agentId: string;
agentName: string;
}) => void | Promise<void>;
/** Presentation-only observer for projecting child activity into a parent surface. */
onEvent?: (event: SessionEvent) => void;
}
export interface SpawnChildAgentResult {
childSessionId?: string;
agentId: string;
agentName: string;
turnId: string;
runId?: string;
status: 'completed' | 'failed' | 'cancelled' | 'running' | 'waiting_for_user';
permissionMode: PermissionMode;
summary: string;
artifactIds: string[];
startedAt: number;
completedAt: number;
durationMs: number;
eventCount: number;
failureClass?: string;
resumedFromRunId?: string;
retriedFromRunId?: string;
}
export interface RetryChildAgentInput {
parentRunId: string;
sourceRunId: string;
execution?: SubagentExecutionRef;
abortSignal?: AbortSignal;
onReady?: (input: {
childSessionId?: string;
turnId: string;
runId?: string;
agentId: string;
agentName: string;
}) => void | Promise<void>;
/** Presentation-only observer for projecting child activity into a parent surface. */
onEvent?: (event: SessionEvent) => void;
}
const CHILD_AGENT_SUMMARY_MAX_CHARS = 4_000;
const MAX_RUNTIME_LEDGER_REPAIR_ATTEMPTS = 8;
export interface AgentListItem {
runId: string;
turnId: string;
parentRunId: string;
agentId?: string;
agentName?: string;
status: AgentRunHeader['status'];
permissionMode: AgentRunHeader['permissionMode'];
createdAt: number;
updatedAt: number;
completedAt?: number;
durationMs?: number;
failureClass?: string;
}
export interface SubagentExecutionListItem {
execution: SubagentExecutionRef;
agentId?: string;
agentName?: string;
profile?: string;
turnId?: string;
status: AgentRunHeader['status'];
permissionMode: PermissionMode;
createdAt: number;
updatedAt: number;
completedAt?: number;
durationMs?: number;
failureClass?: string;
}
export interface AgentListResult {
definitions: AgentDefinitionListItem[];
/** User-configured, host-validated routes the main agent may select by id. */
presets: SubagentPresetListItem[];
/** Canonical mixed projection for new child Sessions and legacy same-session child AgentRuns. */
executions: SubagentExecutionListItem[];
/** Legacy projection retained while callers migrate to executions. */
runs: AgentListItem[];
}
export interface AgentOutputInput {
execution?: SubagentExecutionRef;
runId?: string;
turnId?: string;
maxEvents?: number;
maxBytes?: number;
view?: AgentOutputView;
}
export type AgentOutputView = 'result' | 'events' | 'runtime_events' | 'all';
export interface AgentOutputCommittedResult {
schemaVersion: 1;
status: AgentRunHeader['status'];
graph?: {
graphId: string;
workId: string;
operatorId: string;
};
/** Committed Graph record containing the final non-partial model text, or the terminal record. */
resultRecordId?: string;
terminalRecordId?: string;
sourceRuntimeEventId?: string;
terminalRuntimeEventId?: string;
text?: string;
textTruncated: boolean;
artifactIds: string[];
omittedArtifactIds: number;
failureClass?: string;
}
export interface AgentOutputResult {
execution: SubagentExecutionRef;
header: AgentRunHeader;
result?: AgentOutputCommittedResult;
events: AgentRunEvent[];
runtimeEvents: RuntimeEvent[];
sourceHealth: AgentRunInspectModel['sourceHealth'];
diagnostics: AgentRunInspectModel['diagnostics'];
artifacts: ArtifactRecord[];
truncated: {
events: boolean;
runtimeEvents: boolean;
diagnostics: boolean;
artifacts: boolean;
bytes: boolean;
};
budget: {
view: AgentOutputView;
maxBytes: number;
projectedBytes: number;
};
}
// ============================================================================
// SessionStore contract (matches the storage package surface)
// ============================================================================
// StoredMessage rows remain a projection/cache surface for existing public
// shapes. RuntimeEventStore is the semantic conversation ledger.
export interface VersionedSessionHeader {
readonly header: SessionHeader;
readonly revision: number;
readonly committedAt: number;
}
export interface SessionConfigurationStoreUpdate {
readonly expectedVersion: number;
readonly configuration: {
readonly backend: SessionHeader['backend'];
readonly llmConnectionSlug: string;
readonly connectionLocked: boolean;
readonly model: string;
readonly thinkingLevel: SessionHeader['thinkingLevel'];
readonly permissionMode: SessionHeader['permissionMode'];
readonly collaborationMode: NonNullable<SessionHeader['collaborationMode']>;
readonly orchestrationMode: NonNullable<SessionHeader['orchestrationMode']>;
readonly labels: readonly string[];
};
readonly lifecycle:
| { readonly kind: 'preserve' }
| { readonly kind: 'clear_connection_block'; readonly statusUpdatedAt: number };
}
export interface SessionConfigurationTransitionRequest {
readonly expectedRevision: number;
readonly configuration: Omit<SessionConfigurationStoreUpdate['configuration'], 'labels'>;
}
export type SessionConfigurationTransitionErrorCode =
| 'session_busy'
| 'operation_conflict'
| 'operation_unavailable';
export class SessionConfigurationTransitionError extends Error {
readonly name = 'SessionConfigurationTransitionError';
constructor(
readonly code: SessionConfigurationTransitionErrorCode,
message: string,
) {
super(message);
}
}
export class SessionConfigurationRevisionConflictError extends Error {
readonly name = 'SessionConfigurationRevisionConflictError';
constructor(
readonly expectedRevision: number,
readonly actualRevision: number,
) {
super(
`Session configuration revision conflict: expected ${expectedRevision}, actual ${actualRevision}`,
);
}
}
export class RuntimeRegenerateTurnError extends Error {
readonly name = 'RuntimeRegenerateTurnError';
constructor(
readonly code: 'not_found' | 'operation_conflict',
message: string,
) {
super(message);
}
}
export interface RegenerateTurnSource {
readonly sourceTurnId: string;
readonly content: MessageContent;
}
export interface SessionStore {
create(input: CreateSessionInput, initialBoundary?: ExecutionBoundary): Promise<SessionHeader>;
createSubagent(
input: CreateSessionInput,
initialBoundary?: ExecutionBoundary,
): Promise<{ header: SessionHeader; created: boolean }>;
readExecutionBoundary(sessionId: string): Promise<ExecutionBoundary>;
createSandboxBoundaryRequest?(
input: CreateSandboxBoundaryRequest,
): Promise<SandboxBoundaryRequest>;
listPendingSandboxBoundaryRequests?(sessionId: string): Promise<SandboxBoundaryRequest[]>;
listSandboxBoundaryRestartClosures?(sessionId: string): Promise<SandboxBoundaryRequest[]>;
settleSandboxBoundaryRequest?(
input: SettleSandboxBoundaryRequest,
): Promise<SandboxBoundarySettlement>;
setExecutionBoundaryKind(
sessionId: string,
kind: 'managed' | 'bypass',
projection?: {
permissionMode: SessionHeader['permissionMode'];
labels?: readonly string[];
},
): Promise<ExecutionBoundary>;
createAgentGraphOperator?(
input: CreateSessionInput,
request: AgentGraphOperatorProvisionRequest,
expectedRevision: number,
initialBoundary?: ExecutionBoundary,
): Promise<ProvisionAgentGraphOperatorResult>;
list(filter?: SessionListFilter): Promise<SessionSummary[]>;
readHeader(sessionId: string): Promise<SessionHeader>;
readMessages(sessionId: string): Promise<StoredMessage[]>;
readMessagesSnapshot?(sessionId: string): Promise<StoredMessage[]>;
listTurns(sessionId: string): Promise<TurnRecord[]>;
appendMessage(sessionId: string, m: StoredMessage): Promise<void>;
appendMessages(sessionId: string, ms: StoredMessage[]): Promise<void>;
updateHeader(sessionId: string, patch: Partial<SessionHeader>): Promise<SessionHeader>;
updateHeaderVersioned?(
sessionId: string,
patch: Partial<SessionHeader>,
expectedRevision: number,
): Promise<VersionedSessionHeader>;
readHeaderRecordSnapshot?(sessionId: string): Promise<VersionedSessionHeader>;
updateSessionConfiguration?(
sessionId: string,
input: SessionConfigurationStoreUpdate,
): Promise<VersionedSessionHeader>;
markSessionReadThrough(sessionId: string, readThroughTs: number): Promise<SessionHeader>;
archive(sessionId: string): Promise<void>;
unarchive(sessionId: string): Promise<void>;
setFlagged(sessionId: string, isFlagged: boolean): Promise<void>;
rename(sessionId: string, name: string): Promise<void>;
setGeneratedTitleIfAbsent?(sessionId: string, title: string): Promise<SessionHeader | null>;
remove(sessionId: string): Promise<void>;
}
export interface StrictRecoverySessionStore extends SessionStore {
listForRecovery(): Promise<SessionHeader[]>;
readMessagesForRecovery(sessionId: string): Promise<StoredMessage[]>;
}
export interface StrictRecoveryAgentRunStore extends AgentRunStore {
listSessionRunsForRecovery(sessionId: string): Promise<AgentRunHeader[]>;
readEventsForRecovery(sessionId: string, runId: string): Promise<AgentRunEvent[]>;
}
export interface StrictRecoveryStores {
sessionStore: StrictRecoverySessionStore;
agentRunStore: StrictRecoveryAgentRunStore;
}
// ============================================================================
// BackendRegistry — factory dispatch by BackendKind
// ============================================================================
export interface BackendFactoryContext {
sessionId: string;
workspaceRoot: string;
header: SessionHeader;
store: SessionStore;
/** Process-local cancellation for the execution that owns this activation. */
abortSignal?: AbortSignal;
appendMessage?: (message: StoredMessage) => Promise<void>;
/**
* Child-agent instruction channel. Legacy child runs and linked child
* sessions populate this; an ordinary main-session activation leaves it
* undefined. A
* main-session factory that needs a system prompt must source it from
* its own closure (the desktop path and the headless benchmark path
* both do this) — do NOT route a main-session prompt through this
* field, it is semantically the child instruction, not the session
* system prompt.
*/
systemPrompt?: string;
/**
* Optional hard tool ceiling on the *agent-permission* tools for this backend
* activation. When present, a host may remove tools for stricter local
* policy, but must never append, substitute, or otherwise expose an
* agent-permission tool outside this exact set.
*
* Runtime protocol tools are outside that ceiling by construction (#2026).
* `ArchiveRead` decodes a placeholder the runtime itself generated during
* pruning; it grants no reach the parent did not already exercise, and
* withholding it only strands content the model was explicitly told to
* retrieve. The backend therefore binds it from the archive capability, not
* from this set, which is why narrowing a child's allowlist can no longer
* silently strip the decoder for placeholders that child will still receive.
*/
tools?: readonly MakaTool[];
recordRunTrace?: RunTraceRecorder;
/** Durable AgentRun metadata row written after the private capture artifact. */
recordProviderRequestCapture?: (capture: ProviderRequestCaptureLedgerRecord) => Promise<void>;
/** Best-effort AgentRun row for one physical provider call. */
recordProviderRequestAttempt?: (attempt: ProviderRequestAttemptRecord) => void;
/**
* Durable AgentRun row carrying the canonical accounting record for one
* physical provider call. Distinct from the diagnostic row above: this one is
* the metering source of truth (#1679).
*/
recordModelCallAttempt?: (attempt: ModelCallAttempt) => Promise<void>;
loadHistoryCompactCheckpoint?: () => Promise<HistoryCompactCheckpoint | undefined>;
recordHistoryCompactCheckpoint?: (
checkpoint: HistoryCompactCheckpoint,
turnId: string,
) => Promise<void>;
/**
* Durable read of the given turn's persisted RuntimeEvents from the
* authoritative run ledger. The Runtime reloads this projection between
* provider requests; mid-turn compaction also derives its coverage pool from
* the same durable facts.
*/
loadTurnRuntimeEvents?: (turnId: string) => Promise<RuntimeEvent[]>;
/** Whether this activation may fold its run ledger into session-scoped history. */
allowMidTurnHistoryCompaction?: boolean;
recordActiveFullCompactBlock?: (block: ActiveFullCompactBlock) => void;
recordSemanticCompactBlock?: (block: SemanticCompactBlock) => void;
shellRunContextSummary?: () => Promise<string | undefined>;
}
export type BackendFactory = (ctx: BackendFactoryContext) => AgentBackend | Promise<AgentBackend>;
export class BackendRegistry {
private readonly factories = new Map<BackendKind, BackendFactory>();
register(kind: BackendKind, factory: BackendFactory): void {
this.factories.set(kind, factory);
}
async build(kind: BackendKind, ctx: BackendFactoryContext): Promise<AgentBackend> {
const f = this.factories.get(kind);
if (!f) throw new Error(`No backend factory registered for kind="${kind}"`);
return await f(ctx);
}
has(kind: BackendKind): boolean {
return this.factories.has(kind);
}
}
// ============================================================================
// SessionManager
// ============================================================================
export interface RuntimeHostedAgentGraphExecutionCapability {
readAgentGraphIntentClaim(
graphId: string,
intentId: string,
): Promise<AgentGraphIntentClaim | undefined>;
readRootTurnAdmissionIdentity(
sessionId: string,
turnId: string,
): Promise<{ runId: string; userMessageId: string | null } | undefined>;
}
interface SessionManagerBaseDeps {
store: SessionStore;
planStore?: PlanStore;
runStore?: AgentRunStore;
runtimeEventStore?: RuntimeEventStore;
runtimeCommitSink?: RuntimeCommitSink;
/** Host capability; RuntimeKernel gates it by the selected backend. */
toolBoundaryProtocol?: ToolBoundaryProtocol;
backends: BackendRegistry;
newId: () => string;
now: () => number;
childTools?: readonly MakaTool[];
resolveChildTools?: (sessionId: string) => Promise<readonly MakaTool[]>;
/** Host-owned user catalog. Runtime receives ids from models, never raw model targets. */
subagentCatalog?: {
list(): Promise<SubagentPresetListItem[]>;
resolve(id: string): Promise<SubagentPreset>;
};
/** Host-owned filesystem isolation for worktree-backed child Sessions. */
worktreeChildExecutor?: SubagentWorktreeExecutor;
listArtifactsForTurn?: (sessionId: string, turnId: string) => Promise<ArtifactRecord[]>;
/** Durable publication boundary for terminal worktree-child patches. */
publishChildWorkspacePatch?: (input: {
sessionId: string;
turnId: string;
binding: SubagentWorkspaceBinding;
patch: Uint8Array;
}) => Promise<ArtifactRecord>;
/** Reject patch publication while the child still owns live Runtime Resources. */
assertChildWorkspaceQuiescent?: (sessionId: string) => Promise<void>;
runtimeSource?: InvocationSource;
runtimeInvocationObserver?: (result: InvocationResult) => void | Promise<void>;
runtimeKernel?: RuntimeKernelLike;
/** Optional host-owned parent run authority for runtimes that execute the parent externally. */
isParentRunActive?: (sessionId: string, runId: string, turnId: string) => boolean;
shellRuns?: ShellRunProcessManager;
cleanupHistoryCompactArtifacts?: (input: HistoryCompactCleanupRequest) => Promise<void>;
inspectContinuationSafety?: (sessionId: string) => Promise<RuntimeContinuationSafetyObservation>;
continuationFailpoint?: (point: RuntimeContinuationFailpoint) => Promise<void>;
runBackendActivation?: BackendActivationBoundary;
safeBoundaryResumeEnabled?: boolean;
/** Hosted composition capability. Omit for the production embedded queue. */
messageAuthority?: RuntimeMessageAuthority;
/** Trusted Host-owned graph readers. Hosted graph execution fails closed without them. */
hostedAgentGraphExecution?: RuntimeHostedAgentGraphExecutionCapability;
onContinuationLifecycleEvent?: (event: RuntimeContinuationLifecycleEvent) => void | Promise<void>;
generateSessionTitle?: (input: {
sessionId: string;
header: SessionHeader;
sourceText: string;
}) => Promise<string | undefined>;
onSessionTitleChanged?: (sessionId: string) => void;
}
type SessionManagerInteractionDeps =
| {
/** Hosted composition capabilities. Omit both for embedded interaction ownership. */
interactionAuthority: RuntimeInteractionAuthority;
canonicalPermissionOutcomes: CanonicalPermissionOutcomeReader;
}
| {
interactionAuthority?: undefined;
canonicalPermissionOutcomes?: undefined;
};
export type SessionManagerDeps = SessionManagerBaseDeps & SessionManagerInteractionDeps;
export type RuntimeContinuationLifecycleEvent =
| {
type: 'plan_approved';
sessionId: string;
sourceRunId: string;
targetRunId: string;
}
| {
type: 'plan_parked';
sessionId: string;
sourceRunId: string;
rejectionReasons: readonly string[];
}
| {
type: 'execution_started' | 'execution_completed';
sessionId: string;
sourceRunId: string;
targetRunId: string;
}
| {
type: 'execution_failed';
sessionId: string;
sourceRunId: string;
targetRunId: string;
errorClass: string;
};
export class SessionManager {
private readonly runtimeKernel: RuntimeKernelLike;
private readonly runtimeLedgerRepair?: RuntimeLedgerRepair;
private readonly runtimeCommitSink?: RuntimeCommitSink;
private readonly activeHostedLinkedChildSessions = new Set<string>();
private readonly childSessionSpawns = new Map<
string,
{ requestFingerprint: string; promise: Promise<SpawnChildSessionResult> }
>();
private readonly claimedAgentGraphIntentRuns = new Map<
string,
{ requestFingerprint: string; promise: Promise<ClaimedAgentGraphIntentResult> }
>();
private readonly claimedAgentGraphSessionTails = new Map<string, Promise<void>>();
constructor(private readonly deps: SessionManagerDeps) {
if (deps.runStore && !deps.runtimeEventStore) {
throw new Error('RuntimeEventStore is required when AgentRunStore is configured');
}
if (deps.publishChildWorkspacePatch && !deps.listArtifactsForTurn) {
throw new Error('Child workspace patch publication requires Artifact turn listing');
}
this.runtimeCommitSink =
deps.runtimeCommitSink ?? runtimeCommitSinkFromEventStore(deps.runtimeEventStore);
if (deps.runStore && deps.runtimeEventStore) {
this.runtimeLedgerRepair = new RuntimeLedgerRepair({
runStore: deps.runStore,
runtimeEventStore: deps.runtimeEventStore,
readMessages: (sessionId) => deps.store.readMessages(sessionId),
appendMessage: (sessionId, message) => deps.store.appendMessage(sessionId, message),
appendTurnState: (sessionId, turnId, status, lineage, options) =>
this.appendTurnState(sessionId, turnId, status, lineage, options),
newId: deps.newId,
now: deps.now,
});
}
this.runtimeKernel =
deps.runtimeKernel ??
new RuntimeKernel({
...deps,
repairRunRuntimeLedger: (sessionId, runId) =>
this.repairMissingTerminalFactOnce(sessionId, runId),
});
}
// --------------------------------------------------------------------------
// Session lifecycle
// --------------------------------------------------------------------------
async createSession(
input: CreateSessionInput,
options: { initialBoundary?: ExecutionBoundary } = {},
): Promise<SessionSummary> {
const header = await this.deps.store.create(input, options.initialBoundary);
return headerToSummary(header);
}
/**
* Sessions plus the turn each one is running right now. The persisted status
* cannot carry that: it is written only at the END of `AgentRun.begin`, it
* reads the same before a turn starts and after it ends, and a crash between
* a turn's end and its status write leaves `running` behind for good. The
* live run is the fact, so a client can name what is running and — because
* nothing survives the process — a restart reports the truth by itself.
*/
/**
* The turns this session is running right now. Same live fact `listSessions`
* projects, for callers that need it about one session — notably to name the
* turns a change is about.
*/
runningTurnIds(sessionId: string): string[] {
return this.runtimeKernel.runningTurnIds?.(sessionId) ?? [];
}
async listSessions(filter?: SessionListFilter): Promise<SessionSummary[]> {
const sessions = await this.deps.store.list(filter);
const runningTurnIds = this.runtimeKernel.runningTurnIds?.bind(this.runtimeKernel);
if (!runningTurnIds) return sessions;
return sessions.map((session) => {
const turnIds = runningTurnIds(session.id);
return turnIds.length === 0 ? session : { ...session, runningTurnIds: turnIds };
});
}
async listChildSessions(parentSessionId: string): Promise<SessionSummary[]> {
const sessions = await this.deps.store.list({ subagentParentSessionId: parentSessionId });
return childSessionsForParent(sessions, parentSessionId);
}
private async provisionChildWorkspace(
parent: SessionHeader,
definition: AgentDefinition,
requestFingerprint: string,
): Promise<SubagentWorkspaceBinding | undefined> {
if (definition.contract.workspace !== AGENT_WORKSPACE_WORKTREE) return undefined;
const executor = this.deps.worktreeChildExecutor;
if (!executor) {
throw new Error(
`Agent "${definition.id}" is unavailable: "worktree" workspace isolation requires a worktree child executor.`,
);
}
const fingerprint = requestFingerprint.startsWith('sha256:')
? requestFingerprint.slice('sha256:'.length)
: requestFingerprint;
if (!/^[a-f0-9]{64}$/.test(fingerprint)) {
throw new Error('Child workspace request fingerprint must be SHA-256');
}
return executor.provision({
leaseId: `subagent_worktree_${fingerprint.slice(0, 32)}`,
sourceSessionId: parent.id,
sourceCwd: parent.cwd,
...(parent.projectId !== undefined ? { sourceProjectId: parent.projectId } : {}),
});
}
private async ensureChildWorkspace(header: SessionHeader): Promise<void> {
const binding = header.subagentWorkspace;
if (!binding) return;
const executor = this.deps.worktreeChildExecutor;
if (!executor) {
throw new Error(
`Child Session ${header.id} requires a worktree child executor for ${binding.worktreePath}`,
);
}
if (header.cwd !== binding.worktreePath) {
throw new Error(`Child Session ${header.id} workspace binding disagrees with its cwd`);
}
await executor.ensure(binding);
}
private hasWorktreePatchWriteBack(): boolean {
return Boolean(
this.deps.worktreeChildExecutor &&
this.deps.listArtifactsForTurn &&
this.deps.publishChildWorkspacePatch &&
this.deps.assertChildWorkspaceQuiescent,
);
}
private async isWorktreeChildExecutorAvailable(
header: Pick<SessionHeader, 'cwd' | 'projectId'>,
): Promise<boolean> {
const executor = this.deps.worktreeChildExecutor;
if (!executor) return false;
return await executor.isAvailable({
sourceCwd: header.cwd,
...(header.projectId !== undefined ? { sourceProjectId: header.projectId } : {}),
});
}
private async finalizeAndListChildTurnArtifacts(
sessionId: string,
turnId: string,
status: AgentRunHeader['status'],
): Promise<ArtifactRecord[]> {
const list = this.deps.listArtifactsForTurn;
if (!list) return [];
const artifacts = await list(sessionId, turnId);
if (!isTerminalRunStatus(status) || !this.hasWorktreePatchWriteBack()) return artifacts;
const header = await this.deps.store.readHeader(sessionId);
if (!header.subagentWorkspace) return artifacts;
const existing = artifacts.find((artifact) => artifact.source === 'subagent_writeback');
if (existing) return artifacts;
await this.finalizeChildWorkspacePatches(sessionId);
const finalized = await list(sessionId, turnId);
if (!finalized.some((artifact) => artifact.source === 'subagent_writeback')) {
throw new Error(
`Child Session ${sessionId} cannot reconstruct the historical workspace patch for Turn ${turnId}`,
);
}
return finalized;
}
/** Publish the recoverable write-back owed by the latest terminal worktree child Run. */
async finalizeChildWorkspacePatches(sessionId: string): Promise<void> {
if (!this.hasWorktreePatchWriteBack() || !this.deps.runStore) return;
const header = await this.deps.store.readHeader(sessionId);
const binding = header.subagentWorkspace;
if (!binding) return;
const latest = (await this.deps.runStore.listSessionRuns(sessionId))
.filter(isSessionInlineRun)
.sort(
(left, right) => right.createdAt - left.createdAt || right.runId.localeCompare(left.runId),
)[0];
if (!latest) return;
if (!isTerminalRunStatus(latest.status)) {
throw new Error(
`Child Session ${sessionId} cannot finalize its workspace while Run ${latest.runId} is nonterminal`,
);
}
const artifacts = await this.deps.listArtifactsForTurn!(sessionId, latest.turnId);
if (artifacts.some((artifact) => artifact.source === 'subagent_writeback')) return;
await this.deps.assertChildWorkspaceQuiescent!(sessionId);
await this.ensureChildWorkspace(header);
const patch = await this.deps.worktreeChildExecutor!.capturePatch(binding);
const published = await this.deps.publishChildWorkspacePatch!({
sessionId,
turnId: latest.turnId,
binding,
patch,
});
if (
published.sessionId !== sessionId ||
published.turnId !== latest.turnId ||
published.source !== 'subagent_writeback'
) {
throw new Error('Child workspace patch publisher returned a mismatched Artifact');
}
}
async recoverChildWorkspacePatches(sessionIds: readonly string[]): Promise<void> {
for (const sessionId of sessionIds) await this.finalizeChildWorkspacePatches(sessionId);
}
/** Invalidate backend snapshots now, or immediately after active turns settle. */
refreshIdleBackends(): Promise<void> {
return this.runtimeKernel.invalidateCachedBackends();
}
disposeSessionBackend(sessionId: string): Promise<void> {
return this.runtimeKernel.disposeBackend(sessionId);
}
async transitionSessionConfiguration(
sessionId: string,
input: SessionConfigurationTransitionRequest,
): Promise<VersionedSessionHeader> {
const store = this.requireSessionConfigurationStore();
const next = await this.commitExecutionResourceTransition(
sessionId,
input.configuration.permissionMode,
async () => {
const current = await store.readHeaderRecordSnapshot(sessionId);
if (current.revision !== input.expectedRevision) {
throw new SessionConfigurationRevisionConflictError(
input.expectedRevision,
current.revision,
);
}
if (current.header.isArchived || current.header.status === 'archived') {
throw new SessionConfigurationTransitionError(
'operation_conflict',
'Archived Session configuration cannot be changed',
);
}
if (current.header.status === 'waiting_for_user') {
throw new SessionConfigurationTransitionError(
'session_busy',
'Session has a pending Interaction',
);
}
await this.assertCollaborationTransition(
current.header,
input.configuration.collaborationMode,
);
const leavingDeepResearch =
isDeepResearchSession(current.header.labels) &&
input.configuration.permissionMode !== 'explore';
const labels = leavingDeepResearch
? current.header.labels.filter((label) => label !== DEEP_RESEARCH_SESSION_LABEL)
: current.header.labels;
return () =>
store.updateSessionConfiguration(sessionId, {
expectedVersion: input.expectedRevision,
configuration: {
...input.configuration,
labels,
},
lifecycle:
current.header.blockedReason === 'NO_REAL_CONNECTION'
? {
kind: 'clear_connection_block',
statusUpdatedAt: this.deps.now(),
}
: { kind: 'preserve' },
});
},
);
this.runtimeKernel.updateCachedHeader(sessionId, next.header);
return next;
}
async relocateSessionWorkspace(
sessionId: string,
input: {
readonly expectedRevision: number;
readonly cwd: string;
readonly projectId?: string | null;
},
): Promise<VersionedSessionHeader> {
const updateHeaderVersioned = this.deps.store.updateHeaderVersioned?.bind(this.deps.store);
const readHeaderRecordSnapshot = this.deps.store.readHeaderRecordSnapshot?.bind(
this.deps.store,
);
if (!updateHeaderVersioned || !readHeaderRecordSnapshot) {
throw new SessionConfigurationTransitionError(
'operation_unavailable',
'Session workspace relocation authority is unavailable',
);
}
const next = await this.runSessionQuiescentMutation([sessionId], async () => {
if (this.runtimeKernel.hasActiveRuns(sessionId)) {
throw new SessionConfigurationTransitionError(
'session_busy',
'Session workspace cannot change while a Turn is active',
);
}
const current = await readHeaderRecordSnapshot(sessionId);
if (current.revision !== input.expectedRevision) {
throw new SessionConfigurationRevisionConflictError(
input.expectedRevision,
current.revision,
);
}
if (current.header.isArchived || current.header.status === 'archived') {
throw new SessionConfigurationTransitionError(
'operation_conflict',
'Archived Session workspace cannot be relocated',
);
}
if (current.header.subagentWorkspace || current.header.subagentParent) {
throw new SessionConfigurationTransitionError(
'operation_unavailable',
'Managed child Session workspaces cannot be relocated',
);
}
if (current.header.status === 'waiting_for_user') {
throw new SessionConfigurationTransitionError(
'session_busy',
'Session has a pending Interaction',
);
}
const projectIdChanged =
input.projectId !== undefined && current.header.projectId !== input.projectId;
if (current.header.cwd === input.cwd && !projectIdChanged) return current;
if (current.header.cwd === input.cwd) {
return updateHeaderVersioned(
sessionId,
{ projectId: input.projectId },
input.expectedRevision,
);
}
const shellRunClose = await this.deps.shellRuns?.terminateSession(sessionId);
let committed: VersionedSessionHeader;
try {
await this.runtimeKernel.disposeBackend(sessionId);
committed = await updateHeaderVersioned(
sessionId,
{
cwd: input.cwd,
...(input.projectId === undefined ? {} : { projectId: input.projectId }),
},
input.expectedRevision,
);
} catch (error) {
if (shellRunClose) this.deps.shellRuns?.rollbackSessionClose(shellRunClose);
throw error;
}
if (shellRunClose) await this.deps.shellRuns?.commitSessionClose(shellRunClose);
this.deps.shellRuns?.resumeSession(sessionId);
return committed;
});
this.runtimeKernel.updateCachedHeader(sessionId, next.header);
return next;
}
async getMessages(sessionId: string): Promise<StoredMessage[]> {
return (await this.getSessionView(sessionId)).messages;
}
async getContextDiagnostics(sessionId: string): Promise<ContextDiagnostics> {
const runStore = this.deps.runStore;
return runStore
? readLatestContextDiagnostics(runStore, sessionId)
: { status: 'unavailable', reason: 'trace_unavailable' };
}
async listTurns(sessionId: string): Promise<TurnRecord[]> {
return (await this.getSessionView(sessionId)).turns;
}
async listShellRunUpdates(sessionId: string): Promise<ShellRunUpdate[]> {
const shellRuns = this.deps.shellRuns;
if (!shellRuns) return [];
const ownUpdates = await shellRuns.listSessionUpdates(sessionId);
const ownToolCalls = new Set(ownUpdates.map((update) => update.sourceToolCallId));
const messages = await this.readShellRunProjectionMessages(sessionId);
if (!messages) return ownUpdates;
const bashToolCalls = shellRunBashToolCallIds(messages);
const inherited = new Map<
string,
{
ref: string;
turnId: string;
toolUseId: string;
result: ShellRunUpdate['result'];
}
>();
for (const message of messages) {
if (
message.type === 'tool_result' &&
bashToolCalls.has(message.toolUseId) &&
!ownToolCalls.has(message.toolUseId) &&
message.content.kind === 'shell_run' &&
isActiveShellRunStatus(message.content.status)
) {
const { operation: _operation, ...result } = message.content;
inherited.set(message.toolUseId, {
ref: message.content.ref,
turnId: message.turnId,
toolUseId: message.toolUseId,
result,
});
}
}
if (inherited.size === 0) return ownUpdates;
const inheritedFrom = await this.deps.store.readHeader(sessionId);
const parentSessionId = inheritedFrom.revisionParentSessionId ?? inheritedFrom.parentSessionId;
if (!parentSessionId) return ownUpdates;
const inheritedUpdates = await Promise.all(
[...inherited.values()].map(async (candidate) => {
const owner = await this.resolveShellRunOwner(parentSessionId, candidate.ref);
return {
sessionId,
ownership: owner
? {
kind: 'source_owned',
sourceSessionId: parentSessionId,
ownerSessionId: owner.sessionId,
}
: { kind: 'source_unavailable', sourceSessionId: parentSessionId },
sourceTurnId: candidate.turnId,
sourceToolCallId: candidate.toolUseId,
result: owner?.result ?? candidate.result,
} satisfies ShellRunUpdate;
}),
);
return [...ownUpdates, ...inheritedUpdates];
}
async getShellRunUpdate(sessionId: string, ref: string): Promise<ShellRunUpdate | null> {
const shellRuns = this.deps.shellRuns;
if (!shellRuns) return null;
const own = await shellRuns.getSessionUpdate(sessionId, ref);
if (own) return own;
const messages = await this.readShellRunProjectionMessages(sessionId);
if (!messages) return null;
const bashToolCalls = shellRunBashToolCallIds(messages);
let candidate:
| {
turnId: string;
toolUseId: string;
result: ShellRunUpdate['result'];
}
| undefined;
for (const message of messages) {
if (
message.type === 'tool_result' &&
bashToolCalls.has(message.toolUseId) &&
message.content.kind === 'shell_run' &&
message.content.ref === ref &&
isActiveShellRunStatus(message.content.status)
) {
const { operation: _operation, ...result } = message.content;
candidate = { turnId: message.turnId, toolUseId: message.toolUseId, result };
}
}
if (!candidate) return null;
const inheritedFrom = await this.deps.store.readHeader(sessionId);
const parentSessionId = inheritedFrom.revisionParentSessionId ?? inheritedFrom.parentSessionId;
if (!parentSessionId) return null;
const owner = await this.resolveShellRunOwner(parentSessionId, ref);
return {
sessionId,
ownership: owner
? {
kind: 'source_owned',
sourceSessionId: parentSessionId,
ownerSessionId: owner.sessionId,
}
: { kind: 'source_unavailable', sourceSessionId: parentSessionId },
sourceTurnId: candidate.turnId,
sourceToolCallId: candidate.toolUseId,
result: owner?.result ?? candidate.result,
};
}
async recoverInterruptedSessions(): Promise<string[]> {
return this.recoverInterruptedSessionsWithPolicy({ kind: 'best_effort' });
}
async recoverInterruptedSessionsStrict(stores: StrictRecoveryStores): Promise<string[]> {
if (stores.sessionStore !== this.deps.store || stores.agentRunStore !== this.deps.runStore) {
throw new Error('Strict recovery stores must match the SessionManager composition');
}
return this.recoverInterruptedSessionsWithPolicy({ kind: 'strict', stores });
}
private async recoverInterruptedSessionsWithPolicy(policy: RecoveryPolicy): Promise<string[]> {
const interrupted = (await listSessionsForRecovery(this.deps.store, policy)).filter(
(session) => session.status !== 'archived',
);
const recovered = new Set<string>();
for (const session of interrupted) {
if (this.runtimeLedgerRepair) {
await recoverOr(
policy,
() => this.runtimeLedgerRepair!.repairSteeringMessagesOnce(session.id),
0,
);
}
if (this.runtimeKernel.hasActiveRuns(session.id)) continue;
// Fail-closed: a request whose live owner died can never be answered, so
// it settles as `deny` with a durable `host_restarted` reason. The run
// recovery below reads those settled rows back — it never depends on what
// this pass happened to close (#1612).
if (
!this.deps.interactionAuthority &&
this.deps.store.listPendingSandboxBoundaryRequests &&
this.deps.store.settleSandboxBoundaryRequest
) {
const pendingBoundaryRequests = await recoverOr(
policy,
() => this.deps.store.listPendingSandboxBoundaryRequests!(session.id),
[],
);
for (const request of pendingBoundaryRequests) {
await recoverOr(
policy,
() =>
this.deps.store.settleSandboxBoundaryRequest!({
sessionId: session.id,
requestId: request.requestId,
decision: 'deny',
closureReason: 'host_restarted',
}),
undefined,
);
}
}
if (this.deps.planStore) {
const planRecovery = await recoverOr(
policy,
() => this.deps.planStore!.interruptActiveExecution(session.id, 'runtime_recovery'),
null,
);
if (planRecovery) recovered.add(session.id);
}
if (this.deps.shellRuns) {
const recoveredShellRuns = await recoverOr(
policy,
() => this.deps.shellRuns!.recoverOrphanedSession(session.id),
0,
);
if (recoveredShellRuns > 0) recovered.add(session.id);
}
let messages: StoredMessage[] = [];
let messagesReadable = true;
try {
messages =
policy.kind === 'strict'
? await policy.stores.sessionStore.readMessagesForRecovery(session.id)
: await this.deps.store.readMessages(session.id);
} catch (error) {
if (policy.kind === 'strict') throw error;
messagesReadable = false;
}
if (session.revisionState === 'preparing' && messagesReadable) {
if (hasRevisionUserMessage(messages)) {
await recoverOr(policy, () => this.commitRevisionVersion(session.id), undefined);
} else {
await recoverOr(policy, () => this.remove(session.id), undefined);
recovered.add(session.id);
continue;
}
}
let continuationClaimRecovered = false;
const continuationAuthority = runtimeContinuationAuthority(this.deps.runtimeEventStore);
if (this.deps.runStore && continuationAuthority) {
try {
continuationClaimRecovered = await this.recoverContinuationClaimsBeforeProvider(
session.id,
continuationAuthority,
policy,
);
} catch (error) {
if (policy.kind === 'strict') throw error;
// A configured canonical continuation authority that cannot be read
// is not equivalent to "no continuation claim". Quarantine this
// session from every legacy/generic repair path until the authority
// becomes readable again.
continue;
}
if (continuationClaimRecovered) recovered.add(session.id);
}
if (this.deps.runStore) {
const runRecovery = await recoverOr(
policy,
() => this.recoverAgentRunsFromLedger(session.id, policy),
undefined,
);
if (runRecovery?.hasLedger) {
if (runRecovery.recovered || continuationClaimRecovered) {
await recoverOr(policy, () => this.updateStatus(session.id, 'active'), undefined);
recovered.add(session.id);
} else if (
!messagesReadable &&
(session.status === 'running' || session.status === 'waiting_for_user')
) {
await recoverOr(policy, () => this.updateStatus(session.id, 'active'), undefined);
recovered.add(session.id);
}
continue;
}
}
if (!messagesReadable) {
if (session.status === 'running' || session.status === 'waiting_for_user') {
// Recovery may run in BACKGROUND startup (#456): re-check for a
// run the user started while this session's recovery was in
// flight, so we never stomp a live run's status.
if (this.runtimeKernel.hasActiveRuns(session.id)) continue;
await recoverOr(policy, () => this.updateStatus(session.id, 'active'), undefined);
recovered.add(session.id);
}
continue;
}
const recoveries = interruptedTurnRecoveries(messages);
if (recoveries.length === 0) continue;
for (const recovery of recoveries) {
await recoverOr(
policy,
() =>
this.appendTurnState(session.id, recovery.turnId, 'failed', recovery.lineage, {
errorClass: recovery.errorClass,
}),
undefined,
);
}
if (session.status === 'running' || session.status === 'waiting_for_user') {
// Same double-check as above: a message sent mid-recovery owns
// the session status now (its own transitions will settle it).
if (this.runtimeKernel.hasActiveRuns(session.id)) {
recovered.add(session.id);
continue;
}
await recoverOr(policy, () => this.updateStatus(session.id, 'active'), undefined);
}
recovered.add(session.id);
}
return [...recovered];
}
async updateSession(sessionId: string, patch: Partial<SessionHeader>): Promise<SessionSummary> {
const backendConfigChanged = changesBackendConfig(patch);
if (backendConfigChanged && this.runtimeKernel.hasActiveRuns(sessionId)) {
throw new Error('Cannot change backend configuration while a turn is running');
}
const { permissionMode, name, titleIsManual: _titleIsManual, ...rest } = patch;
const permissionSummary =
permissionMode === undefined
? undefined
: await this.setPermissionMode(sessionId, permissionMode);
if (name === undefined && Object.keys(rest).length === 0) {
return permissionSummary ?? headerToSummary(await this.deps.store.readHeader(sessionId));
}
if (name !== undefined) await this.deps.store.rename(sessionId, name);
const next =
Object.keys(rest).length > 0
? await this.deps.store.updateHeader(sessionId, rest)
: await this.deps.store.readHeader(sessionId);
this.runtimeKernel.updateCachedHeader(sessionId, next);
if (changesBackendConfig(rest)) {
// AgentBackend instances snapshot backend/model config at construction
// time. If a stale session is rebound to a real default connection, the
// next turn must build a fresh backend instead of reusing FakeBackend or
// an AiSdkBackend pointed at a deleted connection.
await this.runtimeKernel.disposeBackend(sessionId);
}
return headerToSummary(next);
}
async archive(sessionId: string): Promise<void> {
const shellRunClose = await this.deps.shellRuns?.terminateSession(sessionId);
try {
await this.deps.store.archive(sessionId);
} catch (error) {
if (shellRunClose) this.deps.shellRuns?.rollbackSessionClose(shellRunClose);
throw error;
}
if (shellRunClose) await this.deps.shellRuns?.commitSessionClose(shellRunClose);
await this.runtimeKernel.disposeBackend(sessionId);
}
async unarchive(sessionId: string): Promise<void> {
await this.deps.store.unarchive(sessionId);
this.deps.shellRuns?.resumeSession(sessionId);
}
async setSessionStatus(
sessionId: string,
status: SessionStatus,
blockedReason?: SessionBlockedReason,
): Promise<SessionSummary> {
const next = await this.deps.store.updateHeader(
sessionId,
buildStatusPatch(status, this.deps.now(), blockedReason),
);
this.runtimeKernel.updateCachedHeader(sessionId, next);
return headerToSummary(next);
}
async commitRevisionVersion(sessionId: string): Promise<SessionSummary> {
const current = await this.deps.store.readHeader(sessionId);
if (current.revisionState !== 'preparing') return headerToSummary(current);
const next = await this.deps.store.updateHeader(sessionId, { revisionState: 'committed' });
this.runtimeKernel.updateCachedHeader(sessionId, next);
return headerToSummary(next);
}
async setFlagged(sessionId: string, isFlagged: boolean): Promise<void> {
await this.deps.store.setFlagged(sessionId, isFlagged);
const header = await this.deps.store.readHeader(sessionId).catch(() => undefined);
if (header) this.runtimeKernel.updateCachedHeader(sessionId, header);
}
async markSessionRead(sessionId: string, readThroughTs: number | undefined): Promise<void> {
if (readThroughTs === undefined || !Number.isFinite(readThroughTs)) return;
const next = await this.deps.store.markSessionReadThrough(sessionId, readThroughTs);
this.runtimeKernel.updateCachedHeader(sessionId, next);
}
async renameSession(sessionId: string, name: string): Promise<void> {
await this.deps.store.rename(sessionId, name);
const header = await this.deps.store.readHeader(sessionId).catch(() => undefined);
if (header) this.runtimeKernel.updateCachedHeader(sessionId, header);
}
async readExecutionBoundary(sessionId: string): Promise<ExecutionBoundary> {
return this.deps.store.readExecutionBoundary(sessionId);
}
async listActiveInteractions(sessionId: string): Promise<ActiveInteractionRequestEvent[]> {
await this.deps.store.readHeader(sessionId);
return this.runtimeKernel.listActiveInteractions?.(sessionId) ?? [];
}
async setPermissionMode(sessionId: string, mode: PermissionMode): Promise<SessionSummary> {
const previous = await this.deps.store.readHeader(sessionId);
const boundary = await this.deps.store.readExecutionBoundary(sessionId);
const leavingDeepResearch = isDeepResearchSession(previous.labels) && mode !== 'explore';
if (
previous.permissionMode === mode &&
executionBoundaryMatchesPermissionMode(boundary, mode) &&
!leavingDeepResearch
) {
return headerToSummary(previous);
}
if (this.runtimeKernel.hasActiveRuns(sessionId)) {
throw new Error('当前对话正在运行,等结束后再切换权限模式。');
}
if (previous.status === 'waiting_for_user') {
throw new Error('当前有工具调用正在等待确认,处理后再切换权限模式。');
}
const labels = leavingDeepResearch
? previous.labels.filter((label) => label !== DEEP_RESEARCH_SESSION_LABEL)
: previous.labels;
const nextKind = mode === 'bypass' ? 'bypass' : 'managed';
await this.commitExecutionBoundaryTransition(sessionId, boundary, nextKind, {
permissionMode: mode,
labels,
});
const next = await this.deps.store.readHeader(sessionId);
this.runtimeKernel.updateCachedHeader(sessionId, next);
await this.deps.store
.appendMessage(sessionId, {
type: 'system_note',
id: this.deps.newId(),
ts: this.deps.now(),
kind: 'mode_change',
data: { from: previous.permissionMode, to: mode },
} satisfies SystemNoteMessage)
.catch(() => undefined);
return headerToSummary(next);
}
async setExecutionBoundaryKind(
sessionId: string,
kind: 'managed' | 'bypass',
): Promise<ExecutionBoundary> {
if (this.runtimeKernel.hasActiveRuns(sessionId)) {
throw new Error('当前对话正在运行,等结束后再切换沙箱边界。');
}
const header = await this.deps.store.readHeader(sessionId);
if (header.status === 'waiting_for_user') {
throw new Error('当前有沙箱边界请求正在等待确认,处理后再切换。');
}
const current = await this.deps.store.readExecutionBoundary(sessionId);
const boundary = await this.commitExecutionBoundaryTransition(sessionId, current, kind);
return boundary;
}
private async commitExecutionBoundaryTransition(
sessionId: string,
current: ExecutionBoundary,
kind: 'managed' | 'bypass',
projection?: {
permissionMode: SessionHeader['permissionMode'];
labels?: readonly string[];
},
): Promise<ExecutionBoundary> {
const nextPermissionMode = projection?.permissionMode ?? (kind === 'bypass' ? 'bypass' : 'ask');
return this.commitExecutionResourceTransition(sessionId, nextPermissionMode, async () => {
const latest = await this.deps.store.readExecutionBoundary(sessionId);
if (latest.revision !== current.revision) {
throw new SessionConfigurationTransitionError(
'operation_conflict',
'Session execution boundary changed before the transition',
);
}
return () => this.deps.store.setExecutionBoundaryKind(sessionId, kind, projection);
});
}
private async commitExecutionResourceTransition<T>(
sessionId: string,
nextPermissionMode: PermissionMode,
prepareCommit: () => Promise<() => Promise<T>>,
): Promise<T> {
const initialBoundary = await this.deps.store.readExecutionBoundary(sessionId);
const initiallyNarrows = narrowsExecutionAuthority(initialBoundary, nextPermissionMode);
const initialDescendants = initiallyNarrows
? await this.listLinkedDescendantSessionIds(sessionId)
: [];
const fencedSessionIds = [sessionId, ...initialDescendants];
return this.runSessionQuiescentMutation<T>(fencedSessionIds, async () => {
const currentBoundary = await this.deps.store.readExecutionBoundary(sessionId);
const narrowsShellAuthority = narrowsExecutionAuthority(currentBoundary, nextPermissionMode);
const descendantSessionIds = narrowsShellAuthority
? await this.listLinkedDescendantSessionIds(sessionId)
: [];
if (
descendantSessionIds.some(
(descendantSessionId) => !fencedSessionIds.includes(descendantSessionId),
)
) {
throw new SessionConfigurationTransitionError(
'operation_conflict',
'Session lineage changed before the configuration transition',
);
}
const lineageSessionIds = [sessionId, ...descendantSessionIds];
if (lineageSessionIds.some((id) => this.runtimeKernel.hasActiveRuns(id))) {
throw new SessionConfigurationTransitionError(
'session_busy',
'Session configuration cannot change while a linked Turn is active',
);
}
if (narrowsShellAuthority && !this.deps.shellRuns) {
throw new SessionConfigurationTransitionError(
'operation_unavailable',
'Session permission narrowing requires Runtime Resource authority',
);
}
const commit = await prepareCommit();
const descendantBoundaries = new Map<string, ExecutionBoundary>();
for (const descendantSessionId of descendantSessionIds) {
descendantBoundaries.set(
descendantSessionId,
await this.deps.store.readExecutionBoundary(descendantSessionId),
);
}
const shellRunCloses: Array<Awaited<ReturnType<ShellRunProcessManager['terminateSession']>>> =
[];
try {
if (narrowsShellAuthority) {
for (const lineageSessionId of lineageSessionIds) {
const close = await this.deps.shellRuns?.terminateSession(lineageSessionId);
if (close) shellRunCloses.push(close);
}
}
await Promise.all(
lineageSessionIds.map((lineageSessionId) =>
this.runtimeKernel.disposeBackend(lineageSessionId),
),
);
} catch {
for (const close of shellRunCloses) this.deps.shellRuns?.rollbackSessionClose(close);
throw new SessionConfigurationTransitionError(
'operation_unavailable',
'Session execution resources could not be refreshed',
);
}
let result: T;
try {
result = await commit();
} catch (error) {
for (const close of shellRunCloses) this.deps.shellRuns?.rollbackSessionClose(close);
throw error;
}
for (const close of shellRunCloses) await this.deps.shellRuns?.commitSessionClose(close);
if (shellRunCloses.length > 0) {
const committedBoundary = await this.deps.store.readExecutionBoundary(sessionId);
this.deps.shellRuns?.resumeSession(sessionId);
for (const [descendantSessionId, descendantBoundary] of descendantBoundaries) {
if (executionBoundaryContains(committedBoundary, descendantBoundary)) {
this.deps.shellRuns?.resumeSession(descendantSessionId);
}
}
}
return result;
});
}
private async runSessionQuiescentMutation<T>(
sessionIds: readonly string[],
operation: () => Promise<T>,
): Promise<T> {
if (!this.runtimeKernel.runSessionQuiescentMutation) {
throw new SessionConfigurationTransitionError(
'operation_unavailable',
'Session execution mutation authority is unavailable',
);
}
try {
return await this.runtimeKernel.runSessionQuiescentMutation(sessionIds, operation);
} catch (error) {
if (error instanceof SessionQuiescentMutationBusyError) {
throw new SessionConfigurationTransitionError(
'session_busy',
'Session configuration cannot change while a linked Turn is active',
);
}
throw error;
}
}
private async listLinkedDescendantSessionIds(sessionId: string): Promise<string[]> {
const sessions = await this.deps.store.list();
const childrenByParent = new Map<string, string[]>();
for (const session of sessions) {
const parentSessionId = session.subagentParent?.parentSessionId;
if (!parentSessionId) continue;
const children = childrenByParent.get(parentSessionId) ?? [];
children.push(session.id);
childrenByParent.set(parentSessionId, children);
}
const descendants: string[] = [];
const pending = [...(childrenByParent.get(sessionId) ?? [])];
const seen = new Set([sessionId]);
while (pending.length > 0) {
const descendantSessionId = pending.shift()!;
if (seen.has(descendantSessionId)) continue;
seen.add(descendantSessionId);
descendants.push(descendantSessionId);
pending.push(...(childrenByParent.get(descendantSessionId) ?? []));
}
return descendants;
}
async getPlanState(sessionId: string): Promise<PlanSessionState> {
return this.requirePlanStore().readState(sessionId);
}
hasPlanAuthority(): boolean {
return this.deps.planStore !== undefined;
}
async setCollaborationMode(sessionId: string, mode: CollaborationMode): Promise<SessionSummary> {
const previous = await this.deps.store.readHeader(sessionId);
const from = previous.collaborationMode ?? 'agent';
if (from === mode) return headerToSummary(previous);
if (mode === 'plan' && previous.subagentParent) {
throw new PlanConflictError('Linked child Sessions cannot enter Plan mode');
}
if (this.runtimeKernel.hasActiveRuns(sessionId)) {
throw new Error('当前对话正在运行,等结束后再切换协作模式。');
}
if (previous.status === 'waiting_for_user') {
throw new Error('当前有工具调用正在等待确认,处理后再切换协作模式。');
}
const planState = await this.requirePlanStore().readState(sessionId);
if (mode === 'plan' && planState.activeExecutionId) {
throw new Error('当前计划仍在执行,结束或中断后才能切换到 Plan Mode。');
}
const latestProposal = planState.proposals.find(
(proposal) => proposal.proposalId === planState.latestProposalId,
);
if (mode === 'agent' && latestProposal?.status === 'pending_approval') {
throw new Error('当前方案正在等待审批,请明确放弃方案后再退出 Plan Mode。');
}
const next = await this.deps.store.updateHeader(sessionId, {
collaborationMode: mode,
});
await this.deps.store.appendMessage(sessionId, {
type: 'system_note',
id: this.deps.newId(),
ts: this.deps.now(),
kind: 'mode_change',
data: { dimension: 'collaboration', from, to: mode },
} satisfies SystemNoteMessage);
this.runtimeKernel.updateCachedHeader(sessionId, next);
await this.runtimeKernel.disposeBackend(sessionId);
return headerToSummary(next);
}
async setOrchestrationMode(sessionId: string, mode: OrchestrationMode): Promise<SessionSummary> {
const previous = await this.deps.store.readHeader(sessionId);
const from = previous.orchestrationMode ?? 'default';
if (from === mode) return headerToSummary(previous);
if (this.runtimeKernel.hasActiveRuns(sessionId)) {
throw new Error('Cannot change orchestration mode while a turn is running.');
}
if (previous.status === 'waiting_for_user') {
throw new Error('Cannot change orchestration mode while a tool call awaits confirmation.');
}
const next = await this.deps.store.updateHeader(sessionId, { orchestrationMode: mode });
await this.deps.store.appendMessage(sessionId, {
type: 'system_note',
id: this.deps.newId(),
ts: this.deps.now(),
kind: 'mode_change',
data: { dimension: 'orchestration', from, to: mode },
} satisfies SystemNoteMessage);
this.runtimeKernel.updateCachedHeader(sessionId, next);
return headerToSummary(next);
}
async requestPlanRevision(
sessionId: string,
proposalId: string,
operationId?: string,
): Promise<PlanMutationResult> {
const input = {
sessionId,
proposalId,
...(operationId ? { operationId } : {}),
};
const replay = await this.isPlanOperationReplay(sessionId, operationId, input);
if (!replay) await this.runtimeKernel.disposeBackend(sessionId);
const result = await this.requirePlanStore().requestRevision(input);
await this.finalizePlanCollaborationMode(sessionId, 'plan');
return result;
}
async abandonPlanProposal(
sessionId: string,
proposalId: string,
operationId?: string,
): Promise<PlanMutationResult> {
const input = {
sessionId,
proposalId,
reason: PLAN_USER_ABANDON_REASON,
...(operationId ? { operationId } : {}),
};
const replay = await this.isPlanOperationReplay(sessionId, operationId, input);
const header = await this.deps.store.readHeader(sessionId);
if (!replay && this.runtimeKernel.hasActiveRuns(sessionId)) {
throw new PlanConflictError('Cannot abandon a Plan while the Session is running');
}
if (!replay && header.status === 'waiting_for_user') {
throw new PlanConflictError('Cannot abandon a Plan while an Interaction is pending');
}
if (!replay) await this.runtimeKernel.disposeBackend(sessionId);
const result = await this.requirePlanStore().abandonProposal(input);
await this.finalizePlanAbandonment(sessionId, operationId, replay);
return result;
}
async approvePlan(input: ApprovePlanProposalInput): Promise<PlanMutationResult> {
const replay = await this.isPlanOperationReplay(input.sessionId, input.operationId, input);
const header = await this.deps.store.readHeader(input.sessionId);
if (!replay && this.runtimeKernel.hasActiveRuns(input.sessionId)) {
throw new PlanConflictError('Cannot approve a Plan while the Session is running');
}
if (!replay && header.status === 'waiting_for_user') {
throw new PlanConflictError('Cannot approve a Plan while an Interaction is pending');
}
if (!replay) await this.runtimeKernel.disposeBackend(input.sessionId);
const result = await this.requirePlanStore().approveProposal(input);
await this.finalizePlanCollaborationMode(input.sessionId, 'agent');
return result;
}
async resumePlanExecution(
sessionId: string,
executionId: string,
operationId?: string,
): Promise<PlanMutationResult> {
const input = { sessionId, executionId };
const replay = await this.isPlanOperationReplay(sessionId, operationId, input);
if (!replay) await this.runtimeKernel.disposeBackend(sessionId);
const result = await this.requirePlanStore().resumeExecution(
sessionId,
executionId,
operationId,
);
await this.finalizePlanCollaborationMode(sessionId, 'agent');
return result;
}
async cancelPlanExecution(
sessionId: string,
executionId: string,
operationId?: string,
): Promise<PlanMutationResult> {
const planStore = this.requirePlanStore();
const input = {
sessionId,
executionId,
reason: PLAN_USER_CANCEL_REASON,
...(operationId ? { operationId } : {}),
};
const replay = await this.isPlanOperationReplay(sessionId, operationId, input);
if (!replay) {
const state = await planStore.readState(sessionId);
const execution = state.executions.find((item) => item.executionId === executionId);
if (execution?.status !== 'interrupted') {
throw new PlanConflictError('Only an interrupted Plan execution can be abandoned');
}
await this.runtimeKernel.disposeBackend(sessionId);
}
const result = await planStore.cancelExecution(input);
return result;
}
async interruptActivePlanExecution(
sessionId: string,
reason: string,
operationId?: string,
): Promise<PlanMutationResult | null> {
const planStore = this.requirePlanStore();
const replay = await this.isPlanOperationReplay(sessionId, operationId, {
sessionId,
reason,
});
if (!replay) {
const state = await planStore.readState(sessionId);
if (!state.activeExecutionId) return null;
await this.runtimeKernel.disposeBackend(sessionId);
}
return planStore.interruptActiveExecution(sessionId, reason, operationId);
}
async remove(sessionId: string): Promise<void> {
const shellRunClose = await this.deps.shellRuns?.terminateSession(sessionId);
try {
await this.runtimeKernel.disposeBackend(sessionId);
await this.deps.store.remove(sessionId);
} catch (error) {
if (shellRunClose) this.deps.shellRuns?.rollbackSessionClose(shellRunClose);
throw error;
}
if (shellRunClose) await this.deps.shellRuns?.commitSessionClose(shellRunClose);
}
// --------------------------------------------------------------------------
// Send / stream — Phase 1 vertical heart
// --------------------------------------------------------------------------
/**
* Send a user message and stream back normalized events. The caller
* (desktop main) is expected to forward the events to the renderer over
* the IPC bridge.
*
* Runtime v2 bridge: SessionManager remains the public facade; RuntimeKernel
* owns AgentRun/AiSdkFlow/RuntimeRunner orchestration and ledger recording.
*/
async *sendMessage(
sessionId: string,
input: UserMessageInput,
options: TurnStartOptions = {},
): AsyncIterable<SessionEvent> {
const sourceText = sessionTitleSource(input);
const onRunStarted = this.deps.generateSessionTitle
? async (runId: string, header: SessionHeader) => {
await options.onRunStarted?.(runId, header);
if (
!header.connectionLocked &&
!header.titleIsManual &&
header.name === DEFAULT_SESSION_NAME &&
sourceText
) {
void this.generateTitleInBackground(sessionId, header, sourceText);
}
}
: options.onRunStarted;
yield* this.runtimeKernel.startTurn(sessionId, input, { ...options, onRunStarted });
}
private async generateTitleInBackground(
sessionId: string,
header: SessionHeader,
sourceText: string,
): Promise<void> {
let generated: string | undefined;
try {
generated = await this.deps.generateSessionTitle?.({ sessionId, header, sourceText });
} catch {}
try {
const title = generated ?? fallbackSessionTitle(sourceText);
if (!title) return;
const next = await this.deps.store.setGeneratedTitleIfAbsent?.(sessionId, title);
if (!next) return;
this.runtimeKernel.updateCachedHeader(sessionId, next);
this.deps.onSessionTitleChanged?.(sessionId);
} catch {}
}
async planSafeBoundaryContinuation(
sessionId: string,
input: PlanSafeBoundaryContinuationInput,
): Promise<SafeBoundaryContinuationPlan> {
const planner = new RuntimeContinuationPlanner({
readSourceRun: async (targetSessionId, runId) => {
if (!this.deps.runStore) throw new Error('AgentRunStore is not configured');
return this.deps.runStore.readRun(targetSessionId, runId);
},
readImmutableRuntimePrefix: async (prefixInput) => {
const authority = runtimeContinuationAuthority(this.deps.runtimeEventStore);
if (!authority) {
throw new Error('Immutable RuntimeEvent prefix reader is not configured');
}
return authority.readImmutableRuntimePrefix(prefixInput);
},
readContinuationClaimStateByBoundary: async (boundaryDigest) => {
const authority = runtimeContinuationAuthority(this.deps.runtimeEventStore);
if (!authority) throw new Error('Continuation authority is not configured');
return authority.readContinuationClaimStateByBoundary(boundaryDigest);
},
findExistingContinuation: async (
targetSessionId,
sourceRunId,
sourceRuntimeEventHighWater,
) => {
if (!this.deps.runStore) throw new Error('AgentRunStore is not configured');
return (await this.deps.runStore.listSessionRuns(targetSessionId)).find(
(run) =>
run.continuationSource?.sourceRunId === sourceRunId &&
run.continuationSource.sourceRuntimeEventHighWater === sourceRuntimeEventHighWater,
);
},
newId: this.deps.newId,
});
const plan = await planner.plan({ sessionId, ...input });
this.recordContinuationPlan(sessionId, input.sourceRunId, plan);
return plan;
}
async planAuthoritativeSafeBoundaryContinuation(
sessionId: string,
input: PlanAuthoritativeSafeBoundaryContinuationInput,
): Promise<SafeBoundaryContinuationPlan> {
if (this.deps.safeBoundaryResumeEnabled !== true) {
const plan = resumeFeatureDisabledPlan();
this.recordContinuationPlan(sessionId, input.sourceRunId, plan);
return plan;
}
if (!this.deps.runStore || !this.deps.inspectContinuationSafety) {
const plan: SafeBoundaryContinuationPlan = {
disposition: 'park',
rejectionReasons: ['safety_observation_unavailable'],
diagnostics: [
{
code: 'safety_observation_unavailable',
message: 'authoritative continuation safety inspection is not configured',
},
],
};
this.recordContinuationPlan(sessionId, input.sourceRunId, plan);
return plan;
}
const sourceRun = await this.deps.runStore
.readRun(sessionId, input.sourceRunId)
.catch(() => undefined);
if (!sourceRun) {
const plan: SafeBoundaryContinuationPlan = {
disposition: 'park',
rejectionReasons: ['source_run_unreadable'],
diagnostics: [
{ code: 'source_run_unreadable', message: 'source AgentRun could not be read' },
],
};
this.recordContinuationPlan(sessionId, input.sourceRunId, plan);
return plan;
}
if (!sourceRun.workspaceIdentity) {
const plan: SafeBoundaryContinuationPlan = {
disposition: 'park',
rejectionReasons: ['workspace_identity_missing'],
diagnostics: [
{
code: 'workspace_identity_missing',
message: 'source AgentRun has no authoritative workspace identity',
},
],
};
this.recordContinuationPlan(sessionId, input.sourceRunId, plan);
return plan;
}
const header = await this.deps.store.readHeader(sessionId);
let observation: RuntimeContinuationSafetyObservation;
try {
observation = await this.deps.inspectContinuationSafety(sessionId);
} catch {
const plan: SafeBoundaryContinuationPlan = {
disposition: 'park',
rejectionReasons: ['safety_observation_unavailable'],
diagnostics: [
{
code: 'safety_observation_unavailable',
message: 'authoritative continuation safety inspection failed',
},
],
};
this.recordContinuationPlan(sessionId, input.sourceRunId, plan);
return plan;
}
return this.planSafeBoundaryContinuation(sessionId, {
sourceRunId: input.sourceRunId,
currentCwd: header.cwd,
sourceWorkspaceIdentity: sourceRun.workspaceIdentity,
currentWorkspaceIdentity: observation.workspaceIdentity,
backgroundOperationsSettled: observation.backgroundOperationsSettled,
availableToolNames: observation.availableToolNames,
...(input.expectedRuntimeEventHighWater !== undefined
? { expectedRuntimeEventHighWater: input.expectedRuntimeEventHighWater }
: {}),
...(observation.workspaceCheckpoint
? { workspaceCheckpoint: observation.workspaceCheckpoint }
: {}),
});
}
async planLatestAuthoritativeSafeBoundaryContinuation(
sessionId: string,
): Promise<SafeBoundaryContinuationPlan> {
if (this.deps.safeBoundaryResumeEnabled !== true) {
const plan = resumeFeatureDisabledPlan();
this.recordContinuationPlan(sessionId, '', plan);
return plan;
}
if (!this.deps.runStore) {
const plan: SafeBoundaryContinuationPlan = {
disposition: 'park',
rejectionReasons: ['resume_candidate_missing'],
diagnostics: [
{
code: 'resume_candidate_missing',
message: 'no AgentRun store is configured for resume discovery',
},
],
};
this.recordContinuationPlan(sessionId, '', plan);
return plan;
}
const candidate = (await this.deps.runStore.listSessionRuns(sessionId))
.filter(
(run) => (run.status === 'failed' || run.status === 'cancelled') && isSessionInlineRun(run),
)
.sort(
(left, right) => right.createdAt - left.createdAt || right.runId.localeCompare(left.runId),
)[0];
if (!candidate) {
const plan: SafeBoundaryContinuationPlan = {
disposition: 'park',
rejectionReasons: ['resume_candidate_missing'],
diagnostics: [
{
code: 'resume_candidate_missing',
message: 'no failed or cancelled top-level continuation candidate exists',
},
],
};
this.recordContinuationPlan(sessionId, '', plan);
return plan;
}
return this.planAuthoritativeSafeBoundaryContinuation(sessionId, {
sourceRunId: candidate.runId,
});
}
async *resumeSafeBoundaryContinuation(
continuation: RuntimeContinuation,
options: ResumeContinuationOptions = {},
): AsyncIterable<SessionEvent> {
const resume = this.runtimeKernel.resumeContinuation;
if (!resume) throw new Error('RuntimeKernel does not support safe-boundary continuation');
this.recordContinuationLifecycleEvent({
type: 'execution_started',
sessionId: continuation.sessionId,
sourceRunId: continuation.sourceRunId,
targetRunId: continuation.runId,
});
try {
yield* resume.call(this.runtimeKernel, continuation, options);
this.recordContinuationLifecycleEvent({
type: 'execution_completed',
sessionId: continuation.sessionId,
sourceRunId: continuation.sourceRunId,
targetRunId: continuation.runId,
});
} catch (error) {
this.recordContinuationLifecycleEvent({
type: 'execution_failed',
sessionId: continuation.sessionId,
sourceRunId: continuation.sourceRunId,
targetRunId: continuation.runId,
errorClass: continuationExecutionErrorClass(error),
});
throw error;
}
}
private recordContinuationPlan(
sessionId: string,
sourceRunId: string,
plan: SafeBoundaryContinuationPlan,
): void {
if (plan.disposition === 'continue' && plan.continuation) {
this.recordContinuationLifecycleEvent({
type: 'plan_approved',
sessionId,
sourceRunId,
targetRunId: plan.continuation.runId,
});
return;
}
this.recordContinuationLifecycleEvent({
type: 'plan_parked',
sessionId,
sourceRunId,
rejectionReasons: plan.rejectionReasons,
});
}
private recordContinuationLifecycleEvent(event: RuntimeContinuationLifecycleEvent): void {
try {
const result = this.deps.onContinuationLifecycleEvent?.(event);
if (result) void Promise.resolve(result).catch(() => {});
} catch {
// Operational telemetry must never alter resume correctness.
}
}
async *compactSession(
sessionId: string,
input: CompactSessionInput = {},
): AsyncIterable<SessionEvent> {
yield* this.runtimeKernel.compactSession(sessionId, input);
}
async preflightContextCompaction(sessionId: string): Promise<void> {
await this.runtimeKernel.preflightContextCompaction(sessionId);
}
async *startChildTurn(
sessionId: string,
input: ChildAgentTurnInput,
): AsyncIterable<SessionEvent> {
const execution = this.runtimeKernel.claimExecution(sessionId);
try {
await this.ensureChildWorkspace(await this.deps.store.readHeader(sessionId));
yield* this.runtimeKernel.startChildTurn(sessionId, input, execution);
} finally {
execution.release();
}
}
async spawnChildAgent(
sessionId: string,
input: SpawnChildAgentInput,
): Promise<SpawnChildAgentResult> {
const execution = this.runtimeKernel.claimExecution(sessionId);
try {
const definition = requireBuiltinAgentDefinition(input.spec.id);
return await this.runChildAgent(sessionId, definition, input, execution);
} finally {
execution.release();
}
}
/**
* Create and run a durable linked child Session without changing the
* parent-facing agent_spawn compatibility path.
*
* Cross-session provenance lives on the child header. The first AgentRun
* intentionally carries no parentRunId, so it is an ordinary session-inline
* run and every later child turn can reuse only the child's own history.
*/
async spawnChildSession(
parentSessionId: string,
input: SpawnChildSessionInput,
): Promise<SpawnChildSessionResult> {
const resolvedInput = await this.resolveChildSessionSelector(input);
const spawnKey = childSessionSpawnKey(parentSessionId, resolvedInput);
const requestFingerprint = childSessionRequestFingerprint(parentSessionId, resolvedInput);
const inFlight = this.childSessionSpawns.get(spawnKey);
if (inFlight) {
if (inFlight.requestFingerprint !== requestFingerprint) {
throw new Error('Child-session spawn identity was reused for different work');
}
return await inFlight.promise;
}
const runtimeOwner = {
execution: this.runtimeKernel.claimExecution(parentSessionId),
};
const promise = this.spawnChildSessionOnce(
parentSessionId,
resolvedInput,
requestFingerprint,
runtimeOwner,
).finally(() => runtimeOwner.execution.release());
this.childSessionSpawns.set(spawnKey, { requestFingerprint, promise });
try {
return await promise;
} finally {
if (this.childSessionSpawns.get(spawnKey)?.promise === promise) {
this.childSessionSpawns.delete(spawnKey);
}
}
}
private async resolveChildSessionSelector(
input: SpawnChildSessionInput,
): Promise<ResolvedSpawnChildSessionInput> {
if (!input.subagentId) return { ...input };
if (!this.deps.subagentCatalog) {
throw new Error('Configured subagent catalog is unavailable in this runtime');
}
const resolvedPreset = await this.deps.subagentCatalog.resolve(input.subagentId);
if (resolvedPreset.profile !== input.agentProfile) {
throw new Error(`Subagent preset "${input.subagentId}" profile changed during spawn`);
}
return { ...input, resolvedPreset };
}
/**
* Atomically materialize one catalog agent as a durable child Session and a
* monotonic graph-topology operator.
*
* This is metadata admission only. The reserved first turn/run is executed
* later through the ordinary claimed graph-intent path.
*/
async provisionAgentGraphOperator(
input: ProvisionAgentGraphOperatorInput,
): Promise<ProvisionAgentGraphOperatorResult> {
if (!this.runtimeKernel.runSessionAdmissionMutation) {
throw new Error('Graph operator provisioning requires Runtime admission mutation authority');
}
return this.runtimeKernel.runSessionAdmissionMutation([input.source.sessionId], () =>
this.provisionAgentGraphOperatorFromParentSnapshot(input),
);
}
private async provisionAgentGraphOperatorFromParentSnapshot(
input: ProvisionAgentGraphOperatorInput,
): Promise<ProvisionAgentGraphOperatorResult> {
const create = this.deps.store.createAgentGraphOperator;
if (!create || !this.deps.runStore || !this.deps.runtimeEventStore) {
throw new Error(
'Graph operator provisioning requires SQLite Session metadata, AgentRunStore, and RuntimeEventStore',
);
}
if (input.source.sessionId.length === 0) {
throw new Error('Graph operator provision requires a supervisor Session');
}
const [parentHeader, sourceRun, parentBoundary] = await Promise.all([
this.deps.store.readHeader(input.source.sessionId),
this.deps.runStore.readRun(input.source.sessionId, input.source.runId),
this.deps.store.readExecutionBoundary(input.source.sessionId),
]);
if (
sourceRun.sessionId !== input.source.sessionId ||
sourceRun.runId !== input.source.runId ||
sourceRun.turnId !== input.source.turnId
) {
throw new Error('Graph schedule source does not match its durable supervisor run');
}
if ((input.agentId ? 1 : 0) + (input.subagentId ? 1 : 0) !== 1) {
throw new Error('Graph operator provision requires exactly one agent or subagent preset id');
}
const resolvedPreset = input.subagentId
? await this.deps.subagentCatalog?.resolve(input.subagentId)
: undefined;
if (input.subagentId && !resolvedPreset) {
throw new Error('Configured subagent catalog is unavailable in this runtime');
}
const definition = resolvedPreset
? requireBuiltinAgentDefinitionByProfile(resolvedPreset.profile)
: requireBuiltinAgentDefinition(input.agentId!);
assertAgentDefinitionRunnable({
definition,
tools: await this.childToolsForSession(input.source.sessionId),
worktreeChildExecutorAvailable: await this.isWorktreeChildExecutorAvailable(parentHeader),
});
const childPermissionMode =
parentHeader.permissionMode === 'bypass' ? 'bypass' : definition.permissionMode;
const initialTurnId = this.deps.newId();
const initialRunId = this.deps.newId();
const identityHash = stableHash({
schemaVersion: AGENT_GRAPH_OPERATOR_PROVISION_SCHEMA_VERSION,
graphId: input.graphId,
workId: input.workId,
}).slice('sha256:'.length, 'sha256:'.length + 32);
const provisionFingerprint = stableHash({
schemaVersion: AGENT_GRAPH_OPERATOR_PROVISION_SCHEMA_VERSION,
graphId: input.graphId,
workId: input.workId,
agentId: definition.id,
operatorId: input.operatorId,
source: input.source,
edges: input.edges,
definition: {
definitionVersion: definition.definitionVersion,
agentId: definition.id,
profile: definition.profile,
workspace: definition.contract.workspace,
permissionMode: childPermissionMode,
toolNames: [...definition.tools],
categoryPolicy: {},
systemPrompt: definition.systemPrompt,
...(resolvedPreset
? {
preset: {
id: resolvedPreset.id,
name: resolvedPreset.name,
connectionSlug: resolvedPreset.connectionSlug,
model: resolvedPreset.model,
thinkingLevel: resolvedPreset.thinkingLevel,
},
}
: {}),
},
});
const workspace = await this.provisionChildWorkspace(
parentHeader,
definition,
provisionFingerprint,
);
const request: AgentGraphOperatorProvisionRequest = {
schemaVersion: AGENT_GRAPH_OPERATOR_PROVISION_SCHEMA_VERSION,
provisionId: `graph_provision_${identityHash}`,
provisionFingerprint,
graphId: input.graphId,
workId: input.workId,
agentId: definition.id,
operatorId: input.operatorId,
initialTurnId,
initialRunId,
edges: input.edges.map((edge) => ({ ...edge })),
};
const result = await create.call(
this.deps.store,
{
cwd: workspace?.worktreePath ?? parentHeader.cwd,
...(parentHeader.projectId !== undefined ? { projectId: parentHeader.projectId } : {}),
name: resolvedPreset?.name ?? definition.name,
backend: parentHeader.backend,
llmConnectionSlug: resolvedPreset?.connectionSlug ?? parentHeader.llmConnectionSlug,
model: resolvedPreset?.model ?? parentHeader.model,
...(resolvedPreset
? resolvedPreset.thinkingLevel !== undefined
? { thinkingLevel: resolvedPreset.thinkingLevel }
: {}
: parentHeader.thinkingLevel !== undefined
? { thinkingLevel: parentHeader.thinkingLevel }
: {}),
permissionMode: childPermissionMode,
collaborationMode: 'agent',
orchestrationMode: 'default',
subagentParent: {
kind: 'subagent',
parentSessionId: input.source.sessionId,
spawnedBy: {
parentRunId: input.source.runId,
parentTurnId: input.source.turnId,
toolCallId: input.source.toolCallId,
},
graph: {
graphId: input.graphId,
workId: input.workId,
operatorId: input.operatorId,
},
lifecycle: 'foreground',
},
subagentRuntime: {
schemaVersion: SUBAGENT_SESSION_RUNTIME_SCHEMA_VERSION,
definitionVersion: definition.definitionVersion,
agentId: definition.id,
agentName: resolvedPreset?.name ?? definition.name,
profile: definition.profile,
...(resolvedPreset ? { presetId: resolvedPreset.id } : {}),
systemPrompt: definition.systemPrompt,
toolNames: [...definition.tools],
categoryPolicy: {},
},
subagentSpawn: {
schemaVersion: SUBAGENT_SESSION_SPAWN_SCHEMA_VERSION,
requestFingerprint: provisionFingerprint.slice('sha256:'.length),
initialTurnId,
initialRunId,
},
...(workspace ? { subagentWorkspace: workspace } : {}),
},
request,
input.expectedScheduleRevision,
parentBoundary,
);
const relation = result.header.subagentParent?.graph;
if (
relation?.graphId !== input.graphId ||
relation.workId !== input.workId ||
relation.operatorId !== result.provision.operatorId ||
result.header.id !== result.provision.targetSessionId ||
!sameSubagentWorkspace(result.header.subagentWorkspace, workspace)
) {
throw new Error('Stored graph operator provision returned mismatched Session metadata');
}
return result;
}
/**
* Execute one durably claimed graph intent through the existing
* session-inline child runtime primitive.
*
* The graph claim is admission authority only. Once its exact run identity
* exists, the AgentRun/RuntimeEvent ledgers are execution authority and a
* retry observes or recovers that run instead of invoking the backend again.
*/
async runClaimedAgentGraphIntent(
input: RunClaimedAgentGraphIntentInput,
): Promise<ClaimedAgentGraphIntentResult> {
const hosted = isRuntimeHostedRootAuthority(this.deps.messageAuthority);
const hostedGraphExecution = hosted ? this.deps.hostedAgentGraphExecution : undefined;
if (hosted && !hostedGraphExecution) {
throw new RuntimeMessageAuthorityInvariantError(
'Hosted claimed graph execution requires its trusted graph execution capability',
);
}
const storedClaim = await (hostedGraphExecution ?? input.claimStore).readAgentGraphIntentClaim(
input.graphId,
input.intentId,
);
if (!storedClaim) {
throw new Error(`Graph intent ${input.graphId}/${input.intentId} has not been claimed`);
}
const claim = decodeAgentGraphIntentClaim(storedClaim);
if (claim.graphId !== input.graphId || claim.intentId !== input.intentId) {
throw new Error('Graph intent claim store returned a mismatched identity');
}
assertAgentGraphIntentExecutionMatchesClaim(claim, input.intent, input.prompt);
const resolved: ResolvedClaimedAgentGraphIntentInput = {
claim,
intent: input.intent,
prompt: input.prompt,
...(input.admitExecution ? { admitExecution: input.admitExecution } : {}),
...(hostedGraphExecution ? { hostedGraphExecution } : {}),
...(input.abortSignal ? { abortSignal: input.abortSignal } : {}),
...(input.onReady ? { onReady: input.onReady } : {}),
...(input.onEvent ? { onEvent: input.onEvent } : {}),
};
const requestFingerprint = claimedAgentGraphIntentRequestFingerprint(resolved);
const inFlight = this.claimedAgentGraphIntentRuns.get(claim.claimId);
if (inFlight) {
if (inFlight.requestFingerprint !== requestFingerprint) {
throw new Error('Graph intent claim identity was reused for different execution input');
}
return await inFlight.promise;
}
const runtimeExecution = this.runtimeKernel.claimExecution(claim.targetSessionId);
const promise = this.enqueueClaimedAgentGraphIntent(resolved, runtimeExecution).finally(() =>
runtimeExecution.release(),
);
this.claimedAgentGraphIntentRuns.set(claim.claimId, {
requestFingerprint,
promise,
});
try {
return await promise;
} finally {
if (this.claimedAgentGraphIntentRuns.get(claim.claimId)?.promise === promise) {
this.claimedAgentGraphIntentRuns.delete(claim.claimId);
}
}
}
private enqueueClaimedAgentGraphIntent(
input: ResolvedClaimedAgentGraphIntentInput,
runtimeExecution: RuntimeExecutionClaim,
): Promise<ClaimedAgentGraphIntentResult> {
const sessionId = input.claim.targetSessionId;
const previous = this.claimedAgentGraphSessionTails.get(sessionId) ?? Promise.resolve();
let enteredQueue = false;
const queuedExecution = previous
.catch(() => {
// A failed predecessor releases the Session slot for the next claim.
})
.then(() => {
if (input.abortSignal?.aborted) {
throw new Error('Claimed graph execution was cancelled before runtime admission');
}
if (runtimeExecution.stopSignal.aborted) {
throw runtimeExecution.stopSignal.reason;
}
enteredQueue = true;
return this.runClaimedAgentGraphIntentOnce(input, runtimeExecution);
});
const tail = queuedExecution.then(
() => {},
() => {},
);
this.claimedAgentGraphSessionTails.set(sessionId, tail);
void tail.then(() => {
if (this.claimedAgentGraphSessionTails.get(sessionId) === tail) {
this.claimedAgentGraphSessionTails.delete(sessionId);
}
});
let rejectStopped!: (reason?: unknown) => void;
const stopped = new Promise<never>((_resolve, reject) => {
rejectStopped = reject;
});
const onRuntimeStop = (): void => {
if (!enteredQueue) rejectStopped(runtimeExecution.stopSignal.reason);
};
runtimeExecution.stopSignal.addEventListener('abort', onRuntimeStop, { once: true });
if (runtimeExecution.stopSignal.aborted) onRuntimeStop();
return Promise.race([queuedExecution, stopped]).finally(() => {
runtimeExecution.stopSignal.removeEventListener('abort', onRuntimeStop);
});
}
private async runClaimedAgentGraphIntentOnce(
input: ResolvedClaimedAgentGraphIntentInput,
runtimeExecution: RuntimeExecutionClaim,
): Promise<ClaimedAgentGraphIntentResult> {
if (!this.deps.runStore || !this.deps.runtimeEventStore) {
throw new Error('Claimed graph execution requires AgentRunStore and RuntimeEventStore');
}
if (input.abortSignal?.aborted) {
throw new Error('Claimed graph execution was cancelled before runtime admission');
}
const { claim } = input;
const child = await this.deps.store.readHeader(claim.targetSessionId);
await this.ensureChildWorkspace(child);
const snapshot = child.subagentRuntime;
if (
child.id !== claim.targetSessionId ||
child.subagentParent?.kind !== 'subagent' ||
!snapshot
) {
throw new Error('Claimed graph execution target must be a linked child session');
}
await this.assertLinkedChildBoundaryMatchesParent(
child.subagentParent.parentSessionId,
child.id,
);
const rootExecution: RootExecutionDescriptor = {
kind: 'claimed_agent_graph_intent',
claim,
agentId: snapshot.agentId,
agentName: snapshot.agentName,
};
const readyInfo = {
claimId: claim.claimId,
graphId: claim.graphId,
intentId: claim.intentId,
operatorId: claim.targetOperatorId,
childSessionId: child.id,
turnId: claim.targetTurnId,
runId: claim.targetRunId,
agentId: snapshot.agentId,
agentName: snapshot.agentName,
};
let readyNotification: Promise<void> | undefined;
const notifyReady = (): Promise<void> => {
readyNotification ??= Promise.resolve()
.then(() => input.onReady?.(readyInfo))
.catch(() => {
// A presentation observer must not change graph execution.
});
return readyNotification;
};
let run = await this.deps.runStore.readRun(child.id, claim.targetRunId).catch((error) => {
if (isNotFoundError(error)) return undefined;
throw error;
});
if (run) {
this.assertClaimedAgentGraphRun(child, snapshot, claim, run);
if (input.hostedGraphExecution) {
const admission = await this.requireClaimedGraphAdmissionIdentity(
claim,
input.hostedGraphExecution,
);
await this.consumeLinkedRootExecution({
sessionId: child.id,
turnId: claim.targetTurnId,
runId: claim.targetRunId,
userMessageId: admission.userMessageId,
execution: rootExecution,
content: { text: input.prompt },
start: () => {
throw new RuntimeMessageAuthorityInvariantError(
'Hosted retry attempted to start an existing claimed graph Run',
);
},
});
run = await this.deps.runStore.readRun(child.id, claim.targetRunId);
this.assertClaimedAgentGraphRun(child, snapshot, claim, run);
await this.assertClaimedAgentGraphPrompt(
child.id,
claim.targetTurnId,
input.prompt,
admission.userMessageId,
);
await notifyReady();
return claimedAgentGraphIntentResult(
claim,
await this.projectExistingChildSpawn(child, run),
);
}
await this.assertClaimedAgentGraphPrompt(child.id, claim.targetTurnId, input.prompt);
await notifyReady();
while (
!isTerminalRunStatus(run.status) &&
this.runtimeKernel.hasActiveRun?.(child.id, run.runId, run.turnId)
) {
await delay(25, undefined, input.abortSignal ? { signal: input.abortSignal } : undefined);
run = await this.deps.runStore.readRun(child.id, claim.targetRunId);
}
if (!isTerminalRunStatus(run.status)) {
await this.recoverAgentRunsFromLedger(child.id);
run = await this.deps.runStore.readRun(child.id, claim.targetRunId);
}
this.assertClaimedAgentGraphRun(child, snapshot, claim, run);
await this.assertClaimedAgentGraphPrompt(child.id, claim.targetTurnId, input.prompt);
return claimedAgentGraphIntentResult(claim, await this.projectExistingChildSpawn(child, run));
}
await this.finalizeChildWorkspacePatches(child.id);
const [runs, messages] = await Promise.all([
this.deps.runStore.listSessionRuns(child.id),
this.deps.store.readMessages(child.id),
]);
const turnOwner = runs.find((candidate) => candidate.turnId === claim.targetTurnId);
if (turnOwner) {
throw new Error(
`Claimed graph turn ${claim.targetTurnId} is already owned by run ${turnOwner.runId}`,
);
}
if (messages.some((message) => 'turnId' in message && message.turnId === claim.targetTurnId)) {
throw new Error(`Claimed graph turn ${claim.targetTurnId} already has durable messages`);
}
if (child.isArchived || child.status === 'archived' || child.status === 'aborted') {
throw new Error('Claimed graph execution target child session is terminated');
}
if (input.abortSignal?.aborted) {
throw new Error('Claimed graph execution was cancelled before runtime admission');
}
const admitExecution = input.admitExecution;
const startedAt = this.deps.now();
const summary = new ChildAgentSummaryAccumulator();
const identity = {
sessionId: child.id,
turnId: claim.targetTurnId,
runId: claim.targetRunId,
};
let aborted = false;
let stopPromise: Promise<void> | undefined;
const userMessageId = await this.claimedGraphUserMessageId(claim, input.hostedGraphExecution);
const execution = this.consumeLinkedRootExecution({
...identity,
userMessageId,
execution: rootExecution,
...(admitExecution ? { admitExecution } : {}),
content: { text: input.prompt },
start: ({ runId, userMessageId, onRunStarted }) =>
this.sendMessage(
child.id,
{
turnId: claim.targetTurnId,
text: input.prompt,
agentId: snapshot.agentId,
agentName: snapshot.agentName,
},
{
runId,
...(userMessageId ? { userMessageId } : {}),
durability: 'required',
...(admitExecution && !input.hostedGraphExecution
? {
admitTurn: async () =>
(await admitExecution()) === 'executing' ? 'admitted' : 'cancelled',
}
: {}),
onRunStarted,
execution: runtimeExecution,
},
),
onReady: notifyReady,
onEvent: (event) => {
summary.add(event);
try {
input.onEvent?.(event);
} catch {
// A presentation observer must not change graph execution.
}
},
});
const onAbort = () => {
aborted = true;
stopPromise ??= this.stopLinkedRoot(identity, { source: 'stop_button' });
};
if (input.abortSignal) {
input.abortSignal.addEventListener('abort', onAbort, { once: true });
if (input.abortSignal.aborted) onAbort();
}
try {
await execution;
} finally {
input.abortSignal?.removeEventListener('abort', onAbort);
if (aborted) await stopPromise;
}
const completedAt = this.deps.now();
const completedRun = await this.deps.runStore.readRun(child.id, claim.targetRunId);
this.assertClaimedAgentGraphRun(child, snapshot, claim, completedRun);
const failureClass = completedRun.failureClass ?? summary.failureClass;
const artifacts = await this.finalizeAndListChildTurnArtifacts(
child.id,
claim.targetTurnId,
completedRun.status,
);
return {
claimId: claim.claimId,
graphId: claim.graphId,
intentId: claim.intentId,
operatorId: claim.targetOperatorId,
childSessionId: child.id,
agentId: snapshot.agentId,
agentName: snapshot.agentName,
profile: snapshot.profile,
turnId: claim.targetTurnId,
runId: claim.targetRunId,
status: agentRunStatusForSpawnResult(completedRun.status),
permissionMode: child.permissionMode,
summary: summary.text(),
artifactIds: artifacts.map((artifact) => artifact.id),
startedAt,
completedAt,
durationMs: Math.max(0, completedAt - startedAt),
eventCount: summary.eventCount,
...(failureClass ? { failureClass } : {}),
};
}
private async claimedGraphUserMessageId(
claim: AgentGraphIntentClaim,
hostedGraphExecution: RuntimeHostedAgentGraphExecutionCapability | undefined,
): Promise<string> {
if (!hostedGraphExecution) return this.deps.newId();
const admission = await hostedGraphExecution.readRootTurnAdmissionIdentity(
claim.targetSessionId,
claim.targetTurnId,
);
if (!admission) return this.deps.newId();
if (admission.runId !== claim.targetRunId) {
throw new RuntimeMessageAuthorityInvariantError(
'Claimed graph RootTurn admission has a mismatched run identity',
);
}
if (admission.userMessageId === null) {
throw new RuntimeMessageAuthorityInvariantError(
'Claimed graph RootTurn admission is missing its user message identity',
);
}
return admission.userMessageId;
}
private async requireClaimedGraphAdmissionIdentity(
claim: AgentGraphIntentClaim,
hostedGraphExecution: RuntimeHostedAgentGraphExecutionCapability,
): Promise<{ runId: string; userMessageId: string }> {
const admission = await hostedGraphExecution.readRootTurnAdmissionIdentity(
claim.targetSessionId,
claim.targetTurnId,
);
if (!admission) {
throw new RuntimeMessageAuthorityInvariantError(
'Existing claimed graph Run is missing its durable RootTurn admission',
);
}
if (admission.runId !== claim.targetRunId) {
throw new RuntimeMessageAuthorityInvariantError(
'Claimed graph RootTurn admission has a mismatched run identity',
);
}
if (admission.userMessageId === null) {
throw new RuntimeMessageAuthorityInvariantError(
'Claimed graph RootTurn admission is missing its user message identity',
);
}
return { runId: admission.runId, userMessageId: admission.userMessageId };
}
private assertClaimedAgentGraphRun(
child: SessionHeader,
snapshot: NonNullable<SessionHeader['subagentRuntime']>,
claim: AgentGraphIntentClaim,
run: AgentRunHeader,
): void {
if (
run.sessionId !== child.id ||
run.runId !== claim.targetRunId ||
run.turnId !== claim.targetTurnId ||
!isSessionInlineRun(run) ||
run.agentId !== snapshot.agentId ||
(run.agentName !== undefined && run.agentName !== snapshot.agentName)
) {
throw new Error('Existing AgentRun does not match the claimed graph activation identity');
}
}
private async assertClaimedAgentGraphPrompt(
sessionId: string,
turnId: string,
prompt: string,
expectedUserMessageId?: string,
): Promise<void> {
const messages = await this.deps.store.readMessages(sessionId);
const userMessages = messages.filter(
(message): message is UserMessage => message.type === 'user' && message.turnId === turnId,
);
if (expectedUserMessageId !== undefined) {
if (
userMessages.length !== 1 ||
userMessages[0]?.id !== expectedUserMessageId ||
!messageContentsEqual(userMessages[0], { text: prompt })
) {
throw new RuntimeMessageAuthorityInvariantError(
'Existing claimed graph Run does not match its durable UserMessage',
);
}
return;
}
if (userMessages.length > 1 || (userMessages[0] && userMessages[0].text !== prompt)) {
throw new Error('Graph intent claim identity was reused for different execution input');
}
}
private async spawnChildSessionOnce(
parentSessionId: string,
input: ResolvedSpawnChildSessionInput,
requestFingerprint: string,
runtimeOwner: { execution: RuntimeExecutionClaim },
): Promise<SpawnChildSessionResult> {
if (input.abortSignal?.aborted) {
throw new Error('Child session spawn was cancelled before creation');
}
if (!this.deps.runStore || !this.deps.runtimeEventStore) {
throw new Error('Child session creation requires AgentRunStore and RuntimeEventStore');
}
const [parentHeader, parentRun, parentBoundary] = await Promise.all([
this.deps.store.readHeader(parentSessionId),
this.deps.runStore.readRun(parentSessionId, input.spawnedBy.parentRunId),
this.deps.store.readExecutionBoundary(parentSessionId),
]);
this.assertActiveParentRun(parentSessionId, parentRun, input.spawnedBy.parentTurnId);
const definition = requireBuiltinAgentDefinitionByProfile(input.agentProfile);
const availableChildTools = await this.childToolsForSession(parentSessionId);
assertAgentDefinitionRunnable({
definition,
tools: availableChildTools,
worktreeChildExecutorAvailable: await this.isWorktreeChildExecutorAvailable(parentHeader),
});
const proposedTurnId = input.turnId ?? this.deps.newId();
const proposedRunId = input.runId ?? this.deps.newId();
const workspace = await this.provisionChildWorkspace(
parentHeader,
definition,
requestFingerprint,
);
const creation = await this.deps.store.createSubagent(
{
cwd: workspace?.worktreePath ?? parentHeader.cwd,
...(parentHeader.projectId !== undefined ? { projectId: parentHeader.projectId } : {}),
name: input.name ?? input.resolvedPreset?.name ?? definition.name,
backend: parentHeader.backend,
llmConnectionSlug: input.resolvedPreset?.connectionSlug ?? parentHeader.llmConnectionSlug,
model: input.resolvedPreset?.model ?? parentHeader.model,
...(input.resolvedPreset
? input.resolvedPreset.thinkingLevel !== undefined
? { thinkingLevel: input.resolvedPreset.thinkingLevel }
: {}
: parentHeader.thinkingLevel !== undefined
? { thinkingLevel: parentHeader.thinkingLevel }
: {}),
permissionMode: definition.permissionMode,
collaborationMode: 'agent',
orchestrationMode: 'default',
subagentParent: {
kind: 'subagent',
parentSessionId,
spawnedBy: input.spawnedBy,
...(input.swarm ? { swarm: input.swarm } : {}),
lifecycle: 'foreground',
},
subagentRuntime: {
schemaVersion: SUBAGENT_SESSION_RUNTIME_SCHEMA_VERSION,
definitionVersion: definition.definitionVersion,
agentId: definition.id,
agentName: input.resolvedPreset?.name ?? definition.name,
profile: definition.profile,
...(input.resolvedPreset ? { presetId: input.resolvedPreset.id } : {}),
systemPrompt: definition.systemPrompt,
toolNames: [...definition.tools],
categoryPolicy: {},
},
subagentSpawn: {
schemaVersion: SUBAGENT_SESSION_SPAWN_SCHEMA_VERSION,
requestFingerprint,
initialTurnId: proposedTurnId,
initialRunId: proposedRunId,
},
...(workspace ? { subagentWorkspace: workspace } : {}),
},
parentBoundary,
);
const child = creation.header;
const snapshot = child.subagentRuntime;
const spawn = child.subagentSpawn;
if (
!snapshot ||
!spawn ||
!child.subagentParent ||
!sameSubagentWorkspace(child.subagentWorkspace, workspace)
) {
throw new Error('Stored child session is missing its durable runtime or spawn identity');
}
try {
runtimeOwner.execution = this.transferRuntimeExecution(runtimeOwner.execution, child.id);
} catch (error) {
if (creation.created) await this.updateStatus(child.id, 'aborted').catch(() => {});
throw error;
}
const releaseHostedExecution = this.acquireHostedLinkedChildExecution(child.id);
try {
const turnId = spawn.initialTurnId;
const runId = spawn.initialRunId;
const readyInfo = {
childSessionId: child.id,
turnId,
runId,
agentId: snapshot.agentId,
agentName: snapshot.agentName,
permissionMode: child.permissionMode,
};
let readyNotification: Promise<void> | undefined;
const notifyReady = (): Promise<void> => {
readyNotification ??= Promise.resolve().then(() => input.onReady?.(readyInfo));
return readyNotification;
};
// Close the create/start race: if the parent settled (or cancellation
// arrived) while metadata was being written, retain an inspectable aborted
// child but do not admit new foreground work.
try {
this.assertActiveParentRun(parentSessionId, parentRun, input.spawnedBy.parentTurnId);
if (input.abortSignal?.aborted) {
throw new Error('Child session spawn was cancelled before its first run');
}
} catch (error) {
if (creation.created) await this.updateStatus(child.id, 'aborted').catch(() => {});
throw error;
}
if (!creation.created) {
const existing = await this.resolveExistingChildSpawn(child, input, notifyReady);
if (existing) return existing;
}
// A committed metadata row without its initial AgentRun is a recoverable
// crash boundary. Revalidate admission after the lookup: the parent or
// caller may have settled while durable state was being inspected.
try {
const latestParentRun = await this.deps.runStore.readRun(
parentSessionId,
input.spawnedBy.parentRunId,
);
this.assertActiveParentRun(parentSessionId, latestParentRun, input.spawnedBy.parentTurnId);
if (input.abortSignal?.aborted) {
throw new Error('Child session spawn was cancelled before its first run');
}
} catch (error) {
await this.updateStatus(child.id, 'aborted').catch(() => {});
throw error;
}
const startedAt = this.deps.now();
const summary = new ChildAgentSummaryAccumulator();
let aborted = false;
let stopPromise: Promise<void> | undefined;
const identity = { sessionId: child.id, turnId, runId };
const execution = this.consumeLinkedRootExecution(
{
...identity,
userMessageId: this.deps.newId(),
execution: {
kind: 'linked_child_initial',
agentId: snapshot.agentId,
agentName: snapshot.agentName,
},
content: { text: input.prompt },
start: ({ runId: admittedRunId, userMessageId, onRunStarted }) =>
this.sendMessage(
child.id,
{
turnId,
text: input.prompt,
agentId: snapshot.agentId,
agentName: snapshot.agentName,
},
{
runId: admittedRunId,
...(userMessageId ? { userMessageId } : {}),
durability: 'required',
onRunStarted,
execution: runtimeOwner.execution,
},
),
onReady: notifyReady,
onEvent: (event) => {
summary.add(event);
try {
input.onEvent?.(event);
} catch {
// A presentation observer must not change the child run outcome.
}
},
},
true,
);
const onAbort = () => {
aborted = true;
stopPromise ??= this.stopLinkedRoot(identity, { source: 'stop_button' });
};
if (input.abortSignal) {
input.abortSignal.addEventListener('abort', onAbort, { once: true });
if (input.abortSignal.aborted) onAbort();
}
try {
await execution;
} finally {
input.abortSignal?.removeEventListener('abort', onAbort);
if (aborted) await stopPromise;
}
const completedAt = this.deps.now();
const run = await this.findRunByTurnId(child.id, turnId);
const failureClass = run?.failureClass ?? summary.failureClass;
const artifacts = run
? await this.finalizeAndListChildTurnArtifacts(child.id, turnId, run.status)
: [];
return {
childSessionId: child.id,
agentId: snapshot.agentId,
agentName: snapshot.agentName,
profile: snapshot.profile,
turnId,
runId,
status: run ? agentRunStatusForSpawnResult(run.status) : summary.status(aborted),
permissionMode: child.permissionMode,
summary: summary.text(),
artifactIds: artifacts.map((artifact) => artifact.id),
startedAt,
completedAt,
durationMs: Math.max(0, completedAt - startedAt),
eventCount: summary.eventCount,
...(failureClass ? { failureClass } : {}),
};
} finally {
releaseHostedExecution();
}
}
private async resolveExistingChildSpawn(
child: SessionHeader,
input: SpawnChildSessionInput,
notifyReady: () => Promise<void>,
): Promise<SpawnChildSessionResult | undefined> {
if (!this.deps.runStore || !this.deps.runtimeEventStore) return undefined;
const snapshot = child.subagentRuntime;
const spawn = child.subagentSpawn;
if (!snapshot || !spawn) {
throw new Error('Stored child session is missing its durable runtime or spawn identity');
}
let run = await this.deps.runStore.readRun(child.id, spawn.initialRunId).catch((error) => {
if (isNotFoundError(error)) return undefined;
throw error;
});
if (!run) return undefined;
await notifyReady();
while (
!isTerminalRunStatus(run.status) &&
this.runtimeKernel.hasActiveRun?.(child.id, run.runId, run.turnId)
) {
await delay(25, undefined, input.abortSignal ? { signal: input.abortSignal } : undefined);
run = await this.deps.runStore.readRun(child.id, spawn.initialRunId);
}
if (!isTerminalRunStatus(run.status)) {
await this.recoverAgentRunsFromLedger(child.id);
run = await this.deps.runStore.readRun(child.id, spawn.initialRunId);
}
return await this.projectExistingChildSpawn(child, run);
}
private async projectExistingChildSpawn(
child: SessionHeader,
run: AgentRunHeader,
): Promise<SpawnChildSessionResult> {
if (!this.deps.runtimeEventStore) {
throw new Error('Child session projection requires RuntimeEventStore');
}
const snapshot = child.subagentRuntime;
if (!snapshot) throw new Error('Stored child session is missing its durable runtime snapshot');
const [messages, runtimeEvents, artifacts] = await Promise.all([
this.deps.store.readMessages(child.id),
this.deps.runtimeEventStore.readRuntimeEvents(child.id, run.runId),
this.finalizeAndListChildTurnArtifacts(child.id, run.turnId, run.status),
]);
const storedSummary =
messages
.filter(
(message): message is Extract<StoredMessage, { type: 'assistant' }> =>
message.type === 'assistant' && message.turnId === run.turnId,
)
.at(-1)?.text ?? '';
const runtimeText = runtimeEvents.filter(
(
event,
): event is RuntimeEvent & {
content: Extract<NonNullable<RuntimeEvent['content']>, { kind: 'text' }>;
} => event.role === 'model' && event.content?.kind === 'text',
);
const durableRuntimeSummary = runtimeText.filter((event) => !event.partial).at(-1)
?.content.text;
const partialRuntimeSummary = runtimeText
.filter((event) => event.partial)
.map((event) => event.content.text)
.join('');
const completedAt = run.completedAt ?? run.updatedAt;
return {
childSessionId: child.id,
agentId: snapshot.agentId,
agentName: snapshot.agentName,
profile: snapshot.profile,
turnId: run.turnId,
runId: run.runId,
status: agentRunStatusForSpawnResult(run.status),
permissionMode: child.permissionMode,
summary: trimSummary(durableRuntimeSummary ?? (storedSummary || partialRuntimeSummary)),
artifactIds: artifacts.map((artifact) => artifact.id),
startedAt: run.createdAt,
completedAt,
durationMs: Math.max(0, completedAt - run.createdAt),
eventCount: runtimeEvents.length,
...(run.failureClass ? { failureClass: run.failureClass } : {}),
};
}
async prepareChildAgentResume(
sessionId: string,
sourceRunId: string,
): Promise<PrepareChildAgentResumeResult> {
if (!this.deps.runStore || !this.deps.runtimeEventStore) {
throw new Error('Child AgentRun resume requires AgentRunStore and RuntimeEventStore');
}
const execution = await this.resolveChildAgentExecution(sessionId, sourceRunId);
return await this.prepareChildAgentResumeForExecution(sessionId, execution, sourceRunId);
}
private async prepareChildAgentResumeForExecution(
sessionId: string,
execution: SubagentExecutionRef,
sourceRunId: string,
): Promise<PrepareChildAgentResumeResult> {
if (!this.deps.runStore || !this.deps.runtimeEventStore) {
throw new Error('Child AgentRun resume requires AgentRunStore and RuntimeEventStore');
}
if (execution.kind === 'child_session') {
return await this.prepareLinkedChildSessionResume(sessionId, execution, sourceRunId);
}
const runs = await this.deps.runStore.listSessionRuns(sessionId);
const runsById = new Map(runs.map((run) => [run.runId, run]));
const source = runsById.get(sourceRunId);
if (!source || !source.parentRunId || isSessionInlineRun(source)) {
throw new Error(`Child AgentRun resume source ${sourceRunId} was not found`);
}
const definition = source.agentId ? getBuiltinAgentDefinition(source.agentId) : undefined;
if (!definition) {
throw new Error(`AgentRun ${sourceRunId} is not a resumable built-in child agent`);
}
const sessionHeader = await this.deps.store.readHeader(sessionId);
await this.ensureChildWorkspace(sessionHeader);
assertAgentDefinitionRunnable({
definition,
tools: await this.childToolsForSession(sessionId),
worktreeChildExecutorAvailable: await this.isWorktreeChildExecutorAvailable(sessionHeader),
});
const visited = new Set<string>();
let cursor: AgentRunHeader | undefined = source;
while (cursor) {
if (visited.has(cursor.runId)) {
throw new Error(`Child AgentRun resume lineage contains a cycle at ${cursor.runId}`);
}
visited.add(cursor.runId);
if (!cursor.parentRunId || isSessionInlineRun(cursor) || cursor.agentId !== definition.id) {
throw new Error(`Child AgentRun resume profile changed at ${cursor.runId}`);
}
if (
cursor.backendKind !== sessionHeader.backend ||
cursor.llmConnectionSlug !== sessionHeader.llmConnectionSlug ||
cursor.modelId !== sessionHeader.model ||
cursor.cwd !== sessionHeader.cwd ||
cursor.permissionMode !== definition.permissionMode
) {
throw new Error(`Child AgentRun resume environment changed for ${cursor.runId}`);
}
const events = await this.deps.runtimeEventStore
.readRuntimeEvents(sessionId, cursor.runId)
.catch(() => []);
const replay = buildRuntimeEventModelReplayPlan(events);
const unsafe = replay.diagnostics.find((diagnostic) =>
isUnsafeChildResumeDiagnostic(diagnostic.code),
);
if (unsafe) {
throw new Error(`Child AgentRun resume history is unsafe: ${unsafe.code}`);
}
const first = replay.items[0];
if (!first || first.kind !== 'text' || first.role !== 'user') {
throw new Error(
`Child AgentRun resume source ${cursor.runId} has no user-anchored history`,
);
}
const terminal = classifyTerminalRuntimeLedger(cursor, events);
if (terminal.kind !== 'fact') {
throw new Error(`Child AgentRun resume source ${cursor.runId} is not durably terminal`);
}
const effective = effectiveRunHeaderFromTerminalFact(cursor, terminal.fact);
if (!['completed', 'failed', 'cancelled'].includes(effective.status)) {
throw new Error(`Child AgentRun resume source ${cursor.runId} is not in a resumable state`);
}
const previousRunId = cursor.resumedFromRunId ?? cursor.retriedFromRunId;
if (!previousRunId) break;
cursor = runsById.get(previousRunId);
if (!cursor) {
throw new Error(`Child AgentRun resume source ${previousRunId} was not found`);
}
}
this.assertChildRunHasNoSuccessor(runs, sourceRunId);
return {
sourceRunId,
execution,
agentId: definition.id,
agentName: definition.name,
profile: definition.profile,
};
}
private async resolveChildAgentExecution(
parentSessionId: string,
sourceRunId: string,
): Promise<SubagentExecutionRef> {
if (!this.deps.runStore) {
throw new Error('Child AgentRun lookup requires AgentRunStore');
}
const legacy = await this.deps.runStore.readRun(parentSessionId, sourceRunId).catch((error) => {
if (isNotFoundError(error)) return undefined;
throw error;
});
if (legacy?.parentRunId && !isSessionInlineRun(legacy)) {
return {
kind: 'legacy_child_run',
sessionId: parentSessionId,
runId: sourceRunId,
};
}
const children = await this.listChildSessions(parentSessionId);
const matches = (
await Promise.all(
children.map(async (child) => {
const run = await this.deps.runStore!.readRun(child.id, sourceRunId).catch((error) => {
if (isNotFoundError(error)) return undefined;
throw error;
});
return run && isSessionInlineRun(run) ? child.id : undefined;
}),
)
).filter((childSessionId): childSessionId is string => childSessionId !== undefined);
if (matches.length !== 1) {
throw new Error(`Child AgentRun resume source ${sourceRunId} was not found`);
}
return {
kind: 'child_session',
sessionId: matches[0]!,
currentRunId: sourceRunId,
};
}
private async prepareLinkedChildSessionResume(
parentSessionId: string,
execution: Extract<SubagentExecutionRef, { kind: 'child_session' }>,
sourceRunId: string,
): Promise<PrepareChildAgentResumeResult> {
if (!this.deps.runStore || !this.deps.runtimeEventStore) {
throw new Error('Child Session resume requires AgentRunStore and RuntimeEventStore');
}
const child = await this.deps.store.readHeader(execution.sessionId);
const snapshot = child.subagentRuntime;
if (
child.subagentParent?.kind !== 'subagent' ||
child.subagentParent.parentSessionId !== parentSessionId ||
!snapshot
) {
throw new Error(`Child AgentRun resume source ${sourceRunId} was not found`);
}
await this.ensureChildWorkspace(child);
await this.assertLinkedChildBoundaryMatchesParent(parentSessionId, child.id);
const runnableTools = buildToolsForAgentDefinition(await this.childToolsForSession(child.id), {
id: snapshot.agentId,
permissionMode: child.permissionMode,
tools: snapshot.toolNames,
});
if (runnableTools.length !== snapshot.toolNames.length) {
throw new Error('Child Session durable runtime tool snapshot is unavailable');
}
const runs = await this.deps.runStore.listSessionRuns(child.id);
const runsById = new Map(runs.map((run) => [run.runId, run]));
const source = runsById.get(sourceRunId);
if (!source || !isSessionInlineRun(source)) {
throw new Error(`Child AgentRun resume source ${sourceRunId} was not found`);
}
const visited = new Set<string>();
const replaySegments: RuntimeEvent[][] = [];
let cursor: AgentRunHeader | undefined = source;
while (cursor) {
if (visited.has(cursor.runId)) {
throw new Error(`Child AgentRun resume lineage contains a cycle at ${cursor.runId}`);
}
visited.add(cursor.runId);
if (!isSessionInlineRun(cursor) || cursor.agentId !== snapshot.agentId) {
throw new Error(`Child AgentRun resume profile changed at ${cursor.runId}`);
}
if (
cursor.backendKind !== child.backend ||
cursor.llmConnectionSlug !== child.llmConnectionSlug ||
cursor.modelId !== child.model ||
cursor.cwd !== child.cwd ||
cursor.permissionMode !== child.permissionMode
) {
throw new Error(`Child AgentRun resume environment changed for ${cursor.runId}`);
}
const events = await this.deps.runtimeEventStore
.readRuntimeEvents(child.id, cursor.runId)
.catch(() => []);
const replay = buildRuntimeEventModelReplayPlan(events);
const unsafe = replay.diagnostics.find((diagnostic) =>
isUnsafeChildResumeDiagnostic(diagnostic.code),
);
if (unsafe) {
throw new Error(`Child AgentRun resume history is unsafe: ${unsafe.code}`);
}
replaySegments.unshift(events);
const terminal = classifyTerminalRuntimeLedger(cursor, events);
if (terminal.kind !== 'fact') {
throw new Error(`Child AgentRun resume source ${cursor.runId} is not durably terminal`);
}
const effective = effectiveRunHeaderFromTerminalFact(cursor, terminal.fact);
if (!['completed', 'failed', 'cancelled'].includes(effective.status)) {
throw new Error(`Child AgentRun resume source ${cursor.runId} is not in a resumable state`);
}
const previousRunId = cursor.resumedFromRunId ?? cursor.retriedFromRunId;
if (!previousRunId) break;
cursor = runsById.get(previousRunId);
if (!cursor) {
throw new Error(`Child AgentRun resume source ${previousRunId} was not found`);
}
}
const replay = buildRuntimeEventModelReplayPlan(replaySegments.flat());
const first = replay.items[0];
if (!first || first.kind !== 'text' || first.role !== 'user') {
throw new Error(`Child AgentRun resume source ${sourceRunId} has no user-anchored history`);
}
await this.effectiveRunHeaderFromRuntimeLedger(source);
await this.finalizeChildWorkspacePatches(child.id);
this.assertChildRunHasNoSuccessor(runs, sourceRunId);
return {
sourceRunId,
execution,
agentId: snapshot.agentId,
agentName: snapshot.agentName,
profile: snapshot.profile,
};
}
async resumeChildAgent(
sessionId: string,
input: ResumeChildAgentInput,
): Promise<SpawnChildAgentResult> {
let runtimeExecution = this.runtimeKernel.claimExecution(sessionId);
try {
throwIfChildExecutionAborted(
input.abortSignal,
'Child agent resume was cancelled before start',
);
const execution = await this.resolveChildAgentExecution(sessionId, input.sourceRunId);
if (execution.kind === 'child_session') {
runtimeExecution = this.transferRuntimeExecution(runtimeExecution, execution.sessionId);
const releaseHostedExecution = this.acquireHostedLinkedChildExecution(execution.sessionId);
try {
const prepared = await this.prepareChildAgentResumeForExecution(
sessionId,
execution,
input.sourceRunId,
);
return await this.resumeLinkedChildSession(
sessionId,
execution,
prepared,
input,
runtimeExecution,
true,
);
} finally {
releaseHostedExecution();
}
}
const prepared = await this.prepareChildAgentResumeForExecution(
sessionId,
execution,
input.sourceRunId,
);
const definition = getBuiltinAgentDefinition(prepared.agentId)!;
return await this.runChildAgent(
sessionId,
definition,
input,
runtimeExecution,
input.sourceRunId,
);
} finally {
runtimeExecution.release();
}
}
private transferRuntimeExecution(
source: RuntimeExecutionClaim,
targetSessionId: string,
): RuntimeExecutionClaim {
if (source.sessionId === targetSessionId) return source;
let target: RuntimeExecutionClaim;
try {
target = this.runtimeKernel.claimExecution(targetSessionId);
} catch (error) {
source.release();
throw error;
}
return this.handoffRuntimeExecution(source, target);
}
private handoffRuntimeExecution(
source: RuntimeExecutionClaim,
target: RuntimeExecutionClaim,
): RuntimeExecutionClaim {
if (source.sessionId === target.sessionId) {
target.release();
return source;
}
const stopped = source.isStopRequested();
source.release();
if (stopped) {
target.release();
throw new Error(
`Session ${source.sessionId} stopped before child execution ownership transferred`,
);
}
return target;
}
private async resumeLinkedChildSession(
parentSessionId: string,
execution: Extract<SubagentExecutionRef, { kind: 'child_session' }>,
prepared: PrepareChildAgentResumeResult,
input: ResumeChildAgentInput,
runtimeExecution: RuntimeExecutionClaim,
hostedGateAlreadyHeld: boolean,
): Promise<SpawnChildAgentResult> {
throwIfChildExecutionAborted(
input.abortSignal,
'Child agent resume was cancelled before start',
);
if (!this.deps.runStore) {
throw new Error('Child Session resume requires AgentRunStore');
}
const [parentRun, child] = await Promise.all([
this.deps.runStore.readRun(parentSessionId, input.parentRunId),
this.deps.store.readHeader(execution.sessionId),
]);
this.assertActiveParentRun(parentSessionId, parentRun, parentRun.turnId);
if (
child.subagentParent?.kind !== 'subagent' ||
child.subagentParent.parentSessionId !== parentSessionId ||
child.subagentRuntime?.agentId !== prepared.agentId
) {
throw new Error(`Child AgentRun resume source ${input.sourceRunId} was not found`);
}
const turnId = input.turnId ?? this.deps.newId();
const runId = this.deps.newId();
const startedAt = this.deps.now();
const summary = new ChildAgentSummaryAccumulator();
let aborted = input.abortSignal?.aborted === true;
let stopPromise: Promise<void> | undefined;
const identity = { sessionId: child.id, turnId, runId };
throwIfChildExecutionAborted(
input.abortSignal,
'Child agent resume was cancelled before start',
);
const executionPromise = this.consumeLinkedRootExecution(
{
...identity,
userMessageId: this.deps.newId(),
execution: {
kind: 'linked_child_resume',
agentId: prepared.agentId,
agentName: prepared.agentName,
sourceRunId: input.sourceRunId,
},
content: { text: input.prompt },
start: ({ runId: admittedRunId, userMessageId, onRunStarted }) =>
this.sendMessage(
child.id,
{
turnId,
text: input.prompt,
resumedFromRunId: input.sourceRunId,
agentId: prepared.agentId,
agentName: prepared.agentName,
},
{
runId: admittedRunId,
...(userMessageId ? { userMessageId } : {}),
durability: 'required',
onRunStarted,
execution: runtimeExecution,
},
),
onReady: () =>
input.onReady?.({
childSessionId: child.id,
turnId,
runId,
agentId: prepared.agentId,
agentName: prepared.agentName,
}),
onEvent: (event) => {
summary.add(event);
try {
input.onEvent?.(event);
} catch {
// A presentation observer must not change the child run outcome.
}
},
},
hostedGateAlreadyHeld,
);
const onAbort = () => {
aborted = true;
stopPromise ??= this.stopLinkedRoot(identity, { source: 'stop_button' });
};
if (input.abortSignal) {
input.abortSignal.addEventListener('abort', onAbort, { once: true });
if (input.abortSignal.aborted) onAbort();
}
try {
await executionPromise;
} finally {
input.abortSignal?.removeEventListener('abort', onAbort);
if (aborted) await stopPromise;
}
const completedAt = this.deps.now();
const run = await this.findRunByTurnId(child.id, turnId);
const failureClass = run?.failureClass ?? summary.failureClass;
const artifacts = run
? await this.finalizeAndListChildTurnArtifacts(child.id, turnId, run.status)
: [];
return {
childSessionId: child.id,
agentId: prepared.agentId,
agentName: prepared.agentName,
turnId,
runId,
resumedFromRunId: input.sourceRunId,
status: run ? agentRunStatusForSpawnResult(run.status) : summary.status(aborted),
permissionMode: child.permissionMode,
summary: summary.text(),
artifactIds: artifacts.map((artifact) => artifact.id),
startedAt,
completedAt,
durationMs: Math.max(0, completedAt - startedAt),
eventCount: summary.eventCount,
...(failureClass ? { failureClass } : {}),
};
}
private async runChildAgent(
sessionId: string,
definition: AgentDefinition,
input: SpawnChildAgentInput | ResumeChildAgentInput,
execution: RuntimeExecutionClaim,
resumedFromRunId?: string,
): Promise<SpawnChildAgentResult> {
throwIfChildExecutionAborted(
input.abortSignal,
'Child agent execution was cancelled before start',
);
const turnId = input.turnId ?? this.deps.newId();
const startedAt = this.deps.now();
const summary = new ChildAgentSummaryAccumulator();
let aborted = input.abortSignal?.aborted === true;
await input.onReady?.({ turnId, agentId: definition.id, agentName: definition.name });
const iterator = this.runtimeKernel
.startChildTurn(
sessionId,
{
turnId,
parentRunId: input.parentRunId,
spec: {
id: definition.id,
name: definition.name,
systemPrompt: definition.systemPrompt,
},
prompt: input.prompt,
...(resumedFromRunId ? { resumedFromRunId } : {}),
},
execution,
)
[Symbol.asyncIterator]();
const onAbort = () => {
aborted = true;
void iterator.return?.();
};
if (input.abortSignal && !input.abortSignal.aborted) {
input.abortSignal.addEventListener('abort', onAbort, { once: true });
}
try {
while (!aborted) {
const next = await iterator.next();
if (next.done) break;
summary.add(next.value);
try {
input.onEvent?.(next.value);
} catch {
// A presentation observer must not change the child run outcome.
}
}
} finally {
input.abortSignal?.removeEventListener('abort', onAbort);
if (aborted) await iterator.return?.();
}
const completedAt = this.deps.now();
const run = await this.findRunByTurnId(sessionId, turnId);
const failureClass = run?.failureClass ?? summary.failureClass;
const artifacts = run
? await this.finalizeAndListChildTurnArtifacts(sessionId, turnId, run.status)
: [];
return {
agentId: definition.id,
agentName: definition.name,
turnId,
...(run?.runId ? { runId: run.runId } : {}),
status: run ? agentRunStatusForSpawnResult(run.status) : summary.status(aborted),
permissionMode: definition.permissionMode,
summary: summary.text(),
artifactIds: artifacts.map((artifact) => artifact.id),
startedAt,
completedAt,
durationMs: Math.max(0, completedAt - startedAt),
eventCount: summary.eventCount,
...(failureClass ? { failureClass } : {}),
...(resumedFromRunId ? { resumedFromRunId } : {}),
};
}
async retryChildAgent(
sessionId: string,
input: RetryChildAgentInput,
): Promise<SpawnChildAgentResult> {
let runtimeExecution = this.runtimeKernel.claimExecution(sessionId);
let hintedTargetExecution: RuntimeExecutionClaim | undefined;
try {
if (input.execution?.kind === 'child_session') {
hintedTargetExecution = this.runtimeKernel.claimExecution(input.execution.sessionId);
}
throwIfChildExecutionAborted(
input.abortSignal,
'Child agent retry was cancelled before start',
);
if (!this.deps.runStore || !this.deps.runtimeEventStore) {
throw new Error('Child agent retry requires AgentRunStore and RuntimeEventStore');
}
const execution = await this.resolveChildAgentExecution(sessionId, input.sourceRunId);
this.assertChildRetryExecutionIdentity(input, execution);
if (execution.kind === 'child_session') {
if (hintedTargetExecution) {
runtimeExecution = this.handoffRuntimeExecution(runtimeExecution, hintedTargetExecution);
hintedTargetExecution = undefined;
} else {
runtimeExecution = this.transferRuntimeExecution(runtimeExecution, execution.sessionId);
}
}
const releaseHostedExecution =
execution.kind === 'child_session'
? this.acquireHostedLinkedChildExecution(execution.sessionId)
: () => {};
try {
return await this.retryChildAgentWithExecution(
sessionId,
input,
execution,
runtimeExecution,
);
} finally {
releaseHostedExecution();
}
} finally {
hintedTargetExecution?.release();
runtimeExecution.release();
}
}
private assertChildRetryExecutionIdentity(
input: RetryChildAgentInput,
execution: SubagentExecutionRef,
): void {
if (
input.execution &&
(input.execution.kind !== execution.kind ||
input.execution.sessionId !== execution.sessionId ||
(input.execution.kind === 'legacy_child_run' &&
(execution.kind !== 'legacy_child_run' || input.execution.runId !== execution.runId)) ||
(input.execution.kind === 'child_session' &&
input.execution.currentRunId !== undefined &&
input.execution.currentRunId !== input.sourceRunId))
) {
throw new Error('Child agent retry execution identity changed');
}
}
private async assertLinkedChildBoundaryMatchesParent(
parentSessionId: string,
childSessionId: string,
): Promise<void> {
const [parentBoundary, childBoundary] = await Promise.all([
this.deps.store.readExecutionBoundary(parentSessionId),
this.deps.store.readExecutionBoundary(childSessionId),
]);
if (!executionBoundaryContains(parentBoundary, childBoundary)) {
throw new Error('Linked child execution boundary no longer matches its parent');
}
}
private async retryChildAgentWithExecution(
sessionId: string,
input: RetryChildAgentInput,
execution: SubagentExecutionRef,
runtimeExecution: RuntimeExecutionClaim,
): Promise<SpawnChildAgentResult> {
throwIfChildExecutionAborted(input.abortSignal, 'Child agent retry was cancelled before start');
if (!this.deps.runStore || !this.deps.runtimeEventStore) {
throw new Error('Child agent retry requires AgentRunStore and RuntimeEventStore');
}
const targetSessionId = execution.sessionId;
const runs = await this.deps.runStore.listSessionRuns(targetSessionId);
const rawSourceRun = runs.find((run) => run.runId === input.sourceRunId);
if (!rawSourceRun) throw new Error('Child agent retry source run was not found');
const sourceRun = await this.effectiveRunHeaderFromRuntimeLedger(rawSourceRun);
let definition: {
id: string;
name: string;
systemPrompt: string;
permissionMode: PermissionMode;
toolNames: string[];
};
if (execution.kind === 'child_session') {
const [parentRun, child] = await Promise.all([
this.deps.runStore.readRun(sessionId, input.parentRunId),
this.deps.store.readHeader(targetSessionId),
]);
this.assertActiveParentRun(sessionId, parentRun, parentRun.turnId);
const snapshot = child.subagentRuntime;
if (
child.subagentParent?.kind !== 'subagent' ||
child.subagentParent.parentSessionId !== sessionId ||
!snapshot ||
!isSessionInlineRun(sourceRun) ||
sourceRun.agentId !== snapshot.agentId
) {
throw new Error('Child agent retry source does not belong to the parent Session');
}
await this.ensureChildWorkspace(child);
await this.assertLinkedChildBoundaryMatchesParent(sessionId, child.id);
definition = {
id: snapshot.agentId,
name: snapshot.agentName,
systemPrompt: snapshot.systemPrompt,
permissionMode: child.permissionMode,
toolNames: [...snapshot.toolNames],
};
} else {
if (!sourceRun.parentRunId || sourceRun.parentRunId !== input.parentRunId) {
throw new Error('Child agent retry source does not belong to the active parent run');
}
if (!sourceRun.agentId) throw new Error('Child agent retry source is missing its agent id');
const resolved = requireBuiltinAgentDefinition(sourceRun.agentId);
definition = {
...resolved,
toolNames: buildToolsForAgentDefinition(
await this.childToolsForSession(sessionId),
resolved,
).map((tool) => tool.name),
};
}
const authority = runtimeContinuationAuthority(this.deps.runtimeEventStore);
const abandonedBeforeProvider =
sourceRun.failureClass === 'continuation_abandoned_before_provider_dispatch'
? await isProvenRecoveredContinuationAbandonment(authority, sourceRun)
: false;
if (
sourceRun.status !== 'failed' ||
(sourceRun.failureClass !== 'RateLimit' && !abandonedBeforeProvider)
) {
throw new Error(
'Child agent retry source must be a provider rate-limit failure or a proven pre-provider continuation abandonment',
);
}
if (execution.kind === 'child_session') {
await this.finalizeChildWorkspacePatches(targetSessionId);
}
this.assertChildRunHasNoSuccessor(runs, sourceRun.runId);
const hasAuthority = authority !== undefined;
const hasSafetyInspector = this.deps.inspectContinuationSafety !== undefined;
if (hasAuthority !== hasSafetyInspector) {
throw new Error(
'Child agent retry continuation authority composition is incomplete; refusing legacy fallback',
);
}
const admissionMode = hasAuthority
? ('durable_continuation' as const)
: ('legacy_provider_retry' as const);
const continuation =
admissionMode === 'durable_continuation'
? await this.planDurableChildProviderRetry({
targetSessionId,
sourceRun,
runs,
authority: authority!,
inspectSafety: this.deps.inspectContinuationSafety!,
availableToolNames: definition.toolNames,
})
: await this.planLegacyChildProviderRetry({
targetSessionId,
rawSourceRun,
sourceRun,
runs,
availableToolNames: definition.toolNames,
});
const retryReplay = buildRuntimeEventModelReplayPlan(continuation.runtimeContext);
const retryAnchor = retryReplay.items[0];
if (!retryAnchor || retryAnchor.kind !== 'text' || retryAnchor.role !== 'user') {
throw new Error('Child agent retry source has no user-anchored history');
}
const { turnId, runId } = continuation;
const startedAt = this.deps.now();
const summary = new ChildAgentSummaryAccumulator();
let aborted = input.abortSignal?.aborted === true;
const startChildRetry = this.runtimeKernel.startChildRetry;
if (!startChildRetry) throw new Error('RuntimeKernel does not support child agent retry');
const linkedIdentity = { sessionId: targetSessionId, turnId, runId };
const emitReady = () =>
input.onReady?.({
...(execution.kind === 'child_session'
? { childSessionId: execution.sessionId, runId }
: {}),
turnId,
agentId: definition.id,
agentName: definition.name,
});
const consumeEvent = (event: SessionEvent) => {
summary.add(event);
try {
input.onEvent?.(event);
} catch {
// A presentation observer must not change the child run outcome.
}
};
throwIfChildExecutionAborted(input.abortSignal, 'Child agent retry was cancelled before start');
const executionPromise =
execution.kind === 'child_session'
? this.consumeLinkedRootExecution(
{
...linkedIdentity,
userMessageId: null,
execution: {
kind: 'linked_child_provider_retry',
agentId: definition.id,
agentName: definition.name,
sourceRunId: sourceRun.runId,
},
content: {
text: retryAnchor.content,
...(retryAnchor.attachments ? { attachments: retryAnchor.attachments } : {}),
},
start: ({ onRunStarted }) =>
startChildRetry.call(
this.runtimeKernel,
targetSessionId,
{
parentRunId: input.parentRunId,
spec: {
id: definition.id,
name: definition.name,
systemPrompt: definition.systemPrompt,
},
continuation,
admissionMode,
linkedSession: true,
onRunStarted,
},
runtimeExecution,
),
onReady: emitReady,
onEvent: consumeEvent,
},
true,
)
: this.consumeRuntimeEvents(
startChildRetry.call(
this.runtimeKernel,
targetSessionId,
{
parentRunId: input.parentRunId,
spec: {
id: definition.id,
name: definition.name,
systemPrompt: definition.systemPrompt,
},
continuation,
admissionMode,
},
runtimeExecution,
),
emitReady,
consumeEvent,
);
let stopPromise: Promise<void> | undefined;
const onAbort = () => {
aborted = true;
if (execution.kind === 'child_session') {
stopPromise ??= this.stopLinkedRoot(linkedIdentity, { source: 'stop_button' });
} else {
stopPromise ??= this.runtimeKernel.stopSession(targetSessionId, {
source: 'stop_button',
});
}
};
if (input.abortSignal) {
input.abortSignal.addEventListener('abort', onAbort, { once: true });
if (input.abortSignal.aborted) onAbort();
}
try {
await executionPromise;
} finally {
input.abortSignal?.removeEventListener('abort', onAbort);
if (aborted) await stopPromise;
}
const completedAt = this.deps.now();
const run = await this.findRunByTurnId(targetSessionId, turnId);
const failureClass = run?.failureClass ?? summary.failureClass;
const artifacts = run
? await this.finalizeAndListChildTurnArtifacts(targetSessionId, turnId, run.status)
: [];
return {
...(execution.kind === 'child_session' ? { childSessionId: execution.sessionId } : {}),
agentId: definition.id,
agentName: definition.name,
turnId,
...(run?.runId ? { runId: run.runId } : { runId }),
retriedFromRunId: sourceRun.runId,
status: run ? agentRunStatusForSpawnResult(run.status) : summary.status(aborted),
permissionMode: definition.permissionMode,
summary: summary.text(),
artifactIds: artifacts.map((artifact) => artifact.id),
startedAt,
completedAt,
durationMs: Math.max(0, completedAt - startedAt),
eventCount: summary.eventCount,
...(failureClass ? { failureClass } : {}),
};
}
private async planDurableChildProviderRetry(input: {
targetSessionId: string;
sourceRun: AgentRunHeader;
runs: readonly AgentRunHeader[];
authority: RuntimeContinuationAuthorityStore;
inspectSafety: (sessionId: string) => Promise<RuntimeContinuationSafetyObservation>;
availableToolNames: readonly string[];
}): Promise<RuntimeContinuation> {
const runsById = new Map(input.runs.map((run) => [run.runId, run]));
const planner = new RuntimeContinuationPlanner({
readSourceRun: async (_sessionId, runId) => {
const raw = runsById.get(runId);
if (!raw) throw new Error(`Child agent retry lineage source ${runId} is missing`);
const effective =
runId === input.sourceRun.runId
? input.sourceRun
: await this.effectiveRunHeaderFromRuntimeLedger(raw);
if (effective.continuationSource) return effective;
const previousRunId = effective.retriedFromRunId ?? effective.resumedFromRunId;
if (!previousRunId) return effective;
const previous = runsById.get(previousRunId);
if (!previous) {
throw new Error(`Child agent retry lineage source ${previousRunId} is missing`);
}
const prefix = await input.authority.readImmutableRuntimePrefix({
sessionId: input.targetSessionId,
runId: previousRunId,
});
return {
...effective,
continuationSource: {
sourceInvocationId: prefix.identity.invocationId,
sourceRunId: prefix.identity.runId,
sourceTurnId: prefix.identity.turnId,
sourceRuntimeEventHighWater: prefix.position.lastEventSeq,
},
};
},
readImmutableRuntimePrefix: (prefixInput) =>
input.authority.readImmutableRuntimePrefix(prefixInput),
readContinuationClaimStateByBoundary: (boundaryDigest) =>
input.authority.readContinuationClaimStateByBoundary(boundaryDigest),
findExistingContinuation: async (_sessionId, sourceRunId, sourceRuntimeEventHighWater) =>
input.runs.find(
(run) =>
run.continuationSource?.sourceRunId === sourceRunId &&
run.continuationSource.sourceRuntimeEventHighWater === sourceRuntimeEventHighWater,
),
newId: this.deps.newId,
});
const [targetHeader, safety] = await Promise.all([
this.deps.store.readHeader(input.targetSessionId),
input.inspectSafety(input.targetSessionId),
]);
const retryPlan = await planner.plan({
sessionId: input.targetSessionId,
sourceRunId: input.sourceRun.runId,
currentCwd: targetHeader.cwd,
sourceWorkspaceIdentity: input.sourceRun.workspaceIdentity ?? input.sourceRun.cwd,
currentWorkspaceIdentity: safety.workspaceIdentity,
backgroundOperationsSettled: safety.backgroundOperationsSettled,
availableToolNames: input.availableToolNames,
...(safety.workspaceCheckpoint ? { workspaceCheckpoint: safety.workspaceCheckpoint } : {}),
});
if (retryPlan.disposition !== 'continue' || !retryPlan.continuation) {
throw new Error(
`Child agent retry source is not safely replayable: ${retryPlan.rejectionReasons.join(', ')}`,
);
}
return retryPlan.continuation;
}
/**
* Preserves the pre-PR-B provider RateLimit retry for compositions that have
* neither continuation authority nor a safety inspector. It is intentionally
* not a durable continuation: no claim or continuation-start is written, and
* it cannot recover a claim-repair abandonment. The host authority lifecycle
* integration replaces this path with a typed SQLite composition.
*/
private async planLegacyChildProviderRetry(input: {
targetSessionId: string;
rawSourceRun: AgentRunHeader;
sourceRun: AgentRunHeader;
runs: readonly AgentRunHeader[];
availableToolNames: readonly string[];
}): Promise<RuntimeContinuation> {
if (!this.deps.runtimeEventStore?.readImmutableRuntimeEvents) {
throw new Error('Legacy child provider retry requires immutable RuntimeEvent reads');
}
if (input.sourceRun.failureClass !== 'RateLimit') {
throw new Error('Legacy child provider retry only supports provider rate-limit failures');
}
const replaySegments: RuntimeEvent[][] = [];
let sourceEvents: RuntimeEvent[] | undefined;
let sourceReplay: RuntimeEvent[] | undefined;
let chainRun: AgentRunHeader | undefined = input.rawSourceRun;
const visited = new Set<string>();
while (chainRun) {
if (visited.has(chainRun.runId)) {
throw new Error('Child agent retry lineage contains a cycle');
}
visited.add(chainRun.runId);
const events = await this.deps.runtimeEventStore.readImmutableRuntimeEvents(
input.targetSessionId,
chainRun.runId,
);
const plan = buildResumePlanFromRuntimeEvents(events);
if (plan.disposition !== 'safe_replay') {
throw new Error(`Child agent retry source is not safely replayable: ${chainRun.runId}`);
}
replaySegments.unshift(plan.replayRuntimeEvents);
if (chainRun.runId === input.sourceRun.runId) {
sourceEvents = events;
sourceReplay = plan.replayRuntimeEvents;
}
const previousRunId: string | undefined =
chainRun.retriedFromRunId ?? chainRun.resumedFromRunId;
if (!previousRunId) break;
chainRun = input.runs.find((run) => run.runId === previousRunId);
if (!chainRun) throw new Error('Child agent retry lineage source is missing');
}
if (!sourceEvents || !sourceReplay) {
throw new Error('Child agent retry source ledger is missing');
}
const sourceInvocationId = input.sourceRun.invocationId ?? sourceEvents[0]?.invocationId;
if (!sourceInvocationId) throw new Error('Child agent retry source has no invocation id');
return {
sessionId: input.targetSessionId,
invocationId: this.deps.newId(),
runId: this.deps.newId(),
turnId: this.deps.newId(),
sourceInvocationId,
sourceRunId: input.sourceRun.runId,
sourceTurnId: input.sourceRun.turnId,
sourceRuntimeEventHighWater: sourceEvents.length,
sourceRuntimeContext: sourceReplay,
runtimeContext: replaySegments.flat(),
safetySnapshot: {
workspaceIdentity: input.sourceRun.workspaceIdentity ?? input.sourceRun.cwd,
backgroundOperationsSettled: true,
availableToolNames: [...input.availableToolNames],
},
};
}
private assertChildRunHasNoSuccessor(runs: readonly AgentRunHeader[], sourceRunId: string): void {
if (
runs.some(
(run) => run.resumedFromRunId === sourceRunId || run.retriedFromRunId === sourceRunId,
)
) {
throw new Error(`Child AgentRun ${sourceRunId} already has a successor`);
}
}
async listChildAgents(sessionId: string): Promise<AgentListResult> {
const [header, tools] = await Promise.all([
this.deps.store.readHeader(sessionId),
this.childToolsForSession(sessionId),
]);
const definitions = listBuiltinAgentDefinitions({
tools,
worktreeChildExecutorAvailable: await this.isWorktreeChildExecutorAvailable(header),
});
const presets = this.deps.subagentCatalog ? await this.deps.subagentCatalog.list() : [];
if (!this.deps.runStore) return { definitions, presets, executions: [], runs: [] };
const runs = await this.deps.runStore.listSessionRuns(sessionId);
const childRuns = await Promise.all(
runs
.filter(
(run): run is AgentRunHeader & { parentRunId: string } =>
!!run.parentRunId && !isSessionInlineRun(run),
)
.map(
async (run): Promise<AgentRunHeader & { parentRunId: string }> => ({
...(await this.effectiveRunHeaderFromRuntimeLedger(run)),
parentRunId: run.parentRunId,
}),
),
);
const legacyRuns = childRuns.map((run) => ({
runId: run.runId,
turnId: run.turnId,
parentRunId: run.parentRunId,
...(run.agentId ? { agentId: run.agentId } : {}),
...(run.agentName ? { agentName: run.agentName } : {}),
status: run.status,
permissionMode: run.permissionMode,
createdAt: run.createdAt,
updatedAt: run.updatedAt,
...(run.completedAt !== undefined ? { completedAt: run.completedAt } : {}),
...(run.completedAt !== undefined
? { durationMs: Math.max(0, run.completedAt - run.createdAt) }
: {}),
...(run.failureClass ? { failureClass: run.failureClass } : {}),
}));
const childSessionHeaders = await Promise.all(
(await this.listChildSessions(sessionId)).map((child) =>
this.deps.store.readHeader(child.id),
),
);
const childSessionExecutions = await Promise.all(
childSessionHeaders.map(async (child): Promise<SubagentExecutionListItem> => {
const childRuns = await this.deps.runStore!.listSessionRuns(child.id);
const latest = childRuns
.slice()
.sort(
(left, right) =>
right.createdAt - left.createdAt ||
right.updatedAt - left.updatedAt ||
right.runId.localeCompare(left.runId),
)[0];
const run = latest ? await this.effectiveRunHeaderFromRuntimeLedger(latest) : undefined;
const currentRunId = run?.runId ?? child.subagentSpawn?.initialRunId;
return {
execution: {
kind: 'child_session',
sessionId: child.id,
...(currentRunId ? { currentRunId } : {}),
},
...(child.subagentRuntime?.agentId ? { agentId: child.subagentRuntime.agentId } : {}),
...(child.subagentRuntime?.agentName
? { agentName: child.subagentRuntime.agentName }
: {}),
...(child.subagentRuntime?.profile ? { profile: child.subagentRuntime.profile } : {}),
...(run?.turnId ? { turnId: run.turnId } : {}),
status: run?.status ?? (child.status === 'aborted' ? 'cancelled' : 'created'),
permissionMode: run?.permissionMode ?? child.permissionMode,
createdAt: run?.createdAt ?? child.createdAt,
updatedAt: run?.updatedAt ?? child.lastUsedAt,
...(run?.completedAt !== undefined ? { completedAt: run.completedAt } : {}),
...(run?.completedAt !== undefined
? { durationMs: Math.max(0, run.completedAt - run.createdAt) }
: {}),
...(run?.failureClass ? { failureClass: run.failureClass } : {}),
};
}),
);
return {
definitions,
presets,
executions: [
...childSessionExecutions,
...childRuns.map(
(run): SubagentExecutionListItem => ({
execution: {
kind: 'legacy_child_run',
sessionId,
runId: run.runId,
},
...(run.agentId ? { agentId: run.agentId } : {}),
...(run.agentName ? { agentName: run.agentName } : {}),
turnId: run.turnId,
status: run.status,
permissionMode: run.permissionMode,
createdAt: run.createdAt,
updatedAt: run.updatedAt,
...(run.completedAt !== undefined ? { completedAt: run.completedAt } : {}),
...(run.completedAt !== undefined
? { durationMs: Math.max(0, run.completedAt - run.createdAt) }
: {}),
...(run.failureClass ? { failureClass: run.failureClass } : {}),
}),
),
],
runs: legacyRuns,
};
}
private async childToolsForSession(sessionId: string): Promise<readonly MakaTool[]> {
return this.deps.resolveChildTools
? await this.deps.resolveChildTools(sessionId)
: (this.deps.childTools ?? []);
}
async readChildAgentOutput(
sessionId: string,
input: AgentOutputInput,
): Promise<AgentOutputResult> {
if (!this.deps.runStore || !this.deps.runtimeEventStore) {
throw new Error('agent_output requires AgentRunStore and RuntimeEventStore');
}
const located = await this.findChildRunForOutput(sessionId, input);
const { header } = located;
const inspected = await inspectAgentRunReadModel(
this.deps.runStore,
this.deps.runtimeEventStore,
{
sessionId: header.sessionId,
runId: header.runId,
header,
},
);
const artifacts = await this.finalizeAndListChildTurnArtifacts(
header.sessionId,
header.turnId,
inspected.header.status,
);
const maxEvents = normalizeAgentOutputMaxEvents(input.maxEvents);
const maxBytes = normalizeAgentOutputMaxBytes(input.maxBytes);
const view = input.view ?? 'runtime_events';
if (view === 'result') {
const boundedResult = buildAgentOutputCommittedResult({
header: inspected.header,
runtimeEvents: inspected.runtimeEvents,
artifacts,
maxArtifacts: maxEvents,
maxBytes,
...(located.graph ? { graph: located.graph } : {}),
});
return {
execution: located.execution,
header: inspected.header,
result: boundedResult.result,
events: [],
runtimeEvents: [],
sourceHealth: inspected.sourceHealth,
diagnostics: [],
artifacts: [],
truncated: {
events: inspected.events.length > 0,
runtimeEvents: inspected.runtimeEvents.length > 0,
diagnostics: inspected.diagnostics.length > 0,
artifacts: artifacts.length > 0,
bytes: boundedResult.truncated,
},
budget: {
view,
maxBytes,
projectedBytes: boundedResult.projectedBytes,
},
};
}
const bounded = boundAgentOutputCollections(
{
events: view === 'runtime_events' ? [] : tail(inspected.events, maxEvents),
runtimeEvents: view === 'events' ? [] : tail(inspected.runtimeEvents, maxEvents),
diagnostics: tail(inspected.diagnostics, maxEvents),
artifacts: tail(artifacts, maxEvents),
},
maxBytes,
);
return {
execution: located.execution,
header: inspected.header,
events: bounded.events,
runtimeEvents: bounded.runtimeEvents,
sourceHealth: inspected.sourceHealth,
diagnostics: bounded.diagnostics,
artifacts: bounded.artifacts,
truncated: {
events:
view === 'runtime_events' ||
inspected.events.length > maxEvents ||
bounded.events.length < Math.min(inspected.events.length, maxEvents),
runtimeEvents:
view === 'events' ||
inspected.runtimeEvents.length > maxEvents ||
bounded.runtimeEvents.length < Math.min(inspected.runtimeEvents.length, maxEvents),
diagnostics:
inspected.diagnostics.length > maxEvents ||
bounded.diagnostics.length < Math.min(inspected.diagnostics.length, maxEvents),
artifacts:
artifacts.length > maxEvents ||
bounded.artifacts.length < Math.min(artifacts.length, maxEvents),
bytes: bounded.truncated,
},
budget: {
view,
maxBytes,
projectedBytes: bounded.projectedBytes,
},
};
}
async stopSession(sessionId: string, input: StopSessionInput = {}): Promise<void> {
const hostedAuthority = isRuntimeHostedRootAuthority(this.deps.messageAuthority)
? this.deps.messageAuthority
: undefined;
const ownStop = hostedAuthority
? hostedAuthority.stopSession(sessionId, input)
: this.runtimeKernel.stopSession(sessionId, input);
let childStops: PromiseSettledResult<void>[] = [];
let childLookupError: unknown;
try {
const children = await this.listChildSessions(sessionId);
childStops = await Promise.allSettled(
children
.filter((child) => child.subagentParent?.lifecycle === 'foreground')
.map((child) =>
hostedAuthority
? hostedAuthority.stopSession(child.id, input)
: this.runtimeKernel.stopSession(child.id, input),
),
);
} catch (error) {
childLookupError = error;
}
await ownStop;
const childStopError = childStops.find(
(result): result is PromiseRejectedResult => result.status === 'rejected',
)?.reason;
if (childLookupError !== undefined) throw childLookupError;
if (childStopError !== undefined) throw childStopError;
}
async deliverHostedRootStop(sessionId: string, input: StopSessionInput = {}): Promise<void> {
const ownStop = this.runtimeKernel.stopSession(sessionId, input);
const authority = isRuntimeHostedRootAuthority(this.deps.messageAuthority)
? this.deps.messageAuthority
: undefined;
let childStops: PromiseSettledResult<void>[] = [];
let childLookupError: unknown;
try {
const children = await this.listChildSessions(sessionId);
childStops = await Promise.allSettled(
children
.filter((child) => child.subagentParent?.lifecycle === 'foreground')
.map((child) =>
authority
? authority.stopSession(child.id, input)
: this.runtimeKernel.stopSession(child.id, input),
),
);
} catch (error) {
childLookupError = error;
}
await ownStop;
const childStopError = childStops.find(
(result): result is PromiseRejectedResult => result.status === 'rejected',
)?.reason;
if (childLookupError !== undefined) throw childLookupError;
if (childStopError !== undefined) throw childStopError;
}
async closePendingHostedAdmission(input: {
sessionId: string;
turnId: string;
runId: string;
admittedAt: number;
execution: Exclude<
RootExecutionDescriptor,
| { kind: 'regenerate' }
| { kind: 'context_compact' }
| { kind: 'automation' }
| { kind: 'safe_boundary_continuation' }
>;
}): Promise<void> {
if (!this.deps.runStore || !this.deps.runtimeEventStore) {
throw new Error('Hosted admission recovery requires execution stores');
}
const session = await this.deps.store.readHeader(input.sessionId);
const headerExtras: Partial<AgentRunHeader> = {};
let recoveryReason: string;
let diagnostic: Record<string, unknown>;
let workspaceIdentity: string | undefined;
if (input.execution.kind === 'goal') {
headerExtras.goalId = input.execution.goalId;
recoveryReason = 'goal_internal_admission_without_run';
diagnostic = {
executionKind: input.execution.kind,
goalId: input.execution.goalId,
};
} else if (input.execution.kind === 'agent_graph_supervisor_wake') {
headerExtras.agentGraphWakeId = input.execution.wakeId;
headerExtras.agentGraphWakeAttemptId = input.execution.attemptId;
headerExtras.orchestrationMode = 'graph';
headerExtras.orchestrationSource = 'turn_override';
headerExtras.agentSwarmAuthorization = 'none';
recoveryReason = 'agent_graph_supervisor_internal_admission_without_run';
diagnostic = {
executionKind: input.execution.kind,
graphId: input.execution.graphId,
wakeId: input.execution.wakeId,
attemptId: input.execution.attemptId,
};
} else if (input.execution.kind !== 'external_message') {
if (
session.subagentParent?.kind !== 'subagent' ||
session.subagentRuntime?.agentId !== input.execution.agentId ||
session.subagentRuntime.agentName !== input.execution.agentName
) {
throw new Error(
`Admitted Turn ${input.turnId} does not match its linked child Session identity`,
);
}
const continuationAuthority = runtimeContinuationAuthority(this.deps.runtimeEventStore);
if (continuationAuthority) {
const claimState = (
await continuationAuthority.listContinuationClaimsForRecovery(input.sessionId)
).find(
(candidate) =>
candidate.claim.target.runId === input.runId ||
candidate.claim.target.turnId === input.turnId,
);
if (claimState) {
assertClaimOwnsHostedLinkedChildAdmission(
{
sessionId: input.sessionId,
turnId: input.turnId,
runId: input.runId,
execution: input.execution,
},
claimState.claim,
);
// The continuation claim is the durable owner of this target identity.
// SessionManager's claim-repair saga must materialize its exact header;
// the generic hosted-admission repair must not steal the same Run ID.
return;
}
}
if (
input.execution.kind === 'linked_child_resume' ||
input.execution.kind === 'linked_child_provider_retry'
) {
const sourceRun = await this.deps.runStore.readRun(
input.sessionId,
input.execution.sourceRunId,
);
if (
sourceRun.agentId !== input.execution.agentId ||
sourceRun.agentName !== input.execution.agentName
) {
throw new Error(
`Admitted Turn ${input.turnId} source changed its trusted agent identity`,
);
}
workspaceIdentity = sourceRun.workspaceIdentity;
if (input.execution.kind === 'linked_child_resume') {
headerExtras.resumedFromRunId = input.execution.sourceRunId;
} else {
headerExtras.retriedFromRunId = input.execution.sourceRunId;
}
}
headerExtras.agentId = input.execution.agentId;
headerExtras.agentName = input.execution.agentName;
recoveryReason = 'child_internal_admission_without_run';
diagnostic = {
executionKind: input.execution.kind,
...(input.execution.kind === 'linked_child_resume' ||
input.execution.kind === 'linked_child_provider_retry'
? { sourceRunId: input.execution.sourceRunId }
: {}),
};
} else {
throw new Error('External message recovery closure is not supported');
}
const run: AgentRunHeader = {
runId: input.runId,
invocationId: input.runId,
sessionId: input.sessionId,
turnId: input.turnId,
status: 'created',
backendKind: session.backend,
llmConnectionSlug: session.llmConnectionSlug,
modelId: session.model,
cwd: session.cwd,
...(workspaceIdentity !== undefined ? { workspaceIdentity } : {}),
permissionMode: session.permissionMode,
collaborationMode: session.collaborationMode ?? 'agent',
createdAt: input.admittedAt,
updatedAt: input.admittedAt,
...headerExtras,
};
await this.deps.runStore.createRun(run, { durable: true });
const ts = this.deps.now();
const terminalEvent = buildRecoveredTerminalRuntimeEvent({
id: this.deps.newId(),
run,
status: 'failed',
ts,
failureClass: 'app_restarted',
recoveryReason,
diagnostic,
message: 'app_restarted',
});
await commitTerminalRunWithRuntimeFact({
runStore: this.deps.runStore,
runtimeEventStore: this.deps.runtimeEventStore,
newId: this.deps.newId,
sessionId: input.sessionId,
runId: input.runId,
turnId: input.turnId,
status: 'failed',
ts,
terminalEvent,
failureClass: 'app_restarted',
runEventData: {
recovered: true,
recoveryReason,
...diagnostic,
},
existingEvents: [],
});
}
private async consumeLinkedRootExecution(
input: RuntimeHostedRootExecutionInput,
hostedGateAlreadyHeld = false,
): Promise<void> {
const authority = isRuntimeHostedRootAuthority(this.deps.messageAuthority)
? this.deps.messageAuthority
: undefined;
if (authority) {
const release = hostedGateAlreadyHeld
? undefined
: this.acquireHostedLinkedChildExecution(input.sessionId);
try {
await authority.executeRoot(input);
return;
} finally {
release?.();
}
}
await this.consumeRuntimeEvents(
input.start({
runId: input.runId,
userMessageId: input.userMessageId,
onRunStarted: () => input.onReady?.(),
}),
undefined,
input.onEvent,
);
}
private acquireHostedLinkedChildExecution(sessionId: string): () => void {
if (!isRuntimeHostedRootAuthority(this.deps.messageAuthority)) return () => {};
if (this.activeHostedLinkedChildSessions.has(sessionId)) {
throw new Error(`Child Session ${sessionId} already has an active execution`);
}
this.activeHostedLinkedChildSessions.add(sessionId);
return () => {
this.activeHostedLinkedChildSessions.delete(sessionId);
};
}
private async consumeRuntimeEvents(
events: AsyncIterable<SessionEvent>,
onReady?: () => void | Promise<void>,
onEvent?: (event: SessionEvent) => void,
): Promise<void> {
await onReady?.();
for await (const event of events) onEvent?.(event);
}
private stopLinkedRoot(
identity: RuntimeMessageRunIdentity,
input: StopSessionInput,
): Promise<void> {
const authority = isRuntimeHostedRootAuthority(this.deps.messageAuthority)
? this.deps.messageAuthority
: undefined;
return authority
? authority.stopRoot(identity, input)
: this.runtimeKernel.stopSession(identity.sessionId, input);
}
/** Queue a user message for mid-turn injection at the next step boundary. */
steer(sessionId: string, text: string): QueueEnqueueOutcome {
return this.runtimeKernel.steer(sessionId, text);
}
/** Queue a user message to open the turn after the current one finishes. */
queueMessage(sessionId: string, text: string): QueueEnqueueOutcome {
return this.runtimeKernel.queueMessage(sessionId, text);
}
/** Drain the followup queue into one `\n\n`-joined prompt, or null if empty. */
drainFollowup(sessionId: string): string | null {
return this.runtimeKernel.drainFollowup(sessionId);
}
/** Take back every queued message (both queues) as one `\n\n`-joined string. */
retractQueue(sessionId: string): string {
return this.runtimeKernel.retractQueue(sessionId);
}
async *regenerateTurn(
sessionId: string,
input: RegenerateTurnInput,
): AsyncIterable<SessionEvent> {
const execution = this.runtimeKernel.claimExecution(sessionId);
try {
const source = await this.prepareRegenerateTurn(sessionId, input.sourceTurnId);
yield* this.sendMessage(
sessionId,
{
turnId: input.turnId ?? this.deps.newId(),
...source.content,
parentTurnId: source.sourceTurnId,
regeneratedFromTurnId: source.sourceTurnId,
},
{ execution },
);
} finally {
execution.release();
}
}
async prepareRegenerateTurn(
sessionId: string,
sourceTurnId: string,
): Promise<RegenerateTurnSource> {
const view = await this.getSessionView(sessionId);
const source = view.turns.find((candidate) => candidate.turnId === sourceTurnId);
if (!source) {
throw new RuntimeRegenerateTurnError(
'not_found',
`Cannot regenerate unknown Turn ${sourceTurnId}`,
);
}
if (
source.status !== 'failed' &&
source.status !== 'aborted' &&
source.status !== 'completed'
) {
throw new RuntimeRegenerateTurnError(
'operation_conflict',
`Cannot regenerate Turn ${sourceTurnId} while it is ${source.status}`,
);
}
const user = view.messages.find(
(message): message is UserMessage =>
message.type === 'user' && message.turnId === sourceTurnId,
);
if (!user) {
throw new RuntimeRegenerateTurnError(
'operation_conflict',
`Turn ${sourceTurnId} has no UserMessage`,
);
}
return {
sourceTurnId,
content: normalizeMessageContent(user),
};
}
async branchFromTurn(sessionId: string, input: BranchFromTurnInput): Promise<SessionSummary> {
const sourceView = await this.getSessionView(sessionId);
const slice = createConversationCopySlice(sourceView.messages, input.sourceTurnId, 'through');
if (!slice) throw new Error(`Cannot branch from unknown turn ${input.sourceTurnId}`);
return this.createBranchSession(sessionId, sourceView, [...slice.messages], input);
}
/** Canonical, repaired source view for a Host-owned cross-Session copy. */
async readConversationCopySnapshot(sessionId: string): Promise<RuntimeReadModelSessionView> {
const readMessagesSnapshot = this.deps.store.readMessagesSnapshot;
if (!readMessagesSnapshot) {
throw new Error('Conversation copy requires a side-effect-free message snapshot');
}
const readMessages = readMessagesSnapshot.bind(this.deps.store);
const view = await this.getSessionView(sessionId, { readMessages });
if (view.runs.length > 0 || view.messages.length > 0) return view;
const messages = await readMessages(sessionId);
if (messages.length === 0) return view;
return {
...view,
messages,
turns: deriveTurnRecords(messages),
};
}
async branchBeforeTurn(sessionId: string, input: BranchFromTurnInput): Promise<SessionSummary> {
const sourceView = await this.getSessionView(sessionId);
const slice = createConversationCopySlice(sourceView.messages, input.sourceTurnId, 'before');
if (!slice) throw new Error(`Cannot branch before unknown turn ${input.sourceTurnId}`);
return this.createBranchSession(sessionId, sourceView, [...slice.messages], input);
}
/**
* Create a non-destructive edit-and-resend version. Unlike branchBeforeTurn,
* this is not a new sidebar conversation: revision lineage lets hosts fold
* every version into one conversation slot while keeping old transcripts.
*/
async reviseBeforeTurn(sessionId: string, input: ReviseBeforeTurnInput): Promise<SessionSummary> {
const sourceView = await this.getSessionView(sessionId);
const slice = createConversationCopySlice(sourceView.messages, input.sourceTurnId, 'before');
if (!slice) throw new Error(`Cannot revise before unknown turn ${input.sourceTurnId}`);
return this.createRevisionSession(sessionId, sourceView, [...slice.messages], input);
}
private async createRevisionSession(
sessionId: string,
sourceView: RuntimeReadModelSessionView,
copied: StoredMessage[],
input: ReviseBeforeTurnInput,
): Promise<SessionSummary> {
const plan = await this.prepareConversationRuntimeLedgerClone(sessionId, sourceView, copied);
const [header, boundary] = await Promise.all([
this.deps.store.readHeader(sessionId),
this.deps.store.readExecutionBoundary(sessionId),
]);
const revisionRootSessionId = header.revisionRootSessionId ?? sessionId;
const family = (await this.deps.store.list()).filter(
(candidate) =>
candidate.id === revisionRootSessionId ||
candidate.revisionRootSessionId === revisionRootSessionId,
);
const revisionIndex =
Math.max(1, ...family.map((candidate) => candidate.revisionIndex ?? 1)) + 1;
const next = await this.deps.store.create(
{
cwd: header.cwd,
...(header.projectId !== undefined ? { projectId: header.projectId } : {}),
backend: header.backend,
llmConnectionSlug: header.llmConnectionSlug,
model: header.model,
thinkingLevel: header.thinkingLevel,
permissionMode: header.permissionMode,
collaborationMode: header.collaborationMode,
orchestrationMode: header.orchestrationMode ?? 'default',
name: header.name,
labels: header.labels,
// A revision of a real branch remains in that branch's conversation
// slot; revision lineage itself must not create a branch banner.
parentSessionId: header.parentSessionId,
branchOfTurnId: header.branchOfTurnId,
revisionRootSessionId,
revisionParentSessionId: sessionId,
revisionOfTurnId: input.sourceTurnId,
revisionIndex,
revisionState: 'preparing',
status: 'active',
},
boundary,
);
try {
const rewritten = await this.cloneConversationRuntimeLedger(next.id, copied, plan);
if (rewritten.length > 0) await this.deps.store.appendMessages(next.id, [...rewritten]);
await this.deps.store.appendMessage(next.id, {
type: 'system_note',
id: this.deps.newId(),
ts: this.deps.now(),
kind: 'session_start',
data: {
revisionRootSessionId,
revisionParentSessionId: sessionId,
revisionOfTurnId: input.sourceTurnId,
revisionIndex,
revisionState: 'preparing',
},
});
await this.deps.store.updateHeader(next.id, {
isFlagged: header.isFlagged,
titleIsManual: header.titleIsManual,
});
return headerToSummary(await this.deps.store.readHeader(next.id));
} catch (error) {
return this.rollbackLegacyConversationCopy(next.id, error);
}
}
private async createBranchSession(
sessionId: string,
sourceView: RuntimeReadModelSessionView,
copied: StoredMessage[],
input: BranchFromTurnInput,
): Promise<SessionSummary> {
const plan = await this.prepareConversationRuntimeLedgerClone(sessionId, sourceView, copied);
const [header, boundary] = await Promise.all([
this.deps.store.readHeader(sessionId),
this.deps.store.readExecutionBoundary(sessionId),
]);
const next = await this.deps.store.create(
{
cwd: header.cwd,
...(header.projectId !== undefined ? { projectId: header.projectId } : {}),
backend: header.backend,
llmConnectionSlug: header.llmConnectionSlug,
model: header.model,
thinkingLevel: header.thinkingLevel,
permissionMode: header.permissionMode,
collaborationMode: header.collaborationMode,
orchestrationMode: header.orchestrationMode ?? 'default',
name: input.name ?? `${header.name} · 分支`,
labels: input.sideConversation
? [...new Set([...header.labels, SIDE_CONVERSATION_SESSION_LABEL])]
: header.labels,
parentSessionId: sessionId,
branchOfTurnId: input.sourceTurnId,
status: 'active',
},
boundary,
);
try {
const rewritten = await this.cloneConversationRuntimeLedger(next.id, copied, plan);
if (rewritten.length > 0) await this.deps.store.appendMessages(next.id, [...rewritten]);
await this.deps.store.appendMessage(next.id, {
type: 'system_note',
id: this.deps.newId(),
ts: this.deps.now(),
kind: 'session_start',
data: { parentSessionId: sessionId, branchOfTurnId: input.sourceTurnId },
});
return headerToSummary(await this.deps.store.readHeader(next.id));
} catch (error) {
return this.rollbackLegacyConversationCopy(next.id, error);
}
}
async respondToSandboxBoundary(
sessionId: string,
response: SandboxBoundaryResponse,
): Promise<void> {
if (this.deps.interactionAuthority) {
throw new RuntimeInteractionInvariantError(
'Hosted permission answers must use the captured continuation',
);
}
await this.runtimeKernel.respondToSandboxBoundary(sessionId, response);
}
async respondToUserQuestion(sessionId: string, response: UserQuestionResponse): Promise<void> {
if (this.deps.interactionAuthority) {
throw new RuntimeInteractionInvariantError(
'Hosted question answers must use the captured continuation',
);
}
await this.runtimeKernel.respondToUserQuestion?.(sessionId, response);
}
// --------------------------------------------------------------------------
// Internal helpers
// --------------------------------------------------------------------------
private async findRunByTurnId(
sessionId: string,
turnId: string,
): Promise<AgentRunHeader | undefined> {
if (!this.deps.runStore) return undefined;
const runs = await this.deps.runStore.listSessionRuns(sessionId).catch(() => []);
const run = runs.find((candidate) => candidate.turnId === turnId);
return run ? this.effectiveRunHeaderFromRuntimeLedger(run) : undefined;
}
private assertActiveParentRun(
parentSessionId: string,
parentRun: AgentRunHeader,
parentTurnId: string,
): void {
if (
parentRun.sessionId !== parentSessionId ||
parentRun.turnId !== parentTurnId ||
!(
this.runtimeKernel.hasActiveRun?.(parentSessionId, parentRun.runId, parentTurnId) ||
this.deps.isParentRunActive?.(parentSessionId, parentRun.runId, parentTurnId)
)
) {
throw new Error('Child session parent run is not active');
}
}
private async resolveShellRunOwner(
firstParentSessionId: string,
ref: string,
): Promise<{ sessionId: string; result: ShellRunUpdate['result'] } | undefined> {
const shellRuns = this.deps.shellRuns;
if (!shellRuns) return undefined;
let ownerSessionId: string | undefined = firstParentSessionId;
const visited = new Set<string>();
while (ownerSessionId && !visited.has(ownerSessionId)) {
visited.add(ownerSessionId);
try {
return {
sessionId: ownerSessionId,
result: await shellRuns.inspectResource(ownerSessionId, ref),
};
} catch (error) {
if (!isNotFoundError(error)) throw error;
try {
const ownerHeader = await this.deps.store.readHeader(ownerSessionId);
ownerSessionId = ownerHeader.revisionParentSessionId ?? ownerHeader.parentSessionId;
} catch (headerError) {
if (isNotFoundError(headerError)) return undefined;
throw headerError;
}
}
}
return undefined;
}
private async readShellRunProjectionMessages(sessionId: string): Promise<StoredMessage[] | null> {
try {
return await this.getMessages(sessionId);
} catch (error) {
if (!(error instanceof RuntimeReadModelError)) throw error;
// ShellRun hydration is a best-effort UI projection. A legacy RuntimeEvent
// incompatibility must not turn its retry loop into a permanent IPC error.
try {
return await this.deps.store.readMessages(sessionId);
} catch {
return null;
}
}
}
private async findChildRunForOutput(
sessionId: string,
input: AgentOutputInput,
): Promise<{
header: AgentRunHeader;
execution: SubagentExecutionRef;
graph?: NonNullable<SubagentSessionParent['graph']>;
}> {
if (Number(!!input.execution) + Number(!!input.runId) + Number(!!input.turnId) !== 1) {
throw new Error('agent_output requires exactly one execution, runId, or turnId locator');
}
if (input.execution?.kind === 'child_session') {
const execution = input.execution;
const child = await this.deps.store.readHeader(execution.sessionId).catch((error) => {
if (isNotFoundError(error)) return undefined;
throw error;
});
if (
!child ||
child.subagentParent?.kind !== 'subagent' ||
child.subagentParent.parentSessionId !== sessionId
) {
throw new Error('agent_output could not find the requested child session');
}
const runs = await this.deps.runStore?.listSessionRuns(child.id);
const selected = execution.currentRunId
? runs?.find((run) => run.runId === execution.currentRunId)
: runs
?.slice()
.sort(
(left, right) =>
right.createdAt - left.createdAt ||
right.updatedAt - left.updatedAt ||
right.runId.localeCompare(left.runId),
)[0];
if (!selected || !isSessionInlineRun(selected)) {
throw new Error('agent_output could not find the requested child session run');
}
const header = await this.effectiveRunHeaderFromRuntimeLedger(selected);
return {
header,
execution: {
kind: 'child_session',
sessionId: child.id,
currentRunId: header.runId,
},
...(child.subagentParent.graph ? { graph: child.subagentParent.graph } : {}),
};
}
const legacyExecution =
input.execution?.kind === 'legacy_child_run' ? input.execution : undefined;
if (legacyExecution && legacyExecution.sessionId !== sessionId) {
throw new Error('agent_output could not find the requested legacy child run');
}
const runs = await this.deps.runStore?.listSessionRuns(sessionId);
const header = runs?.find((run) =>
legacyExecution
? run.runId === legacyExecution.runId
: input.runId
? run.runId === input.runId
: input.turnId
? run.turnId === input.turnId
: false,
);
if (!header) throw new Error('agent_output could not find the requested child agent run');
if (!header.parentRunId || isSessionInlineRun(header)) {
throw new Error('agent_output only reads child agent runs');
}
return {
header: await this.effectiveRunHeaderFromRuntimeLedger(header),
execution: {
kind: 'legacy_child_run',
sessionId,
runId: header.runId,
},
};
}
private async effectiveRunHeaderFromRuntimeLedger(run: AgentRunHeader): Promise<AgentRunHeader> {
if (!this.deps.runtimeEventStore) return run;
const runtimeEvents = await this.deps.runtimeEventStore
.readRuntimeEvents(run.sessionId, run.runId)
.catch(() => undefined);
if (!runtimeEvents) return run;
const ledger = classifyTerminalRuntimeLedger(run, runtimeEvents);
return ledger.kind === 'fact' ? effectiveRunHeaderFromTerminalFact(run, ledger.fact) : run;
}
private async updateStatus(
sessionId: string,
status: SessionStatus,
blockedReason?: SessionBlockedReason,
ts = this.deps.now(),
): Promise<void> {
await this.updateHeader(sessionId, buildStatusPatch(status, ts, blockedReason));
}
private async updateHeader(
sessionId: string,
patch: Partial<SessionHeader>,
): Promise<SessionHeader> {
const next = await this.deps.store.updateHeader(sessionId, patch);
this.runtimeKernel.updateCachedHeader(sessionId, next);
return next;
}
private async isPlanOperationReplay(
sessionId: string,
operationId: string | undefined,
operationInput: unknown,
): Promise<boolean> {
if (!operationId) return false;
return (
(await this.requirePlanStore().readOperationReceipt(
sessionId,
operationId,
operationInput,
)) !== undefined
);
}
private async finalizePlanCollaborationMode(
sessionId: string,
mode: CollaborationMode,
): Promise<void> {
const header = await this.deps.store.readHeader(sessionId);
const changed = (header.collaborationMode ?? 'agent') !== mode;
const next = changed
? await this.deps.store.updateHeader(sessionId, { collaborationMode: mode })
: header;
this.runtimeKernel.updateCachedHeader(sessionId, next);
}
private async finalizePlanAbandonment(
sessionId: string,
operationId: string | undefined,
replay: boolean,
): Promise<void> {
const header = await this.deps.store.readHeader(sessionId);
const from = header.collaborationMode ?? 'agent';
const changed = from !== 'agent';
const next = changed
? await this.deps.store.updateHeader(sessionId, { collaborationMode: 'agent' })
: header;
this.runtimeKernel.updateCachedHeader(sessionId, next);
if (!changed && !replay) return;
if (!operationId) {
await this.deps.store.appendMessage(sessionId, {
type: 'system_note',
id: this.deps.newId(),
ts: this.deps.now(),
kind: 'mode_change',
data: { dimension: 'collaboration', from, to: 'agent' },
} satisfies SystemNoteMessage);
return;
}
const noteId = planAbandonmentNoteId(operationId);
const messages = await this.deps.store.readMessages(sessionId);
if (messages.some((message) => message.id === noteId)) return;
await this.deps.store.appendMessage(sessionId, {
type: 'system_note',
id: noteId,
ts: this.deps.now(),
kind: 'mode_change',
data: { dimension: 'collaboration', from: 'plan', to: 'agent' },
} satisfies SystemNoteMessage);
}
private requirePlanStore(): PlanStore {
if (!this.deps.planStore) throw new Error('Plan Mode is unavailable on this surface');
return this.deps.planStore;
}
private requireSessionConfigurationStore(): SessionStore &
Required<Pick<SessionStore, 'readHeaderRecordSnapshot' | 'updateSessionConfiguration'>> {
if (!this.deps.store.readHeaderRecordSnapshot || !this.deps.store.updateSessionConfiguration) {
throw new SessionConfigurationTransitionError(
'operation_unavailable',
'Session configuration authority is unavailable',
);
}
return this.deps.store as SessionStore &
Required<Pick<SessionStore, 'readHeaderRecordSnapshot' | 'updateSessionConfiguration'>>;
}
private async assertCollaborationTransition(
current: SessionHeader,
nextMode: CollaborationMode,
): Promise<void> {
if ((current.collaborationMode ?? 'agent') === nextMode) return;
if (nextMode === 'plan' && current.subagentParent) {
throw new SessionConfigurationTransitionError(
'operation_unavailable',
'Linked child Sessions cannot enter Plan mode',
);
}
const planStore = this.deps.planStore;
if (!planStore) {
throw new SessionConfigurationTransitionError(
'operation_unavailable',
'Collaboration mode changes require Plan authority',
);
}
const planState = await planStore.readState(current.id);
if (nextMode === 'plan' && planState.activeExecutionId) {
throw new SessionConfigurationTransitionError(
'session_busy',
'An active Plan execution prevents collaboration mode changes',
);
}
const latestProposal = planState.proposals.find(
(proposal) => proposal.proposalId === planState.latestProposalId,
);
if (nextMode === 'agent' && latestProposal?.status === 'pending_approval') {
throw new SessionConfigurationTransitionError(
'operation_conflict',
'A pending Plan proposal must be resolved before leaving Plan mode',
);
}
}
private async appendTurnState(
sessionId: string,
turnId: string,
status: TurnRecord['status'],
lineage: AgentRunLineage = {},
options: { ts?: number; errorClass?: string; abortSource?: string } = {},
): Promise<void> {
const ts = options.ts ?? this.deps.now();
await this.deps.store.appendMessage(
sessionId,
buildTurnStateMessage({
id: this.deps.newId(),
turnId,
ts,
status,
lineage,
...(options.abortSource ? { abortSource: options.abortSource } : {}),
...(options.errorClass !== undefined ? { errorClass: options.errorClass } : {}),
partialOutputRetained: await this.turnHasRetainedOutput(sessionId, turnId),
}),
);
}
private async turnHasRetainedOutput(sessionId: string, turnId: string): Promise<boolean> {
const messages = await this.deps.store.readMessages(sessionId).catch(() => []);
return messagesHaveRetainedOutput(messages, turnId);
}
private async requireTurnForAction(
sessionId: string,
turnId: string,
allowed: readonly TurnRecord['status'][],
action: string,
): Promise<TurnRecord> {
const turn = (await this.getSessionView(sessionId)).turns.find(
(candidate) => candidate.turnId === turnId,
);
if (!turn) throw new Error(`Cannot ${action}: unknown turn ${turnId}`);
if (!allowed.includes(turn.status)) {
throw new Error(`Cannot ${action}: turn ${turnId} is ${turn.status}`);
}
return turn;
}
private async getSessionView(
sessionId: string,
projectionCache: RuntimeReadModelProjectionCache = this.deps.store,
): Promise<RuntimeReadModelSessionView> {
const repaired = new Set<string>();
for (let attempt = 0; attempt < MAX_RUNTIME_LEDGER_REPAIR_ATTEMPTS; attempt += 1) {
try {
const view = await this.readModel(projectionCache).getSessionView(sessionId);
const runId = firstRuntimeRepairRunId(view.diagnostics, repaired);
if (!runId) return view;
if (!(await this.repairMissingTerminalFactOnce(sessionId, runId))) return view;
repaired.add(runId);
} catch (error) {
if (!(error instanceof RuntimeReadModelError)) throw error;
const runId = firstRuntimeRepairRunId(error.diagnostics, repaired);
if (!runId) throw error;
if (!(await this.repairMissingTerminalFactOnce(sessionId, runId))) throw error;
repaired.add(runId);
}
}
return this.readModel(projectionCache).getSessionView(sessionId);
}
private readModel(
projectionCache: RuntimeReadModelProjectionCache = this.deps.store,
): RuntimeReadModel {
if (!this.deps.runStore || !this.deps.runtimeEventStore) {
throw new Error('RuntimeReadModel requires AgentRunStore and RuntimeEventStore');
}
return new RuntimeReadModel({
runStore: this.deps.runStore,
runtimeEventStore: this.deps.runtimeEventStore,
projectionCache,
...(this.deps.canonicalPermissionOutcomes
? { canonicalPermissionOutcomes: this.deps.canonicalPermissionOutcomes }
: {}),
});
}
private async repairMissingTerminalFactOnce(sessionId: string, runId: string): Promise<boolean> {
return (
(await this.runtimeLedgerRepair?.repairMissingTerminalFactOnce(sessionId, runId)) ?? false
);
}
private async prepareConversationRuntimeLedgerClone(
sourceSessionId: string,
sourceView: RuntimeReadModelSessionView,
copiedMessages: readonly StoredMessage[],
): Promise<ConversationRuntimeLedgerCopyPlan | undefined> {
if (!this.deps.runStore || !this.deps.runtimeEventStore) return undefined;
return prepareConversationRuntimeLedgerCopy({
sourceSessionId,
sourceEvents: sourceView.events,
copiedMessages,
runStore: this.deps.runStore,
runtimeEventStore: this.deps.runtimeEventStore,
});
}
private async cloneConversationRuntimeLedger(
childSessionId: string,
copiedMessages: readonly StoredMessage[],
plan: ConversationRuntimeLedgerCopyPlan | undefined,
): Promise<readonly StoredMessage[]> {
if (!plan || !this.deps.runStore || !this.deps.runtimeEventStore) return copiedMessages;
const copied = await cloneConversationLedger({
plan,
copiedMessages,
referenceMap: {
mode: 'preserve_external',
sourceSessionId: plan.sourceSessionId,
targetSessionId: childSessionId,
},
runStore: this.deps.runStore,
runtimeEventStore: this.deps.runtimeEventStore,
newId: this.deps.newId,
});
return copied.copiedMessages;
}
private async rollbackLegacyConversationCopy(sessionId: string, error: unknown): Promise<never> {
try {
await this.deps.store.remove(sessionId);
} catch (cleanupError) {
throw new AggregateError(
[error, cleanupError],
`Conversation copy ${sessionId} failed and could not be removed`,
);
}
throw error;
}
/**
* Closes only the two provably pre-provider crash windows owned by B2:
* claim-only and target-Run-created-without-start. A repaired claim never
* dispatches a provider. It first materializes the exact target header
* committed in the claim, commits a deterministic continuation-start as
* event 1, then records an auditable failed terminal fact.
*
* A start written by the normal admission path is provider T1. Without an
* exclusive cross-process owner proof we cannot know that its provider is
* dead, so that state remains `continuation_started_indeterminate` and is
* deliberately left non-terminal.
*/
private async recoverContinuationClaimsBeforeProvider(
sessionId: string,
authority: RuntimeContinuationAuthorityStore,
_policy: RecoveryPolicy,
): Promise<boolean> {
if (!this.deps.runStore) return false;
const states = await authority.listContinuationClaimsForRecovery(sessionId);
let recovered = false;
for (const initialState of states) {
const { claim } = initialState;
let run: AgentRunHeader;
try {
run = await this.deps.runStore.readRun(sessionId, claim.target.runId);
} catch (error) {
if (!isMissingRunError(error)) throw error;
try {
await this.deps.runStore.createRun(claim.targetRunHeader, { durable: true });
run = await this.deps.runStore.readRun(sessionId, claim.target.runId);
} catch (createError) {
try {
run = await this.deps.runStore.readRun(sessionId, claim.target.runId);
} catch {
throw createError;
}
}
recovered = true;
}
let state =
(await authority.readContinuationClaimStateByBoundary(claim.boundaryDigest)) ??
initialState;
if (
state.startEventId
? !claimTargetRunHeaderIsCompatible(run, claim.targetRunHeader)
: !isDeepStrictEqual(run, claim.targetRunHeader)
) {
throw new Error(
`Continuation claim target Run header conflicts with claim ${claim.claimId}`,
);
}
let targetEvents = await readImmutableRuntimeEventsOrEmpty(
authority,
claim.target.sessionId,
claim.target.runId,
);
if (!state.startEventId) {
if (targetEvents.length > 0) {
throw new Error(
`Continuation claim ${claim.claimId} has target events without continuation-start`,
);
}
const repairStart = buildContinuationRepairStartEvent(claim);
await authority.commitContinuationRepairStart({ claim, event: repairStart });
state =
(await authority.readContinuationClaimStateByBoundary(claim.boundaryDigest)) ??
(() => {
throw new Error(`Continuation claim ${claim.claimId} disappeared during repair`);
})();
targetEvents = await readImmutableRuntimeEventsOrEmpty(
authority,
claim.target.sessionId,
claim.target.runId,
);
recovered = true;
}
const start = targetEvents[0];
if (!start || start.id !== state.startEventId) {
throw new Error(`Continuation claim ${claim.claimId} has an invalid start boundary`);
}
const repairedBeforeProvider = state.startKind === 'claim_repair';
if (!repairedBeforeProvider) continue;
const failureClass = 'continuation_abandoned_before_provider_dispatch';
const expectedTerminal = buildRecoveredTerminalRuntimeEvent({
id: continuationRepairEventId('terminal', claim.claimId),
run,
status: 'failed',
ts: Math.max(start.ts + 1, claim.claimedAt + 1),
recoveryReason: failureClass,
invocationId: claim.target.invocationId,
failureClass,
message: failureClass,
});
const terminal = targetEvents.find(isTerminalRuntimeEvent);
if (terminal && !isDeepStrictEqual(terminal, expectedTerminal)) {
throw new Error(`Continuation claim ${claim.claimId} has a conflicting repair terminal`);
}
const existingRunEvents = await this.deps.runStore.readEvents(
claim.target.sessionId,
claim.target.runId,
);
const projectionComplete =
terminal !== undefined &&
run.status === 'failed' &&
run.failureClass === failureClass &&
existingRunEvents.some((event) => event.type === 'run_failed');
if (projectionComplete) continue;
await commitTerminalRunWithRuntimeFact({
runStore: this.deps.runStore,
runtimeEventStore: authority,
newId: () => continuationRepairEventId('run-terminal', claim.claimId),
sessionId: claim.target.sessionId,
runId: claim.target.runId,
turnId: claim.target.turnId,
status: 'failed',
ts: expectedTerminal.ts,
terminalEvent: expectedTerminal,
failureClass,
runEventData: {
recovered: true,
recoveryReason: failureClass,
continuationClaimId: claim.claimId,
},
existingEvents: existingRunEvents,
});
recovered = true;
}
return recovered;
}
private async recoverAgentRunsFromLedger(
sessionId: string,
policy: RecoveryPolicy = { kind: 'best_effort' },
): Promise<{ hasLedger: boolean; recovered: boolean }> {
if (!this.deps.runStore || !this.deps.runtimeEventStore)
return { hasLedger: false, recovered: false };
const runs =
policy.kind === 'strict'
? await policy.stores.agentRunStore.listSessionRunsForRecovery(sessionId)
: await this.deps.runStore.listSessionRuns(sessionId);
if (runs.length === 0) return { hasLedger: false, recovered: false };
const continuationAuthority = runtimeContinuationAuthority(this.deps.runtimeEventStore);
const claimOwnedUnsettledRunIds = new Set<string>();
if (continuationAuthority) {
for (const state of await continuationAuthority.listContinuationClaimsForRecovery(
sessionId,
)) {
const events = await continuationAuthority.readImmutableRuntimeEvents(
state.claim.target.sessionId,
state.claim.target.runId,
);
if (!events.some(isTerminalRuntimeEvent)) {
claimOwnedUnsettledRunIds.add(state.claim.target.runId);
}
}
}
// Read once per recovered session, and only when a failure actually needs
// attributing, so healthy sessions pay nothing for the query.
let boundaryClosures: readonly SandboxBoundaryRequest[] | undefined;
const readBoundaryClosures = async (): Promise<readonly SandboxBoundaryRequest[]> => {
if (!boundaryClosures) {
boundaryClosures = this.deps.store.listSandboxBoundaryRestartClosures
? await recoverOr(
policy,
() => this.deps.store.listSandboxBoundaryRestartClosures!(sessionId),
[],
)
: [];
}
return boundaryClosures;
};
let recovered = false;
for (const run of runs) {
if (policy.kind === 'strict') {
await policy.stores.agentRunStore.readEventsForRecovery(sessionId, run.runId);
}
let inspected = await inspectAgentRunReadModel(
this.deps.runStore,
this.deps.runtimeEventStore,
{ sessionId, runId: run.runId, header: run },
);
if (inspected.sourceHealth.runtimeLedger === 'read_failed') {
if (policy.kind === 'strict') {
throw new Error(`RuntimeEvent ledger is unreadable for run ${run.runId}`);
}
continue;
}
if (
policy.kind === 'strict' &&
inspected.diagnostics.some(
(diagnostic) =>
diagnostic.code === 'operational_ledger_read_failed' ||
diagnostic.code === 'operational_event_corrupt',
)
) {
throw new Error(`AgentRun event ledger is unreadable for run ${run.runId}`);
}
if (
claimOwnedUnsettledRunIds.has(run.runId) &&
!inspected.runtimeEvents.some(isTerminalRuntimeEvent)
) {
// Every unresolved claim target belongs to the claim saga. This
// includes claim-only, deterministic repair-start, and live provider
// T1 states; generic app-restart repair must never write into them.
continue;
}
if (this.runtimeCommitSink) {
const interruptedOutcomes = buildInterruptedCodeModeOutcomeCommits(
inspected.runtimeEvents,
this.deps.now(),
run.toolMode ?? DEFAULT_TOOL_MODE,
);
let outcomeCommitFailed = false;
for (const outcome of interruptedOutcomes) {
const committed = await commitInterruptedOutcomeWithRetry(policy, () =>
this.runtimeCommitSink!.commitToolOutcome(outcome),
);
if (!committed) {
// Keep the run non-terminal so a later recovery pass can retry the
// missing outcome before any terminal repair seals the ledger.
outcomeCommitFailed = true;
} else {
recovered ||= committed.created;
}
}
if (outcomeCommitFailed) {
continue;
}
if (interruptedOutcomes.length > 0) {
inspected = await inspectAgentRunReadModel(
this.deps.runStore,
this.deps.runtimeEventStore,
{ sessionId, runId: run.runId, header: run },
);
}
}
const terminalLedger = classifyTerminalRuntimeLedger(run, inspected.runtimeEvents);
if (terminalLedger.kind === 'ambiguous') {
if (policy.kind === 'strict') {
throw new Error(`RuntimeEvent ledger has ambiguous terminal facts for run ${run.runId}`);
}
continue;
}
if (isTerminalRunStatus(run.status) && !inspected.terminalRuntimeFact) {
const repaired = await this.repairMissingTerminalFactOnce(sessionId, run.runId);
if (repaired) {
recovered = true;
} else if (policy.kind === 'strict') {
throw new Error(`Unable to repair the terminal RuntimeEvent fact for run ${run.runId}`);
}
continue;
}
const runtimeDecision = this.classifyRuntimeEventRecovery(inspected);
const classified = runtimeDecision ?? classifyAgentRunRecovery(run, inspected.events);
if (!classified) continue;
const decision =
classified.status === 'failed'
? attributeSandboxBoundaryRestartClosure(classified, await readBoundaryClosures())
: classified;
if (await this.applyAgentRunRecovery(sessionId, decision, inspected, policy)) {
recovered = true;
}
}
return { hasLedger: true, recovered };
}
private classifyRuntimeEventRecovery(
inspected: AgentRunInspectModel,
): AgentRunRecoveryDecision | undefined {
if (isTerminalRunStatus(inspected.header.status) || !inspected.terminalRuntimeFact)
return undefined;
return runtimeTerminalFactToRecoveryDecision(inspected.header, inspected.terminalRuntimeFact);
}
private async applyAgentRunRecovery(
sessionId: string,
decision: AgentRunRecoveryDecision,
inspected: AgentRunInspectModel,
policy: RecoveryPolicy = { kind: 'best_effort' },
): Promise<boolean> {
if (!this.deps.runStore || !this.deps.runtimeEventStore) return false;
const ts = this.deps.now();
const terminalLedger = classifyTerminalRuntimeLedger(inspected.header, inspected.runtimeEvents);
const existingTerminal =
inspected.terminalRuntimeFact?.terminalEvent ??
(terminalLedger.kind === 'incomplete_single_terminal'
? terminalLedger.terminalEvent
: undefined);
const status = existingTerminal
? (terminalRunStatusFromRuntimeEvent(existingTerminal) ?? decision.status)
: decision.status;
const failureClass =
status === 'failed' ? (decision.failureClass ?? 'app_restarted') : undefined;
const abortSource = status === 'cancelled' ? (decision.abortSource ?? 'unknown') : undefined;
const terminalEvent =
existingTerminal ??
buildRecoveredTerminalRuntimeEvent({
id: this.deps.newId(),
run: inspected.header,
status,
ts,
recoveryReason: diagnosticRecoveryReason(decision.diagnostic),
...(inspected.runtimeEvents[0]?.invocationId
? { invocationId: inspected.runtimeEvents[0].invocationId }
: {}),
...(failureClass ? { failureClass, message: failureClass } : {}),
...(abortSource ? { abortSource } : {}),
...(decision.diagnostic ? { diagnostic: decision.diagnostic } : {}),
});
try {
await commitTerminalRunWithRuntimeFact({
runStore: this.deps.runStore,
runtimeEventStore: this.deps.runtimeEventStore,
newId: this.deps.newId,
sessionId,
runId: decision.runId,
turnId: decision.turnId,
status,
ts,
terminalEvent,
...(failureClass ? { failureClass } : {}),
...(abortSource ? { abortSource } : {}),
runEventData: { recovered: true, ...decision.diagnostic },
existingEvents: inspected.events,
});
} catch (error) {
if (policy.kind === 'strict') throw error;
return false;
}
await recoverOr(
policy,
() =>
this.appendTerminalTurnStateIfNeeded(
sessionId,
inspected.header,
decision,
terminalTurnStatus(status),
{
ts,
...(failureClass ? { errorClass: failureClass } : {}),
...(abortSource ? { abortSource } : {}),
},
policy,
),
undefined,
);
return true;
}
private async appendTerminalTurnStateIfNeeded(
sessionId: string,
run: AgentRunHeader,
decision: AgentRunRecoveryDecision,
status: TurnRecord['status'],
options: { ts: number; errorClass?: string; abortSource?: string },
policy: RecoveryPolicy = { kind: 'best_effort' },
): Promise<void> {
if (!isSessionInlineRun(run)) return;
const messages = await recoverOr(
policy,
() => this.deps.store.readMessages(sessionId),
[] as StoredMessage[],
);
const latest = latestTurnState(messages, decision.turnId);
if (latest && isTerminalTurnStatus(latest.status) && latest.status === status) return;
await this.appendTurnState(sessionId, decision.turnId, status, decision.lineage, options);
}
}
function planAbandonmentNoteId(operationId: string): string {
return `plan-abandonment-${createHash('sha256').update(operationId).digest('hex')}`;
}
function resumeFeatureDisabledPlan(): SafeBoundaryContinuationPlan {
return {
disposition: 'park',
rejectionReasons: ['resume_feature_disabled'],
diagnostics: [
{
code: 'resume_feature_disabled',
message: 'safe-boundary resume is disabled by the host feature flag',
},
],
};
}
function continuationExecutionErrorClass(error: unknown): string {
if (
error instanceof Error &&
'code' in error &&
typeof (error as Error & { code?: unknown }).code === 'string'
) {
return (error as Error & { code: string }).code;
}
return error instanceof Error ? error.name : 'unknown';
}
type RecoveryPolicy = { kind: 'best_effort' } | { kind: 'strict'; stores: StrictRecoveryStores };
const MAX_BEST_EFFORT_OUTCOME_COMMIT_ATTEMPTS = 2;
function listSessionsForRecovery(
store: SessionStore,
policy: RecoveryPolicy,
): Promise<Array<SessionHeader | SessionSummary>> {
return policy.kind === 'strict' ? policy.stores.sessionStore.listForRecovery() : store.list();
}
async function recoverOr<T>(
policy: RecoveryPolicy,
operation: () => Promise<T>,
fallback: T,
): Promise<T> {
try {
return await operation();
} catch (error) {
if (policy.kind === 'strict') throw error;
return fallback;
}
}
async function commitInterruptedOutcomeWithRetry(
policy: RecoveryPolicy,
operation: () => Promise<RuntimeCommitResult>,
): Promise<RuntimeCommitResult | undefined> {
const attempts = policy.kind === 'strict' ? 1 : MAX_BEST_EFFORT_OUTCOME_COMMIT_ATTEMPTS;
for (let attempt = 0; attempt < attempts; attempt += 1) {
try {
return await operation();
} catch (error) {
if (policy.kind === 'strict') throw error;
}
}
return undefined;
}
function continuationRepairEventId(
kind: 'start' | 'terminal' | 'run-terminal',
claimId: string,
): string {
return `continuation-repair-${kind}-${createHash('sha256')
.update(`maka.continuation-repair.${kind}.v1\0${claimId}`)
.digest('hex')
.slice(0, 32)}`;
}
function buildContinuationRepairStartEvent(claim: ContinuationClaimV1): RuntimeEvent {
const source = claim.boundary.segments.at(-1)!;
return {
id: continuationRepairEventId('start', claim.claimId),
...claim.target,
ts: claim.claimedAt,
partial: false,
role: 'system',
author: 'system',
actions: {
continuationStart: {
protocol: 'continuation_start_v2',
provenance: 'claim_repair',
claimId: claim.claimId,
boundaryDigest: claim.boundaryDigest,
immediateSource: {
sessionId: source.identity.sessionId,
invocationId: source.identity.invocationId,
runId: source.identity.runId,
turnId: source.identity.turnId,
highWater: source.position.lastEventSeq,
prefixDigest: source.prefixDigest,
},
replayManifestDigest: claim.boundary.manifestDigest,
providerProjectionVersion: claim.providerProjectionVersion,
providerReplayDigest: claim.providerReplayDigest,
},
},
};
}
function assertClaimOwnsHostedLinkedChildAdmission(
input: {
sessionId: string;
turnId: string;
runId: string;
execution: Exclude<
RootExecutionDescriptor,
{ kind: 'external_message' } | { kind: 'automation' }
>;
},
claim: ContinuationClaimV1,
): void {
if (
input.execution.kind !== 'linked_child_resume' &&
input.execution.kind !== 'linked_child_provider_retry'
) {
throw new Error('Only linked child retry or resume admission can use a continuation claim');
}
if (
claim.target.sessionId !== input.sessionId ||
claim.target.runId !== input.runId ||
claim.target.turnId !== input.turnId
) {
throw new Error('Linked child admission conflicts with its continuation claim target');
}
const header = claim.targetRunHeader;
const source = claim.boundary.segments.at(-1)!;
const continuationSource = header.continuationSource;
const continuationSourceV2 =
continuationSource !== undefined &&
'protocol' in continuationSource &&
continuationSource.protocol === 'continuation_source_v2'
? continuationSource
: undefined;
if (
header.sessionId !== claim.target.sessionId ||
header.invocationId !== claim.target.invocationId ||
header.runId !== claim.target.runId ||
header.turnId !== claim.target.turnId ||
header.status !== 'created' ||
header.agentId !== input.execution.agentId ||
header.agentName !== input.execution.agentName ||
source.identity.sessionId !== input.sessionId ||
source.identity.runId !== input.execution.sourceRunId ||
!continuationSourceV2 ||
continuationSourceV2.claimId !== claim.claimId ||
continuationSourceV2.boundaryDigest !== claim.boundaryDigest ||
continuationSourceV2.sourceRunId !== input.execution.sourceRunId
) {
throw new Error('Linked child admission continuation claim identity is inconsistent');
}
if (
input.execution.kind === 'linked_child_resume'
? header.resumedFromRunId !== input.execution.sourceRunId ||
header.retriedFromRunId !== undefined
: header.retriedFromRunId !== input.execution.sourceRunId ||
header.resumedFromRunId !== undefined
) {
throw new Error('Linked child admission continuation lineage is inconsistent');
}
}
async function isProvenRecoveredContinuationAbandonment(
authority: RuntimeContinuationAuthorityStore | undefined,
run: AgentRunHeader,
): Promise<boolean> {
const continuationSource = run.continuationSource;
const continuationSourceV2 =
continuationSource !== undefined &&
'protocol' in continuationSource &&
continuationSource.protocol === 'continuation_source_v2'
? continuationSource
: undefined;
if (
!authority ||
!continuationSourceV2 ||
run.failureClass !== 'continuation_abandoned_before_provider_dispatch'
) {
return false;
}
let state: ContinuationClaimStateV1 | undefined;
let events: RuntimeEvent[];
try {
state = await authority.readContinuationClaimStateByBoundary(
continuationSourceV2.boundaryDigest,
);
events = await authority.readImmutableRuntimeEvents(run.sessionId, run.runId);
} catch {
return false;
}
if (!state) return false;
const { claim } = state;
const expectedStart = buildContinuationRepairStartEvent(claim);
const expectedTerminal = buildRecoveredTerminalRuntimeEvent({
id: continuationRepairEventId('terminal', claim.claimId),
run: claim.targetRunHeader,
status: 'failed',
ts: Math.max(expectedStart.ts + 1, claim.claimedAt + 1),
recoveryReason: 'continuation_abandoned_before_provider_dispatch',
invocationId: claim.target.invocationId,
failureClass: 'continuation_abandoned_before_provider_dispatch',
message: 'continuation_abandoned_before_provider_dispatch',
});
const terminalFact = classifyRuntimeEventTerminalFact(run, events).fact;
return (
claim.claimId === continuationSourceV2.claimId &&
claim.target.sessionId === run.sessionId &&
claim.target.invocationId === run.invocationId &&
claim.target.runId === run.runId &&
claim.target.turnId === run.turnId &&
claimTargetRunHeaderIsCompatible(run, claim.targetRunHeader) &&
state.startEventId === expectedStart.id &&
events.length === 2 &&
isDeepStrictEqual(events[0], expectedStart) &&
isDeepStrictEqual(events[1], expectedTerminal) &&
terminalFact?.terminalEvent.id === expectedTerminal.id &&
terminalRunHeaderMatchesFact(run, terminalFact)
);
}
async function readImmutableRuntimeEventsOrEmpty(
authority: RuntimeContinuationAuthorityStore,
sessionId: string,
runId: string,
): Promise<RuntimeEvent[]> {
return authority.readImmutableRuntimeEvents(sessionId, runId);
}
function claimTargetRunHeaderIsCompatible(
actual: AgentRunHeader,
expected: AgentRunHeader,
): boolean {
const immutable = (header: AgentRunHeader) => {
const {
status: _status,
updatedAt: _updatedAt,
completedAt: _completedAt,
failureClass: _failureClass,
failureMessage: _failureMessage,
abortSource: _abortSource,
traceWriteError: _traceWriteError,
...rest
} = header;
return rest;
};
return isDeepStrictEqual(immutable(actual), immutable(expected));
}
function isMissingRunError(error: unknown): boolean {
return (
isNotFoundError(error) ||
(error instanceof Error && /unknown run|run does not exist|missing run/i.test(error.message))
);
}
// ============================================================================
// Helpers
// ============================================================================
export function headerToSummary(h: SessionHeader): SessionSummary {
const summary: SessionSummary = {
id: h.id,
cwd: h.cwd,
...(h.projectId !== undefined ? { projectId: h.projectId } : {}),
name: h.name === 'New Session' ? DEFAULT_SESSION_NAME : h.name,
isFlagged: h.isFlagged,
isArchived: h.isArchived,
labels: h.labels,
hasUnread: h.hasUnread,
status: h.status,
...(h.blockedReason ? { blockedReason: h.blockedReason } : {}),
...(h.statusUpdatedAt !== undefined ? { statusUpdatedAt: h.statusUpdatedAt } : {}),
...(h.parentSessionId ? { parentSessionId: h.parentSessionId } : {}),
...(h.branchOfTurnId ? { branchOfTurnId: h.branchOfTurnId } : {}),
...(h.subagentParent ? { subagentParent: h.subagentParent } : {}),
...(h.subagentRuntime
? { subagentRuntime: subagentSessionRuntimeSummary(h.subagentRuntime) }
: {}),
...(h.subagentWorkspace ? { subagentWorkspace: h.subagentWorkspace } : {}),
...(h.revisionRootSessionId ? { revisionRootSessionId: h.revisionRootSessionId } : {}),
...(h.revisionParentSessionId ? { revisionParentSessionId: h.revisionParentSessionId } : {}),
...(h.revisionOfTurnId ? { revisionOfTurnId: h.revisionOfTurnId } : {}),
...(h.revisionIndex !== undefined ? { revisionIndex: h.revisionIndex } : {}),
...(h.revisionState ? { revisionState: h.revisionState } : {}),
backend: h.backend,
llmConnectionSlug: h.llmConnectionSlug,
connectionLocked: h.connectionLocked,
model: h.model,
permissionMode: h.permissionMode ?? 'ask',
collaborationMode: h.collaborationMode ?? 'agent',
orchestrationMode: h.orchestrationMode ?? 'default',
};
if (h.thinkingLevel !== undefined) summary.thinkingLevel = h.thinkingLevel;
if (h.lastMessageAt !== undefined) {
summary.lastMessageAt = h.lastMessageAt;
}
return summary;
}
function isNotFoundError(error: unknown): error is NodeJS.ErrnoException {
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
}
function sameSubagentWorkspace(
left: SubagentWorkspaceBinding | undefined,
right: SubagentWorkspaceBinding | undefined,
): boolean {
if (!left || !right) return left === right;
return (
left.schemaVersion === right.schemaVersion &&
left.kind === right.kind &&
left.leaseId === right.leaseId &&
left.gitCommonDir === right.gitCommonDir &&
left.worktreePath === right.worktreePath &&
left.branch === right.branch &&
left.baseCommit === right.baseCommit
);
}
function childSessionSpawnKey(
parentSessionId: string,
input: Pick<SpawnChildSessionInput, 'spawnedBy' | 'swarm'>,
): string {
return JSON.stringify([
1,
parentSessionId,
input.spawnedBy.parentRunId,
input.spawnedBy.toolCallId,
input.swarm?.swarmId ?? null,
input.swarm?.itemId ?? null,
]);
}
function childSessionRequestFingerprint(
parentSessionId: string,
input: Pick<
ResolvedSpawnChildSessionInput,
'spawnedBy' | 'agentProfile' | 'prompt' | 'swarm' | 'resolvedPreset'
>,
): string {
const payload = input.resolvedPreset
? [
2,
parentSessionId,
input.spawnedBy.parentRunId,
input.spawnedBy.parentTurnId,
input.spawnedBy.toolCallId,
input.agentProfile,
input.resolvedPreset,
input.prompt,
input.swarm?.swarmId ?? null,
input.swarm?.itemId ?? null,
]
: [
1,
parentSessionId,
input.spawnedBy.parentRunId,
input.spawnedBy.parentTurnId,
input.spawnedBy.toolCallId,
input.agentProfile,
input.prompt,
input.swarm?.swarmId ?? null,
input.swarm?.itemId ?? null,
];
return createHash('sha256').update(JSON.stringify(payload)).digest('hex');
}
function claimedAgentGraphIntentRequestFingerprint(
input: Pick<ResolvedClaimedAgentGraphIntentInput, 'claim' | 'prompt'>,
): string {
return createHash('sha256')
.update(JSON.stringify([1, input.claim, input.prompt]))
.digest('hex');
}
function assertAgentGraphIntentExecutionMatchesClaim(
claim: AgentGraphIntentClaim,
intent: AgentGraphRunnableIntent,
prompt: string,
): void {
if (
intent.graphId !== claim.graphId ||
intent.intentId !== claim.intentId ||
intent.readinessContextFingerprint !== claim.readinessContextFingerprint ||
intent.operatorId !== claim.targetOperatorId ||
intent.targetSessionId !== claim.targetSessionId ||
fingerprintAgentGraphRunnableIntent({
intent,
executionInput: { prompt },
}) !== claim.intentFingerprint
) {
throw new Error('Claimed graph intent execution does not match its durable claim');
}
}
function claimedAgentGraphIntentResult(
claim: AgentGraphIntentClaim,
result: SpawnChildSessionResult,
): ClaimedAgentGraphIntentResult {
return {
claimId: claim.claimId,
graphId: claim.graphId,
intentId: claim.intentId,
operatorId: claim.targetOperatorId,
...result,
};
}
export function changesBackendConfig(patch: Partial<SessionHeader>): boolean {
return (
'backend' in patch ||
'llmConnectionSlug' in patch ||
'model' in patch ||
'thinkingLevel' in patch ||
'cwd' in patch ||
'collaborationMode' in patch ||
// AiSdkBackend snapshots the header at construction and ToolRuntime
// reads `header.permissionMode` at every decision, so a mode change
// that does not rebuild the backend is persisted but NOT enforced —
// the live session keeps deciding with the old mode.
//
// `setPermissionMode` disposes the backend for exactly this reason.
// `updateSession` did not, so every other path that lowers a mode was
// advisory: notably the bot-incoming guard re-pinning a conversation
// to `explore`, which left an already-built `execute`/`bypass` backend
// serving the remote sender.
'permissionMode' in patch
);
}
function executionBoundaryMatchesPermissionMode(
boundary: ExecutionBoundary,
mode: PermissionMode,
): boolean {
if (mode === 'bypass') return boundary.kind === 'bypass';
if (boundary.kind !== 'managed') return false;
return mode === 'explore'
? boundary.profile.name === 'read-only'
: boundary.profile.name !== 'read-only';
}
function narrowsExecutionAuthority(
boundary: ExecutionBoundary,
nextPermissionMode: PermissionMode,
): boolean {
if (nextPermissionMode === 'bypass') return false;
if (boundary.kind !== 'managed') return true;
return nextPermissionMode === 'explore' && boundary.profile.name !== 'read-only';
}
function agentRunStatusForSpawnResult(
status: AgentRunHeader['status'],
): SpawnChildAgentResult['status'] {
if (status === 'waiting_for_user') return 'waiting_for_user';
if (status === 'cancelled') return 'cancelled';
if (status === 'failed') return 'failed';
if (status === 'running' || status === 'created') return 'running';
return 'completed';
}
function isUnsafeChildResumeDiagnostic(code: string): boolean {
return (
code === 'unmatched_tool_call' ||
code === 'unmatched_tool_result' ||
code === 'tool_id_mismatch' ||
code === 'unsupported_role' ||
code === 'unsupported_content'
);
}
function trimSummary(text: string): string {
const trimmed = text.trim();
return trimmed.length <= CHILD_AGENT_SUMMARY_MAX_CHARS
? trimmed
: `${trimmed.slice(0, CHILD_AGENT_SUMMARY_MAX_CHARS - 1)}…`;
}
class ChildAgentSummaryAccumulator {
eventCount = 0;
failureClass: string | undefined;
private terminalStatus: SpawnChildAgentResult['status'] | undefined;
private lastTextComplete = '';
private textDeltaTail = '';
private textDeltaTruncated = false;
private lastError = '';
add(event: SessionEvent): void {
this.eventCount += 1;
switch (event.type) {
case 'text_complete':
this.lastTextComplete = trimSummary(event.text);
break;
case 'text_delta':
this.appendTextDelta(event.text);
break;
case 'error':
this.terminalStatus = 'failed';
this.lastError = trimSummary(event.message);
break;
case 'abort':
this.terminalStatus = 'cancelled';
break;
case 'complete':
this.failureClass = failureClassFromCompleteStopReason(event.stopReason);
if (this.failureClass) this.terminalStatus = 'failed';
else if (event.stopReason === 'user_stop') this.terminalStatus = 'cancelled';
else this.terminalStatus = 'completed';
break;
}
}
status(aborted: boolean): SpawnChildAgentResult['status'] {
if (aborted) return 'cancelled';
return this.terminalStatus ?? 'running';
}
text(): string {
if (this.lastTextComplete.trim()) return this.lastTextComplete;
if (this.textDeltaTail.trim()) {
return this.textDeltaTruncated
? `…${this.textDeltaTail.slice(1)}`
: this.textDeltaTail.trim();
}
return this.lastError;
}
private appendTextDelta(text: string): void {
this.textDeltaTail += text;
if (this.textDeltaTail.length <= CHILD_AGENT_SUMMARY_MAX_CHARS) return;
this.textDeltaTruncated = true;
this.textDeltaTail = this.textDeltaTail.slice(-CHILD_AGENT_SUMMARY_MAX_CHARS);
}
}
interface InterruptedTurnRecovery {
turnId: string;
errorClass: string;
lineage: Partial<
Pick<
UserMessageInput,
| 'parentTurnId'
| 'retriedFromTurnId'
| 'regeneratedFromTurnId'
| 'branchOfTurnId'
| 'parentSessionId'
>
>;
}
function hasRevisionUserMessage(messages: readonly StoredMessage[]): boolean {
let boundary = -1;
for (let index = 0; index < messages.length; index += 1) {
const message = messages[index]!;
if (
message.type === 'system_note' &&
message.kind === 'session_start' &&
message.data &&
typeof message.data === 'object' &&
'revisionRootSessionId' in message.data
) {
boundary = index;
}
}
return boundary >= 0 && messages.slice(boundary + 1).some((message) => message.type === 'user');
}
function interruptedTurnRecoveries(messages: readonly StoredMessage[]): InterruptedTurnRecovery[] {
const byTurn = new Map<
string,
{
hasAssistant: boolean;
states: Array<Extract<StoredMessage, { type: 'turn_state' }>>;
}
>();
for (const message of messages) {
const turnId = (message as { turnId?: string }).turnId;
if (!turnId) continue;
const bucket = byTurn.get(turnId) ?? { hasAssistant: false, states: [] };
if (message.type === 'assistant') bucket.hasAssistant = true;
if (message.type === 'turn_state') bucket.states.push(message);
byTurn.set(turnId, bucket);
}
const recoveries: InterruptedTurnRecovery[] = [];
for (const [turnId, bucket] of byTurn) {
const latest = bucket.states.at(-1);
if (!latest) continue;
if (latest.status === 'running') {
recoveries.push({
turnId,
errorClass: 'app_restarted',
lineage: turnStateLineage(latest),
});
continue;
}
const failed = [...bucket.states].reverse().find((state) => state.status === 'failed');
if (latest.status === 'completed' && !bucket.hasAssistant && failed) {
recoveries.push({
turnId,
errorClass: failed.errorClass ?? 'unknown',
lineage: turnStateLineage(failed),
});
}
}
return recoveries;
}
function turnStateLineage(
state: Extract<StoredMessage, { type: 'turn_state' }>,
): Partial<
Pick<
UserMessageInput,
| 'parentTurnId'
| 'retriedFromTurnId'
| 'regeneratedFromTurnId'
| 'branchOfTurnId'
| 'parentSessionId'
>
> {
return {
...(state.parentTurnId ? { parentTurnId: state.parentTurnId } : {}),
...(state.retriedFromTurnId ? { retriedFromTurnId: state.retriedFromTurnId } : {}),
...(state.regeneratedFromTurnId ? { regeneratedFromTurnId: state.regeneratedFromTurnId } : {}),
...(state.branchOfTurnId ? { branchOfTurnId: state.branchOfTurnId } : {}),
...(state.parentSessionId ? { parentSessionId: state.parentSessionId } : {}),
};
}
function isTerminalRunStatus(status: AgentRunHeader['status']): boolean {
return status === 'completed' || status === 'failed' || status === 'cancelled';
}
function isTerminalTurnStatus(status: TurnRecord['status']): boolean {
return status === 'completed' || status === 'failed' || status === 'aborted';
}
function terminalTurnStatus(status: AgentRunRecoveryDecision['status']): TurnRecord['status'] {
if (status === 'cancelled') return 'aborted';
return status;
}
function diagnosticRecoveryReason(diagnostic: Record<string, unknown> | undefined): string {
const recoveryReason = diagnostic?.recoveryReason;
return typeof recoveryReason === 'string' && recoveryReason.length > 0
? recoveryReason
: 'agent_run_recovery';
}
function latestTurnState(
messages: readonly StoredMessage[],
turnId: string,
): Extract<StoredMessage, { type: 'turn_state' }> | undefined {
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];
if (message?.type === 'turn_state' && message.turnId === turnId) return message;
}
return undefined;
}
function runtimeTerminalFactToRecoveryDecision(
header: AgentRunHeader,
fact: RuntimeEventTerminalFact,
): AgentRunRecoveryDecision {
return {
runId: fact.runId,
turnId: fact.turnId,
status: fact.runStatus,
...(fact.failureClass ? { failureClass: fact.failureClass } : {}),
...(fact.abortSource ? { abortSource: fact.abortSource } : {}),
diagnostic: {
recoveryReason: 'runtime_event_terminal_fact',
runtimeEventId: fact.terminalEvent.id,
runtimeEventStatus: fact.terminalEvent.status,
},
lineage: headerLineage(header),
};
}
function headerLineage(header: AgentRunHeader): AgentRunRecoveryDecision['lineage'] {
return {
...(header.parentRunId ? { parentRunId: header.parentRunId } : {}),
...(header.parentTurnId ? { parentTurnId: header.parentTurnId } : {}),
...(header.retriedFromTurnId ? { retriedFromTurnId: header.retriedFromTurnId } : {}),
...(header.regeneratedFromTurnId
? { regeneratedFromTurnId: header.regeneratedFromTurnId }
: {}),
...(header.branchOfTurnId ? { branchOfTurnId: header.branchOfTurnId } : {}),
...(header.parentSessionId ? { parentSessionId: header.parentSessionId } : {}),
};
}
function normalizeAgentOutputMaxEvents(value: number | undefined): number {
if (typeof value !== 'number' || !Number.isFinite(value)) return 20;
return Math.min(100, Math.max(1, Math.floor(value)));
}
const DEFAULT_AGENT_OUTPUT_MAX_BYTES = 32 * 1024;
const MAX_AGENT_OUTPUT_MAX_BYTES = 128 * 1024;
function normalizeAgentOutputMaxBytes(value: number | undefined): number {
if (typeof value !== 'number' || !Number.isFinite(value)) {
return DEFAULT_AGENT_OUTPUT_MAX_BYTES;
}
return Math.min(MAX_AGENT_OUTPUT_MAX_BYTES, Math.max(1024, Math.floor(value)));
}
function buildAgentOutputCommittedResult(input: {
header: AgentRunHeader;
runtimeEvents: readonly RuntimeEvent[];
artifacts: readonly ArtifactRecord[];
maxArtifacts: number;
maxBytes: number;
graph?: NonNullable<SubagentSessionParent['graph']>;
}): {
result: AgentOutputCommittedResult;
projectedBytes: number;
truncated: boolean;
} {
const finalTextEvent = findLastMatching(
input.runtimeEvents,
(event) =>
event.role === 'model' &&
event.partial !== true &&
event.content?.kind === 'text' &&
event.content.text.trim().length > 0,
);
const graphRecords =
input.graph && input.runtimeEvents.length > 0
? projectAgentGraphRecords({
graphId: input.graph.graphId,
streams: [
{
operator: {
operatorId: input.graph.operatorId,
sessionId: input.header.sessionId,
},
run: input.header,
events: input.runtimeEvents,
},
],
}).records
: [];
const graphRecordByRuntimeEventId = new Map(
graphRecords.map((record) => [record.source.runtimeEventId, record]),
);
const terminalRecord = findLastMatching(graphRecords, (record) =>
record.supervisorSignals.some((signal) => signal.kind === 'terminal'),
);
const outputRecord = finalTextEvent
? graphRecordByRuntimeEventId.get(finalTextEvent.id)
: undefined;
let artifactIds = tail(
input.artifacts.map((artifact) => artifact.id),
input.maxArtifacts,
);
const base = (): AgentOutputCommittedResult => ({
schemaVersion: 1,
status: input.header.status,
...(input.graph ? { graph: { ...input.graph } } : {}),
...(outputRecord || terminalRecord
? { resultRecordId: (outputRecord ?? terminalRecord)!.recordId }
: {}),
...(terminalRecord ? { terminalRecordId: terminalRecord.recordId } : {}),
...(finalTextEvent ? { sourceRuntimeEventId: finalTextEvent.id } : {}),
...(terminalRecord ? { terminalRuntimeEventId: terminalRecord.source.runtimeEventId } : {}),
textTruncated: false,
artifactIds,
omittedArtifactIds: Math.max(0, input.artifacts.length - artifactIds.length),
...(input.header.failureClass ? { failureClass: input.header.failureClass } : {}),
});
while (artifactIds.length > 0 && serializedBytes(base()) > input.maxBytes) {
artifactIds = artifactIds.slice(1);
}
const text = finalTextEvent?.content?.kind === 'text' ? finalTextEvent.content.text : undefined;
const withoutText = base();
if (text === undefined) {
return {
result: withoutText,
projectedBytes: serializedBytes(withoutText),
truncated: withoutText.omittedArtifactIds > 0,
};
}
const fullResult = { ...withoutText, text };
const fullBytes = serializedBytes(fullResult);
if (fullBytes <= input.maxBytes) {
return {
result: fullResult,
projectedBytes: fullBytes,
truncated: withoutText.omittedArtifactIds > 0,
};
}
const codePoints = Array.from(text);
let low = 0;
let high = codePoints.length;
let best: AgentOutputCommittedResult = { ...withoutText, textTruncated: true };
while (low <= high) {
const middle = Math.floor((low + high) / 2);
const candidate: AgentOutputCommittedResult = {
...withoutText,
text: `${codePoints.slice(0, middle).join('')}…`,
textTruncated: true,
};
if (serializedBytes(candidate) <= input.maxBytes) {
best = candidate;
low = middle + 1;
} else {
high = middle - 1;
}
}
return {
result: best,
projectedBytes: serializedBytes(best),
truncated: true,
};
}
function serializedBytes(value: unknown): number {
return Buffer.byteLength(JSON.stringify(value), 'utf8');
}
function findLastMatching<T>(items: readonly T[], predicate: (item: T) => boolean): T | undefined {
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index]!;
if (predicate(item)) return item;
}
return undefined;
}
function boundAgentOutputCollections(
input: {
events: AgentRunEvent[];
runtimeEvents: RuntimeEvent[];
diagnostics: AgentRunInspectModel['diagnostics'];
artifacts: ArtifactRecord[];
},
maxBytes: number,
): {
events: AgentRunEvent[];
runtimeEvents: RuntimeEvent[];
diagnostics: AgentRunInspectModel['diagnostics'];
artifacts: ArtifactRecord[];
projectedBytes: number;
truncated: boolean;
} {
let remaining = maxBytes;
let projectedBytes = 0;
let truncated = false;
const takeBoundedTail = <T>(items: readonly T[]): T[] => {
const selected: T[] = [];
for (let index = items.length - 1; index >= 0; index -= 1) {
const item = items[index]!;
const bytes = serializedBytes(item);
if (bytes > remaining) {
truncated = true;
break;
}
selected.push(item);
projectedBytes += bytes;
remaining -= bytes;
}
selected.reverse();
return selected;
};
// RuntimeEvents are the semantic child transcript and therefore receive the
// budget first. AgentRun events and diagnostics remain available through
// explicit views without duplicating an unbounded second event stream.
const runtimeEvents = takeBoundedTail(input.runtimeEvents);
const events = takeBoundedTail(input.events);
const diagnostics = takeBoundedTail(input.diagnostics);
const artifacts = takeBoundedTail(input.artifacts);
return { events, runtimeEvents, diagnostics, artifacts, projectedBytes, truncated };
}
function tail<T>(items: readonly T[], max: number): T[] {
if (items.length <= max) return [...items];
return items.slice(items.length - max);
}
function shellRunBashToolCallIds(messages: readonly StoredMessage[]): Set<string> {
return new Set(
messages.flatMap((message) =>
message.type === 'tool_call' && message.toolName === 'Bash' ? [message.id] : [],
),
);
}
function throwIfChildExecutionAborted(signal: AbortSignal | undefined, message: string): void {
if (!signal?.aborted) return;
throw Object.assign(new Error(message), { name: 'AbortError' });
}
// Re-export the suppressed-unused types so this file is the canonical home
// for them. (Avoids TS "imported but unused" warnings.)
export type {
TextDeltaEvent,
CompleteEvent,
ErrorEvent,
AbortEvent,
PermissionRequestEvent,
PermissionDecisionAckEvent,
PermissionDecisionMessage,
};