import * as acp from '@agentclientprotocol/sdk'; import type { CommonPermissions } from '../config.js'; import type { AcpMcpServer } from '../harness/types.js'; import { ConversationEventStore } from './conversation-store.js'; import type { ConversationSnapshot, PromptOrigin, PromptReceipt, SubmitPromptCommand } from './conversation-types.js'; import type { ConversationHandlePage, ExitRecord, InterruptOutcome, QueuedPrompt, SessionEvent, AgentSession, RuntimeSelectorMetadata, SessionSnapshot, SubmitPromptOptions, TurnCancellationSource, TurnOutcome, TurnResult } from './types.js'; /** Bound safe-boundary waiting without turning a hung tool into cancellation. */ export declare const AFTER_TOOL_BOUNDARY_TIMEOUT_MS = 120000; /** * How long a steering-started turn is presumed to still own the adapter after * its last update. Such a turn has no prompt id, so it never reports a * stopReason and there is no exact end to observe — silence is the only signal * available, and this is the bound that turns it into a decision. * * Sized from the fleet's own scheduled-run history: across 1513 completed * scheduled runs the longest silence WITHIN a working turn was 120.2 s (p99 * 41.0 s; 5 runs above 60 s). A shorter grace would release the lease while the * adapter is still working and re-admit a prompt into a busy turn, which is the * FLEET-003 failure itself. The costs are deliberately asymmetric: holding too * long skips one best-effort maintenance tick, releasing too early SIGTERMs a * live role. */ export declare const STEERING_OCCUPANCY_IDLE_MS = 150000; /** Consumed only by Fleet's authenticated bundled-Codex app-server proxy. */ export declare const CODEX_DISABLE_INHERITED_MCP_ENV = "OURS_FLEET_CODEX_DISABLE_INHERITED_MCP"; /** Server-generated typed provenance followed by the exact human-authored body. */ export declare function promptContentBlocks(text: string, origin?: PromptOrigin): acp.ContentBlock[]; export declare function runtimeSelector(options: acp.SessionConfigOption[] | null | undefined, category: string): RuntimeSelectorMetadata | undefined; /** Fresh ACP response including the legacy model report still used by some adapters. */ export type AcpStartupSessionResponse = acp.NewSessionResponse & { models?: { currentModelId?: string; }; }; export interface AcpSessionOptions { /** Opt-in Fleet watchdog, owned by this ACP session, never a process restart. */ stallRecovery?: { timeoutMs?: number; tickMs?: number; cancelWaitMs?: number; }; name: string; /** Harness identity used only for honest optional capability reporting. */ harness?: string; argv: string[]; cwd: string; env: Record; /** Merge the parent environment before env; false uses only the supplied env. Defaults to true. */ inheritEnvironment?: boolean; stateDir: string; mode: 'fresh' | 'resume'; permissions: CommonPermissions; /** Native permission-mode id to request via session/set_mode; undefined keeps the agent default. */ modeId?: string; /** Require modeId to be advertised and session/set_mode to succeed before readiness. */ requireMode?: boolean; /** * Validate initialize and session/new reports before persistence or readiness. * Throw/reject to fail startup. Not called for session/load or session/resume. */ validateStartupResponse?: (initialized: acp.InitializeResponse, created: AcpStartupSessionResponse) => void | Promise; /** Ordered explicit Brain choices that must be applied before readiness. */ configSelections?: Array<{ configId: string; value: string; }>; /** Adapter-resolved live permission policy; separate from ACP agent-specific session modes. */ permissionMode?: NonNullable; /** Adapter-authenticated request-metadata vocabulary; never inferred from ACP `_meta`. */ permissionMetadataSource?: 'codex-acp'; /** Fleet-managed ours proxies must never receive the obsolete presence-sensitive flag. */ scrubObsoleteOursAutostart?: boolean; /** * MCP servers the ROLE declares, for every session/new, resume and load. * Omitted preserves inherited configuration (encoded as ACP's required `[]`); * an explicit empty array disables every inherited server through the * authenticated bundled-adapter compatibility path. */ mcpServers?: AcpMcpServer[]; /** * Adapter-supplied `_meta` for session/new — the only route by which a * capability the CLI takes as a flag reaches an agent that accepts none. * Per-agent vocabulary, so the ADAPTER decides whether there is anything to * send; this layer only forwards it. Never sent on resume or load: it carries * session-creation options the agent has already applied. */ sessionMeta?: Record; log(line: string): void; /** Test seam for the cancel-escalation grace period; production uses the default. */ cancelGraceMs?: number; /** Test seam for SIGTERM -> SIGKILL escalation after an ignored cancellation. */ cancelTerminateGraceMs?: number; /** How long a pending permission may wait for a human before it expires. */ permissionTimeoutMs?: number; /** Grace after the last controller detaches before the unattended policy applies. */ controllerGraceMs?: number; /** Test seam; production uses AFTER_TOOL_BOUNDARY_TIMEOUT_MS. */ afterToolBoundaryTimeoutMs?: number; /** Test seam; production uses STEERING_OCCUPANCY_IDLE_MS. */ steeringOccupancyIdleMs?: number; } /** * Classify an ACP `stopReason` into a terminal outcome. A refusal and a * cancellation are the two ways a delivered prompt ends without being carried * out; every other stop reason ran the turn to an end the agent chose. */ export declare function classifyStopReason(stopReason: string | undefined): TurnOutcome; /** * Persistent ACP v1 client. It is the sole owner of the agent's stdio; all * human/automation attachment happens through the fleet role-control protocol. */ export declare class AcpSession implements AgentSession { private readonly options; readonly backend: "acp"; readonly capabilities: import("./types.js").AgentSessionCapabilities; readonly pid: number; private readonly child; private readonly events; private readonly conversation; /** Cursor before this runner generation began; older durable events stay off the live console. */ private readonly conversationStartCursor?; /** New on every runner start; permission/turn IDs from prior generations are stale. */ private readonly sessionGeneration; /** True while `session/load` replays history as ordinary updates. */ private replaying; private readonly sessionFile; private readonly pendingPermissions; private connection; private sessionId?; private readiness; /** * Last non-replayed session update from the agent. `readiness` cannot answer * "is this agent working" for a steered turn (FLEET-002), and this is the * evidence that can. */ private lastUpdateAt?; private lastError?; private promptTail; private queueDepth; private exit; private steeringSupported; private agentCapabilities?; private runtimeModel?; private reasoningEffort?; private controllerCount; private closing; /** Armed when the last controller detaches; unattended policy applies on fire. */ private controllerGrace?; private cancelEscalation?; private cancelForceKill?; private cancelRecoveryReason?; /** * Held while a steering-started turn is believed to own the adapter. It is a * lease, not a latch: `steeringRelease` always fires, so the role can never be * stranded busy by a wake whose turn ended without telling anyone. */ private steeringOccupied; private steeringRelease?; /** * Rejects the moment the adapter process is gone. Every in-flight ACP request * races it, so a dead adapter can never leave a turn — and therefore a * scheduled run's `activeRunId` or an admission claim — unsettled forever. */ private readonly terminated; private terminate; /** ACP-authenticated in-flight calls, including independently reserved permissions. */ private readonly activeToolCalls; private stallWatchdog?; private stallToolHistory?; private stallRecoveryClaimed; private managedTurnCount; private steeringWasUsed; private steeringRequests; private retryNativeTurnId?; private stallTimer?; private stallAttempt?; private readonly toolBoundaryWaiters; private activeTurn?; private constructor(); static start(options: AcpSessionOptions): Promise; /** * Honest restart recovery: a prompt that was admitted but never * started is safe to dispatch again; a turn that had already started may * have caused side effects, so it is closed as `unknown_after_restart` — * never silently replayed. */ private recoverOpenPrompts; isAlive(): boolean; /** * Take the occupancy lease for a turn the adapter started on its own behalf. * Refreshed by every adapter update, so it tracks work actually happening * rather than a fixed guess at how long a wake takes. */ private holdSteeringOccupancy; private refreshSteeringOccupancy; /** * Every exit from occupancy comes through here, including the ones that are * not the timer: a real turn boundary, close, and adapter exit. A lease that * can leak is worse than the bug it fixes — it would leave the role reporting * `running` forever and starve scheduled admission permanently. */ private releaseSteeringOccupancy; snapshot(): SessionSnapshot; private toolCall; private reserveTool; private reservePermission; private allowPermission; private releasePermission; private releaseTool; private releaseToolIfIdle; private releaseAllTools; private waitForToolBoundary; private recordAfterToolDelivery; /** * Steering is an optional admission fast path, not the only safe way to * deliver a wake. Codex can reject `_session/steering` while a long-running * turn is between tools. Queue one ordinary, non-cancelling prompt in that * case and wait for its terminal result. This keeps the monitor's cursor * uncommitted until the wake really runs and, critically, keeps one rejected * steering response from becoming a tight replay loop. */ private steerOrQueueWake; /** * Monitor-only safe-boundary delivery. Steering is the preferred live * insertion and rejected steering is queued: this path never calls * session/cancel and never resolves a pending permission. */ submitPromptAfterTool(text: string, options?: SubmitPromptOptions): Promise; /** * Accept responsibility for a prompt, then return. The turn itself may run * for minutes behind other queued turns; making an interactive caller wait * for it is what turned a busy agent into a timeout and then into "dead". */ queuePrompt(text: string, options?: SubmitPromptOptions): Promise; /** * Prepare the session for a prompt that asked to pre-empt current work. * * The old behaviour was one unconditional `session/cancel` notification * followed immediately by `session/prompt`. That is what produced the owner's * "request failed before completion": * * - `cancelActive` only awaits settlement when `this.activeTurn` is set, and * a turn the ADAPTER started (steering's `startedNewTurn`) is never tracked * here. So the cancel raced the adapter's own transcript repair and the new * prompt landed while the last assistant message still held an unresolved * `tool_use` — rejected with `stop_reason=tool_use`. * - With nothing running at all, it still sent the cancel, and the prompt * landed on a bare interrupted user message — rejected with * `stop_reason=null`. * * So: never cancel across a tool boundary, and never cancel something whose * settlement cannot be awaited. Everything else is queued, which the ACP queue * already does correctly. The returned state is what the caller may claim to a * human — `interrupted` only when a turn really was cancelled. */ private prepareInterruptingDelivery; /** * Durably record a prompt admission BEFORE acceptance is returned. Browser * admissions are transactional — a prompt the ledger cannot hold is refused, * because an acknowledged-then-lost prompt is worse than an error. Every * other source degrades to best-effort so the agent keeps working. */ private admitToLedger; /** Idempotent browser prompt admission (control v3 `submit_prompt_v2`). */ submitPromptBrowser(command: SubmitPromptCommand): Promise; submitPrompt(text: string, options?: SubmitPromptOptions): Promise; /** * Explicit cancellation on behalf of a human or an operator. Forced recovery * is reported as an outcome, never as a thrown failure: by the time this * resolves the turn is over either way, and only the durable-ingress path * (`queuePrompt({ interrupt: true })`) needs the typed error, because only it * still owes an undelivered message a replay. */ interrupt(source?: TurnCancellationSource): Promise; private cancelActive; /** * Do not admit work behind a turn whose adapter may already require restart. * A cooperative adapter settles this promise immediately through runPrompt's * finally block. A stubborn adapter receives SIGTERM at the deadline and * SIGKILL after one more bounded grace; callers get a typed recovery error so * durable ingress can leave the next request replayable for the resumed run. */ private awaitCancellationSettlement; respondPermission(permissionId: string, optionId: string): boolean; /** * A v2 decision binds to the session generation it was shown under. A stale * generation, an already-settled request, or an unknown option are all the * same answer: someone else's decision (or a restart) got there first. */ respondPermissionV2(permissionId: string, optionId: string, sessionGeneration: string): 'accepted' | 'stale'; eventsSince(seq: number): SessionEvent[]; subscribe(listener: (event: SessionEvent) => void): () => void; setControllerAttached(attached: boolean): void; private armControllerGrace; /** * Settle one pending request without a human decision — unattended policy, * expiry, or cancellation — and leave the same durable evidence a manual * decision would. A denial selects the agent's own one-shot reject option; * everything else resolves as cancelled toward the agent. */ private settlePendingAutomatically; exitResult(): ExitRecord | null; close(): Promise; /** ACP requires the field. Bundled agents treat [] as no client-added servers. */ private declaredMcpServers; private initialize; private captureRuntimeMetadata; private startStallWatchdog; private checkStallWatchdog; private stallObservation; private recoverStall; /** Keep the original queue slot (including startup) until recovery finishes. */ private runPrompt; private runSinglePrompt; private steerPrompt; private requestPermission; /** * Resolve a permission request from policy alone and leave a record of it. * Nothing else in the system can observe an automatic decision, so an * unrecorded one is indistinguishable from a request that was never made. */ private settleAutomatically; private withinAutomaticBoundary; /** * Codex ACP 1.1.7 marks its protected MCP elicitation bridge on a locationless * execute request. The marker is meaningful only together with the runner's * independently supplied, adapter-authenticated metadata vocabulary and effective * mode: an arbitrary ACP process cannot gain this path by copying `_meta` alone. * Exact option ids/kinds bind recognition to the protected-MCP shape and keep * malformed requests on the ordinary fail-closed path. */ private isEffectiveCodexProtectedMcpApproval; /** Pinned codex-acp 1.1.7 structured metadata, never stderr or assistant text. */ private recordStallMetadata; private recordUpdate; /** * Codex ACP's phase extension is the only currently supported visibility * signal. Never infer commentary from text, message order, or unknown meta. */ private codexMessagePhase; /** Normalize every ACP update losslessly into the durable ledger. */ private recordConversationUpdate; conversationPage(request?: { after?: string; limit?: number; }): ConversationHandlePage; conversationSnapshot(): ConversationSnapshot; subscribeConversation(listener: Parameters[0]): () => void; private isCurrentConversationEvent; private fail; }