| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| /** |
| * 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 type { WorkHubActionReceipt } from '@maka/core/workhub-action-result'; |
| 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, |
| ShellRunUpdate, |
| MessageContent, |
| } from '@maka/core/events'; |
| import { messageContentsEqual, normalizeMessageContent } from '@maka/core/events'; |
| import type { |
| SessionHeader, |
| SessionHeaderPatch, |
| SessionBlockedReason, |
| SessionStatus, |
| SessionSummary, |
| StoredMessage, |
| RuntimeSystemNoteKind, |
| SubagentSessionParent, |
| TurnRecord, |
| UserMessage, |
| AssistantMessage, |
| PermissionDecisionMessage, |
| PersistedBackendKind, |
| } from '@maka/core/session'; |
| import type { |
| CreateSessionInput, |
| RegenerateTurnInput, |
| UserMessageInput, |
| SessionListFilter, |
| } from '@maka/core/runtime-inputs'; |
| import type { SandboxBoundaryResponse } from '@maka/core/sandbox-boundary'; |
| import type { UserQuestionResponse } from '@maka/core/user-question'; |
| import type { PermissionMode } from '@maka/core/permission'; |
| import { isCanonicalReadOnlyPermissionProfile } from '@maka/core/permission-profile'; |
| import { DEFAULT_TOOL_MODE } from '@maka/core/tool-mode'; |
| import type { |
| CreateSandboxBoundaryRequest, |
| ExecutionBoundary, |
| SandboxBoundaryRequest, |
| SandboxBoundarySettlement, |
| SettleSandboxBoundaryRequest, |
| } from '@maka/core/sandbox-boundary'; |
| import type { CollaborationMode } from '@maka/core/collaboration'; |
| import type { OrchestrationMode } from '@maka/core/orchestration'; |
| import { |
| PLAN_USER_ABANDON_REASON, |
| PLAN_USER_CANCEL_REASON, |
| PlanConflictError, |
| type ApprovePlanProposalInput, |
| type PlanMutationResult, |
| type PlanSessionState, |
| type PlanStore, |
| } from '@maka/core/plan'; |
| import { DEFAULT_SESSION_NAME } from '@maka/core/session-name'; |
| import { DEEP_RESEARCH_SESSION_LABEL, isDeepResearchSession } from '@maka/core/deep-research'; |
| import { |
| SUBAGENT_SESSION_RUNTIME_SCHEMA_VERSION, |
| SUBAGENT_SESSION_SPAWN_SCHEMA_VERSION, |
| childSessionsForParent, |
| subagentSessionRuntimeSummary, |
| } from '@maka/core/session'; |
| import { decodeAgentGraphIntentClaim } from '@maka/core/agent-graph-control'; |
| import { executionBoundaryContains } from '@maka/core/sandbox-boundary'; |
| import { failureClassFromCompleteStopReason } from '@maka/core/events'; |
| import { isActiveShellRunStatus } from '@maka/core/shell-run'; |
| import { isTerminalRuntimeEvent } from '@maka/core/runtime-event'; |
| import { runtimeHandoffPause, type RuntimeHandoffIntent } from '@maka/core/runtime-handoff'; |
| import { readLogicalRuntimeExecutionForRun } from '@maka/core/runtime-logical-execution'; |
| import { |
| buildInvocationOpenedEvent, |
| isSessionInlineInvocation, |
| runtimeInvocationOutcome, |
| type RootExecutionDescriptor, |
| type RuntimeInvocationRecord, |
| } from '@maka/core/runtime-invocation'; |
| import type { |
| AgentGraphIntentClaim, |
| AgentGraphIntentClaimStore, |
| } from '@maka/core/agent-graph-control'; |
| import type { |
| AgentGraphOperatorProvisionRequest, |
| AgentGraphOperatorProvisionResult, |
| AgentGraphProvisionedEdge, |
| } from '@maka/core/agent-graph-topology'; |
| import type { AgentGraphScheduleUpdateSource } from '@maka/core/agent-graph-schedule'; |
| import type { AgentRunEvent, AgentRunStore } from '@maka/core/agent-run'; |
| import { isArtifactChildResultOutput, type ArtifactRecord } from '@maka/core/artifacts'; |
| import { invocationMatchesClaimTarget } from '@maka/core/runtime-boundary'; |
| import type { ContinuationClaimV1 } from '@maka/core/runtime-boundary'; |
| import { |
| readRunInvocation, |
| type RuntimeEventStore, |
| type RuntimeContinuationAuthorityStore, |
| } from '@maka/core/runtime-event-store'; |
| import type { |
| RuntimeEvent, |
| RuntimeEventInvocationOpenedContent, |
| RuntimeInvocationConfiguration, |
| RuntimeInvocationLineage, |
| RuntimeInvocationRootAuthority, |
| ToolBoundaryProtocol, |
| } from '@maka/core/runtime-event'; |
| import type { |
| RequestCompositionSnapshotInput, |
| RunCompositionSnapshot, |
| } from '@maka/core/run-composition'; |
| import type { |
| SubagentWorkspaceBinding, |
| SubagentWorktreeExecutor, |
| } from '@maka/core/subagent-workspace'; |
| import type { SubagentPreset } from '@maka/core/subagent-settings'; |
| import type { ResolvedSubagentPreset } from './configured-subagent-catalog.js'; |
| import { AGENT_GRAPH_OPERATOR_PROVISION_SCHEMA_VERSION } from '@maka/core/agent-graph-topology'; |
| import { |
| runtimeInvocationFailureClass, |
| type RuntimeEventTerminalFact, |
| } from './runtime-event-read-model.js'; |
| import { |
| RuntimeReadModel, |
| RuntimeReadModelError, |
| type RuntimeReadModelSessionView, |
| } from './runtime-read-model.js'; |
| import { inspectAgentRunReadModel, type AgentRunInspectModel } from './agent-run-inspect.js'; |
| import { isTranscriptLedgerInvocation, RuntimeLedgerRepair } from './runtime-ledger-repair.js'; |
| import { |
| buildRecoveredTerminalRuntimeEvent, |
| classifyTerminalRuntimeLedger, |
| commitTerminalRunWithRuntimeFact, |
| terminalRunStatusFromRuntimeEvent, |
| } from './terminal-run-commit.js'; |
| |
| import type { AgentBackend, BackendStopMode } from '@maka/core/backend-types'; |
| import type { MakaTool } from './tool-runtime.js'; |
| import type { TurnShellPlan } from './shell-detect.js'; |
| import type { RunTraceRecorder } from './run-trace.js'; |
| import type { ModelCallAttempt } from '@maka/core/model-call-attempt'; |
| import { readLatestContextDiagnostics, type ContextDiagnostics } from './context-diagnostics.js'; |
| import type { ModelCallCommit } from '@maka/core/agent-run'; |
| import type { ShellRunProcessManager } from './shell-run-manager.js'; |
| import type { HistoryCompactCheckpoint } from './history-compact-checkpoint.js'; |
| import type { ModelProjectionTransition } from '@maka/core/model-projection-transition'; |
| import type { LoadedModelProjectionTransitions } from './model-projection-transition-ledger.js'; |
| import type { 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 { |
| 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 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, type RunLifecycleStatus } from './session-projection-helpers.js'; |
| import { |
| assertAgentDefinitionRunnable, |
| buildToolsForAgentDefinition, |
| listBuiltinAgentDefinitions, |
| requireBuiltinAgentDefinition, |
| requireBuiltinAgentDefinitionByProfile, |
| AGENT_WORKSPACE_WORKTREE, |
| type AgentProfile, |
| type AgentDefinition, |
| type AgentDefinitionListItem, |
| type SubagentPresetListItem, |
| } from './agent-catalog.js'; |
| import { stableHash } from './request-shape.js'; |
| import type { SubagentExecutionRef } from './subagent-execution.js'; |
| import { |
| 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 type StopSessionInput = |
| | { |
| source?: 'stop_button' | 'graph_supervisor'; |
| workHubActionId?: never; |
| mode?: BackendStopMode; |
| } |
| | { |
| source: 'workhub_direct_stop'; |
| workHubActionId: string; |
| mode?: BackendStopMode; |
| }; |
| |
| export { |
| normalizeStopSessionSource, |
| workHubDirectStopAbortSource, |
| } from './session-projection-helpers.js'; |
| |
| export type CompactSessionInput = |
| | { |
| turnId?: string; |
| hostedRoot?: never; |
| } |
| | { |
| turnId: string; |
| hostedRoot: { |
| runId: string; |
| onRunStarted?: () => void | Promise<void>; |
| }; |
| }; |
| |
| export type PlanSafeBoundaryContinuationInput = Omit< |
| RuntimeContinuationPlannerInput, |
| 'sessionId' | 'admissionRoute' |
| >; |
| |
| export interface PlanAuthoritativeSafeBoundaryContinuationInput { |
| purpose?: 'handoff'; |
| sourceRunId: string; |
| expectedRuntimeEventHighWater?: number; |
| } |
| |
| 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?: ResolvedSubagentPreset; |
| }; |
| |
| export interface SpawnChildSessionResult { |
| childSessionId: string; |
| agentId: string; |
| agentName: string; |
| turnId: string; |
| runId: string; |
| profile: 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; |
| } |
| |
| 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; |
| }; |
| |
| const CHILD_AGENT_SUMMARY_MAX_CHARS = 4_000; |
| |
| export interface AgentListItem { |
| runId: string; |
| turnId: string; |
| parentRunId: string; |
| agentId?: string; |
| agentName?: string; |
| status: RunLifecycleStatus; |
| permissionMode: 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: RunLifecycleStatus; |
| 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: RunLifecycleStatus; |
| 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; |
| invocation: RuntimeInvocationRecord; |
| 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 llmConnectionId?: string; |
| 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 clearConnectionBlock: boolean; |
| readonly permissionModeOnly: boolean; |
| 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[]>; |
| hasExplicitSandboxBoundaryDenial?( |
| identities: readonly { sessionId: string; runId: string; turnId: string }[], |
| ): Promise<boolean>; |
| 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>; |
| /** One forward page of the legacy rows the transcript converter lifts. */ |
| readMessagesAfter( |
| sessionId: string, |
| request: { afterSequence?: number; maxMessages: number; maxStoredBytes: number }, |
| ): Promise<{ |
| records: readonly { sequence: number; message: StoredMessage }[]; |
| highWaterSequence: number | null; |
| }>; |
| /** Commit the Session-list facts a durable message carries. */ |
| commitMessageCatalogProjection?( |
| sessionId: string, |
| message: UserMessage | AssistantMessage, |
| ): Promise<void>; |
| updateHeader(sessionId: string, patch: SessionHeaderPatch): Promise<SessionHeader>; |
| updateHeaderVersioned?( |
| sessionId: string, |
| patch: SessionHeaderPatch, |
| expectedRevision: number, |
| ): Promise<VersionedSessionHeader>; |
| readHeaderRecordSnapshot?(sessionId: string): Promise<VersionedSessionHeader>; |
| updateSessionConfiguration?( |
| sessionId: string, |
| input: SessionConfigurationStoreUpdate, |
| ): Promise<VersionedSessionHeader>; |
| setFlagged(sessionId: string, isFlagged: boolean): Promise<void>; |
| rename(sessionId: string, name: string): Promise<void>; |
| remove(sessionId: string): Promise<void>; |
| } |
| |
| export interface StrictRecoverySessionStore extends SessionStore { |
| listForRecovery(): Promise<SessionHeader[]>; |
| } |
| |
| export interface StrictRecoveryAgentRunStore extends AgentRunStore { |
| readEventsForRecovery(sessionId: string, runId: string): Promise<AgentRunEvent[]>; |
| } |
| |
| export interface StrictRecoveryStores { |
| sessionStore: StrictRecoverySessionStore; |
| agentRunStore: StrictRecoveryAgentRunStore; |
| } |
| |
| // ============================================================================ |
| // BackendRegistry — factory dispatch by the session header's durable backend |
| // ============================================================================ |
| |
| export interface BackendFactoryContext { |
| sessionId: string; |
| workspaceRoot: string; |
| header: SessionHeader; |
| store: SessionStore; |
| /** Process-local cancellation for the execution that owns this activation. */ |
| abortSignal?: AbortSignal; |
| /** |
| * Child-agent instruction channel. 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 — 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. |
| * |
| * Session tool-result reads remain available through Read when archiving is |
| * enabled. The backend wraps an existing Read or supplies a resource-only |
| * Read; it never adds filesystem access to a restricted tool set. |
| */ |
| tools?: readonly MakaTool[]; |
| /** Turn-scoped shell plan captured with a bound child tool ceiling. */ |
| turnShellPlan?: TurnShellPlan; |
| recordRunTrace?: RunTraceRecorder; |
| /** |
| * Durable AgentRun row carrying the canonical record for one physical |
| * provider call, including metering and its prepared-request observation. |
| */ |
| recordModelCallAttempt?: (commit: ModelCallCommit<ModelCallAttempt>) => Promise<void>; |
| /** |
| * Writes one runtime note — something that happened inside the running |
| * invocation — to that invocation's RuntimeEvent ledger. |
| */ |
| recordSystemNote?: (kind: RuntimeSystemNoteKind, turnId: string, data?: unknown) => Promise<void>; |
| /** Immutable Run policy snapshot; provider dispatch waits for this durable commit. */ |
| recordRunComposition?: (runId: string, snapshot: RunCompositionSnapshot) => Promise<void>; |
| /** Append-only logical request surface; provider dispatch waits for this durable epoch. */ |
| recordRequestComposition?: ( |
| runId: string, |
| snapshot: RequestCompositionSnapshotInput, |
| ) => Promise<string>; |
| loadHistoryCompactCheckpoint?: () => Promise<HistoryCompactCheckpoint | undefined>; |
| recordHistoryCompactCheckpoint?: ( |
| checkpoint: HistoryCompactCheckpoint, |
| turnId: string, |
| ) => Promise<void>; |
| /** |
| * Session-scoped read of every committed model-projection transition (#4283). |
| * The reducer folds these onto the RuntimeEvent ledger, so a lossy rewrite |
| * survives the Turn that made it. |
| */ |
| loadModelProjectionTransitions?: () => Promise<LoadedModelProjectionTransitions>; |
| /** Durable append for one transition; persistence precedes any model-visible loss. */ |
| recordModelProjectionTransition?: ( |
| transition: ModelProjectionTransition, |
| 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; |
| } |
| |
| export type BackendFactory = (ctx: BackendFactoryContext) => AgentBackend | Promise<AgentBackend>; |
| |
| export type BackendPreparationContext = Pick< |
| BackendFactoryContext, |
| 'sessionId' | 'workspaceRoot' | 'header' | 'abortSignal' |
| >; |
| |
| export interface PreparedBackendActivation { |
| readonly providerStateIdentity?: `sha256:${string}`; |
| build(ctx: BackendFactoryContext): AgentBackend | Promise<AgentBackend>; |
| } |
| |
| export interface PreparedBackendFactory { |
| prepare(ctx: BackendPreparationContext): Promise<PreparedBackendActivation>; |
| } |
| |
| type BackendRegistration = BackendFactory | PreparedBackendFactory; |
| |
| export class BackendRegistry { |
| private readonly registrations = new Map<PersistedBackendKind, BackendRegistration>(); |
| |
| register(kind: PersistedBackendKind, registration: BackendRegistration): void { |
| this.registrations.set(kind, registration); |
| } |
| |
| async prepare( |
| kind: PersistedBackendKind, |
| ctx: BackendPreparationContext, |
| ): Promise<PreparedBackendActivation> { |
| const registration = this.registrations.get(kind); |
| if (!registration) throw new Error(`No backend factory registered for kind="${kind}"`); |
| if (typeof registration === 'function') { |
| return { build: registration }; |
| } |
| return await registration.prepare(ctx); |
| } |
| |
| has(kind: PersistedBackendKind): boolean { |
| return this.registrations.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<ResolvedChildToolActivation>; |
| /** Host-owned user catalog. Runtime receives ids from models, never raw model targets. */ |
| subagentCatalog?: { |
| list(): Promise<SubagentPresetListItem[]>; |
| resolve(id: string): Promise<ResolvedSubagentPreset>; |
| }; |
| /** 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>; |
| 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>; |
| } |
| |
| export interface ResolvedChildToolActivation { |
| readonly tools: readonly MakaTool[]; |
| readonly shell?: TurnShellPlan; |
| } |
| |
| 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 preparedTranscriptLedgers = new Set<string>(); |
| 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({ |
| runtimeEventStore: deps.runtimeEventStore, |
| readMessagesAfter: (sessionId, request) => deps.store.readMessagesAfter(sessionId, request), |
| }); |
| } |
| this.runtimeKernel = deps.runtimeKernel ?? new RuntimeKernel({ ...deps }); |
| } |
| |
| // -------------------------------------------------------------------------- |
| // 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) ?? []; |
| } |
| |
| #projectLiveRunState(sessions: SessionSummary[]): SessionSummary[] { |
| const runningTurnIds = this.runtimeKernel.runningTurnIds?.bind(this.runtimeKernel); |
| if (!runningTurnIds) return sessions; |
| return sessions.map((session) => ({ |
| ...session, |
| runningTurnIds: runningTurnIds(session.id), |
| })); |
| } |
| |
| async listSessions(filter?: SessionListFilter): Promise<SessionSummary[]> { |
| return this.#projectLiveRunState(await this.deps.store.list(filter)); |
| } |
| |
| async listChildSessions(parentSessionId: string): Promise<SessionSummary[]> { |
| const sessions = await this.deps.store.list({ subagentParentSessionId: parentSessionId }); |
| return this.#projectLiveRunState(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: RunLifecycleStatus, |
| ): Promise<ArtifactRecord[]> { |
| const list = this.deps.listArtifactsForTurn; |
| if (!list) return []; |
| let artifacts = await list(sessionId, turnId); |
| if (isTerminalRunStatus(status) && this.hasWorktreePatchWriteBack()) { |
| const header = await this.deps.store.readHeader(sessionId); |
| if ( |
| header.subagentWorkspace && |
| !artifacts.some((artifact) => artifact.source === 'subagent_writeback') |
| ) { |
| await this.finalizeChildWorkspacePatches(sessionId); |
| artifacts = await list(sessionId, turnId); |
| if (!artifacts.some((artifact) => artifact.source === 'subagent_writeback')) { |
| throw new Error( |
| `Child Session ${sessionId} cannot reconstruct the historical workspace patch for Turn ${turnId}`, |
| ); |
| } |
| } |
| } |
| return artifacts.filter(isArtifactChildResultOutput); |
| } |
| |
| /** |
| * The Session's invocations, enumerated from the events that define them. |
| * |
| * There is no run table to consult: an invocation exists because its opening |
| * fact does, and it has ended because its terminal event does. |
| */ |
| private async listInvocations(sessionId: string): Promise<RuntimeInvocationRecord[]> { |
| const store = this.deps.runtimeEventStore; |
| if (!store) return []; |
| return store.listSessionInvocations(sessionId); |
| } |
| |
| /** One invocation by run id. Absent means no opening fact ever named it. */ |
| private async readInvocation(sessionId: string, runId: string): Promise<RuntimeInvocationRecord> { |
| const store = this.deps.runtimeEventStore; |
| const invocation = store ? await readRunInvocation(store, sessionId, runId) : undefined; |
| if (!invocation) { |
| const error = new Error(`AgentRun ${runId} not found`) as Error & { code?: string }; |
| error.code = 'ENOENT'; |
| throw error; |
| } |
| return invocation; |
| } |
| |
| /** 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 = latestInvocation( |
| (await this.listInvocations(sessionId)).filter((run) => |
| isSessionInlineInvocation(run.opening), |
| ), |
| ); |
| if (!latest) return; |
| if (!latest.terminalEvent) { |
| 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 observed = await store.readHeaderRecordSnapshot(sessionId); |
| if (observed.revision !== input.expectedRevision) { |
| throw new SessionConfigurationRevisionConflictError( |
| input.expectedRevision, |
| observed.revision, |
| ); |
| } |
| if ( |
| !input.clearConnectionBlock && |
| sessionConfigurationMatches(observed.header, input.configuration) |
| ) { |
| return observed; |
| } |
| const permissionModeOnly = |
| input.permissionModeOnly && |
| sessionConfigurationMatchesExceptPermissionMode(observed.header, input.configuration); |
| const prepareCommit = async (): Promise<() => Promise<VersionedSessionHeader>> => { |
| const current = await store.readHeaderRecordSnapshot(sessionId); |
| if (current.revision !== input.expectedRevision) { |
| throw new SessionConfigurationRevisionConflictError( |
| input.expectedRevision, |
| current.revision, |
| ); |
| } |
| if (current.header.isArchived) { |
| 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: |
| input.clearConnectionBlock && current.header.blockedReason === 'NO_REAL_CONNECTION' |
| ? { |
| kind: 'clear_connection_block', |
| statusUpdatedAt: this.deps.now(), |
| } |
| : { kind: 'preserve' }, |
| }); |
| }; |
| const next = permissionModeOnly |
| ? await this.commitExecutionBoundaryTransition( |
| sessionId, |
| await this.deps.store.readExecutionBoundary(sessionId), |
| input.configuration.permissionMode, |
| prepareCommit, |
| ) |
| : await this.commitExecutionResourceTransition( |
| sessionId, |
| input.configuration.permissionMode, |
| prepareCommit, |
| ); |
| 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) { |
| 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, |
| (await this.listInvocations(sessionId)) |
| .filter((run) => isSessionInlineInvocation(run.opening)) |
| .map((run) => run.runId), |
| ) |
| : { 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.isArchived, |
| ); |
| const recovered = new Set<string>(); |
| for (const session of interrupted) { |
| 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.shellRuns) { |
| const recoveredShellRuns = await recoverOr( |
| policy, |
| () => this.deps.shellRuns!.recoverOrphanedSession(session.id), |
| 0, |
| ); |
| if (recoveredShellRuns > 0) recovered.add(session.id); |
| } |
| // A revision copy still `preparing` is settled by the Host's revision |
| // coordinator, which reads the admission ledger and runs before this |
| // recovery. Deciding it a second time here — off a transcript scan, and |
| // ending in `remove()` — could only ever contradict it. |
| |
| 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.planStore) { |
| const preservesHandoff = await recoverOr( |
| policy, |
| async () => { |
| if (!continuationAuthority) return false; |
| const latest = latestInvocation( |
| (await this.listInvocations(session.id)).filter((run) => |
| isSessionInlineInvocation(run.opening), |
| ), |
| ); |
| if (!latest) return false; |
| // Only the current logical execution may preserve a session-owned Plan. |
| // Read after claim repair: an abandoned successor now has a real failure. |
| return Boolean( |
| (await readLogicalRuntimeExecutionForRun(continuationAuthority, latest)) |
| ?.pendingHandoff, |
| ); |
| }, |
| false, |
| ); |
| if (!preservesHandoff) { |
| const planRecovery = await recoverOr( |
| policy, |
| () => this.deps.planStore!.interruptActiveExecution(session.id, 'runtime_recovery'), |
| null, |
| ); |
| if (planRecovery) 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); |
| } |
| continue; |
| } |
| } |
| |
| // No ledger and nothing to recover from it. A Session whose turns were |
| // interrupted before this process started is settled by the transcript |
| // importer, which converts a turn that never recorded how it ended into |
| // the failed terminal fact it actually was — this recovery has no second |
| // transcript to read that from. |
| 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); |
| } |
| } |
| return [...recovered]; |
| } |
| |
| 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 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 readHeaderRecordSnapshot = this.deps.store.readHeaderRecordSnapshot?.bind( |
| this.deps.store, |
| ); |
| if (!readHeaderRecordSnapshot || !this.deps.store.updateSessionConfiguration) { |
| // Temporary compatibility bridge for SessionStore embeddings that predate |
| // versioned configuration authority. A follow-up PR will shortly remove |
| // setPermissionMode and this redundant fallback after callers migrate. |
| return this.setPermissionModeWithLegacyStore(sessionId, mode); |
| } |
| const current = await readHeaderRecordSnapshot(sessionId); |
| const next = await this.transitionSessionConfiguration(sessionId, { |
| expectedRevision: current.revision, |
| clearConnectionBlock: false, |
| permissionModeOnly: true, |
| configuration: sessionConfigurationWithPermissionMode(current.header, mode), |
| }); |
| return headerToSummary(next.header); |
| } |
| |
| private async setPermissionModeWithLegacyStore( |
| 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); |
| } |
| |
| const labels = leavingDeepResearch |
| ? previous.labels.filter((label) => label !== DEEP_RESEARCH_SESSION_LABEL) |
| : previous.labels; |
| const kind = mode === 'bypass' ? 'bypass' : 'managed'; |
| await this.commitExecutionBoundaryTransition(sessionId, boundary, mode, async () => { |
| const current = await this.deps.store.readHeader(sessionId); |
| if (current.status === 'waiting_for_user') { |
| throw new SessionConfigurationTransitionError( |
| 'session_busy', |
| 'Session has a pending Interaction', |
| ); |
| } |
| return () => |
| this.deps.store.setExecutionBoundaryKind(sessionId, kind, { |
| permissionMode: mode, |
| labels, |
| }); |
| }); |
| const next = await this.deps.store.readHeader(sessionId); |
| this.runtimeKernel.updateCachedHeader(sessionId, next); |
| return headerToSummary(next); |
| } |
| |
| async setExecutionBoundaryKind( |
| sessionId: string, |
| kind: 'managed' | 'bypass', |
| ): Promise<ExecutionBoundary> { |
| const current = await this.deps.store.readExecutionBoundary(sessionId); |
| const header = await this.deps.store.readHeader(sessionId); |
| // Managed includes Explore. Match Storage's default projection, then pass |
| // it explicitly so classification and commit describe the same transition. |
| const permissionMode = |
| kind === 'bypass' |
| ? 'bypass' |
| : header.permissionMode === 'bypass' |
| ? 'ask' |
| : header.permissionMode; |
| const narrows = narrowsExecutionAuthority(current, permissionMode); |
| if (narrows && this.runtimeKernel.hasActiveRuns(sessionId)) { |
| throw new SessionConfigurationTransitionError( |
| 'session_busy', |
| 'Execution boundary cannot change while a Turn is running', |
| ); |
| } |
| if (header.status === 'waiting_for_user') { |
| throw new SessionConfigurationTransitionError( |
| 'session_busy', |
| 'Execution boundary cannot change while an Interaction is pending', |
| ); |
| } |
| const boundary = await this.commitExecutionBoundaryTransition( |
| sessionId, |
| current, |
| permissionMode, |
| async () => () => |
| this.deps.store.setExecutionBoundaryKind(sessionId, kind, { permissionMode }), |
| ); |
| return boundary; |
| } |
| |
| private async commitExecutionBoundaryTransition<T>( |
| sessionId: string, |
| current: ExecutionBoundary, |
| nextPermissionMode: PermissionMode, |
| prepareCommit: () => Promise<() => Promise<T>>, |
| ): Promise<T> { |
| const prepareBoundaryCommit = async (): Promise<() => Promise<T>> => { |
| 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 prepareCommit(); |
| }; |
| if (!narrowsExecutionAuthority(current, nextPermissionMode)) { |
| // Widening needs no quiescence. Every consumer that froze the old, tighter |
| // boundary fails closed against a wider one, and a descendant's admission |
| // check only gets easier — so the grant is just written. Waiting for the |
| // Session to go idle is what let a running Turn, or a Goal's continuation |
| // holding a claim near-continuously, keep the user's own grant out. |
| if (!this.runtimeKernel.runSessionAdmissionMutation) { |
| throw new SessionConfigurationTransitionError( |
| 'operation_unavailable', |
| 'Session boundary changes require Runtime admission mutation authority', |
| ); |
| } |
| // Serialize the revision check through commit without requiring an idle |
| // Turn. Otherwise two unversioned writes can both pass the check, then |
| // the later write can narrow the first grant using its stale classification. |
| const result = await this.runtimeKernel.runSessionAdmissionMutation([sessionId], async () => { |
| const commit = await prepareBoundaryCommit(); |
| return commit(); |
| }); |
| // Not `disposeBackend`: disposing a live Turn's backend stops that Turn. |
| // Invalidation refreshes it now when the Session is idle, and otherwise |
| // defers to the next activation, which disposes before it starts. |
| await this.runtimeKernel.invalidateBackend(sessionId); |
| return result; |
| } |
| return this.commitExecutionResourceTransition( |
| sessionId, |
| nextPermissionMode, |
| prepareBoundaryCommit, |
| ); |
| } |
| |
| 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; |
| } |
| } |
| |
| /** |
| * Fences a Session and every subagent Session under it for one operation. |
| * |
| * Preparing a bundle reads a Session's whole subtree, and a child holds the |
| * result of a tool call its parent made -- so a Turn starting anywhere in |
| * that tree while the read is in progress produces a bundle describing two |
| * moments. Refuses outright if any of them is already running. |
| */ |
| async runSessionSubtreeQuiescentMutation<T>( |
| sessionId: string, |
| operation: (fencedSessionIds: readonly string[]) => Promise<T>, |
| ): Promise<T> { |
| const descendants = await this.listLinkedDescendantSessionIds(sessionId); |
| const fenced = [sessionId, ...descendants]; |
| return this.runSessionQuiescentMutation(fenced, async () => { |
| // Discovered again, now that the fence is held. The first walk happened |
| // before it, so a child created in that gap is in the subtree and NOT in |
| // what was fenced -- the operation would read it without it being held |
| // still. Refusing is the only honest answer: fencing it now would be |
| // fencing a set this call never admitted. |
| const current = await this.listLinkedDescendantSessionIds(sessionId); |
| if (current.length !== descendants.length || current.some((id) => !fenced.includes(id))) { |
| throw new SessionConfigurationTransitionError( |
| 'operation_conflict', |
| 'Session lineage changed while the subtree was being fenced', |
| ); |
| } |
| return operation(fenced); |
| }); |
| } |
| |
| 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 SessionConfigurationTransitionError( |
| 'session_busy', |
| 'Collaboration mode cannot change while a Turn is running', |
| ); |
| } |
| if (previous.status === 'waiting_for_user') { |
| throw new SessionConfigurationTransitionError( |
| 'session_busy', |
| 'Collaboration mode cannot change while an Interaction is pending', |
| ); |
| } |
| const planState = await this.requirePlanStore().readState(sessionId); |
| if (mode === 'plan' && planState.activeExecutionId) { |
| throw new SessionConfigurationTransitionError( |
| 'session_busy', |
| 'An active Plan execution prevents entering Plan mode', |
| ); |
| } |
| const latestProposal = planState.proposals.find( |
| (proposal) => proposal.proposalId === planState.latestProposalId, |
| ); |
| if (mode === 'agent' && latestProposal?.status === 'pending_approval') { |
| throw new SessionConfigurationTransitionError( |
| 'operation_conflict', |
| 'A pending Plan proposal must be resolved before leaving Plan mode', |
| ); |
| } |
| |
| const next = await this.deps.store.updateHeader(sessionId, { |
| collaborationMode: mode, |
| }); |
| 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 }); |
| 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); |
| 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 orchestration, backend execution, and ledger recording. |
| */ |
| async *sendMessage( |
| sessionId: string, |
| input: UserMessageInput, |
| options: TurnStartOptions = {}, |
| ): AsyncIterable<SessionEvent> { |
| if (input.origin?.kind === 'legacy_automation') { |
| throw new Error('Live Turn cannot use removed Automation authority'); |
| } |
| const repair = input.agentId ? undefined : this.runtimeLedgerRepair; |
| const admitTurn = repair |
| ? async () => { |
| await this.ensureTranscriptLedger(sessionId, repair, 'compatibility'); |
| return (await options.admitTurn?.()) ?? 'admitted'; |
| } |
| : options.admitTurn; |
| yield* this.runtimeKernel.startTurn(sessionId, input, { ...options, admitTurn }); |
| } |
| |
| async planSafeBoundaryContinuation( |
| sessionId: string, |
| input: PlanSafeBoundaryContinuationInput, |
| preview?: RuntimeEvent, |
| ): Promise<SafeBoundaryContinuationPlan> { |
| let admissionRoute: RuntimeContinuationPlannerInput['admissionRoute']; |
| try { |
| if (!this.deps.runStore) throw new Error('AgentRunStore is not configured'); |
| const [header, invocations] = await Promise.all([ |
| this.deps.store.readHeader(sessionId), |
| this.listInvocations(sessionId), |
| ]); |
| const targetProviderStateIdentity = ( |
| await this.deps.backends.prepare(header.backend, { |
| sessionId, |
| workspaceRoot: header.workspaceRoot, |
| header, |
| }) |
| ).providerStateIdentity; |
| admissionRoute = { |
| invocations, |
| targetProviderStateIdentity, |
| targetModelId: header.model, |
| }; |
| } catch { |
| const plan: SafeBoundaryContinuationPlan = { |
| disposition: 'park', |
| rejectionReasons: ['continuation_authority_unavailable'], |
| diagnostics: [ |
| { |
| code: 'continuation_authority_unavailable', |
| message: 'provider replay admission authority is unavailable', |
| }, |
| ], |
| }; |
| this.recordContinuationPlan(sessionId, input.sourceRunId, plan); |
| return plan; |
| } |
| const planner = new RuntimeContinuationPlanner({ |
| readSourceInvocation: async (targetSessionId, runId) => { |
| if (!this.deps.runStore) throw new Error('AgentRunStore is not configured'); |
| return this.readInvocation(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.listInvocations(targetSessionId)).find((run) => { |
| const source = run.opening.source; |
| return ( |
| source.kind !== 'fresh' && |
| source.sourceRunId === sourceRunId && |
| source.sourceRuntimeEventHighWater === sourceRuntimeEventHighWater |
| ); |
| }); |
| }, |
| newId: this.deps.newId, |
| }); |
| const plannerInput = { sessionId, admissionRoute, ...input }; |
| const plan = preview |
| ? await planner.previewHandoff(plannerInput, preview) |
| : await planner.plan(plannerInput); |
| if (!preview) this.recordContinuationPlan(sessionId, input.sourceRunId, plan); |
| return plan; |
| } |
| |
| async planAuthoritativeSafeBoundaryContinuation( |
| sessionId: string, |
| input: PlanAuthoritativeSafeBoundaryContinuationInput, |
| preview?: RuntimeEvent, |
| ): Promise<SafeBoundaryContinuationPlan> { |
| if (input.purpose !== 'handoff' && 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.readInvocation(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.opening.configuration.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, |
| { |
| ...(input.purpose ? { purpose: input.purpose } : {}), |
| sourceRunId: input.sourceRunId, |
| currentCwd: header.cwd, |
| sourceWorkspaceIdentity: sourceRun.opening.configuration.workspaceIdentity, |
| currentWorkspaceIdentity: observation.workspaceIdentity, |
| backgroundOperationsSettled: observation.backgroundOperationsSettled, |
| availableToolNames: observation.availableToolNames, |
| ...(input.expectedRuntimeEventHighWater !== undefined |
| ? { expectedRuntimeEventHighWater: input.expectedRuntimeEventHighWater } |
| : {}), |
| ...(observation.workspaceCheckpoint |
| ? { workspaceCheckpoint: observation.workspaceCheckpoint } |
| : {}), |
| }, |
| preview, |
| ); |
| } |
| |
| 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 = latestInvocation( |
| (await this.listInvocations(sessionId)).filter((run) => { |
| const outcome = runtimeInvocationOutcome(run); |
| return ( |
| (outcome === 'failed' || outcome === 'cancelled') && |
| isSessionInlineInvocation(run.opening) |
| ); |
| }), |
| ); |
| 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, |
| }); |
| } |
| |
| requestRunHandoff( |
| sessionId: string, |
| runId: string, |
| intent: RuntimeHandoffIntent, |
| signal: AbortSignal, |
| ) { |
| const request = this.runtimeKernel.requestRunHandoff?.(sessionId, runId, intent, signal); |
| if (!request) return undefined; |
| let verified = false; |
| const ready = request.ready.then(async (held) => { |
| if (!held) return false; |
| try { |
| const assessment = await this.planAuthoritativeSafeBoundaryContinuation( |
| sessionId, |
| { sourceRunId: runId, purpose: 'handoff' }, |
| request.preview(), |
| ); |
| request.preview(); // Cancellation or Stop may have released the gate during inspection. |
| verified = assessment.disposition === 'continue' && !signal.aborted; |
| if (!verified) request.cancel(); |
| return verified; |
| } catch { |
| request.cancel(); |
| return false; |
| } |
| }); |
| return { |
| ready, |
| sealed: request.sealed, |
| commit: () => verified && request.commit(), |
| cancel: () => { |
| verified = false; |
| request.cancel(); |
| }, |
| }; |
| } |
| |
| 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. |
| } |
| } |
| |
| runCoordinationOperation( |
| sessionId: string, |
| input: UserMessageInput, |
| options: TurnStartOptions, |
| execute: () => Promise<WorkHubActionReceipt>, |
| ): AsyncIterable<SessionEvent> { |
| return this.runtimeKernel.runCoordinationOperation(sessionId, input, options, execute); |
| } |
| |
| 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); |
| } |
| |
| /** |
| * Create and run a durable linked child Session. |
| * |
| * 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.readInvocation(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!); |
| const availableChildTools = await this.childToolsForSession(input.source.sessionId); |
| assertAgentDefinitionRunnable({ |
| definition, |
| tools: availableChildTools, |
| worktreeChildExecutorAvailable: await this.isWorktreeChildExecutorAvailable(parentHeader), |
| }); |
| const resolvedToolNames = buildToolsForAgentDefinition(availableChildTools, definition).map( |
| (tool) => tool.name, |
| ); |
| 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: resolvedToolNames, |
| 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, |
| ...(resolvedPreset |
| ? { llmConnectionId: resolvedPreset.connectionId } |
| : parentHeader.llmConnectionId === undefined |
| ? {} |
| : { llmConnectionId: parentHeader.llmConnectionId }), |
| 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', |
| toolMode: parentHeader.toolMode ?? DEFAULT_TOOL_MODE, |
| 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: resolvedToolNames, |
| 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.readInvocation(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.readInvocation(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 ( |
| !run.terminalEvent && |
| this.runtimeKernel.hasActiveRun?.(child.id, run.runId, run.turnId) |
| ) { |
| await delay(25, undefined, input.abortSignal ? { signal: input.abortSignal } : undefined); |
| run = await this.readInvocation(child.id, claim.targetRunId); |
| } |
| if (!run.terminalEvent) { |
| await this.recoverAgentRunsFromLedger(child.id); |
| run = await this.readInvocation(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); |
| |
| // An invocation is what makes a Turn exist on the ledger, so the run listing |
| // is the whole occupancy check: a Turn with durable content has one. |
| const turnOwner = (await this.listInvocations(child.id)).find( |
| (candidate) => candidate.turnId === claim.targetTurnId, |
| ); |
| if (turnOwner) { |
| throw new Error( |
| `Claimed graph turn ${claim.targetTurnId} is already owned by run ${turnOwner.runId}`, |
| ); |
| } |
| if (child.isArchived || 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.readInvocation(child.id, claim.targetRunId); |
| this.assertClaimedAgentGraphRun(child, snapshot, claim, completedRun); |
| const completedFacts = invocationListingFacts(completedRun); |
| const failureClass = completedFacts.failureClass ?? summary.failureClass; |
| const artifacts = await this.finalizeAndListChildTurnArtifacts( |
| child.id, |
| claim.targetTurnId, |
| completedFacts.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(completedFacts.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: RuntimeInvocationRecord, |
| ): void { |
| const lineage = run.opening.lineage; |
| if ( |
| run.sessionId !== child.id || |
| run.runId !== claim.targetRunId || |
| run.turnId !== claim.targetTurnId || |
| !isSessionInlineInvocation(run.opening) || |
| lineage?.agentId !== snapshot.agentId || |
| (lineage?.agentName !== undefined && lineage.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.getSessionView(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.readInvocation(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 resolvedToolNames = buildToolsForAgentDefinition(availableChildTools, definition).map( |
| (tool) => tool.name, |
| ); |
| |
| 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, |
| ...(input.resolvedPreset |
| ? { llmConnectionId: input.resolvedPreset.connectionId } |
| : parentHeader.llmConnectionId === undefined |
| ? {} |
| : { llmConnectionId: parentHeader.llmConnectionId }), |
| 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', |
| toolMode: parentHeader.toolMode ?? DEFAULT_TOOL_MODE, |
| 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: resolvedToolNames, |
| 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.readInvocation( |
| 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 facts = run ? invocationListingFacts(run) : undefined; |
| const failureClass = facts?.failureClass ?? summary.failureClass; |
| const artifacts = facts |
| ? await this.finalizeAndListChildTurnArtifacts(child.id, turnId, facts.status) |
| : []; |
| return { |
| childSessionId: child.id, |
| agentId: snapshot.agentId, |
| agentName: snapshot.agentName, |
| profile: snapshot.profile, |
| turnId, |
| runId, |
| status: facts ? agentRunStatusForSpawnResult(facts.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.readInvocation(child.id, spawn.initialRunId).catch((error) => { |
| if (isNotFoundError(error)) return undefined; |
| throw error; |
| }); |
| if (!run) return undefined; |
| await notifyReady(); |
| |
| while ( |
| !run.terminalEvent && |
| this.runtimeKernel.hasActiveRun?.(child.id, run.runId, run.turnId) |
| ) { |
| await delay(25, undefined, input.abortSignal ? { signal: input.abortSignal } : undefined); |
| run = await this.readInvocation(child.id, spawn.initialRunId); |
| } |
| if (!run.terminalEvent) { |
| await this.recoverAgentRunsFromLedger(child.id); |
| run = await this.readInvocation(child.id, spawn.initialRunId); |
| } |
| return await this.projectExistingChildSpawn(child, run); |
| } |
| |
| private async projectExistingChildSpawn( |
| child: SessionHeader, |
| run: RuntimeInvocationRecord, |
| ): 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 facts = invocationListingFacts(run); |
| const [runtimeEvents, artifacts] = await Promise.all([ |
| this.deps.runtimeEventStore.readRuntimeEvents(child.id, run.runId), |
| this.finalizeAndListChildTurnArtifacts(child.id, run.turnId, facts.status), |
| ]); |
| 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(''); |
| return { |
| childSessionId: child.id, |
| agentId: snapshot.agentId, |
| agentName: snapshot.agentName, |
| profile: snapshot.profile, |
| turnId: run.turnId, |
| runId: run.runId, |
| status: agentRunStatusForSpawnResult(facts.status), |
| permissionMode: child.permissionMode, |
| summary: trimSummary(durableRuntimeSummary ?? partialRuntimeSummary), |
| artifactIds: artifacts.map((artifact) => artifact.id), |
| startedAt: facts.createdAt, |
| completedAt: facts.updatedAt, |
| durationMs: facts.durationMs ?? 0, |
| eventCount: runtimeEvents.length, |
| ...(facts.failureClass ? { failureClass: facts.failureClass } : {}), |
| }; |
| } |
| |
| 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 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'); |
| } |
| } |
| |
| 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 childRuns = (await this.listInvocations(sessionId)).filter( |
| (run) => !!run.opening.lineage?.parentRunId && !isSessionInlineInvocation(run.opening), |
| ); |
| const legacyRuns = childRuns.map((run) => { |
| const facts = invocationListingFacts(run); |
| return { |
| runId: run.runId, |
| turnId: run.turnId, |
| parentRunId: run.opening.lineage!.parentRunId!, |
| ...(run.opening.lineage?.agentId ? { agentId: run.opening.lineage.agentId } : {}), |
| ...(run.opening.lineage?.agentName ? { agentName: run.opening.lineage.agentName } : {}), |
| ...facts, |
| }; |
| }); |
| 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 run = latestInvocation(await this.listInvocations(child.id)); |
| const facts = run ? invocationListingFacts(run) : 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: facts?.status ?? (child.status === 'aborted' ? 'cancelled' : 'running'), |
| permissionMode: facts?.permissionMode ?? child.permissionMode, |
| createdAt: facts?.createdAt ?? child.createdAt, |
| updatedAt: facts?.updatedAt ?? child.lastMessageAt ?? child.createdAt, |
| ...(facts?.completedAt !== undefined ? { completedAt: facts.completedAt } : {}), |
| ...(facts?.durationMs !== undefined ? { durationMs: facts.durationMs } : {}), |
| ...(facts?.failureClass ? { failureClass: facts.failureClass } : {}), |
| }; |
| }), |
| ); |
| return { |
| definitions, |
| presets, |
| executions: [ |
| ...childSessionExecutions, |
| ...legacyRuns.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.durationMs !== undefined ? { durationMs: run.durationMs } : {}), |
| ...(run.failureClass ? { failureClass: run.failureClass } : {}), |
| }), |
| ), |
| ], |
| runs: legacyRuns, |
| }; |
| } |
| |
| private async childToolsForSession(sessionId: string): Promise<readonly MakaTool[]> { |
| if (!this.deps.resolveChildTools) return this.deps.childTools ?? []; |
| return (await this.deps.resolveChildTools(sessionId)).tools; |
| } |
| |
| 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 { invocation } = located; |
| const inspected = await inspectAgentRunReadModel( |
| this.deps.runStore, |
| this.deps.runtimeEventStore, |
| { sessionId: invocation.sessionId, runId: invocation.runId, invocation }, |
| ); |
| const artifacts = await this.finalizeAndListChildTurnArtifacts( |
| invocation.sessionId, |
| invocation.turnId, |
| runtimeInvocationOutcome(inspected.invocation) ?? 'running', |
| ); |
| const maxEvents = normalizeAgentOutputMaxEvents(input.maxEvents); |
| const maxBytes = normalizeAgentOutputMaxBytes(input.maxBytes); |
| const view = input.view ?? 'runtime_events'; |
| if (view === 'result') { |
| const boundedResult = buildAgentOutputCommittedResult({ |
| invocation: inspected.invocation, |
| runtimeEvents: inspected.runtimeEvents, |
| artifacts, |
| maxArtifacts: maxEvents, |
| maxBytes, |
| ...(located.graph ? { graph: located.graph } : {}), |
| }); |
| return { |
| execution: located.execution, |
| invocation: inspected.invocation, |
| 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, |
| invocation: inspected.invocation, |
| 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; |
| await this.#stopSessionTree( |
| sessionId, |
| hostedAuthority |
| ? hostedAuthority.stopSession(sessionId, input) |
| : this.runtimeKernel.stopSession(sessionId, input), |
| (childSessionId) => |
| hostedAuthority |
| ? hostedAuthority.stopSession(childSessionId, input) |
| : this.runtimeKernel.stopSession(childSessionId, input), |
| ); |
| } |
| |
| async deliverHostedRootStop(sessionId: string, input: StopSessionInput = {}): Promise<void> { |
| const authority = isRuntimeHostedRootAuthority(this.deps.messageAuthority) |
| ? this.deps.messageAuthority |
| : undefined; |
| await this.#stopSessionTree( |
| sessionId, |
| this.runtimeKernel.stopSession(sessionId, input), |
| (childSessionId) => |
| authority |
| ? authority.stopSession(childSessionId, input) |
| : this.runtimeKernel.stopSession(childSessionId, input), |
| ); |
| } |
| |
| async #stopSessionTree( |
| sessionId: string, |
| ownStop: Promise<void>, |
| stopChild: (childSessionId: string) => Promise<void>, |
| ): Promise<void> { |
| // Observe immediately while child lookup runs; await below still propagates the original error. |
| void ownStop.catch(() => 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) => stopChild(child.id)), |
| ); |
| } 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; |
| /** |
| * The message this admission owns, when the crash beat the Run that would |
| * have recorded it. Recovery writes it into the invocation it opens below, |
| * so the Turn the user sees still carries what they asked for. |
| */ |
| userMessage?: { id: string; content: MessageContent; origin?: UserMessage['origin'] }; |
| execution: Exclude< |
| RootExecutionDescriptor, |
| | { kind: 'regenerate' } |
| | { kind: 'context_compact' } |
| | { kind: 'scheduled_task' } |
| | { 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); |
| let root: RuntimeInvocationRootAuthority = { kind: 'user' }; |
| let orchestration: Pick< |
| RuntimeInvocationConfiguration, |
| 'orchestrationMode' | 'orchestrationSource' | 'agentSwarmAuthorization' |
| > = { |
| orchestrationMode: session.orchestrationMode ?? 'default', |
| orchestrationSource: 'session', |
| agentSwarmAuthorization: 'none', |
| }; |
| const lineage: RuntimeInvocationLineage = {}; |
| let recoveryReason: string; |
| let diagnostic: Record<string, unknown>; |
| let workspaceIdentity: string | undefined; |
| if (input.execution.kind === 'goal') { |
| root = { kind: 'goal', goalId: input.execution.goalId }; |
| recoveryReason = 'goal_internal_admission_without_run'; |
| diagnostic = { |
| executionKind: input.execution.kind, |
| goalId: input.execution.goalId, |
| }; |
| } else if ( |
| input.execution.kind === 'workhub_coordination' && |
| input.execution.operation === 'action' |
| ) { |
| recoveryReason = 'coordination_action_admission_without_run'; |
| diagnostic = { executionKind: input.execution.kind, operation: input.execution.operation }; |
| } else if (input.execution.kind === 'legacy_automation') { |
| root = { kind: 'legacy_automation', legacyAutomationId: input.execution.automationId }; |
| recoveryReason = 'legacy_automation_authority_removed'; |
| diagnostic = { |
| executionKind: input.execution.kind, |
| automationId: input.execution.automationId, |
| }; |
| } else if (input.execution.kind === 'agent_graph_supervisor_wake') { |
| root = { |
| kind: 'agent_graph_supervisor_wake', |
| wakeId: input.execution.wakeId, |
| attemptId: input.execution.attemptId, |
| }; |
| orchestration = { |
| orchestrationMode: 'graph', |
| orchestrationSource: 'turn_override', |
| 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' && |
| input.execution.kind !== 'workhub_coordination' |
| ) { |
| 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.readInvocation(input.sessionId, input.execution.sourceRunId); |
| if ( |
| sourceRun.opening.lineage?.agentId !== input.execution.agentId || |
| sourceRun.opening.lineage.agentName !== input.execution.agentName |
| ) { |
| throw new Error( |
| `Admitted Turn ${input.turnId} source changed its trusted agent identity`, |
| ); |
| } |
| workspaceIdentity = sourceRun.opening.configuration.workspaceIdentity; |
| if (input.execution.kind === 'linked_child_resume') { |
| lineage.resumedFromRunId = input.execution.sourceRunId; |
| } else { |
| lineage.retriedFromRunId = input.execution.sourceRunId; |
| } |
| } |
| lineage.agentId = input.execution.agentId; |
| lineage.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 = { |
| sessionId: input.sessionId, |
| invocationId: input.runId, |
| runId: input.runId, |
| turnId: input.turnId, |
| }; |
| const opening: RuntimeEventInvocationOpenedContent = { |
| kind: 'invocation_opened', |
| protocol: 'invocation_opened_v1', |
| route: |
| session.llmConnectionId === undefined |
| ? { |
| provenance: 'unknown', |
| backendKind: session.backend, |
| llmConnectionSlug: session.llmConnectionSlug, |
| modelId: session.model, |
| } |
| : { |
| provenance: 'runtime', |
| backendKind: session.backend, |
| llmConnectionId: session.llmConnectionId, |
| llmConnectionSlug: session.llmConnectionSlug, |
| modelId: session.model, |
| }, |
| configuration: { |
| cwd: session.cwd, |
| permissionMode: session.permissionMode, |
| collaborationMode: session.collaborationMode ?? 'agent', |
| toolMode: session.toolMode ?? DEFAULT_TOOL_MODE, |
| ...orchestration, |
| ...(workspaceIdentity !== undefined ? { workspaceIdentity } : {}), |
| }, |
| root, |
| source: { kind: 'fresh' }, |
| ...(Object.keys(lineage).length > 0 ? { lineage } : {}), |
| }; |
| // The admission never reached an AgentRun, so nothing else will ever open |
| // this invocation. Recovery opens and closes it in one pass so the Turn |
| // ends up on the spine like any other, with its own reason for ending. |
| await this.deps.runtimeEventStore.appendRuntimeEvent( |
| input.sessionId, |
| input.runId, |
| buildInvocationOpenedEvent({ |
| id: this.deps.newId(), |
| run, |
| openedAt: input.admittedAt, |
| opening, |
| }), |
| ); |
| |
| if (input.userMessage) { |
| await this.deps.runtimeEventStore.appendRuntimeEvent(input.sessionId, input.runId, { |
| id: input.userMessage.id, |
| ...run, |
| ts: input.admittedAt, |
| partial: false, |
| role: 'user', |
| author: input.userMessage.origin ? 'host' : 'user', |
| content: { |
| kind: 'text', |
| text: input.userMessage.content.text, |
| ...(input.userMessage.content.displayText !== undefined |
| ? { displayText: input.userMessage.content.displayText } |
| : {}), |
| ...(input.userMessage.content.attachments?.length |
| ? { attachments: input.userMessage.content.attachments } |
| : {}), |
| ...(input.userMessage.content.directoryReferences?.length |
| ? { directoryReferences: input.userMessage.content.directoryReferences } |
| : {}), |
| ...(input.userMessage.content.quotes?.length |
| ? { quotes: input.userMessage.content.quotes } |
| : {}), |
| ...(input.userMessage.origin ? { origin: input.userMessage.origin } : {}), |
| }, |
| }); |
| } |
| |
| 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({ |
| runtimeEventStore: this.deps.runtimeEventStore, |
| newId: this.deps.newId, |
| sessionId: input.sessionId, |
| runId: input.runId, |
| turnId: input.turnId, |
| status: 'failed', |
| ts, |
| terminalEvent, |
| failureClass: 'app_restarted', |
| }); |
| } |
| |
| 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); |
| } |
| |
| 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), |
| }; |
| } |
| |
| /** Canonical, repaired source view for a Host-owned cross-Session copy. */ |
| async readConversationCopySnapshot(sessionId: string): Promise<RuntimeReadModelSessionView> { |
| return this.getSessionView(sessionId); |
| } |
| |
| 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<RuntimeInvocationRecord | undefined> { |
| if (!this.deps.runStore) return undefined; |
| const runs = await this.listInvocations(sessionId).catch(() => []); |
| return runs.find((candidate) => candidate.turnId === turnId); |
| } |
| |
| private assertActiveParentRun( |
| parentSessionId: string, |
| parentRun: RuntimeInvocationRecord, |
| 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 ledger the read |
| // model cannot project yet must not turn its retry loop into a permanent |
| // IPC error; there is no second transcript to fall back to. |
| return null; |
| } |
| } |
| |
| private async findChildRunForOutput( |
| sessionId: string, |
| input: AgentOutputInput, |
| ): Promise<{ |
| invocation: RuntimeInvocationRecord; |
| 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.listInvocations(child.id); |
| const selected = execution.currentRunId |
| ? runs.find((run) => run.runId === execution.currentRunId) |
| : latestInvocation(runs); |
| if (!selected || !isSessionInlineInvocation(selected.opening)) { |
| throw new Error('agent_output could not find the requested child session run'); |
| } |
| return { |
| invocation: selected, |
| execution: { |
| kind: 'child_session', |
| sessionId: child.id, |
| currentRunId: selected.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 invocation = (await this.listInvocations(sessionId)).find((run) => |
| legacyExecution |
| ? run.runId === legacyExecution.runId |
| : input.runId |
| ? run.runId === input.runId |
| : input.turnId |
| ? run.turnId === input.turnId |
| : false, |
| ); |
| if (!invocation) throw new Error('agent_output could not find the requested child agent run'); |
| if (!invocation.opening.lineage?.parentRunId || isSessionInlineInvocation(invocation.opening)) { |
| throw new Error('agent_output only reads child agent runs'); |
| } |
| return { |
| invocation, |
| execution: { |
| kind: 'legacy_child_run', |
| sessionId, |
| runId: invocation.runId, |
| }, |
| }; |
| } |
| |
| 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: SessionHeaderPatch): 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): Promise<void> { |
| const header = await this.deps.store.readHeader(sessionId); |
| const next = |
| (header.collaborationMode ?? 'agent') === 'agent' |
| ? header |
| : await this.deps.store.updateHeader(sessionId, { collaborationMode: 'agent' }); |
| this.runtimeKernel.updateCachedHeader(sessionId, next); |
| } |
| |
| 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 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; |
| } |
| |
| /** |
| * The Session as its ledger tells it. |
| * |
| * A transcript written before the ledger owned execution facts is converted |
| * here, on the first read, because there is no second transcript left to read |
| * it from: the importer is what gives those turns an invocation to be |
| * projected from, so a read that skipped it would report the Session empty. |
| */ |
| private async getSessionView(sessionId: string): Promise<RuntimeReadModelSessionView> { |
| await this.ensureTranscriptLedgerForRead(sessionId); |
| return this.readModel().getSessionView(sessionId); |
| } |
| |
| private readModel(): RuntimeReadModel { |
| if (!this.deps.runStore || !this.deps.runtimeEventStore) { |
| throw new Error('RuntimeReadModel requires AgentRunStore and RuntimeEventStore'); |
| } |
| return new RuntimeReadModel({ |
| runtimeEventStore: this.deps.runtimeEventStore, |
| ...(this.deps.canonicalPermissionOutcomes |
| ? { canonicalPermissionOutcomes: this.deps.canonicalPermissionOutcomes } |
| : {}), |
| }); |
| } |
| |
| /** |
| * Convert a transcript written before the ledger owned execution facts, so a |
| * reader that goes straight to the ledger still sees the whole Session. |
| * |
| * Idempotent and cheap after the first call: the conversion is remembered per |
| * Session, and a Session born on the ledger has nothing to convert. |
| */ |
| async ensureTranscriptLedgerForRead(sessionId: string): Promise<void> { |
| const repair = this.runtimeLedgerRepair; |
| if (repair) await this.ensureTranscriptLedger(sessionId, repair, 'compatibility'); |
| } |
| |
| async prepareImportedSessionHistory(sessionId: string): Promise<void> { |
| const repair = this.runtimeLedgerRepair; |
| if (!repair) throw new Error('Imported Session history requires canonical Runtime stores'); |
| await this.ensureTranscriptLedger(sessionId, repair, 'import'); |
| } |
| |
| private async ensureTranscriptLedger( |
| sessionId: string, |
| repair: RuntimeLedgerRepair, |
| source: 'compatibility' | 'import', |
| ): Promise<void> { |
| if (this.preparedTranscriptLedgers.has(sessionId)) return; |
| const header = await this.deps.store.readHeader(sessionId); |
| if (header.transcriptLedgerVersion === 0 && source !== 'import') { |
| throw new Error('Imported Session history is still being prepared'); |
| } |
| // Version 1 says a conversion ran, not that every legacy fact reached the |
| // ledger. A released build set it on the first send and went on writing |
| // context notes to the transcript alone, so those notes stay behind on |
| // Sessions it touched. Re-running the converter cannot reach them: they |
| // belong to turns a real run already sealed, and a sealed run refuses the |
| // append. They are hidden from the model and describe context, so they are |
| // the accepted cost of the cutover — do not read this marker as proof that |
| // nothing is left in `session_messages`. |
| if (header.transcriptLedgerVersion !== 1) { |
| await repair.materializeTranscriptLedger(header); |
| await this.updateHeader(sessionId, { transcriptLedgerVersion: 1 }); |
| } |
| this.preparedTranscriptLedgers.add(sessionId); |
| } |
| |
| /** |
| * 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; |
| // The target's opening fact rides its continuation-start event, so an |
| // invocation that does not exist yet is exactly the case the repair |
| // start below commits. There is no separate run record to create. |
| const invocation = await this.readInvocation(sessionId, claim.target.runId).catch((error) => { |
| if (isMissingRunError(error)) return undefined; |
| throw error; |
| }); |
| let state = |
| (await authority.readContinuationClaimStateByBoundary(claim.boundaryDigest)) ?? |
| initialState; |
| if (invocation && !invocationMatchesClaimTarget(invocation, claim)) { |
| throw new Error( |
| `Continuation claim target invocation 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: claim.target, |
| 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`); |
| } |
| if (terminal) continue; |
| await commitTerminalRunWithRuntimeFact({ |
| 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, |
| }); |
| 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 }; |
| // The importer may have committed only a prefix before a restart. Sealing |
| // that prefix here would make the next read skip the unconverted history. |
| const runs = (await this.listInvocations(sessionId)).filter( |
| (run) => !isTranscriptLedgerInvocation(run), |
| ); |
| 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, invocation: 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 (run.terminalEvent && runtimeHandoffPause(run.terminalEvent)) { |
| if (inspected.runtimeEvents.at(-1)?.id !== run.terminalEvent.id) { |
| throw new Error(`Handoff source has events after its seal: ${run.runId}`); |
| } |
| // The original Root admission owns this logical execution. Generic |
| // restart repair must neither replay its tools nor manufacture failure. |
| continue; |
| } |
| 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.opening.configuration.toolMode, |
| ); |
| 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, invocation: run }, |
| ); |
| } |
| } |
| const terminalLedger = classifyTerminalRuntimeLedger(run, inspected.runtimeEvents); |
| if (terminalLedger.kind === 'corrupt') { |
| if (policy.kind === 'strict') { |
| throw new Error( |
| `RuntimeEvent ledger has more than one terminal event 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 (!inspected.terminalRuntimeFact) return undefined; |
| return runtimeTerminalFactToRecoveryDecision( |
| inspected.invocation, |
| 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 existingTerminal = inspected.terminalRuntimeFact?.terminalEvent; |
| 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.invocation, |
| 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({ |
| runtimeEventStore: this.deps.runtimeEventStore, |
| newId: this.deps.newId, |
| sessionId, |
| runId: decision.runId, |
| turnId: decision.turnId, |
| status, |
| ts, |
| terminalEvent, |
| ...(failureClass ? { failureClass } : {}), |
| ...(abortSource ? { abortSource } : {}), |
| }); |
| } catch (error) { |
| if (policy.kind === 'strict') throw error; |
| return false; |
| } |
| |
| // A run that already carried a complete terminal fact had nothing to |
| // recover. Saying otherwise makes recovery rewrite the Session status of |
| // every healthy run it walks past. |
| return inspected.terminalRuntimeFact === undefined; |
| } |
| } |
| |
| 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', |
| modelVisibility: 'hidden', |
| content: claim.targetOpening, |
| 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' }>; |
| }, |
| 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 { lineage, source: openSource } = claim.targetOpening; |
| const source = claim.boundary.segments.at(-1)!; |
| if ( |
| lineage?.agentId !== input.execution.agentId || |
| lineage.agentName !== input.execution.agentName || |
| source.identity.sessionId !== input.sessionId || |
| source.identity.runId !== input.execution.sourceRunId || |
| openSource.kind !== 'continuation' || |
| openSource.claimId !== claim.claimId || |
| openSource.boundaryDigest !== claim.boundaryDigest || |
| openSource.sourceRunId !== input.execution.sourceRunId |
| ) { |
| throw new Error('Linked child admission continuation claim identity is inconsistent'); |
| } |
| if ( |
| input.execution.kind === 'linked_child_resume' |
| ? lineage.resumedFromRunId !== input.execution.sourceRunId || |
| lineage.retriedFromRunId !== undefined |
| : lineage.retriedFromRunId !== input.execution.sourceRunId || |
| lineage.resumedFromRunId !== undefined |
| ) { |
| throw new Error('Linked child admission continuation lineage is inconsistent'); |
| } |
| } |
| |
| async function readImmutableRuntimeEventsOrEmpty( |
| authority: RuntimeContinuationAuthorityStore, |
| sessionId: string, |
| runId: string, |
| ): Promise<RuntimeEvent[]> { |
| return authority.readImmutableRuntimeEvents(sessionId, runId); |
| } |
| |
| 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, |
| ...(h.llmConnectionId === undefined ? {} : { llmConnectionId: h.llmConnectionId }), |
| 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; |
| } |
| |
| /** |
| * What a listing shows about one invocation, read entirely off its own facts. |
| * |
| * Every field here used to be a mutable column on the Run header that a writer |
| * had to keep in step with the events. Deriving them means a listing cannot |
| * disagree with the ledger it is listing. |
| */ |
| function invocationListingFacts(invocation: RuntimeInvocationRecord): { |
| status: RunLifecycleStatus; |
| permissionMode: PermissionMode; |
| createdAt: number; |
| updatedAt: number; |
| completedAt?: number; |
| durationMs?: number; |
| failureClass?: string; |
| } { |
| const completedAt = invocation.terminalEvent?.ts; |
| const failureClass = runtimeInvocationFailureClass(invocation); |
| return { |
| status: runtimeInvocationOutcome(invocation) ?? 'running', |
| permissionMode: invocation.opening.configuration.permissionMode, |
| createdAt: invocation.openedAt, |
| updatedAt: completedAt ?? invocation.openedAt, |
| ...(completedAt !== undefined ? { completedAt } : {}), |
| ...(completedAt !== undefined |
| ? { durationMs: Math.max(0, completedAt - invocation.openedAt) } |
| : {}), |
| ...(failureClass ? { failureClass } : {}), |
| }; |
| } |
| |
| /** The most recently opened invocation, breaking ties on run id. */ |
| function latestInvocation( |
| invocations: readonly RuntimeInvocationRecord[], |
| ): RuntimeInvocationRecord | undefined { |
| return invocations |
| .slice() |
| .sort( |
| (left, right) => right.openedAt - left.openedAt || right.runId.localeCompare(left.runId), |
| )[0]; |
| } |
| |
| 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, |
| }; |
| } |
| |
| function sessionConfigurationWithPermissionMode( |
| header: SessionHeader, |
| permissionMode: PermissionMode, |
| ): SessionConfigurationTransitionRequest['configuration'] { |
| return { |
| backend: header.backend, |
| ...(header.llmConnectionId === undefined ? {} : { llmConnectionId: header.llmConnectionId }), |
| llmConnectionSlug: header.llmConnectionSlug, |
| connectionLocked: header.connectionLocked, |
| model: header.model, |
| thinkingLevel: header.thinkingLevel, |
| permissionMode, |
| collaborationMode: header.collaborationMode ?? 'agent', |
| orchestrationMode: header.orchestrationMode ?? 'default', |
| }; |
| } |
| |
| function sessionConfigurationMatchesExceptPermissionMode( |
| header: SessionHeader, |
| configuration: SessionConfigurationTransitionRequest['configuration'], |
| ): boolean { |
| return ( |
| header.backend === configuration.backend && |
| header.llmConnectionId === configuration.llmConnectionId && |
| header.llmConnectionSlug === configuration.llmConnectionSlug && |
| header.connectionLocked === configuration.connectionLocked && |
| header.model === configuration.model && |
| header.thinkingLevel === configuration.thinkingLevel && |
| (header.collaborationMode ?? 'agent') === configuration.collaborationMode && |
| (header.orchestrationMode ?? 'default') === configuration.orchestrationMode |
| ); |
| } |
| |
| function sessionConfigurationMatches( |
| header: SessionHeader, |
| configuration: SessionConfigurationTransitionRequest['configuration'], |
| ): boolean { |
| return ( |
| header.permissionMode === configuration.permissionMode && |
| sessionConfigurationMatchesExceptPermissionMode(header, configuration) |
| ); |
| } |
| |
| 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' && !isCanonicalReadOnlyPermissionProfile(boundary.profile) |
| ); |
| } |
| |
| function agentRunStatusForSpawnResult( |
| status: RunLifecycleStatus, |
| ): SpawnChildSessionResult['status'] { |
| if (status === 'waiting_for_user') return 'waiting_for_user'; |
| if (status === 'cancelled') return 'cancelled'; |
| if (status === 'failed') return 'failed'; |
| if (status === 'running') return 'running'; |
| return 'completed'; |
| } |
| |
| 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: SpawnChildSessionResult['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): SpawnChildSessionResult['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); |
| } |
| } |
| |
| function isTerminalRunStatus(status: RunLifecycleStatus): boolean { |
| return status === 'completed' || status === 'failed' || status === 'cancelled'; |
| } |
| |
| function diagnosticRecoveryReason(diagnostic: Record<string, unknown> | undefined): string { |
| const recoveryReason = diagnostic?.recoveryReason; |
| return typeof recoveryReason === 'string' && recoveryReason.length > 0 |
| ? recoveryReason |
| : 'agent_run_recovery'; |
| } |
| |
| function runtimeTerminalFactToRecoveryDecision( |
| invocation: RuntimeInvocationRecord, |
| 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: openingLineage(invocation), |
| }; |
| } |
| |
| function openingLineage(invocation: RuntimeInvocationRecord): AgentRunRecoveryDecision['lineage'] { |
| const lineage = invocation.opening.lineage; |
| if (!lineage) return {}; |
| return { |
| ...(lineage.parentRunId ? { parentRunId: lineage.parentRunId } : {}), |
| ...(lineage.parentTurnId ? { parentTurnId: lineage.parentTurnId } : {}), |
| ...(lineage.retriedFromTurnId ? { retriedFromTurnId: lineage.retriedFromTurnId } : {}), |
| ...(lineage.regeneratedFromTurnId |
| ? { regeneratedFromTurnId: lineage.regeneratedFromTurnId } |
| : {}), |
| ...(lineage.branchOfTurnId ? { branchOfTurnId: lineage.branchOfTurnId } : {}), |
| ...(lineage.parentSessionId ? { parentSessionId: lineage.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: { |
| invocation: RuntimeInvocationRecord; |
| 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.invocation.sessionId, |
| }, |
| run: input.invocation, |
| 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: runtimeInvocationOutcome(input.invocation) ?? 'running', |
| ...(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), |
| ...(runtimeInvocationFailureClass(input.invocation) |
| ? { failureClass: runtimeInvocationFailureClass(input.invocation) } |
| : {}), |
| }); |
| |
| 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: string[] = []; |
| for (const point of text) { |
| if (codePoints.length >= input.maxBytes) break; |
| codePoints.push(point); |
| } |
| 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] : [], |
| ), |
| ); |
| } |
| |
| // 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, |
| }; |