Files
PaperClipAI/packages/paperclip-runner/src/drivers/acpx/codex-acpx-driver.ts
T
DottaandPaperclip 54cc522e86 feat(runner): add gated native Antigravity ACP execution (#18188)
## Thinking Path

> - Paperclip manages agents and their work.
> - Native Runner must bind each provider turn to the selected task,
account, model, and instructions.
> - Antigravity needs native ACP admission and physical process
ownership across continuation.
> - A saved conversation must not reuse an old process owner or silently
switch its effective model.
> - This pull request adds the native candidate behind exact operator
admission.
> - The default release gate remains closed while qualification is
pending.

## Linked Issues or Issue Description

Builds on merged #18187. Product account setup and the candidate
producer are in dependent #18189. The repeatable browser journey is in
#18190. Refs: #12503, #14938; those proposals use legacy CLI adapters.

**Agent or provider**

Google Antigravity ACP 1.4.3.

**Why this adapter is useful**

Users need to set up an Antigravity agent and run real work through
their managed Google subscription.

**How the agent is invoked**

Native Paperclip Runner uses the pinned Antigravity ACP runtime. Exact
operator admission is required while qualification remains pending.

## What Changed

- Add native Antigravity ACP model and instruction admission,
selected-account homes, and fenced refresh ownership.
- Carry exact candidate authority from the control plane through Runner,
Rust, the sidecar, and each current run. Clamp daemon lifetime to the
absolute permission expiry, including replacement owners.
- Preserve a rotated OAuth refresh token before later identity
verification can fail, including failed or cancelled startup admission.
Make private lease-close retries safe.
- Bound native probe retirement and retry the exact original owner; do
not delete credential homes before physical proof.
- Preserve Gemini API-key recovery when shared Google subscription
capability is added.
- Resume the same tagged native conversation with fresh per-turn process
ownership and physical native retirement.
- Include native manifests in isolated Rust image stages, release cache
keys, and dev rebuild detection. The workflow changes only extend
existing cache input lists.
- Keep current master provider behavior and append Grok 13, Pi 35, and
Copilot 84 input identities. Preserve prior identities, including saved
Copilot 82 and 83 runs.

## Verification

- Runner TypeScript main checks, profile generators, Rust formatting,
and offline workspace checks pass. The native backend integration target
passes 28 tests.
- The complete transport and cleanup group passes 387 tests without
unhandled rejections. These checks use local scripted children and
actual process and port ownership.
- Saved-profile decoding, native-execution parsing, installation, and
selected-credential controls pass 208 focused tests. All 109
pre-existing profile identity files remain byte-for-byte unchanged.
Copilot, Grok, and shared TypeScript/Rust generation checks pass.
- The final recovery fixture passes all seven cases. After the master
rebase, the combined recovery and owned-Node-runtime group passes all 10
cases. Each expiry case sets one 45-second absolute grant before
startup. It includes the deliberate six-second cold delay, four existing
eight-second command budgets, and active-turn headroom. The test proves
an active turn before expiry and then checks the daemon, owned process
group, child processes, and all three native port fences. The 60-second
test deadline does not renew provider authority.
- The consolidated model-discovery and pending-release group passes 22
tests. Google credential compatibility and auth recovery pass 25 tests,
including existing Gemini API-key recovery. The full product settings
test group remains intact in #18189.
- Earlier failures remain in their original CI runs and local records.
They exposed the Copilot 82 decoder gap, an expired five-second fixture
bootstrap ticket, and a fixture grant consumed before activation on
loaded CI. Each repair has a separate result. Production ticket limits
and admission behavior are unchanged by the final test commits.
- An unrelated decision-expiry check failed in [the earlier CI
run](https://github.com/paperclipai/paperclip/actions/runs/40941986698/job/123688668997).
Its owning code is unchanged from the base. The unchanged suite passes
20 tests on isolated PostgreSQL. A controlled database rollback
reproduces the earlier wake: clean startup schedules the 120-second
decision expiry, while rollback reconciliation leaves a 30-second
continuation-coalescing wake. The decision stays open, with no wake
delivery or error. The fresh final-head server shard passes; the failed
result is preserved.
- Final native commit `ec6bc7e4d77f5ae0cc4946dbba6c662e95a87f5b` passed
`ci / verify` and `ci / e2e`. Fresh Greptile and Paperclip reviews both
score 5/5, with no open findings. Antigravity retains the recorded
native `.5` closure and exact low-model profile. This PR does not claim
global or cross-platform qualification.

## Risks

- The new transport carries private managed credentials. Exact
ownership, account generation, model, instructions, and native
retirement are fail-closed.
- Only local macOS ARM64 has a verified complete Antigravity runtime.
Default Antigravity task execution remains disabled without current
trusted operator admission. The Google server sign-in lane and
production candidate producer ship in #18189; this native slice does not
expose the subscription catalog choice.
- Shared launch inputs change provider identities. New identities are
append-only; existing recorded identities remain available.

## Model Used

OpenAI Codex (GPT-6). The host did not expose the exact model revision
or context limit. The model used tools for code edits, tests, and
review.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-29 16:36:12 -05:00

2579 lines
92 KiB
TypeScript

import { type CopilotToolEvidence } from "./copilot-tool-evidence.js";
import { resolveCompletionContinuation } from "../completion-continuation.js";
import { isProviderMode } from "../../contracts/provider-mode.js";
import { acpxProfileActivity, type AcpxActivityAdapter, type AcpxToolEvidence } from "./profile-activity.js";
import { requireAcpxResponseDelivery } from "./response-delivery.js";
import { createPiMessageProjection, piBoundaryClearsFinal, type PiProjectedMessageEvent } from "./pi-message-projection.js";
import { acpxProfileClientCapabilities, bindAcpxExtensionTurn, validateAcpxRichEvent, createAcpxProfileExtensionAdapter, type AcpxExtensionInput } from "./profile-extensions.js";
import { createHash, randomBytes } from "node:crypto";
import type {
AcpElicitationContext,
AcpElicitationRequest,
AcpElicitationResponse,
AcpRuntimeEvent,
AcpPermissionRequest,
AcpPermissionDecision,
} from "acpx/runtime";
import {
PRP_BLOCK_TOOL_NAME,
PRP_COMPLETION_TOOL_NAME,
} from "../../contracts/completion-result.js";
import {
HarnessCapabilityUnavailableError,
HarnessRuntimeRequestResolutionError,
HarnessStaleTurnError,
harnessRuntimeInputExpiredOutcome,
harnessRuntimeRequestOutcome,
parseHarnessRuntimeRequestResolution,
type HarnessDriver,
type HarnessDriverConfigValidation,
type HarnessDriverDescriptor,
type HarnessSession,
type HarnessSessionRecoveryOptions,
type HarnessSessionRecoveryResult,
type HarnessRuntimeRequest,
type HarnessRuntimeRequestHandoff,
type HarnessRuntimeRequestResolution,
type HarnessTranscriptSnapshot,
type OpenHarnessSessionInput,
type PersistedHarnessSession,
} from "../../contracts/harness-driver.js";
import { PAPERCLIP_RUNTIME_REQUEST_SCHEMA_V2 } from "../../contracts/question-set.js";
import type { NativeAcpxPermissionMode } from "../../contracts/native-execution.js";
import type { NativeUserMessage } from "../../contracts/types.js";
import type {
PrpEvent,
PrpStructuredRunResult,
} from "../../protocol/replay-contract.js";
import { validatePrpStructuredRunResult } from "../../protocol/replay-contract.js";
import {
canonicalProviderEventsFromAcpxRuntimeEvent,
createAcpxToolEventNormalizer,
createGrokMessageNormalizer,
} from "../../provider-events.js";
import {
canonicalRunnerToolName,
type RunnerToolCall,
} from "../runner-tool-bridge.js";
import {
DEFAULT_CODEX_ACPX_RUNTIME_SHUTDOWN_BOUND_MS,
AcpxProviderProcessLostError,
openCodexAcpxRuntime,
} from "./codex-runtime-adapter.js";
import { normalizeAcpxPermission } from "./acp-permission-adapter.js";
import {
normalizeAcpFormElicitation,
type NormalizedAcpForm,
} from "./acp-question-adapter.js";
import {
acpxDriverDescriptor,
acpxCapabilities,
validateAcpxDriverConfig,
} from "./driver-profile.js";
import {
assertAcpxProfileReleased,
resolveQualifiedAcpxProfile,
type QualifiedAcpxAgent,
} from "./qualified-profiles.js";
import {
ACPX_TURN_CANCELLATION_SHUTDOWN_BOUND_MS,
AcpxRuntimeHost,
type AcpxRetainedCleanupFailure,
type AcpxRuntimeTurn,
type OpenAcpxRuntimeHostOptions,
} from "./runtime-host.js";
import {
readAcpxRecoveryWorkspace,
type AcpxRecoveryWorkspaceLease,
} from "./runtime-sandbox.js";
import { AcpxTurnControlLedger, parseAcpxTurnControl, type AcpxTurnControlMode } from "./turn-controls.js";
import { acpxUsageEstimateNotice, persistedAcpxTurnUsage, persistedCursorUsageNotice } from "./usage-accounting.js";
const MAX_BUFFERED_EVENTS = 512;
const TERMINAL_EVENT_RESERVE = 3;
const TURN_START_EVENT_COUNT = 3;
const MAX_TRANSCRIPT_EVENTS = 1_024;
const MAX_TRANSCRIPT_BYTES = 8 * 1024 * 1024;
const MAX_RECOVERY_TERMINAL_TURNS = 4_096;
const MAX_RECOVERY_TERMINAL_BYTES = 8 * 1024 * 1024;
const MAX_PENDING_RUNTIME_REQUESTS = 16;
const CLOSE_TURN_SETTLEMENT_TIMEOUT_MS = 2_000;
const MAX_AUTONOMOUS_HOST_CLOSE_RETRIES = 3;
const MAX_QUARANTINED_HOST_CLOSE_RETRIES = 3;
const QUARANTINED_HOST_CLOSE_RETRY_MS = 60_000;
const MAX_QUARANTINED_HOST_ATTEMPT_RETRY_DELAY_MS = 1_000;
// Host shutdown first bounds active-turn cancellation and then performs the
// adapter's bounded protocol/TERM/KILL cleanup. Bound each ownership phase that
// admission observes: an inherited owner may install a distinct replacement
// recovery, which receives its own finite grace rather than inheriting an
// almost-expired deadline. Cover every attempt and bounded inter-attempt delay,
// plus one second of scheduling margin.
const QUARANTINED_HOST_CLOSE_ATTEMPT_BOUND_MS =
ACPX_TURN_CANCELLATION_SHUTDOWN_BOUND_MS +
DEFAULT_CODEX_ACPX_RUNTIME_SHUTDOWN_BOUND_MS;
const MAX_INHERITED_QUARANTINED_HOST_CLOSE_ATTEMPTS = 1;
const QUARANTINED_HOST_ADMISSION_GRACE_MS =
(MAX_INHERITED_QUARANTINED_HOST_CLOSE_ATTEMPTS +
MAX_QUARANTINED_HOST_CLOSE_RETRIES) *
QUARANTINED_HOST_CLOSE_ATTEMPT_BOUND_MS +
(MAX_QUARANTINED_HOST_CLOSE_RETRIES - 1) *
MAX_QUARANTINED_HOST_ATTEMPT_RETRY_DELAY_MS +
1_000;
interface PendingAcpxRuntimeRequest {
request: HarnessRuntimeRequest;
responseDelivery: Promise<void>;
deliveredEvidence?: () => void;
prepareResolution(resolution: HarnessRuntimeRequestResolution): () => void;
cancel(): void;
cleanup(): void;
settling: boolean;
}
export interface CodexAcpxDynamicToolCall {
tool: string;
callId: string;
providerSessionId: string;
turnId: string;
arguments: unknown;
signal: AbortSignal;
}
export interface CodexAcpxDriverOptions {
/** Defaults to Codex for backward compatibility. */
agent?: QualifiedAcpxAgent;
runtimeDirectory: string;
model: string;
permissionMode?: NativeAcpxPermissionMode;
mode?: string;
piThinkingLevel?: "off" | "low" | "high" | "max";
claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max";
grokEffort?: "minimal" | "low" | "medium" | "high" | "xhigh";
grokDisableWebSearch?: boolean;
providerPolicy?: { readOnly: boolean };
runtimeContext?: OpenAcpxRuntimeHostOptions["runtimeContext"];
systemInstructions?: string;
environment?: NodeJS.ProcessEnv;
managedCodexCredentialSourcePath?: string;
dynamicTools?: readonly Readonly<Record<string, unknown>>[];
dynamicToolHandler?: (call: CodexAcpxDynamicToolCall) => Promise<unknown>;
/** Server-owned task completion validation, shared with Codex tool calls. */
completionFeedback?: (result: PrpStructuredRunResult) => Promise<string>;
now?: () => Date;
}
interface CodexAcpxHost {
identity(): ReturnType<AcpxRuntimeHost["identity"]>;
binding(): ReturnType<AcpxRuntimeHost["binding"]>;
status(): ReturnType<AcpxRuntimeHost["status"]>;
startTurn(
input: Parameters<AcpxRuntimeHost["startTurn"]>[0],
): AcpxRuntimeTurn;
steeringCapability?: AcpxRuntimeHost["steeringCapability"];
steerActiveTurn?: AcpxRuntimeHost["steerActiveTurn"];
queueFollowUp?: AcpxRuntimeHost["queueFollowUp"];
interruptActiveTurn(reason: string): Promise<void>;
close(input: { reason: string }): Promise<void>;
}
interface QuarantinedHostCleanup {
host: CodexAcpxHost;
reason: string;
attempt: Promise<void> | null;
recovery: Promise<void> | null;
recoveryMaxAttempts: number | null;
timer: ReturnType<typeof setTimeout> | null;
}
export interface CodexAcpxDriverDependencies {
openHost?: (options: OpenAcpxRuntimeHostOptions) => Promise<CodexAcpxHost>;
/** Internal test seam; production uses the fixed close-settlement bound. */
closeSettlementTimeoutMs?: number;
/** Internal test seam; production uses the fixed event-retention bound. */
maxBufferedEvents?: number;
/** Internal test seam; production reserves fixed terminal-event capacity. */
terminalEventReserve?: number;
readRecoveryWorkspace?: (input: {
runtimeDirectory: string;
normalizedSessionId: string;
signal?: AbortSignal;
}) => Promise<AcpxRecoveryWorkspaceLease>;
}
export interface ProbeQualifiedAcpxEnvironmentOptions {
runtimeDirectory: string;
agent: QualifiedAcpxAgent;
model: string;
environment?: NodeJS.ProcessEnv;
}
export interface QualifiedAcpxEnvironmentProbe {
effectiveModel: string;
commandDigest: string;
}
/**
* Admit and cleanly close the same qualified ACPX host used by production
* sessions without exposing that host as part of the public package surface.
*/
export async function probeQualifiedAcpxEnvironment(
options: ProbeQualifiedAcpxEnvironmentOptions,
): Promise<QualifiedAcpxEnvironmentProbe> {
const profile = resolveQualifiedAcpxProfile(options.agent, options.model);
const driver = new CodexAcpxDriver({
runtimeDirectory: options.runtimeDirectory,
agent: options.agent,
model: options.model,
permissionMode: "deny-all",
providerPolicy: { readOnly: true },
systemInstructions:
"Paperclip Runner environment qualification probe. Do not execute a provider turn.",
...(options.environment === undefined
? {}
: { environment: options.environment }),
dynamicTools: [],
dynamicToolHandler: async () => {
throw new Error("environment probe exposes no semantic tools");
},
});
const session = await driver.openSession({
runId: "environment-probe",
normalizedSessionId: "environment-probe",
workingDirectory: options.runtimeDirectory,
});
try {
const snapshot = await session.snapshot();
if (snapshot.providerIdentity?.kind !== "acpx") {
throw new Error("ACPX environment probe returned no provider identity");
}
return Object.freeze({
effectiveModel: snapshot.providerIdentity.effectiveModel,
commandDigest: profile.commandDigest,
});
} finally {
// A successful return is authoritative proof that the driver's bounded
// close released the provider, credential lease, semantic bridge, and
// verified command. A failed close remains owned by the driver's retained
// recovery/quarantine path, so callers must preserve runtimeDirectory.
await session.close({ reason: "environment probe complete" });
}
}
function openProductionAcpxHost(
options: OpenAcpxRuntimeHostOptions,
): Promise<AcpxRuntimeHost> {
return AcpxRuntimeHost.open(options, {
openRuntime: openCodexAcpxRuntime,
reportRetainedCleanupFailure: reportRetainedAcpxCleanupFailure,
});
}
/** Qualified HarnessDriver backed by the admitted ACPX runtime host. */
export class CodexAcpxDriver implements HarnessDriver {
readonly #options: CodexAcpxDriverOptions;
readonly #openHost: NonNullable<CodexAcpxDriverDependencies["openHost"]>;
readonly #closeSettlementTimeoutMs: number;
readonly #maxBufferedEvents: number;
readonly #terminalEventReserve: number;
readonly #cleanupOwners = new Set<Promise<void>>();
readonly #quarantinedHostCleanups = new Set<QuarantinedHostCleanup>();
readonly #readRecoveryWorkspace: NonNullable<
CodexAcpxDriverDependencies["readRecoveryWorkspace"]
>;
constructor(
options: CodexAcpxDriverOptions,
dependencies: CodexAcpxDriverDependencies = {},
) {
assertAcpxProfileReleased(options.agent ?? "codex");
if (["pi", "cursor", "copilot"].includes(options.agent ?? "codex") && typeof options.providerPolicy?.readOnly !== "boolean") {
throw new Error("ACP candidate requires an explicit provider read-only policy");
}
this.#options = {
...options,
agent: options.agent ?? "codex",
...(options.providerPolicy ? { providerPolicy: { readOnly: options.providerPolicy.readOnly } } : {}),
...(options.environment
? { environment: { ...options.environment } }
: {}),
...(options.dynamicTools
? { dynamicTools: structuredClone(options.dynamicTools) }
: {}),
};
this.#openHost = dependencies.openHost ?? openProductionAcpxHost;
this.#closeSettlementTimeoutMs =
dependencies.closeSettlementTimeoutMs ?? CLOSE_TURN_SETTLEMENT_TIMEOUT_MS;
this.#terminalEventReserve = Math.max(
0,
Math.floor(dependencies.terminalEventReserve ?? TERMINAL_EVENT_RESERVE),
);
this.#maxBufferedEvents = Math.max(
this.#terminalEventReserve + TURN_START_EVENT_COUNT,
Math.floor(dependencies.maxBufferedEvents ?? MAX_BUFFERED_EVENTS),
);
this.#readRecoveryWorkspace =
dependencies.readRecoveryWorkspace ?? readAcpxRecoveryWorkspace;
}
async descriptor(): Promise<HarnessDriverDescriptor> {
const descriptor = acpxDriverDescriptor(this.#options.agent ?? "codex");
return {
...descriptor,
runtimeContextCapabilities: {
instructions: "native",
skills: "unsupported",
mcp: "native",
},
capabilities: {
...descriptor.capabilities,
resume: true,
runtimeRequestResolution: true,
runtimeRequestHandoff: true,
unsupported: descriptor.capabilities.unsupported,
},
};
}
async validateConfig(value: unknown): Promise<HarnessDriverConfigValidation> {
const validation = validateAcpxDriverConfig(value);
if (
!validation.ok ||
validation.config.agent === (this.#options.agent ?? "codex")
)
return validation;
return {
ok: false,
config: null,
issues: [
{
path: "agent",
code: "unsupported_agent",
message: `This ACPX driver is bound to ${this.#options.agent ?? "codex"}.`,
},
],
};
}
async openSession(input: OpenHarnessSessionInput): Promise<HarnessSession> {
await runAbortableDriverAdmission(input.signal, () =>
this.#retryQuarantinedHostCleanups(),
);
return await this.#open(input, null);
}
async recoverSession(
snapshot: PersistedHarnessSession,
options: HarnessSessionRecoveryOptions = {
signal: new AbortController().signal,
},
): Promise<HarnessSessionRecoveryResult> {
try {
await runAbortableDriverAdmission(options.signal, () =>
this.#retryQuarantinedHostCleanups(),
);
validateRecoverySnapshot(snapshot);
const terminalTurnIds = new Set(
(snapshot.terminalTurns ?? []).map(({ turnId }) => turnId),
);
if (
snapshot.activeTurnId &&
!terminalTurnIds.has(snapshot.activeTurnId)
) {
return {
recovered: false,
reason: "active Codex ACPX turn continuity is unavailable",
};
}
const workspaceLease = await this.#readRecoveryWorkspaceForAdmission(
{
runtimeDirectory: this.#options.runtimeDirectory,
normalizedSessionId: snapshot.normalizedSessionId!,
signal: options.signal,
},
options.signal,
);
let recoveredSession: HarnessSession | null = null;
try {
recoveredSession = await this.#open(
{
runId: snapshot.runId!,
normalizedSessionId: snapshot.normalizedSessionId!,
workingDirectory: workspaceLease.path,
signal: options.signal,
},
snapshot,
workspaceLease.assertHeld,
);
await workspaceLease.close();
return { recovered: true, session: recoveredSession };
} catch (error) {
await workspaceLease.close().catch(() => undefined);
if (recoveredSession) {
await recoveredSession
.close({
reason: "ACPX recovery workspace lease cleanup failed",
force: true,
})
.catch(() => undefined);
}
throw error;
}
} catch (error) {
return { recovered: false, reason: safeMessage(error) };
}
}
async #readRecoveryWorkspaceForAdmission(
input: Parameters<
NonNullable<CodexAcpxDriverDependencies["readRecoveryWorkspace"]>
>[0],
signal: AbortSignal | undefined,
): Promise<AcpxRecoveryWorkspaceLease> {
if (signal === undefined) return await this.#readRecoveryWorkspace(input);
signal.throwIfAborted();
const pending = Promise.resolve().then(() =>
this.#readRecoveryWorkspace(input),
);
try {
return await raceDriverAdmissionWithAbort(pending, signal);
} catch (error) {
if (signal.aborted) {
this.#retainCleanup(
pending.then(
(lease) => lease.close(),
() => undefined,
),
);
}
throw error;
}
}
async #open(
input: OpenHarnessSessionInput,
snapshot: PersistedHarnessSession | null,
assertWorkspaceHeld?: () => void,
): Promise<HarnessSession> {
let session: CodexAcpxSession | null = null;
const host = await this.#openHostForAdmission(
{
runtimeDirectory: this.#options.runtimeDirectory,
normalizedSessionId: input.normalizedSessionId,
workingDirectory: input.workingDirectory,
agent: this.#options.agent ?? "codex",
clientCapabilities: acpxProfileClientCapabilities(this.#options.agent ?? "codex"),
model: this.#options.model,
permissionMode: this.#options.permissionMode ?? "approve-all",
mode: this.#options.mode,
piThinkingLevel: this.#options.piThinkingLevel,
claudeEffort: this.#options.claudeEffort,
grokEffort: this.#options.grokEffort, grokDisableWebSearch: this.#options.grokDisableWebSearch,
providerPolicy: this.#options.providerPolicy,
runtimeContext: this.#options.runtimeContext,
systemInstructions: this.#options.systemInstructions,
environment: this.#options.environment,
managedCodexCredentialSourcePath:
this.#options.managedCodexCredentialSourcePath,
...(assertWorkspaceHeld === undefined ? {} : { assertWorkspaceHeld }),
...(snapshot?.providerIdentity?.kind === "acpx"
? { expectedIdentity: snapshot.providerIdentity }
: {}),
...(input.signal === undefined ? {} : { signal: input.signal }),
semanticTools: {
...(this.#options.agent === "copilot" ? { captureSemanticReceipt: () => session?.captureSemanticReceipt() } : {}),
tools: this.#options.dynamicTools ?? [],
handler: (call) => {
if (!session) {
throw new Error("Codex ACPX session is not ready for tool calls");
}
return session.dispatchTool(call);
},
},
},
input.signal,
);
try {
input.signal?.throwIfAborted();
session = new CodexAcpxSession({
host,
agent: this.#options.agent ?? "codex",
input,
dynamicToolHandler: this.#options.dynamicToolHandler,
completionFeedback: this.#options.completionFeedback,
now: this.#options.now ?? (() => new Date()),
closeSettlementTimeoutMs: this.#closeSettlementTimeoutMs,
maxBufferedEvents: this.#maxBufferedEvents,
terminalEventReserve: this.#terminalEventReserve,
retainCleanup: (cleanup) => this.#retainCleanup(cleanup),
quarantineCleanup: (hostToRetain, reason) =>
this.#quarantineHostCleanup(hostToRetain, reason),
snapshot,
});
return session;
} catch (error) {
const reason = "Codex ACPX session initialization failed";
const cleanup = Promise.resolve().then(() => host.close({ reason }));
this.#retainCleanup(cleanup);
void cleanup.catch(() => this.#quarantineHostCleanup(host, reason));
await settleWithin(cleanup, this.#closeSettlementTimeoutMs).catch(
() => undefined,
);
throw error;
}
}
async #openHostForAdmission(
options: OpenAcpxRuntimeHostOptions,
signal: AbortSignal | undefined,
): Promise<CodexAcpxHost> {
if (signal === undefined) return await this.#openHost(options);
signal.throwIfAborted();
const opening = Promise.resolve().then(() => this.#openHost(options));
try {
return await raceDriverAdmissionWithAbort(opening, signal);
} catch (error) {
if (signal.aborted) {
const lateHostCleanup = opening.then(
(host) => this.#closeLateAdmissionHost(host),
() => undefined,
);
this.#retainCleanup(lateHostCleanup);
}
throw error;
}
}
async #closeLateAdmissionHost(host: CodexAcpxHost): Promise<void> {
const reason = "Codex ACPX host resolved after admission was aborted";
const cleanup = Promise.resolve().then(() => host.close({ reason }));
this.#retainCleanup(cleanup);
try {
await cleanup;
} catch {
this.#quarantineHostCleanup(host, reason);
}
}
#retainCleanup(cleanup: Promise<void>): void {
this.#cleanupOwners.add(cleanup);
void cleanup
.finally(() => this.#cleanupOwners.delete(cleanup))
.catch(() => undefined);
}
#quarantineHostCleanup(host: CodexAcpxHost, reason: string): void {
if (
[...this.#quarantinedHostCleanups].some((entry) => entry.host === host)
) {
return;
}
const cleanup: QuarantinedHostCleanup = {
host,
reason,
attempt: null,
recovery: null,
recoveryMaxAttempts: null,
timer: null,
};
this.#quarantinedHostCleanups.add(cleanup);
this.#startQuarantinedHostCleanupRecovery(
cleanup,
MAX_QUARANTINED_HOST_CLOSE_RETRIES,
"quarantined cleanup recovery",
);
}
#startQuarantinedHostCleanupRecovery(
cleanup: QuarantinedHostCleanup,
maxAttempts: number,
reason: string,
): Promise<void> {
if (cleanup.recovery) return cleanup.recovery;
const recovery = (async () => {
for (
let attemptCount = 0;
attemptCount < maxAttempts &&
this.#quarantinedHostCleanups.has(cleanup);
attemptCount += 1
) {
// Start the first retry immediately so admission's finite grace applies
// to provider cleanup rather than to this rate-limit delay. Only later
// attempts wait, preserving sequential bounded retry behavior.
if (attemptCount > 0) {
await waitForCleanupRetry(
Math.max(
1,
Math.min(
MAX_QUARANTINED_HOST_ATTEMPT_RETRY_DELAY_MS,
this.#closeSettlementTimeoutMs,
),
),
);
}
const attempt = Promise.resolve().then(() =>
cleanup.host.close({
reason: `${cleanup.reason} (${reason})`,
}),
);
cleanup.attempt = attempt;
try {
await attempt;
this.#quarantinedHostCleanups.delete(cleanup);
} catch {
// Retain the quarantine after this finite, sequential retry batch.
} finally {
if (cleanup.attempt === attempt) cleanup.attempt = null;
}
}
})();
cleanup.recovery = recovery;
cleanup.recoveryMaxAttempts = maxAttempts;
this.#retainCleanup(recovery);
void recovery
.finally(() => {
if (cleanup.recovery === recovery) {
cleanup.recovery = null;
cleanup.recoveryMaxAttempts = null;
}
this.#scheduleQuarantinedHostCleanup(cleanup);
})
.catch(() => undefined);
return recovery;
}
#scheduleQuarantinedHostCleanup(cleanup: QuarantinedHostCleanup): void {
if (
!this.#quarantinedHostCleanups.has(cleanup) ||
cleanup.recovery ||
cleanup.attempt ||
cleanup.timer
) {
return;
}
// A quarantined host remains actively owned, but recovery is rate-limited:
// each entry holds at most one unref'd timer and one sequential close.
// Admission may pull that timer forward; it never creates a parallel try.
cleanup.timer = setTimeout(() => {
cleanup.timer = null;
if (!this.#quarantinedHostCleanups.has(cleanup)) return;
this.#startQuarantinedHostCleanupRecovery(
cleanup,
1,
"scheduled quarantined cleanup recovery",
);
}, QUARANTINED_HOST_CLOSE_RETRY_MS);
cleanup.timer.unref?.();
}
async #retryQuarantinedHostCleanups(): Promise<void> {
const observedOwners = new Set<Promise<void>>();
const acceleratedCleanups = new Set<QuarantinedHostCleanup>();
try {
while (true) {
// A close that is still pending has not entered quarantine yet, but it
// owns the same provider resources. Observe every retained owner and any
// replacement quarantine recovery it installs before admitting a host.
const cleanupOwners = new Set<Promise<void>>(this.#cleanupOwners);
for (const cleanup of this.#quarantinedHostCleanups) {
if (cleanup.timer) {
clearTimeout(cleanup.timer);
cleanup.timer = null;
}
if (cleanup.recovery) {
// A scheduled autonomous owner has only one attempt. Admission
// observes it first, then starts one complete bounded batch if that
// attempt fails. A pre-existing full batch already consumed that
// allowance and must never be duplicated.
if (
(cleanup.recoveryMaxAttempts ?? 0) >=
MAX_QUARANTINED_HOST_CLOSE_RETRIES
) {
acceleratedCleanups.add(cleanup);
}
cleanupOwners.add(cleanup.recovery);
} else if (!acceleratedCleanups.has(cleanup)) {
acceleratedCleanups.add(cleanup);
cleanupOwners.add(
this.#startQuarantinedHostCleanupRecovery(
cleanup,
MAX_QUARANTINED_HOST_CLOSE_RETRIES,
"quarantined cleanup admission recovery",
),
);
}
}
const replacementOwners = [...cleanupOwners].filter(
(owner) => !observedOwners.has(owner),
);
if (replacementOwners.length === 0) break;
replacementOwners.forEach((owner) => observedOwners.add(owner));
if (
!(await settlesWithin(
Promise.all(
replacementOwners.map((owner) => owner.catch(() => undefined)),
),
QUARANTINED_HOST_ADMISSION_GRACE_MS,
))
) {
throw new Error(
"Codex ACPX cannot open a new session because quarantined host cleanup exceeded the admission grace",
);
}
}
} finally {
// Admission accelerates autonomous timers, but it must never strand a
// different quarantined host if observing any owner exhausts the grace.
for (const cleanup of this.#quarantinedHostCleanups) {
this.#scheduleQuarantinedHostCleanup(cleanup);
}
}
if (
this.#cleanupOwners.size > 0 ||
this.#quarantinedHostCleanups.size > 0
) {
throw new Error(
"Codex ACPX cannot open a new session while quarantined host cleanup remains incomplete",
);
}
}
}
class CodexAcpxSession implements HarnessSession {
readonly #host: CodexAcpxHost;
readonly #agent: QualifiedAcpxAgent;
readonly #input: OpenHarnessSessionInput;
readonly #dynamicToolHandler?: CodexAcpxDriverOptions["dynamicToolHandler"];
readonly #completionFeedback?: CodexAcpxDriverOptions["completionFeedback"];
readonly #now: () => Date;
readonly #closeSettlementTimeoutMs: number;
readonly #maxBufferedEvents: number;
readonly #terminalEventReserve: number;
readonly #events: AsyncQueue<PrpEvent>;
readonly #retainCleanup: (cleanup: Promise<void>) => void;
readonly #quarantineCleanup: (host: CodexAcpxHost, reason: string) => void;
readonly #transcript: Array<{ event: PrpEvent; bytes: number }> = [];
readonly #terminalTurns = new Map<string, string>();
readonly #pendingRuntimeRequests = new Map<
string,
PendingAcpxRuntimeRequest
>();
readonly #sourceInstanceId: string;
readonly #providerRecoveryPolicy: NonNullable<
PersistedHarnessSession["providerRecoveryPolicy"]
>;
#sourceSequence = 0;
#activeTurnId: string | null = null;
#copilotToolEvidence: CopilotToolEvidence | undefined;
#semanticResult: PrpStructuredRunResult | null = null;
#semanticFingerprint: string | null = null;
#semanticCallId: string | null = null;
#semanticTurnId: string | null = null;
#pendingSemanticTransfer: {
result: PrpStructuredRunResult;
fingerprint: string;
callId: string;
turnId: string;
} | null = null;
#usage: Record<string, unknown> | null = null;
#assistantText = "";
#assistantMessageId: string | null = null;
#closed = false;
#closingStarted = false;
#eventStreamClosed = false;
#activePump: Promise<void> | null = null;
#closePromise: Promise<void> | null = null;
#hostClosePromise: Promise<void> | null = null;
#hostCloseRecoveryPromise: Promise<void> | null = null;
#hostClosed = false;
#transcriptBytes = 0;
#transcriptEventCount = 0;
#transcriptOmitted = false;
#eventStreamOmitted = false;
#pendingTerminal: {
turnId: string;
fingerprint: string;
eventType: "turn.completed" | "turn.failed" | "turn.interrupted";
payload: Record<string, unknown>;
} | null = null;
#runtimeRequestSequence = 0;
readonly #turnControls = new AcpxTurnControlLedger();
constructor(input: {
host: CodexAcpxHost;
agent: QualifiedAcpxAgent;
input: OpenHarnessSessionInput;
dynamicToolHandler?: CodexAcpxDriverOptions["dynamicToolHandler"];
completionFeedback?: CodexAcpxDriverOptions["completionFeedback"];
now: () => Date;
closeSettlementTimeoutMs: number;
maxBufferedEvents: number;
terminalEventReserve: number;
retainCleanup: (cleanup: Promise<void>) => void;
quarantineCleanup: (host: CodexAcpxHost, reason: string) => void;
snapshot: PersistedHarnessSession | null;
}) {
const identity = input.host.identity();
if (identity.normalizedSessionId !== input.input.normalizedSessionId) {
throw new Error("Codex ACPX host returned a different session identity");
}
this.#host = input.host;
this.#agent = input.agent;
this.#input = structuredClone(input.input);
this.#dynamicToolHandler = input.dynamicToolHandler;
this.#completionFeedback = input.completionFeedback;
this.#now = input.now;
this.#closeSettlementTimeoutMs = input.closeSettlementTimeoutMs;
this.#maxBufferedEvents = input.maxBufferedEvents;
this.#terminalEventReserve = input.terminalEventReserve;
this.#retainCleanup = input.retainCleanup;
this.#quarantineCleanup = input.quarantineCleanup;
this.#events = new AsyncQueue<PrpEvent>(input.maxBufferedEvents);
this.#sourceInstanceId = stableId(
"paperclip-acpx",
input.input.normalizedSessionId,
);
this.#sourceSequence = input.snapshot?.lastSourceSequence ?? 0;
this.#activeTurnId = input.snapshot?.activeTurnId ?? null;
this.#providerRecoveryPolicy =
input.snapshot?.providerRecoveryPolicy ?? "same_session_only";
const semantic = input.snapshot?.semanticResult;
if (semantic) {
this.#semanticResult = structuredClone(semantic.result);
this.#semanticFingerprint = semantic.fingerprint;
this.#semanticCallId = semantic.callId ?? null;
this.#semanticTurnId = semantic.turnId;
}
for (const terminal of input.snapshot?.terminalTurns ?? []) {
this.#terminalTurns.set(terminal.turnId, terminal.fingerprint);
}
if (this.#activeTurnId && this.#terminalTurns.has(this.#activeTurnId)) {
this.#activeTurnId = null;
}
}
ids() {
const identity = this.#host.identity();
return {
driverSessionId: identity.acpxRecordId,
providerSessionId: identity.agentSessionId,
displayId: identity.agentSessionId,
};
}
events(): AsyncIterable<PrpEvent> {
return this.#events;
}
turnControlCapabilities() {
const capability = acpxCapabilities(this.#agent, this.#host.steeringCapability?.());
return { steering: capability.steering, queuedFollowUp: capability.queuedFollowUp === true };
}
async startTurn(input: {
message: NativeUserMessage;
}): Promise<{ turnId: string }> {
this.#assertOpen();
if (this.#activeTurnId) {
throw new Error("Codex ACPX session already has an active turn");
}
if (
!this.#events.hasCapacity(
this.#terminalEventReserve + TURN_START_EVENT_COUNT,
)
) {
throw new HarnessCapabilityUnavailableError(
"turn.start",
"the event consumer must drain the previous turn before another turn can start",
);
}
if (this.#terminalTurns.size >= this.#maxBufferedEvents) {
throw new HarnessCapabilityUnavailableError(
"turn.start",
"the bounded session turn limit was reached; open a new session",
);
}
const turnId = `turn-${randomBytes(12).toString("hex")}`;
this.#activeTurnId = turnId;
this.#turnControls.begin(turnId);
this.#assistantText = "";
this.#assistantMessageId = null;
this.#emit("turn.submitted", { text: input.message.text }, { turnId });
this.#emit("turn.accepted", { turnId }, { turnId });
this.#emit("turn.started", { status: "inProgress" }, { turnId });
const extensions = bindAcpxExtensionTurn({
adapter: createAcpxProfileExtensionAdapter(this.#agent, {
workspacePath: this.#input.workingDirectory, sessionId: this.#host.identity().backendSessionId, turnId,
}),
active: () => this.#activeTurnId === turnId && !this.#closingStarted,
sessionId: this.#host.identity().backendSessionId,
waitForInput: (input, context) => this.#handleExtensionInput(turnId, input, context),
emit: event => {
if (!this.#emit(event.eventType, event.payload, { turnId, itemId: event.itemId })) {
throw new Error("ACP rich activity could not be retained");
}
},
});
const activity = acpxProfileActivity(this.#agent);
const toolEvidence = activity.createToolEvidence?.({
sessionId: this.#host.identity().backendSessionId, turnId, workingDirectory: this.#input.workingDirectory,
active: () => this.#activeTurnId === turnId && !this.#closingStarted,
emit: event => {
validateAcpxRichEvent(event);
if (!this.#emit(event.eventType, event.payload, { turnId, itemId: event.itemId })) throw new Error("ACP tool activity could not be retained");
},
});
this.#copilotToolEvidence = toolEvidence && "captureSemanticReceipt" in toolEvidence ? toolEvidence as CopilotToolEvidence : undefined;
let turn: AcpxRuntimeTurn;
const usageBefore = await readUsageStatus(this.#host);
try {
this.#assertOpen();
if (this.#activeTurnId !== turnId) throw new HarnessStaleTurnError(turnId);
turn = this.#host.startTurn({
text: input.message.text,
requestId: `${safeId(this.#input.runId, "run")}:${turnId}`,
onExtensionRequest: extensions.onExtensionRequest,
onExtensionNotification: extensions.onExtensionNotification,
onPermissionRequest: (request, context) =>
this.#handlePermission(turnId, request, context, toolEvidence),
onElicitation: (request, context) =>
this.#handleElicitation(turnId, request, context),
});
} catch (error) {
this.#publishTerminal(
turnId,
canonicalJson({ status: "failed" }),
"turn.failed",
{ status: "failed", error: { message: safeMessage(error) } },
);
throw error;
}
const pump = this.#pumpTurn(turnId, turn, extensions.drain, usageBefore, activity, toolEvidence);
this.#activePump = pump;
void pump
.finally(() => {
if (this.#activePump === pump) this.#activePump = null;
})
.catch(() => undefined);
return { turnId };
}
async steer(input: { turnId: string; message: NativeUserMessage; correlationId?: string; mode?: AcpxTurnControlMode }): Promise<void> {
this.#assertOpen();
if (input.turnId !== this.#activeTurnId || this.#pendingTerminal) throw new HarnessStaleTurnError(input.turnId);
const mode = input.mode ?? "steer";
const capability = this.turnControlCapabilities();
const operation = mode === "steer" ? this.#host.steerActiveTurn : this.#host.queueFollowUp;
if (!(mode === "steer" ? capability?.steering : capability?.queuedFollowUp) || !operation) {
throw new HarnessCapabilityUnavailableError(mode, "the ACP provider did not negotiate this turn control");
}
const control = parseAcpxTurnControl({ turnId: input.turnId, message: input.message.text, mode,
controlId: input.correlationId ?? `control-${randomBytes(12).toString("hex")}` });
this.#turnControls.reserve(control, this.#activeTurnId);
await operation.call(this.#host, control.message, `${safeId(this.#input.runId, "run")}:${control.turnId}`);
if (this.#activeTurnId !== control.turnId || this.#closingStarted) throw new HarnessStaleTurnError(control.turnId);
this.#emit("item.completed", { kind: "steering_acknowledgement", mode,
text: mode === "steer" ? "Steering acknowledged for the active turn." : "Follow-up queued after the active work.", status: "acknowledged" },
{ turnId: control.turnId, itemId: stableId("acpx-control", `${control.turnId}:${control.controlId}`) });
}
async interrupt(input: { turnId?: string; reason?: string }): Promise<void> {
this.#assertOpen();
if (input.turnId && input.turnId !== this.#activeTurnId) {
throw new HarnessStaleTurnError(input.turnId);
}
if (this.#pendingTerminal) {
throw new HarnessCapabilityUnavailableError(
"interruption",
"the active turn is awaiting canonical terminal-event retention",
);
}
if (!this.#activeTurnId) {
throw new HarnessCapabilityUnavailableError(
"interruption",
"there is no active Codex ACPX turn",
);
}
await this.#host.interruptActiveTurn(input.reason ?? "interrupted");
}
pendingRuntimeRequests(): HarnessRuntimeRequest[] {
return [...this.#pendingRuntimeRequests.values()].map(({ request }) =>
structuredClone(request),
);
}
async resolveRuntimeRequest(input: {
requestId: string;
turnId: string;
resolution: HarnessRuntimeRequestResolution;
}): Promise<void> {
this.#assertOpen();
const pending = this.#pendingRuntimeRequests.get(input.requestId);
if (!pending) {
throw new HarnessCapabilityUnavailableError(
"runtime request resolution",
`request ${input.requestId} is no longer pending`,
);
}
if (
pending.request.turnId !== input.turnId ||
this.#activeTurnId !== input.turnId
) {
throw new HarnessStaleTurnError(input.turnId);
}
if (pending.settling) {
throw new HarnessCapabilityUnavailableError(
"runtime request resolution",
`request ${input.requestId} is already settling`,
);
}
pending.settling = true;
let dispatched = false;
try {
const resolution = parseHarnessRuntimeRequestResolution(
pending.request.requestKind,
input.resolution,
pending.request.input,
);
const deliver = pending.prepareResolution(resolution);
if (!this.#pendingRuntimeRequests.delete(input.requestId)) return;
pending.cleanup();
dispatched = true;
deliver();
await pending.responseDelivery;
pending.deliveredEvidence?.();
this.#emit(
"runtime_request.resolved",
harnessRuntimeRequestOutcome(pending.request, {
action: resolution.action,
...(resolution.action === "submit" && "response" in resolution
? { response: resolution.response }
: {}),
}),
{ turnId: input.turnId, itemId: pending.request.itemId },
);
} catch (error) {
if (!dispatched) { pending.settling = false; throw error; }
this.#emit("runtime_request.expired", {
...harnessRuntimeRequestOutcome(pending.request, { reason: "response_delivery_failed" }),
replayAllowed: false,
}, { turnId: input.turnId, itemId: pending.request.itemId });
try { await this.close({ reason: "ACP response delivery failed" }); }
catch (cleanupError) { throw new AggregateError([error, cleanupError], "ACP response delivery and provider cleanup failed"); }
throw error;
}
}
handoffRuntimeRequest(input: {
requestId: string;
turnId: string;
reason: "durable_handoff";
signal: AbortSignal;
}): HarnessRuntimeRequestHandoff {
if (input.signal.aborted) {
return { result: "already_settled", cleanup: Promise.resolve() };
}
this.#assertOpen();
const pending = this.#pendingRuntimeRequests.get(input.requestId);
if (
!pending ||
pending.request.turnId !== input.turnId ||
this.#activeTurnId !== input.turnId ||
pending.settling
) {
return { result: "already_settled", cleanup: Promise.resolve() };
}
if (
!this.#emit(
"runtime_request.expired",
harnessRuntimeInputExpiredOutcome(pending.request, input.reason),
{ turnId: input.turnId, itemId: pending.request.itemId },
)
) {
throw new HarnessCapabilityUnavailableError(
"runtime request handoff",
"the event consumer must drain provider events before the durable handoff can be retained",
);
}
if (!this.#pendingRuntimeRequests.delete(input.requestId)) {
throw new Error(
`ACPX runtime request ${input.requestId} changed during its synchronous handoff`,
);
}
pending.cleanup();
pending.cancel();
const cleanup = Promise.resolve()
.then(() =>
this.#host.interruptActiveTurn(
"Paperclip parked the ACPX input on a durable wait.",
),
)
.catch((error: unknown) => {
if (
this.#activeTurnId === input.turnId &&
!this.#terminalTurns.has(input.turnId)
) {
throw error;
}
});
return { result: "handed_off", cleanup };
}
captureSemanticReceipt() {
return this.#copilotToolEvidence?.captureSemanticReceipt();
}
async dispatchTool(call: RunnerToolCall): Promise<unknown> {
this.#assertOpen();
if (this.#pendingTerminal) {
throw new HarnessCapabilityUnavailableError(
"tool.dispatch",
"the active turn is awaiting canonical terminal-event retention",
);
}
const turnId = this.#activeTurnId;
if (!turnId) {
throw new Error("Codex ACPX tool call is not bound to an active turn");
}
const tool = canonicalRunnerToolName(call.tool);
if (tool === PRP_COMPLETION_TOOL_NAME || tool === PRP_BLOCK_TOOL_NAME) {
const completionInput = await resolveCompletionContinuation(call.arguments, this.#input.runId,
this.#dynamicToolHandler ? () => this.#dynamicToolHandler!({
tool: "get_task_context", callId: `${call.callId}:continuation`,
providerSessionId: this.#host.identity().agentSessionId, turnId, arguments: {}, signal: call.signal,
}) : undefined);
this.#assertOpen();
if (this.#activeTurnId !== turnId || this.#pendingTerminal || call.signal.aborted) {
throw new Error("The turn ended while resolving completion");
}
const validation = validatePrpStructuredRunResult(completionInput);
if (!validation.ok) throw new Error("Invalid semantic run result");
const blocked = validation.result.reportedWorkDisposition === "blocked";
if (
(tool === PRP_BLOCK_TOOL_NAME && !blocked) ||
(tool === PRP_COMPLETION_TOOL_NAME && blocked)
) {
throw new Error(
"Semantic result disposition does not match the terminal tool",
);
}
const fingerprint = canonicalJson(validation.result);
if (
this.#semanticFingerprint !== null &&
this.#semanticFingerprint !== fingerprint
) {
throw new Error("A different semantic result is already committed");
}
const claimsLaterTurn =
this.#semanticFingerprint === fingerprint &&
this.#semanticTurnId !== turnId;
const repeatsPendingTransfer =
claimsLaterTurn &&
this.#pendingSemanticTransfer?.fingerprint === fingerprint &&
this.#pendingSemanticTransfer.turnId === turnId;
let feedback = "Completion report accepted. Task status is committed after this turn and workspace finalization finish.";
if (this.#semanticFingerprint === null || (claimsLaterTurn && !repeatsPendingTransfer)) {
try {
feedback = await this.#completionFeedback?.(validation.result) ?? feedback;
} catch (error) {
this.#emit(
"run.result.rejected",
{
result: validation.result,
reason: error instanceof Error ? error.message : String(error),
recovery: { required: true, recoverable: true },
},
{ turnId, itemId: call.callId },
);
return {
accepted: false,
error: error instanceof Error ? error.message : String(error),
};
}
if (this.#activeTurnId !== turnId) {
return {
accepted: false,
error: "The turn ended while checking completion. The result was not accepted.",
};
}
}
if (
this.#semanticFingerprint === null ||
(claimsLaterTurn && !repeatsPendingTransfer)
) {
const commitNormalizedInput = call.captureNormalizedInput?.(validation.result);
if (
!this.#emit("run.result.proposed", validation.result, {
turnId,
itemId: call.callId,
})
) {
throw new HarnessCapabilityUnavailableError(
"run.result.proposed",
"the event consumer must drain provider events before a semantic result can be accepted",
);
}
commitNormalizedInput?.();
if (claimsLaterTurn) {
// A reaffirming retry does not own the durable result until its
// provider turn completes successfully. A failed or interrupted
// retry must leave the last completed owner recoverable.
this.#pendingSemanticTransfer = {
result: structuredClone(validation.result),
fingerprint,
callId: call.callId,
turnId,
};
} else {
this.#semanticResult = structuredClone(validation.result);
this.#semanticFingerprint = fingerprint;
this.#semanticCallId = call.callId;
this.#semanticTurnId = turnId;
}
}
return { accepted: true, feedback };
}
if (!this.#dynamicToolHandler) {
throw new Error(`Unsupported Paperclip operation ${tool}`);
}
return await this.#dynamicToolHandler({
tool,
callId: call.callId,
providerSessionId: this.#host.identity().agentSessionId,
turnId,
arguments: structuredClone(call.arguments),
signal: call.signal,
});
}
async read(): Promise<Record<string, unknown>> {
return {
identity: this.#host.identity(),
binding: this.#host.binding(),
status: await this.#host.status(),
};
}
async reconcile(): Promise<Record<string, unknown>> {
const identity = this.#host.identity();
const status = await this.#host.status();
if (
status.agentSessionId &&
status.agentSessionId !== identity.agentSessionId
) {
throw new Error("ACPX reconciliation changed the provider session");
}
return { identity, status };
}
async usage(): Promise<Record<string, unknown> | null> {
return this.#usage === null ? null : structuredClone(this.#usage);
}
async transcript(): Promise<HarnessTranscriptSnapshot> {
return {
schema: "paperclip-runner/harness-transcript/v1",
complete: !this.#transcriptOmitted,
eventCount: this.#transcriptEventCount,
events: structuredClone(this.#transcript.map(({ event }) => event)),
omissionReason: this.#transcriptOmitted ? "retention_limit" : null,
};
}
async snapshot(): Promise<PersistedHarnessSession> {
const identity = this.#host.identity();
return {
driverKind: "acpx_runtime",
driverSessionId: identity.acpxRecordId,
providerSessionId: identity.agentSessionId,
providerRecoveryPolicy: this.#providerRecoveryPolicy,
runId: this.#input.runId,
normalizedSessionId: this.#input.normalizedSessionId,
activeTurnId: this.#activeTurnId,
lastSourceSequence: this.#sourceSequence,
providerIdentity: {
kind: "acpx",
normalizedSessionId: identity.normalizedSessionId,
acpxRecordId: identity.acpxRecordId,
backendSessionId: identity.backendSessionId,
agentSessionId: identity.agentSessionId,
profileDigest: identity.profileDigest,
workspaceDigest: identity.workspaceDigest,
requestedModel: identity.requestedModel,
effectiveModel: identity.effectiveModel,
permissionMode: identity.permissionMode,
...(identity.mode === undefined ? {} : { mode: identity.mode }),
...(identity.piThinkingLevel === undefined ? {} : { piThinkingLevel: identity.piThinkingLevel }),
...(identity.claudeEffort === undefined ? {} : { claudeEffort: identity.claudeEffort }),
...(identity.grokEffort === undefined ? {} : { grokEffort: identity.grokEffort }),
...(identity.grokDisableWebSearch === undefined ? {} : { grokDisableWebSearch: identity.grokDisableWebSearch }),
providerLifetimeFenceCandidates:
identity.providerLifetimeFenceCandidates,
},
semanticResult:
this.#semanticResult &&
this.#semanticFingerprint &&
this.#semanticTurnId
? {
result: structuredClone(this.#semanticResult),
fingerprint: this.#semanticFingerprint,
callId: this.#semanticCallId,
turnId: this.#semanticTurnId,
}
: null,
terminalTurns: [...this.#terminalTurns].map(
([terminalTurnId, fingerprint]) => ({
turnId: terminalTurnId,
fingerprint,
}),
),
pendingRuntimeRequests: this.pendingRuntimeRequests(),
};
}
async close(input: { reason: string }): Promise<void> {
if (this.#closePromise) return await this.#closePromise;
if (this.#closed) return;
this.#closingStarted = true;
const closePromise = this.#finishClose(input.reason);
this.#closePromise = closePromise;
try {
await closePromise;
this.#closed = true;
} catch (error) {
this.#scheduleHostCloseRecovery(input.reason);
throw error;
} finally {
if (this.#closePromise === closePromise) this.#closePromise = null;
}
}
async #finishClose(reason: string): Promise<void> {
const closingTurnId = this.#activeTurnId;
const pump = this.#activePump;
this.#cancelPendingRuntimeRequests(reason);
const hostClose =
this.#hostClosePromise ?? this.#startHostClose({ reason });
let hostCloseError: unknown = null;
try {
await settleWithin(hostClose, this.#closeSettlementTimeoutMs);
} catch (error) {
hostCloseError = error;
}
if (pump) {
await settleWithin(
pump.catch(() => undefined),
this.#closeSettlementTimeoutMs,
).catch(() => undefined);
}
if (closingTurnId && !this.#terminalTurns.has(closingTurnId)) {
const pendingTerminal = this.#pendingTerminal;
if (pendingTerminal?.turnId === closingTurnId) {
this.#publishTerminal(
pendingTerminal.turnId,
pendingTerminal.fingerprint,
pendingTerminal.eventType,
pendingTerminal.payload,
);
} else {
const reaffirmedSemanticResult =
this.#pendingSemanticTransfer?.turnId === closingTurnId
? this.#pendingSemanticTransfer.fingerprint
: null;
this.#publishTerminal(
closingTurnId,
canonicalJson({
status: "interrupted",
...(reaffirmedSemanticResult === null
? {}
: { reaffirmedSemanticResult }),
}),
"turn.interrupted",
{ status: "interrupted", stopReason: "session_closed" },
);
}
}
if (hostCloseError) {
this.#emit(
"harness.diagnostic",
{
code: "acpx_host_cleanup_deferred",
message:
"ACPX host cleanup exceeded its caller wait bound; the driver retains the exact cleanup until it settles.",
},
closingTurnId ? { turnId: closingTurnId } : {},
0,
);
}
this.#eventStreamClosed = true;
this.#events.close();
if (hostCloseError) {
throw hostCloseError;
}
}
#startHostClose(input: { reason: string }): Promise<void> {
const closePromise = this.#host.close(input).then(() => {
this.#hostClosed = true;
});
this.#hostClosePromise = closePromise;
this.#retainCleanup(closePromise);
return closePromise;
}
#scheduleHostCloseRecovery(reason: string): void {
if (this.#hostClosed || this.#hostCloseRecoveryPromise) return;
const failedOrPendingClose = this.#hostClosePromise;
if (!failedOrPendingClose) return;
// Never overlap the exact cleanup that exceeded the caller's wait bound.
// Once an attempt rejects, keep one delay-bounded recovery owner alive for
// a finite retry budget. A permanently pending attempt remains the sole
// owner while the runtime adapter independently terminates its children;
// repeated terminal failures settle instead of creating an immortal loop.
// Host cleanup does not logically close the harness session: #finishClose
// still owns terminal publication and event-stream closure after recovery.
const recovery = (async () => {
let attempt = failedOrPendingClose;
let retryCount = 0;
while (!this.#hostClosed) {
try {
await attempt;
return;
} catch (error) {
if (retryCount >= MAX_AUTONOMOUS_HOST_CLOSE_RETRIES) {
this.#quarantineCleanup(this.#host, reason);
throw error;
}
retryCount += 1;
}
await waitForCleanupRetry(
Math.max(1, Math.min(1_000, this.#closeSettlementTimeoutMs)),
);
if (this.#hostClosed) {
return;
}
if (this.#hostClosePromise === attempt) {
this.#hostClosePromise = null;
}
attempt = this.#startHostClose({
reason: `${reason} (automatic cleanup recovery ${retryCount})`,
});
}
})();
this.#hostCloseRecoveryPromise = recovery;
this.#retainCleanup(recovery);
void recovery
.finally(() => {
if (this.#hostCloseRecoveryPromise === recovery) {
this.#hostCloseRecoveryPromise = null;
}
})
.catch(() => undefined);
}
async #pumpTurn(turnId: string, turn: AcpxRuntimeTurn, drainExtensions: () => Promise<void>, usageBefore: unknown, activity: AcpxActivityAdapter, toolEvidence?: AcpxToolEvidence): Promise<void> {
try {
let index = 0;
const normalizeToolEvent =
createAcpxToolEventNormalizer<AcpRuntimeEvent>();
const piMessages = this.#agent === "pi" ? createPiMessageProjection<AcpRuntimeEvent>() : null;
const normalizeMessage = piMessages ? piMessages.normalize : this.#agent === "grok"
? createGrokMessageNormalizer<AcpRuntimeEvent>() : (event: AcpRuntimeEvent) => event;
for await (const event of turn.events) {
toolEvidence?.tool(event);
const projected = activity.toolExecutionId && event.type === "tool_call" && typeof event.toolCallId === "string"
? { ...event, toolCallId: activity.toolExecutionId(event.toolCallId) } : event;
this.#mapRuntimeEvent(normalizeMessage(normalizeToolEvent(projected)), turnId, ++index);
}
const result = await turn.result;
if (result.status === "completed") piMessages?.settle();
await drainExtensions();
const usageAfter = await readUsageStatus(this.#host);
// Diagnostic projection failures cannot replace the provider's terminal result.
try {
const notice = activity.usageNotice?.(usageBefore, usageAfter, turn.requestId, turnId);
if (notice) { validateAcpxRichEvent(notice); this.#emit(notice.eventType, notice.payload, { turnId, itemId: notice.itemId }); }
} catch { /* Partial native diagnostics are optional, never settlement authority. */ }
const receipt = persistedAcpxTurnUsage(usageBefore, usageAfter, turn.requestId, this.#agent);
if (receipt) {
this.#mapRuntimeEvent(receipt as unknown as AcpRuntimeEvent, turnId, ++index);
const estimate = acpxUsageEstimateNotice(receipt, `${turnId}:usage-estimate`);
if (estimate) { validateAcpxRichEvent(estimate); this.#emit(estimate.eventType, estimate.payload, { turnId, itemId: estimate.itemId }); }
}
this.#cancelPendingRuntimeRequests("provider turn settled", turnId);
if (this.#terminalTurns.has(turnId)) return;
if (result.status === "completed") {
const completedSemanticFingerprint =
this.#pendingSemanticTransfer?.turnId === turnId
? this.#pendingSemanticTransfer.fingerprint
: this.#semanticTurnId === turnId
? this.#semanticFingerprint
: null;
const finalText = this.#assistantText.trim();
if (finalText) {
this.#emit(
"item.completed",
{ kind: "agentMessage", channel: "final", text: finalText },
{ turnId, itemId: this.#assistantItemId(turnId, this.#assistantMessageId) },
);
}
this.#publishTerminal(
turnId,
canonicalJson({
status: "completed",
semanticResult: completedSemanticFingerprint,
}),
"turn.completed",
{ status: "completed", stopReason: result.stopReason ?? null },
);
} else if (result.status === "cancelled") {
const reaffirmedSemanticResult =
this.#pendingSemanticTransfer?.turnId === turnId
? this.#pendingSemanticTransfer.fingerprint
: null;
this.#publishTerminal(
turnId,
canonicalJson({
status: "interrupted",
...(reaffirmedSemanticResult === null
? {}
: { reaffirmedSemanticResult }),
}),
"turn.interrupted",
{
status: "interrupted",
stopReason: result.stopReason ?? "cancelled",
},
);
} else {
const reaffirmedSemanticResult =
this.#pendingSemanticTransfer?.turnId === turnId
? this.#pendingSemanticTransfer.fingerprint
: null;
this.#emit(
"provider.notice.recorded",
{
schema: "paperclip.provider.notice.v1",
noticeId: `${turnId}:failure`,
severity: "error",
category: "acpx_turn_failed",
scope: "turn",
recoverable: result.error.retryable ?? false,
userActionable: true,
summary: safeMessage(result.error.message),
},
{ turnId, itemId: `${turnId}:failure` },
);
this.#publishTerminal(
turnId,
canonicalJson({
status: "failed",
...(reaffirmedSemanticResult === null
? {}
: { reaffirmedSemanticResult }),
}),
"turn.failed",
{
status: "failed",
error: {
code: result.error.code ?? null,
message: safeMessage(result.error.message),
},
},
);
}
} catch (error) {
if (this.#terminalTurns.has(turnId)) return;
if (error instanceof TerminalEventCapacityError) throw error;
this.#cancelPendingRuntimeRequests(error instanceof AcpxProviderProcessLostError ? error : "provider turn failed", turnId);
if (this.#closed || this.#closingStarted) {
const reaffirmedSemanticResult =
this.#pendingSemanticTransfer?.turnId === turnId
? this.#pendingSemanticTransfer.fingerprint
: null;
this.#publishTerminal(
turnId,
canonicalJson({
status: "interrupted",
...(reaffirmedSemanticResult === null
? {}
: { reaffirmedSemanticResult }),
}),
"turn.interrupted",
{ status: "interrupted", stopReason: "session_closed" },
);
} else {
const reaffirmedSemanticResult =
this.#pendingSemanticTransfer?.turnId === turnId
? this.#pendingSemanticTransfer.fingerprint
: null;
this.#publishTerminal(
turnId,
canonicalJson({
status: "failed",
...(reaffirmedSemanticResult === null
? {}
: { reaffirmedSemanticResult }),
}),
"turn.failed",
{ status: "failed", error: { ...(error instanceof AcpxProviderProcessLostError ? { code: error.code } : {}), message: safeMessage(error) } },
);
}
}
}
#publishTerminal(
turnId: string,
fingerprint: string,
eventType: "turn.completed" | "turn.failed" | "turn.interrupted",
payload: Record<string, unknown>,
): void {
if (this.#terminalTurns.has(turnId)) return;
const pendingTerminal = this.#pendingTerminal;
if (
pendingTerminal !== null &&
(pendingTerminal.turnId !== turnId ||
pendingTerminal.fingerprint !== fingerprint ||
pendingTerminal.eventType !== eventType ||
canonicalJson(pendingTerminal.payload) !== canonicalJson(payload))
) {
throw new Error(
`Codex ACPX terminal disposition changed while ${pendingTerminal.turnId} awaited stream capacity`,
);
}
const terminal = pendingTerminal ?? {
turnId,
fingerprint,
eventType,
payload: structuredClone(payload),
};
this.#pendingTerminal = terminal;
if (!this.#emit(terminal.eventType, terminal.payload, { turnId })) {
// The persisted terminal index is the driver's settlement authority.
// Never advance it unless the matching canonical event is already in
// the bounded stream. A caller may drain and retry close, but it cannot
// observe a settled turn whose terminal fact was omitted.
throw new TerminalEventCapacityError(turnId, eventType);
}
this.#terminalTurns.set(turnId, fingerprint);
this.#pendingTerminal = null;
if (this.#pendingSemanticTransfer?.turnId === turnId) {
if (eventType === "turn.completed") {
this.#semanticResult = structuredClone(
this.#pendingSemanticTransfer.result,
);
this.#semanticFingerprint = this.#pendingSemanticTransfer.fingerprint;
this.#semanticCallId = this.#pendingSemanticTransfer.callId;
this.#semanticTurnId = turnId;
}
this.#pendingSemanticTransfer = null;
}
if (this.#activeTurnId === turnId) this.#activeTurnId = null;
}
#assistantItemId(turnId: string, messageId: string | null): string {
if (this.#agent !== "copilot") return `${turnId}:assistant-message`;
return messageId ? `${turnId}:assistant-message:${messageId}` : `${turnId}:assistant-activity`;
}
#mapRuntimeEvent(
event: PiProjectedMessageEvent<AcpRuntimeEvent>,
turnId: string,
index: number,
): void {
const fallbackItemId = `${turnId}:acp:${index}`;
if (event.type === "text_delta") {
const output = boundedText(event.text, 64 * 1024);
const isReasoning =
event.stream === "thought" || event.tag === "agent_thought_chunk";
const messageId = typeof event.messageId === "string" && event.messageId ? event.messageId : null;
// The pinned Copilot mapper supplies actual native identity, including
// empty starts. Unidentified session info/warnings remain activity only.
if (!isReasoning && !event.piMessageHistory && (this.#agent !== "copilot" || messageId)) {
if (piBoundaryClearsFinal(event)) this.#assistantText = "";
if (messageId && this.#assistantMessageId && messageId !== this.#assistantMessageId) {
if (this.#agent === "copilot" && this.#assistantText) {
this.#emit("item.completed", { kind: "agentMessage", channel: "commentary", text: this.#assistantText },
{ turnId, itemId: this.#assistantItemId(turnId, this.#assistantMessageId) });
}
this.#assistantText = "";
}
if (messageId) this.#assistantMessageId = messageId;
this.#assistantText = boundedText(
`${this.#assistantText}${output}`,
256 * 1024,
);
}
this.#emit(
"item.delta",
{
kind: isReasoning ? "reasoning" : "agentMessage",
channel: isReasoning ? "summary" : "unknown",
text: output,
},
{
turnId,
itemId: isReasoning
? `${turnId}:reasoning`
: this.#assistantItemId(turnId, messageId),
},
);
}
if (event.type === "status" && event.tag === "usage_update") {
this.#usage = boundedRecord({
cumulative: event.breakdown,
cost: event.cost,
});
}
for (const canonical of canonicalProviderEventsFromAcpxRuntimeEvent(
event,
fallbackItemId,
turnId,
)) {
this.#emit(canonical.eventType, canonical.payload, {
turnId,
itemId: canonical.itemId,
});
}
}
async #handleExtensionInput(
turnId: string,
input: AcpxExtensionInput,
context: { requestId: string | number; signal: AbortSignal; responseDelivery?: Promise<void> },
): Promise<Record<string, unknown>> {
if (this.#closed || this.#activeTurnId !== turnId || context.signal.aborted
|| this.#pendingRuntimeRequests.size >= MAX_PENDING_RUNTIME_REQUESTS) return input.cancel();
const responseDelivery = requireAcpxResponseDelivery(context);
const requestId = stableId("acpx-request", `${turnId}:${++this.#runtimeRequestSequence}:${typeof context.requestId}:${context.requestId}`);
const activity = acpxProfileActivity(this.#agent);
const toolCallId = activity.inputToolIdentity?.(input);
const itemId = toolCallId === undefined ? requestId : activity.toolExecutionId?.(toolCallId) ?? toolCallId;
const request: HarnessRuntimeRequest = {
requestId, requestKind: "elicitation", method: input.method, turnId, itemId,
status: "pending", prompt: boundedText(input.questionSet.title ?? "Provider needs input", 1_000),
details: input.details ?? {}, input: structuredClone(input.questionSet),
origin: { adapter: "acpx-runtime", provider: this.#agent, method: input.method },
};
return await new Promise((settle) => {
const cancel = () => {
const pending = this.#pendingRuntimeRequests.get(requestId);
if (!pending || pending.settling || !this.#pendingRuntimeRequests.delete(requestId)) return;
pending.cleanup();
this.#emitRequestAbort(request, context.signal.reason);
settle(input.cancel());
};
this.#pendingRuntimeRequests.set(requestId, {
request, responseDelivery,
prepareResolution: resolution => {
const response = input.resolve(resolution);
return () => settle(response);
},
cancel: () => settle(input.cancel()),
cleanup: () => context.signal.removeEventListener("abort", cancel),
settling: false,
});
context.signal.addEventListener("abort", cancel, { once: true });
if (!this.#emit("runtime_request.created", { request: runtimeInputProtocolPayload(request) },
{ turnId, itemId }) || context.signal.aborted) cancel();
});
}
async #handlePermission(
turnId: string,
request: AcpPermissionRequest,
context: { signal: AbortSignal; responseDelivery?: Promise<void> },
toolEvidence?: AcpxToolEvidence,
): Promise<AcpPermissionDecision> {
const { signal } = context;
if (this.#closed || this.#activeTurnId !== turnId || signal.aborted
|| this.#pendingRuntimeRequests.size >= MAX_PENDING_RUNTIME_REQUESTS) {
return { outcome: "cancel" };
}
const responseDelivery = requireAcpxResponseDelivery(context);
const normalized = normalizeAcpxPermission(request, {
provider: this.#agent, workingDirectory: this.#input.workingDirectory,
...(["pi", "copilot"].includes(this.#agent) ? { allowAlwaysScope: "session" } : {}),
});
const requestId = stableId("acpx-permission", `${turnId}:${++this.#runtimeRequestSequence}:${normalized.toolCallId}`);
const runtimeRequest: HarnessRuntimeRequest = {
requestId, requestKind: "permission_approval", method: "session/request_permission",
turnId, itemId: requestId, status: "pending", prompt: normalized.title,
details: { choices: normalized.choices, toolCallId: normalized.toolCallId, kind: normalized.kind },
origin: { adapter: "acpx-runtime", provider: this.#agent, method: "session/request_permission" },
};
const deliveredEvidence = toolEvidence?.permission(request, requestId, normalized.choices.map(choice => choice.key));
let deliveredOutcome: AcpPermissionDecision["outcome"] | undefined;
return await new Promise<AcpPermissionDecision>((settle) => {
const cancel = () => {
const pending = this.#pendingRuntimeRequests.get(requestId);
if (!pending || pending.settling || !this.#pendingRuntimeRequests.delete(requestId)) return;
pending.cleanup();
this.#emitRequestAbort(runtimeRequest, signal.reason);
settle({ outcome: "cancel" });
};
this.#pendingRuntimeRequests.set(requestId, {
request: runtimeRequest, responseDelivery,
deliveredEvidence: () => { if (deliveredOutcome) deliveredEvidence?.(deliveredOutcome); },
prepareResolution: (resolution) => {
const response = normalized.resolve(resolution);
deliveredOutcome = response.outcome;
return () => settle(response);
},
cancel: () => settle({ outcome: "cancel" }),
cleanup: () => signal.removeEventListener("abort", cancel),
settling: false,
});
signal.addEventListener("abort", cancel, { once: true });
if (!this.#emit("runtime_request.created", { request: {
schema: PAPERCLIP_RUNTIME_REQUEST_SCHEMA_V2, requestKind: "permission_approval",
requestId, type: "permission", status: "pending", prompt: normalized.title,
choices: normalized.choices, details: runtimeRequest.details,
origin: runtimeRequest.origin, turnId, itemId: requestId,
} }, { turnId, itemId: requestId }) || signal.aborted) cancel();
});
}
async #handleElicitation(
turnId: string,
request: AcpElicitationRequest,
context: AcpElicitationContext,
): Promise<AcpElicitationResponse> {
if (
this.#closed ||
this.#activeTurnId !== turnId ||
context.signal.aborted
) {
return { action: "cancel" };
}
if (this.#pendingRuntimeRequests.size >= MAX_PENDING_RUNTIME_REQUESTS) {
this.#emit(
"harness.diagnostic",
{
code: "runtime_input_limit_reached",
adapter: "acpx-runtime",
reason: "The active ACPX turn has too many pending input requests.",
},
{ turnId },
);
return { action: "cancel" };
}
let normalized: NormalizedAcpForm | null;
try {
normalized = normalizeAcpFormElicitation(request);
} catch (error) {
this.#emit(
"harness.diagnostic",
{
code: "runtime_input_rejected",
adapter: "acpx-runtime",
reason: safeMessage(error),
},
{ turnId },
);
return { action: "cancel" };
}
if (!normalized) {
this.#emit(
"harness.diagnostic",
{
code: "runtime_input_unsupported",
adapter: "acpx-runtime",
reason: "The ACPX provider requested an unsupported input mode.",
},
{ turnId },
);
return { action: "cancel" };
}
if (
normalized.questionSet.questions.some(
(question) => question.textValidation?.pattern !== undefined,
)
) {
this.#emit(
"harness.diagnostic",
{
code: "runtime_input_pattern_unsupported",
adapter: "acpx-runtime",
reason:
"ACPX form patterns require a bounded regular expression dialect.",
},
{ turnId },
);
return { action: "cancel" };
}
const responseDelivery = requireAcpxResponseDelivery(context);
const requestId = stableId(
"acpx-request",
`${turnId}:${++this.#runtimeRequestSequence}:${typeof context.requestId}:${String(context.requestId)}`,
);
const runtimeRequest: HarnessRuntimeRequest = {
requestId,
requestKind: "elicitation",
method: "elicitation/create",
turnId,
itemId: requestId,
status: "pending",
prompt: boundedText(
normalized.questionSet.title ??
normalized.questionSet.description ??
"Additional information is required.",
1_000,
),
details: { mode: "form" },
input: structuredClone(normalized.questionSet),
origin: {
adapter: "acpx-runtime",
provider: this.#agent,
method: "elicitation/create",
},
};
if (
!this.#emit(
"runtime_request.created",
{ request: runtimeInputProtocolPayload(runtimeRequest) },
{ turnId, itemId: requestId },
)
) {
return { action: "cancel" };
}
return await new Promise<AcpElicitationResponse>((settle) => {
const cancel = () => {
const pending = this.#pendingRuntimeRequests.get(requestId);
if (!pending || pending.settling) return;
if (!this.#pendingRuntimeRequests.delete(requestId)) return;
pending.cleanup();
this.#emitRequestAbort(runtimeRequest, context.signal.reason);
settle({ action: "cancel" });
};
context.signal.addEventListener("abort", cancel, { once: true });
this.#pendingRuntimeRequests.set(requestId, {
request: runtimeRequest, responseDelivery,
prepareResolution: (resolution) => {
const response = acpElicitationResponse(normalized, resolution);
return () => settle(response);
},
cancel: () => settle({ action: "cancel" }),
cleanup: () => context.signal.removeEventListener("abort", cancel),
settling: false,
});
if (context.signal.aborted) cancel();
});
}
#emitRequestAbort(request: HarnessRuntimeRequest, reason: unknown): void {
const lost = reason instanceof AcpxProviderProcessLostError;
this.#emit(lost ? "runtime_request.expired" : "runtime_request.cancelled", {
...harnessRuntimeRequestOutcome(request, {
...(lost ? {} : { action: "cancel" as const }),
reason: lost ? "provider_process_lost" : typeof reason === "string" ? boundedText(reason, 1_000) : "provider request aborted",
}),
...(lost ? { replayAllowed: false } : {}),
}, { turnId: request.turnId, itemId: request.itemId });
}
#cancelPendingRuntimeRequests(reason: string | AcpxProviderProcessLostError, turnId?: string): void {
for (const [requestId, pending] of this.#pendingRuntimeRequests) {
if (turnId && pending.request.turnId !== turnId) continue;
if (!this.#pendingRuntimeRequests.delete(requestId)) continue;
pending.cleanup();
this.#emitRequestAbort(pending.request, reason);
pending.cancel();
}
}
#emit(
eventType: PrpEvent["eventType"],
payload: Record<string, unknown>,
refs: { turnId?: string; itemId?: string } = {},
reservedAfter = isTerminalEvent(eventType)
? 0
: eventType === "run.result.proposed"
? Math.max(0, this.#terminalEventReserve - 1)
: this.#terminalEventReserve,
): boolean {
if (this.#eventStreamClosed) return false;
if (this.#eventStreamOmitted && isTerminalEvent(eventType)) {
this.#eventStreamOmitted = false;
const recordedOmission = this.#emit(
"harness.diagnostic",
{
code: "event_stream_retention_limit",
message:
"Earlier provider events were omitted because the consumer exceeded the bounded event buffer.",
},
refs,
1,
);
if (!recordedOmission) this.#eventStreamOmitted = true;
}
const sourceSeq = this.#sourceSequence + 1;
const event: PrpEvent = {
schema: "paperclip.prp.event.v1",
sourceEventId: `${this.#sourceInstanceId}:${sourceSeq}`,
sourceSeq,
sourceInstanceId: this.#sourceInstanceId,
sourceKind: "runner",
runId: this.#input.runId,
normalizedSessionId: this.#input.normalizedSessionId,
...(refs.turnId ? { turnId: refs.turnId } : {}),
...(refs.itemId ? { itemId: refs.itemId } : {}),
eventType,
schemaVersion: 1,
priority: eventType === "run.result.proposed" ? 0 : 1,
emittedAt: this.#now().toISOString(),
payload: structuredClone(payload),
};
if (!this.#events.push(event, reservedAfter)) {
this.#eventStreamOmitted = true;
this.#transcriptEventCount += 1;
this.#transcriptOmitted = true;
return false;
}
this.#sourceSequence = sourceSeq;
this.#retainTranscriptEvent(event);
return true;
}
#retainTranscriptEvent(event: PrpEvent): void {
this.#transcriptEventCount += 1;
const retained = structuredClone(event);
let bytes: number;
try {
bytes = Buffer.byteLength(JSON.stringify(retained));
} catch {
this.#transcriptOmitted = true;
return;
}
if (bytes > MAX_TRANSCRIPT_BYTES) {
this.#transcriptOmitted = true;
return;
}
this.#transcript.push({ event: retained, bytes });
this.#transcriptBytes += bytes;
while (
this.#transcript.length > MAX_TRANSCRIPT_EVENTS ||
this.#transcriptBytes > MAX_TRANSCRIPT_BYTES
) {
const omitted = this.#transcript.shift();
if (omitted) this.#transcriptBytes -= omitted.bytes;
this.#transcriptOmitted = true;
}
}
#assertOpen(): void {
if (this.#closed || this.#closingStarted) {
throw new Error("Codex ACPX session is closing or closed");
}
}
}
function waitForCleanupRetry(delayMs: number): Promise<void> {
return new Promise((resolve) => {
const timer = setTimeout(resolve, delayMs);
timer.unref();
});
}
function canonicalJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
if (typeof value === "object" && value !== null) {
const record = value as Record<string, unknown>;
return `{${Object.keys(record)
.sort()
.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
.join(",")}}`;
}
return JSON.stringify(value) ?? "undefined";
}
function acpElicitationResponse(
normalized: NormalizedAcpForm,
resolution: HarnessRuntimeRequestResolution,
): AcpElicitationResponse {
if (resolution.action === "submit") {
if (!("response" in resolution)) {
throw new HarnessRuntimeRequestResolutionError(
"elicitation",
"ACPX form submissions require a canonical question response",
);
}
return normalized.accept(resolution.response);
}
if (resolution.action === "accept_for_session") {
throw new HarnessRuntimeRequestResolutionError(
"elicitation",
"ACPX form input does not support session acceptance",
);
}
return { action: resolution.action };
}
function runtimeInputProtocolPayload(
request: HarnessRuntimeRequest,
): Record<string, unknown> {
if (!request.input) {
throw new Error("ACPX runtime input request omitted its question set");
}
return {
schema: PAPERCLIP_RUNTIME_REQUEST_SCHEMA_V2,
requestKind: "runtime",
requestId: request.requestId,
type: "input",
status: request.status,
prompt: request.prompt,
input: structuredClone(request.input),
origin: structuredClone(request.origin),
turnId: request.turnId,
itemId: request.itemId,
};
}
function validateRecoverySnapshot(snapshot: PersistedHarnessSession): void {
if (
snapshot.driverKind !== "acpx_runtime" ||
!snapshot.runId?.trim() ||
!snapshot.normalizedSessionId?.trim() ||
snapshot.providerIdentity?.kind !== "acpx"
) {
throw new Error("persisted Codex ACPX session identity is incomplete");
}
const identity = snapshot.providerIdentity;
if (
!boundedIdentity(snapshot.runId) ||
!boundedIdentity(snapshot.normalizedSessionId) ||
identity.normalizedSessionId !== snapshot.normalizedSessionId ||
identity.acpxRecordId !== snapshot.driverSessionId ||
identity.agentSessionId !== snapshot.providerSessionId ||
![
identity.normalizedSessionId,
identity.acpxRecordId,
identity.backendSessionId,
identity.agentSessionId,
identity.requestedModel,
identity.effectiveModel,
].every(boundedIdentity) ||
!/^sha256:[a-f0-9]{64}$/.test(identity.profileDigest) ||
!/^sha256:[a-f0-9]{64}$/.test(identity.workspaceDigest) ||
(identity.permissionMode !== undefined &&
!["approve-all", "approve-paperclip", "approve-reads", "deny-all"].includes(
identity.permissionMode,
)) ||
(identity.mode !== undefined && !isProviderMode(identity.mode)) ||
(identity.claudeEffort !== undefined && !["low", "medium", "high", "xhigh", "max"].includes(identity.claudeEffort)) ||
(identity.grokDisableWebSearch !== undefined && typeof identity.grokDisableWebSearch !== "boolean") ||
(identity.grokEffort !== undefined && !["minimal", "low", "medium", "high", "xhigh"].includes(identity.grokEffort)) ||
(identity.piThinkingLevel !== undefined && !["off", "low", "high", "max"].includes(identity.piThinkingLevel)) ||
!validProviderLifetimeFenceCandidates(
identity.providerLifetimeFenceCandidates,
)
) {
throw new Error("persisted Codex ACPX session identity is inconsistent");
}
if (
snapshot.providerRecoveryPolicy !== undefined &&
snapshot.providerRecoveryPolicy !== "same_session_only"
) {
throw new Error("persisted Codex ACPX recovery policy is unsupported");
}
if (
(snapshot.pendingRuntimeRequests?.length ?? 0) > 0 ||
(snapshot.lineage?.length ?? 0) > 0 ||
snapshot.goal != null
) {
throw new Error("persisted Codex ACPX snapshot has unsupported state");
}
if (
snapshot.lastSourceSequence !== undefined &&
(!Number.isSafeInteger(snapshot.lastSourceSequence) ||
snapshot.lastSourceSequence < 0)
) {
throw new Error("persisted Codex ACPX source sequence is invalid");
}
if (
snapshot.terminalTurns !== undefined &&
!Array.isArray(snapshot.terminalTurns)
) {
throw new Error("persisted Codex ACPX terminal history is invalid");
}
const terminalTurns = snapshot.terminalTurns ?? [];
if (terminalTurns.length > MAX_RECOVERY_TERMINAL_TURNS) {
throw new Error("persisted Codex ACPX terminal history exceeds its limit");
}
const terminalTurnIds = new Set<string>();
let terminalBytes = 0;
for (const terminal of terminalTurns) {
terminalBytes +=
Buffer.byteLength(terminal.turnId ?? "") +
Buffer.byteLength(terminal.fingerprint ?? "");
if (
!boundedIdentity(terminal.turnId) ||
!terminal.fingerprint ||
Buffer.byteLength(terminal.fingerprint) > 256 * 1024 ||
terminalBytes > MAX_RECOVERY_TERMINAL_BYTES ||
terminalTurnIds.has(terminal.turnId)
) {
throw new Error("persisted Codex ACPX terminal turn is invalid");
}
terminalTurnIds.add(terminal.turnId);
}
if (
snapshot.activeTurnId !== undefined &&
snapshot.activeTurnId !== null &&
!boundedIdentity(snapshot.activeTurnId)
) {
throw new Error("persisted Codex ACPX active turn is invalid");
}
const semantic = snapshot.semanticResult;
if (semantic) {
const validation = validatePrpStructuredRunResult(semantic.result);
if (
!validation.ok ||
semantic.fingerprint !== canonicalJson(validation.result) ||
!boundedIdentity(semantic.turnId) ||
(semantic.callId !== undefined &&
semantic.callId !== null &&
!boundedIdentity(semantic.callId))
) {
throw new Error("persisted Codex ACPX semantic result is invalid");
}
const semanticTerminalIndex = terminalTurns.findIndex(
(terminal) => terminal.turnId === semantic.turnId,
);
const semanticTerminal = terminalTurns[semanticTerminalIndex];
if (
!semanticTerminal ||
!isCompletedSemanticTerminal(
semanticTerminal.fingerprint,
semantic.fingerprint,
)
) {
throw new Error(
"persisted Codex ACPX semantic result has no completed terminal turn",
);
}
if (semanticTerminalIndex !== terminalTurns.length - 1) {
// A later failed or interrupted turn may have reaffirmed the same result,
// but it still supersedes the earlier settlement as the latest durable
// provider fact. Recovery must not finalize an earlier success after a
// newer attempt failed to complete.
throw new Error(
"persisted Codex ACPX semantic result is not the latest terminal settlement",
);
}
if (snapshot.activeTurnId !== undefined && snapshot.activeTurnId !== null) {
const activeTerminal = terminalTurns.find(
(terminal) => terminal.turnId === snapshot.activeTurnId,
);
if (
snapshot.activeTurnId !== semantic.turnId ||
!activeTerminal ||
!isCompletedSemanticTerminal(
activeTerminal.fingerprint,
semantic.fingerprint,
)
) {
throw new Error(
"persisted Codex ACPX active turn is not the completed semantic settlement",
);
}
}
} else if (terminalTurns.length > 0) {
const latestTerminalTurnId = terminalTurns.at(-1)!.turnId;
const settlementTurnId = snapshot.activeTurnId ?? latestTerminalTurnId;
const settlement = terminalTurns.find(
(terminal) => terminal.turnId === settlementTurnId,
);
if (
settlementTurnId !== latestTerminalTurnId ||
!settlement ||
!isCompletedTerminal(settlement.fingerprint)
) {
throw new Error(
"persisted Codex ACPX resultless recovery requires a completed terminal turn",
);
}
}
}
function validProviderLifetimeFenceCandidates(
value: unknown,
): value is readonly [number, number, number] {
return (
Array.isArray(value) &&
value.length === 3 &&
value.every(
(port) => Number.isSafeInteger(port) && port >= 49_152 && port <= 65_535,
) &&
new Set(value).size === 3
);
}
function isCompletedTerminal(terminalFingerprint: string): boolean {
try {
const value: unknown = JSON.parse(terminalFingerprint);
return (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
(value as Record<string, unknown>).status === "completed"
);
} catch {
return false;
}
}
function isCompletedSemanticTerminal(
terminalFingerprint: string,
semanticFingerprint: string,
): boolean {
try {
const value: unknown = JSON.parse(terminalFingerprint);
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return false;
}
const terminal = value as Record<string, unknown>;
return (
terminal.status === "completed" &&
terminal.semanticResult === semanticFingerprint
);
} catch {
return false;
}
}
function boundedIdentity(value: unknown): value is string {
return (
typeof value === "string" &&
value.length > 0 &&
value.length <= 240 &&
!/[\u0000-\u001f\u007f]/.test(value)
);
}
function boundedRecord(value: unknown): Record<string, unknown> {
const serialized = JSON.stringify(value);
if (!serialized || Buffer.byteLength(serialized) > 64 * 1024) {
return { omitted: true, reason: "payload_limit" };
}
const parsed: unknown = JSON.parse(serialized);
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
? (parsed as Record<string, unknown>)
: {};
}
function boundedText(value: string, maxBytes: number): string {
const bytes = Buffer.from(value);
return bytes.length <= maxBytes
? value
: bytes.subarray(bytes.length - maxBytes).toString("utf8");
}
function safeId(value: string, fallback: string): string {
const candidate = value.replace(/[^A-Za-z0-9._:-]/g, "-").slice(0, 160);
return /^[A-Za-z0-9]/.test(candidate) ? candidate : fallback;
}
function stableId(prefix: string, value: string): string {
const readable = safeId(value, "session").slice(0, 80);
const suffix = createHash("sha256").update(value).digest("hex").slice(0, 16);
return `${prefix}-${readable}-${suffix}`;
}
function safeMessage(error: unknown): string {
const message = error instanceof Error ? error.message : String(error);
return message
.replace(
/(\bauthorization\s*[:=]\s*)(?:"(?:\\.|[^"\r\n])*"|'(?:\\.|[^'\r\n])*'|[^\r\n,}\]]*)/gi,
"$1[REDACTED]",
)
.replace(
/(\b(?:key|token|secret|password)\s*[:=]\s*)(?:"(?:\\.|[^"\r\n])*"|'(?:\\.|[^'\r\n])*'|[^\s,}\]]+)/gi,
"$1[REDACTED]",
)
.slice(0, 4_000);
}
function reportRetainedAcpxCleanupFailure(
input: AcpxRetainedCleanupFailure,
): void {
const errorName = input.error instanceof Error ? input.error.name : "Error";
process.emitWarning(
JSON.stringify({
schema: "paperclip.runner.retained_cleanup_failure.v1",
resource: input.resource,
attempt: input.attempt,
errorName,
}),
{
code: "PAPERCLIP_ACPX_RETAINED_CLEANUP_FAILURE",
type: "PaperclipRunnerCleanupWarning",
},
);
}
function isTerminalEvent(eventType: PrpEvent["eventType"]): boolean {
return (
eventType === "turn.completed" ||
eventType === "turn.failed" ||
eventType === "turn.interrupted"
);
}
class TerminalEventCapacityError extends Error {
constructor(turnId: string, eventType: PrpEvent["eventType"]) {
super(
`Codex ACPX cannot settle ${turnId} before its ${eventType} event is retained`,
);
this.name = "TerminalEventCapacityError";
}
}
class AsyncQueue<T> implements AsyncIterable<T> {
readonly #items: T[] = [];
readonly #waiters: Array<(result: IteratorResult<T>) => void> = [];
readonly #maxItems: number;
#closed = false;
constructor(maxItems: number) {
this.#maxItems = maxItems;
}
hasCapacity(requiredItems: number): boolean {
return (
requiredItems <=
this.#waiters.length + this.#maxItems - this.#items.length
);
}
push(item: T, reservedAfter = 0): boolean {
if (this.#closed) return false;
// Admission and insertion are one synchronous operation so every regular
// event leaves the terminal lane intact. In particular, a lagging
// consumer cannot occupy terminal capacity between a check and a push.
if (!this.hasCapacity(1 + reservedAfter)) return false;
const waiter = this.#waiters.shift();
if (waiter) {
waiter({ done: false, value: item });
return true;
}
if (this.#items.length >= this.#maxItems) return false;
this.#items.push(item);
return true;
}
close(): void {
if (this.#closed) return;
this.#closed = true;
for (const waiter of this.#waiters.splice(0)) {
waiter({ done: true, value: undefined });
}
}
[Symbol.asyncIterator](): AsyncIterator<T> {
return {
next: () => {
const item = this.#items.shift();
if (item !== undefined) {
return Promise.resolve({ done: false, value: item });
}
if (this.#closed) {
return Promise.resolve({ done: true, value: undefined });
}
return new Promise((resolve) => this.#waiters.push(resolve));
},
};
}
}
async function settleWithin(
promise: Promise<void>,
timeoutMs: number,
): Promise<void> {
let timer: ReturnType<typeof setTimeout> | undefined;
try {
await Promise.race([
promise,
new Promise<void>((_resolve, reject) => {
timer = setTimeout(() => {
reject(new Error("ACPX host cleanup exceeded its shutdown timeout"));
}, timeoutMs);
timer.unref();
}),
]);
} finally {
if (timer) clearTimeout(timer);
}
}
async function runAbortableDriverAdmission<T>(
signal: AbortSignal | undefined,
operation: () => Promise<T>,
): Promise<T> {
if (signal === undefined) return await operation();
signal.throwIfAborted();
return await raceDriverAdmissionWithAbort(
Promise.resolve().then(operation),
signal,
);
}
function raceDriverAdmissionWithAbort<T>(
pending: Promise<T>,
signal: AbortSignal,
): Promise<T> {
return new Promise<T>((resolve, reject) => {
let settled = false;
const settle = (operation: () => void): void => {
if (settled) return;
settled = true;
signal.removeEventListener("abort", onAbort);
operation();
};
const onAbort = (): void => settle(() => reject(signal.reason));
signal.addEventListener("abort", onAbort, { once: true });
if (signal.aborted) {
onAbort();
return;
}
void pending.then(
(value) => settle(() => resolve(value)),
(error: unknown) => settle(() => reject(error)),
);
});
}
async function settlesWithin(
promise: Promise<unknown>,
timeoutMs: number,
): Promise<boolean> {
let timer: ReturnType<typeof setTimeout> | undefined;
try {
return await Promise.race([
promise.then(() => true),
new Promise<false>((resolve) => {
timer = setTimeout(() => resolve(false), timeoutMs);
timer.unref?.();
}),
]);
} finally {
if (timer) clearTimeout(timer);
}
}
/** Accounting failure cannot replace the provider's terminal result. */
async function readUsageStatus(host: CodexAcpxHost): Promise<unknown> {
let timer: ReturnType<typeof setTimeout> | undefined;
try {
return await Promise.race([
Promise.resolve().then(() => host.status()),
new Promise<undefined>(resolve => { timer = setTimeout(() => resolve(undefined), 1_000); timer.unref?.(); }),
]);
} catch { return undefined; }
finally { if (timer) clearTimeout(timer); }
}