mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 04:25:52 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The server prepares an execution environment before it starts an agent. > - Operators need to distinguish preparation, provider wait, and response persistence. > - Existing preparation phases start separate traces, and the native session trace ends before the final response is saved. > - This pull request adds an execution-attempt trace and detailed spans at the existing work boundaries. > - The benefit is a measurable startup and settlement path without changing execution policy. ## Linked Issues or Issue Description **What existing behavior does this improve?** Operator-configured OpenTelemetry tracing for agent preparation, startup, and durable response storage. **Subsystem affected** Server orchestration, native Runner startup, and the Honeycomb run link. **Current behavior** Preparation phases start independent roots. Workspace lock waits, authorization groups, runtime asset work, provider-pack fallback, and the gap after provider completion are hard to attribute. **Proposed behavior** Use a server-owned async context for each execution attempt. Parent nested work to that attempt. Link the separately sampled native session because its ingress and queue history can begin earlier. Record transaction entry, lock waits, grouped authorization, asset preparation, artifact fallback, copyback, and response commit. **Reason and benefit** Operators can identify expensive work from recorded boundaries instead of inferring the cause from elapsed time. Nested and concurrent durations remain visible and must not be added together. **Breaking changes** No API or execution-policy change. New Observability span names and closed count attributes are documented. The existing local run-performance schema also records the additional provider-pack and harness measurements. Refs: #16178, #16205, #16330. ## What Changed - Add `task.execution_attempt` with same-run async ownership, independent root sampling, and native-session links. - Measure workspace transaction entry and settlement, advisory and row waits, source scans, reader groups, notifications, and writes. - Measure runtime selection and each concurrent instructions, skills, and MCP branch. Separate manifest, cache, materialization, and verification work. - Measure provider-pack discovery, verified reuse or upload, final verification, and remote harness state checks. - Measure workspace settlement, accounting, liveness, response authorization, comment commit, and publication enqueue. - Preserve the silent endpoint-off path, including when another library has registered a tracer. Bound repeated detail spans to 16 per name per attempt. - Extend the Honeycomb run link and document the actual coverage and remaining gaps. ## Verification - After resolving the conflict with retained-reader batching on `b98cdaea531c8447637cd4dcc19c61b3f5f41844`, all 61 targeted query, authorization, workspace-preparation and trace-boundary tests passed. The server typecheck passed. Reader tests still bound query count, avoid loading retained run payloads and enforce fresh project grants on every publication attempt. - Before that base integration, the tracing/exporter group passed 855 tests with 2 optional-SDK skips across 10 files and no inherited OTLP endpoint. It covers real PostgreSQL waits/rollback, durable response commit, assembled heartbeat preparation, concurrent parentage, sampling and tracer failure isolation. Native workspace/history tests passed 23 cases. These results remain supporting evidence for the unchanged tracing paths. - Full workspace typecheck and build passed during implementation. Full sharded CI, release validation, E2E and Apex 5/5 passed on prior head `8cebad80f3856dbcff6f3a6d5d34880ff77a20f2`. Fresh CI, security and Apex review are required on the current head before merge. - `pnpm check:token-gates` passed. No styles changed. - Earlier CI runs exposed exact-source-expression assertions changed by wrappers, missing run hashes on transaction-boundary spans, and sampling fixtures without an enabled endpoint. The fixes preserve the calls, add the declared hash and span cap, and enable tracing explicitly in the sampling fixtures. Focused repairs passed; the full prior-head CI passed after those repairs. Earlier failed run: https://github.com/paperclipai/paperclip/actions/runs/38729131366. - The full local `pnpm test:run` was stopped after the base advanced during that process. Its mixed-version results are not final-head evidence. A full local-suite pass is not claimed. - The old-head Superagent scan stalled at 35%. The current head has a fresh scan; no security pass is inferred from the old pending result. - No deployed Honeycomb acceptance is claimed by this code change. ## Risks - Enabled tracing creates more spans. Repeated detail spans are bounded, and inventories use group spans. - The attempt and native session remain linked traces. Historical queue time can precede the attempt, and nested intervals overlap. - A committed comment proves durable server storage. Publication enqueue and realtime dispatch do not prove external delivery or browser paint. - New custom attributes use only the existing run hash and validated file/byte counts. No SQL, paths, permissions, content, or exception text is added. ## Model Used OpenAI GPT-6 through Codex. The serving model ID and context window size are not exposed in this session. The agent used reasoning, tools, code editing, and code execution. ## 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 - [ ] All Paperclip CI gates are green - [ ] 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>
2501 lines
86 KiB
TypeScript
2501 lines
86 KiB
TypeScript
import { createHash, randomUUID } from "node:crypto";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import {
|
|
chmod,
|
|
mkdir,
|
|
mkdtemp,
|
|
readFile,
|
|
readdir,
|
|
rm,
|
|
writeFile,
|
|
} from "node:fs/promises";
|
|
import { createServer } from "node:http";
|
|
import { tmpdir } from "node:os";
|
|
import { join, resolve } from "node:path";
|
|
import { eq, sql } from "drizzle-orm";
|
|
import {
|
|
agents,
|
|
companies,
|
|
completionContracts,
|
|
createDb,
|
|
heartbeatRunEvents,
|
|
heartbeatRuns,
|
|
issues,
|
|
nativeRunResults,
|
|
} from "@paperclipai/db";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
CodexAppServerDriver,
|
|
HarnessDriverBackend,
|
|
createCodexTaskEnvelope,
|
|
createRunnerdCodexTransport as createCapabilityRunnerdCodexTransport,
|
|
} from "@paperclipai/paperclip-runner";
|
|
import {
|
|
getEmbeddedPostgresTestSupport,
|
|
startEmbeddedPostgresTestDatabase,
|
|
} from "../../__tests__/helpers/embedded-postgres.js";
|
|
import {
|
|
canonicalNativeRuntimeContextDigest,
|
|
defaultCapabilityRunnerdBinary,
|
|
executeNativeSession,
|
|
parseNativeExecutionInput,
|
|
type NativeExecutionInput,
|
|
type NativeSession,
|
|
} from "../../vendor/paperclip-runner/index.js";
|
|
import {
|
|
runnerPrpWebSocketInternals,
|
|
setupRunnerPrpWebSocketServer,
|
|
} from "../../realtime/runner-prp-ws.js";
|
|
import {
|
|
appendRetainedNativeCleanupEvent,
|
|
createRunnerdBackend,
|
|
retainedNativeCleanupJournalMatches,
|
|
} from "./native-session-executor.js";
|
|
import * as publicationSignals from "../chat-publication-reconciliation.js";
|
|
import { PaperclipControlPlanePort } from "./paperclip-control-plane-port.js";
|
|
import {
|
|
LIST_CHAT_ATTACHMENTS_TOOL_DEFINITION,
|
|
REUSE_CHAT_ATTACHMENT_TOOL_DEFINITION,
|
|
} from "./chat-attachment-reuse.js";
|
|
import { buildNativeExecutionInput } from "./native-execution-input.js";
|
|
import {
|
|
buildNativeExecutionWithCheckpoint,
|
|
findNativeSessionResumeRun,
|
|
NATIVE_TOOL_CONTRACT_FINGERPRINT,
|
|
isUnusedLegacyNativeRetryReplacement,
|
|
nativeToolContractFingerprintForTarget,
|
|
nativeSessionIdForBootstrapPersistence,
|
|
nativeSessionProviderEvidence,
|
|
prepareNativeSessionBootstrapPersistence,
|
|
rebindNativeSessionCheckpoint,
|
|
selectNativeSessionResumeRun,
|
|
} from "./native-session-resume.js";
|
|
import { nativeRuntimeContextFixture } from "./runtime-context.test-fixture.js";
|
|
|
|
const companyId = "10000000-0000-4000-8000-000000000001";
|
|
const issueId = "20000000-0000-4000-8000-000000000002";
|
|
const agentId = "30000000-0000-4000-8000-000000000003";
|
|
const previousRunId = "40000000-0000-4000-8000-000000000004";
|
|
const currentRunId = "50000000-0000-4000-8000-000000000005";
|
|
const normalizedSessionId = "60000000-0000-4000-8000-000000000006";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function execution(
|
|
runId: string,
|
|
cwd = "/workspace",
|
|
workMode = "standard",
|
|
workspace: {
|
|
id?: string;
|
|
repoUrl?: string | null;
|
|
repoRef?: string | null;
|
|
branchName?: string | null;
|
|
} = {},
|
|
) {
|
|
return buildNativeExecutionInput({
|
|
companyId,
|
|
runId,
|
|
issue: {
|
|
id: issueId,
|
|
identifier: "DOT-2",
|
|
title: "Test",
|
|
description: null,
|
|
workMode,
|
|
},
|
|
taskPrompt: "Only the current turn",
|
|
agentId,
|
|
workspace: {
|
|
id: workspace.id ?? runId,
|
|
cwd,
|
|
repoUrl: workspace.repoUrl ?? null,
|
|
repoRef: workspace.repoRef ?? null,
|
|
branchName: workspace.branchName ?? null,
|
|
},
|
|
normalizedSessionId,
|
|
provider: "codex",
|
|
completionContract: {
|
|
id: "70000000-0000-4000-8000-000000000007",
|
|
sha256: `sha256:${"a".repeat(64)}`,
|
|
schemaVersion: "paperclip.run-result.v1",
|
|
contract: {
|
|
revision: "1",
|
|
objective: "Test session resumption",
|
|
criteria: [{ id: "objective", requirement: "Answer the current turn" }],
|
|
},
|
|
},
|
|
runtimeContext: nativeRuntimeContextFixture(),
|
|
});
|
|
}
|
|
|
|
function planningExecution(runId: string, revisionId: string) {
|
|
return buildNativeExecutionInput({
|
|
companyId,
|
|
runId,
|
|
issue: {
|
|
id: issueId,
|
|
identifier: "DOT-2",
|
|
title: "Test",
|
|
description: null,
|
|
workMode: "planning",
|
|
},
|
|
taskPrompt: "Revise the plan",
|
|
agentId,
|
|
workspace: {
|
|
id: runId,
|
|
cwd: "/workspace",
|
|
repoUrl: null,
|
|
repoRef: null,
|
|
branchName: null,
|
|
},
|
|
normalizedSessionId,
|
|
provider: "codex",
|
|
executionMode: "plan",
|
|
planningContext: {
|
|
documentId: "80000000-0000-4000-8000-000000000008",
|
|
baseRevisionId: revisionId,
|
|
baseRevisionNumber: revisionId.endsWith("9") ? 9 : 8,
|
|
markdown: `Plan at ${revisionId}`,
|
|
sha256: `sha256:${"b".repeat(64)}`,
|
|
reviewContext: {},
|
|
},
|
|
completionContract: execution(runId).completionContract,
|
|
runtimeContext: nativeRuntimeContextFixture(),
|
|
});
|
|
}
|
|
|
|
function previousRun(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
id: previousRunId,
|
|
companyId,
|
|
agentId,
|
|
nativeSessionId: normalizedSessionId,
|
|
runnerProfileJson: {
|
|
nativeToolContractFingerprint: NATIVE_TOOL_CONTRACT_FINGERPRINT,
|
|
nativeExecutionInput: execution(previousRunId),
|
|
sessionCheckpoint: {
|
|
backendKind: "runner",
|
|
driverKind: "codex_app_server",
|
|
sessionId: "provider-thread-123",
|
|
providerSessionId: "provider-thread-123",
|
|
cursor: "42",
|
|
identity: {
|
|
runId: previousRunId,
|
|
sessionId: normalizedSessionId,
|
|
companyId,
|
|
issueId,
|
|
agentId,
|
|
},
|
|
semanticResult: {
|
|
schema: "paperclip.run-result.v1",
|
|
reportedWorkDisposition: "done",
|
|
summary: "old",
|
|
},
|
|
terminal: {
|
|
schema: "paperclip.prp.terminal.v1",
|
|
turnTerminalState: "completed",
|
|
runTerminalState: "succeeded",
|
|
reportedWorkDisposition: "done",
|
|
},
|
|
activeTurnId: "old-turn",
|
|
terminalTurns: [{ turnId: "old-turn", state: "completed" }],
|
|
pendingRuntimeRequests: [{ requestId: "old-request" }],
|
|
lineage: [{ threadId: "provider-thread-123" }],
|
|
},
|
|
...overrides,
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("capability-gated connection tool refresh", () => {
|
|
it("retains the exact provider session for an MCP-only change when the harness supports it", () => {
|
|
const current = execution(currentRunId);
|
|
current.runtimeContext.mcp.digest = "b".repeat(64);
|
|
current.runtimeContext.aggregateDigest = canonicalNativeRuntimeContextDigest(current.runtimeContext);
|
|
const rebound = rebindNativeSessionCheckpoint({ previousRun: previousRun(), currentExecution: current, toolRefreshOnResume: true, refreshTools: true });
|
|
expect(rebound).toMatchObject({ sessionId: "provider-thread-123", providerSessionId: "provider-thread-123", identity: { runId: currentRunId }, providerRecoveryPolicy: "allow_replacement_after_resume_failure" });
|
|
expect(rebindNativeSessionCheckpoint({ previousRun: previousRun(), currentExecution: current })).toBeNull();
|
|
expect(rebindNativeSessionCheckpoint({ previousRun: previousRun(), currentExecution: current, refreshTools: true, toolRefreshOnResume: false })).toBeNull();
|
|
});
|
|
|
|
it.each(["add", "edit", "remove"])("replaces sessions when connection instructions %s, even with live tool refresh", (change) => {
|
|
const block = (text: string) => ({ text, digest: createHash("sha256").update(text).digest("hex") });
|
|
const previous = execution(previousRunId);
|
|
const current = execution(currentRunId);
|
|
if (change !== "add") previous.runtimeContext.connectionInstructions = block("Prior connection instructions.");
|
|
if (change !== "remove") current.runtimeContext.connectionInstructions = block("Current connection instructions.");
|
|
previous.runtimeContext.aggregateDigest = canonicalNativeRuntimeContextDigest(previous.runtimeContext);
|
|
current.runtimeContext.aggregateDigest = canonicalNativeRuntimeContextDigest(current.runtimeContext);
|
|
const prior = previousRun({ nativeExecutionInput: previous });
|
|
expect(rebindNativeSessionCheckpoint({ previousRun: prior, currentExecution: current, toolRefreshOnResume: true, refreshTools: true })).toBeNull();
|
|
current.runtimeContext = previous.runtimeContext;
|
|
expect(rebindNativeSessionCheckpoint({ previousRun: prior, currentExecution: current })).not.toBeNull();
|
|
});
|
|
|
|
it("preserves instruction and identity fences even during a supported refresh", () => {
|
|
const current = execution(currentRunId);
|
|
current.runtimeContext.instructions.bundle.digest = "b".repeat(64);
|
|
current.runtimeContext.aggregateDigest = canonicalNativeRuntimeContextDigest(current.runtimeContext);
|
|
expect(rebindNativeSessionCheckpoint({ previousRun: previousRun(), currentExecution: current, toolRefreshOnResume: true, refreshTools: true })).toBeNull();
|
|
current.runtimeContext = nativeRuntimeContextFixture();
|
|
expect(rebindNativeSessionCheckpoint({ previousRun: previousRun(), currentExecution: current, toolRefreshOnResume: true, refreshTools: true })).not.toBeNull();
|
|
current.binding.agentId = randomUUID();
|
|
expect(rebindNativeSessionCheckpoint({ previousRun: previousRun(), currentExecution: current, toolRefreshOnResume: true, refreshTools: true })).toBeNull();
|
|
});
|
|
|
|
it("rebuilds the bootstrap when refresh requires a fresh session", () => {
|
|
const buildExecution = vi.fn(({ normalizedSessionId: sessionId, resumedSession }) => {
|
|
const built = execution(currentRunId);
|
|
built.session.normalizedSessionId = sessionId;
|
|
built.task.prompt = resumedSession ? "wake delta" : "original goal and complete handoff";
|
|
return built;
|
|
});
|
|
const result = buildNativeExecutionWithCheckpoint({ previousRun: previousRun(), normalizedSessionId, refreshTools: true, toolRefreshOnResume: false, buildExecution });
|
|
expect(result.checkpoint).toBeNull();
|
|
expect(result.normalizedSessionId).not.toBe(normalizedSessionId);
|
|
expect(result.execution.task.prompt).toContain("complete handoff");
|
|
expect(buildExecution).toHaveBeenLastCalledWith({ normalizedSessionId: result.normalizedSessionId, resumedSession: false });
|
|
});
|
|
});
|
|
|
|
it("wires exact-session recovery and guarded selected identity into heartbeat persistence", () => {
|
|
const source = readFileSync(
|
|
new URL("../heartbeat/runtime-selection.ts", import.meta.url),
|
|
"utf8",
|
|
);
|
|
expect(source).toContain("findNativeSessionResumeRun(db,");
|
|
expect(source).toContain(
|
|
"prepareNativeSessionBootstrapPersistence(tx,",
|
|
);
|
|
expect(source).toContain("nativeSessionId: persistedNativeSessionId,");
|
|
expect(source).toContain("lockedProfile.sessionCheckpoint != null");
|
|
const build = source.slice(
|
|
source.indexOf("const nativeExecutionWithCheckpoint ="),
|
|
source.indexOf("const nativeSandboxLifecycle ="),
|
|
);
|
|
expect(build).toContain("previousRun: previousNativeRun,");
|
|
});
|
|
|
|
const embeddedSupport = await getEmbeddedPostgresTestSupport();
|
|
const recoveryFakeCodex = resolve(
|
|
import.meta.dirname,
|
|
"../../../../packages/paperclip-runner/test/fixtures/fake-final-burst-codex-app-server.mjs",
|
|
);
|
|
|
|
(embeddedSupport.supported && existsSync(defaultCapabilityRunnerdBinary())
|
|
? it
|
|
: it.skip)(
|
|
"keeps actual normalized driver events separate from raw maintenance receipts",
|
|
async () => {
|
|
const database = await startEmbeddedPostgresTestDatabase(
|
|
"native-cleanup-topology-",
|
|
);
|
|
const db = createDb(database.connectionString);
|
|
const scratch = await mkdtemp(join(tmpdir(), "native-cleanup-topology-"));
|
|
const root = join(scratch, "state");
|
|
const home = join(scratch, "home");
|
|
const workspace = join(scratch, "workspace");
|
|
const companyId = randomUUID(),
|
|
agentId = randomUUID(),
|
|
issueId = randomUUID(),
|
|
runId = randomUUID();
|
|
const runnerInstanceId = randomUUID(),
|
|
normalizedSessionId = randomUUID();
|
|
let session: NativeSession | undefined;
|
|
const signal = vi.spyOn(
|
|
publicationSignals,
|
|
"publishChatPublicationCommitSignal",
|
|
);
|
|
let bundle:
|
|
ReturnType<typeof createCapabilityRunnerdCodexTransport> | undefined;
|
|
try {
|
|
await mkdir(home);
|
|
await mkdir(workspace);
|
|
await db
|
|
.insert(companies)
|
|
.values({
|
|
id: companyId,
|
|
name: "Cleanup topology",
|
|
issuePrefix: "NCT",
|
|
});
|
|
await db
|
|
.insert(agents)
|
|
.values({
|
|
id: agentId,
|
|
companyId,
|
|
name: "Fixture",
|
|
status: "active",
|
|
adapterType: "paperclip_runner",
|
|
});
|
|
await db
|
|
.insert(issues)
|
|
.values({
|
|
id: issueId,
|
|
companyId,
|
|
title: "Exact topology",
|
|
assigneeAgentId: agentId,
|
|
status: "in_progress",
|
|
});
|
|
await db
|
|
.insert(heartbeatRuns)
|
|
.values({
|
|
id: runId,
|
|
companyId,
|
|
agentId,
|
|
status: "running",
|
|
runtimeMode: "native",
|
|
nativeIssueId: issueId,
|
|
nativeSessionId: normalizedSessionId,
|
|
runnerInstanceId,
|
|
});
|
|
bundle = createCapabilityRunnerdCodexTransport({
|
|
stateDirectory: root,
|
|
sourceCodexHome: home,
|
|
// The packaged runnerd does not imply local Rust test binaries exist.
|
|
// Reuse the credential-free provider fixture available in every checkout.
|
|
codexCommand: process.execPath,
|
|
codexArgs: [recoveryFakeCodex, join(scratch, "fake.json"), "16"],
|
|
prpIdentity: {
|
|
runId,
|
|
runnerInstanceId,
|
|
normalizedSessionId,
|
|
environmentLeaseId: runId,
|
|
turnId: `turn-${runId}`,
|
|
itemId: `item-${runId}`,
|
|
},
|
|
});
|
|
const backend = new HarnessDriverBackend(
|
|
new CodexAppServerDriver({
|
|
runnerInstanceId,
|
|
approvalPolicy: "never",
|
|
taskEnvelope: createCodexTaskEnvelope({
|
|
objective: "Open only the fixture session",
|
|
}),
|
|
transportFactory: () => bundle!.transport,
|
|
environment: { HOME: home, CODEX_HOME: home },
|
|
requireProviderSessionIdentity: true,
|
|
}),
|
|
);
|
|
session = await backend.openSession({
|
|
identity: {
|
|
companyId,
|
|
issueId,
|
|
agentId,
|
|
runId,
|
|
sessionId: normalizedSessionId,
|
|
},
|
|
workingDirectory: workspace,
|
|
});
|
|
const stream = session.events()[Symbol.asyncIterator]();
|
|
const normalized = (await stream.next()).value;
|
|
expect(normalized).toMatchObject({
|
|
sourceInstanceId: runnerInstanceId,
|
|
sourceSeq: 1,
|
|
});
|
|
expect(normalized.sourceEventId).toBe(`${runnerInstanceId}:${runId}:1`);
|
|
expect(normalized.payload).not.toHaveProperty("processId");
|
|
const port = new PaperclipControlPlanePort(db, {
|
|
companyId,
|
|
issueId,
|
|
agentId,
|
|
runId,
|
|
sessionId: normalizedSessionId,
|
|
sourceInstanceId: runnerInstanceId,
|
|
controlPlaneSourceInstanceId: `${runnerInstanceId}:control`,
|
|
completionContractId: randomUUID(),
|
|
completionContractSha256: "fixture-contract",
|
|
});
|
|
await port.appendEvent(normalized);
|
|
const original = await db
|
|
.select()
|
|
.from(heartbeatRunEvents)
|
|
.where(eq(heartbeatRunEvents.runId, runId));
|
|
expect(original).toHaveLength(1);
|
|
const control = JSON.parse(
|
|
await readFile(
|
|
join(root, "control-plane/control-plane-state.json"),
|
|
"utf8",
|
|
),
|
|
);
|
|
const raw = control.committedEvents
|
|
.map(
|
|
(entry: { envelope: { payload: Record<string, unknown> } }) =>
|
|
entry.envelope.payload,
|
|
)
|
|
.find((event: Record<string, unknown>) =>
|
|
["session.started", "session.resumed"].includes(
|
|
String(event.eventType),
|
|
),
|
|
);
|
|
expect(raw).toMatchObject({
|
|
sourceInstanceId: runnerInstanceId,
|
|
sourceSeq: 1,
|
|
payload: {
|
|
providerSessionId: normalized.payload.driverSessionId,
|
|
providerAccountSessionId: normalized.payload.providerSessionId,
|
|
},
|
|
});
|
|
expect(raw.payload.processId).toBeGreaterThan(0);
|
|
expect(raw.sourceEventId).not.toBe(normalized.sourceEventId);
|
|
// The original normalized seq=1 is already durable. Reusing that
|
|
// namespace for the raw wire seq=1 must remain an integrity violation.
|
|
await expect(port.appendEvent(raw)).rejects.toMatchObject({
|
|
code: "native_event_replay_conflict",
|
|
});
|
|
signal.mockClear();
|
|
const input = {
|
|
companyId,
|
|
agentId,
|
|
runId,
|
|
nativeSessionId: normalizedSessionId,
|
|
runnerInstanceId,
|
|
requestId: `fixture:${randomUUID()}`,
|
|
event: raw,
|
|
};
|
|
await appendRetainedNativeCleanupEvent(db, input);
|
|
await appendRetainedNativeCleanupEvent(db, input);
|
|
const rows = await db
|
|
.select()
|
|
.from(heartbeatRunEvents)
|
|
.where(eq(heartbeatRunEvents.runId, runId));
|
|
expect(rows).toHaveLength(2);
|
|
expect(rows.find((row) => row.id === original[0]!.id)).toEqual(
|
|
original[0],
|
|
);
|
|
const receipt = rows.find(
|
|
(row) => row.eventType === "native.cleanup.event",
|
|
)!;
|
|
expect(receipt.sourceSeq).toBe(1);
|
|
expect(receipt.sourceInstanceId).not.toBe(runnerInstanceId);
|
|
expect(receipt.payload).not.toHaveProperty("prpEvent");
|
|
expect(
|
|
Object.keys(
|
|
receipt.payload!.nativeCleanupEvent as Record<string, unknown>,
|
|
).sort(),
|
|
).toEqual([
|
|
"rawCanonicalSha256",
|
|
"rawEventType",
|
|
"rawSourceEventId",
|
|
"rawSourceInstanceId",
|
|
"rawSourceSeq",
|
|
"requestId",
|
|
"schema",
|
|
]);
|
|
await expect(
|
|
appendRetainedNativeCleanupEvent(db, {
|
|
...input,
|
|
event: {
|
|
...raw,
|
|
payload: {
|
|
...raw.payload,
|
|
privateData: "MUST_NOT_REACH_MAINTENANCE_RECEIPTS",
|
|
},
|
|
},
|
|
}),
|
|
).rejects.toMatchObject({ code: "native_event_replay_conflict" });
|
|
expect(
|
|
await db
|
|
.select()
|
|
.from(heartbeatRunEvents)
|
|
.where(eq(heartbeatRunEvents.runId, runId)),
|
|
).toHaveLength(2);
|
|
expect(JSON.stringify(receipt.payload)).not.toContain("MUST_NOT_REACH");
|
|
expect(signal).not.toHaveBeenCalled();
|
|
} finally {
|
|
signal.mockRestore();
|
|
await session
|
|
?.close({ reason: "Dispose exact test session" })
|
|
.catch(() => undefined);
|
|
await bundle?.transport.close().catch(() => undefined);
|
|
await database.cleanup();
|
|
await rm(scratch, { recursive: true, force: true });
|
|
}
|
|
},
|
|
30_000,
|
|
);
|
|
|
|
(embeddedSupport.supported &&
|
|
existsSync(defaultCapabilityRunnerdBinary()) &&
|
|
existsSync(recoveryFakeCodex)
|
|
? it
|
|
: it.skip)(
|
|
"archives a damaged prior epoch before completing guarded same-task replacement with real runnerd",
|
|
async () => {
|
|
const database = await startEmbeddedPostgresTestDatabase(
|
|
"native-damaged-resume-",
|
|
);
|
|
const db = createDb(database.connectionString);
|
|
const scratch = await mkdtemp(join(tmpdir(), "native-damaged-resume-"));
|
|
const stateBase = join(scratch, "sessions");
|
|
const bin = join(scratch, "bin");
|
|
const workspace = join(scratch, "workspace");
|
|
const sourceHome = join(scratch, "source-home");
|
|
const previousStateBase = process.env.PAPERCLIP_RUNNER_STATE_DIR;
|
|
const server = createServer();
|
|
let firstSession: NativeSession | undefined;
|
|
const ownedSessions = new Set<NativeSession>();
|
|
const ownedSpawns: Array<{ pid: number; processGroupId: number | null; startedAt: string }> = [];
|
|
const onSpawn = async (meta: typeof ownedSpawns[number]) => { ownedSpawns.push(meta); };
|
|
let executionCloseCompleted = false;
|
|
const runnerDiagnostics: string[] = [];
|
|
const onRunnerLog = async (_stream: "stdout" | "stderr", chunk: string) => {
|
|
runnerDiagnostics.push(chunk.slice(-4_096));
|
|
if (runnerDiagnostics.length > 32) runnerDiagnostics.shift();
|
|
};
|
|
// The production resolver prefers installed Codex over PATH. Explicitly
|
|
// bind this disposable fixture so it can never invoke a real provider.
|
|
const codexCommandResolver = await vi.importActual<{ resolveCodexCommand: () => string }>(
|
|
"../../../../packages/paperclip-runner/src/drivers/codex/codex-command.js",
|
|
);
|
|
const resolveFixtureCodex = vi.spyOn(codexCommandResolver, "resolveCodexCommand")
|
|
.mockReturnValue(join(bin, "codex"));
|
|
process.env.PAPERCLIP_RUNNER_STATE_DIR = stateBase;
|
|
try {
|
|
await Promise.all(
|
|
[bin, workspace, sourceHome].map((path) =>
|
|
mkdir(path, { recursive: true }),
|
|
),
|
|
);
|
|
// Only the provider boundary is synthetic; driver, runnerd, authority
|
|
// rotation, prior-owner DB checks and archive/replacement are real.
|
|
const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
const command = join(bin, "codex");
|
|
await writeFile(
|
|
command,
|
|
`#!/bin/sh\nexec ${quote(process.execPath)} ${quote(recoveryFakeCodex)} "$CODEX_HOME/fake-state.json" 16\n`,
|
|
);
|
|
await chmod(command, 0o700);
|
|
await new Promise<void>((done) => server.listen(0, "127.0.0.1", done));
|
|
const address = server.address();
|
|
if (!address || typeof address === "string")
|
|
throw new Error("Missing test listener");
|
|
setupRunnerPrpWebSocketServer(server, {
|
|
apiUrl: `http://127.0.0.1:${address.port}`,
|
|
});
|
|
const makeExecution = (runId: string): NativeExecutionInput => {
|
|
const { runtimeContext: _runtimeContext, ...value } = execution(
|
|
runId,
|
|
workspace,
|
|
);
|
|
return {
|
|
...value,
|
|
schema: "paperclip.native-execution-input.v2",
|
|
provider: { kind: "codex", model: "gpt-5.6-luna" },
|
|
completionContract: {
|
|
...value.completionContract,
|
|
contract: {
|
|
revision: "burst-v1",
|
|
objective: "Complete a fresh response after guarded recovery",
|
|
criteria: [
|
|
{ id: "burst", requirement: "Complete the new response" },
|
|
],
|
|
},
|
|
},
|
|
};
|
|
};
|
|
const firstExecution = makeExecution(previousRunId);
|
|
const currentExecution = makeExecution(currentRunId);
|
|
expect(() => parseNativeExecutionInput(firstExecution)).not.toThrow();
|
|
const runnerInstanceId = randomUUID();
|
|
const environment = {
|
|
PATH: `${bin}:${process.env.PATH ?? "/usr/bin:/bin"}`,
|
|
HOME: sourceHome,
|
|
CODEX_HOME: sourceHome,
|
|
};
|
|
await db.insert(companies).values({
|
|
id: companyId,
|
|
name: "Damaged recovery",
|
|
issuePrefix: "DNR",
|
|
});
|
|
await db.insert(agents).values({
|
|
id: agentId,
|
|
companyId,
|
|
name: "Recovery runner",
|
|
status: "active",
|
|
adapterType: "paperclip_runner",
|
|
});
|
|
await db.insert(issues).values({
|
|
id: issueId,
|
|
companyId,
|
|
title: "Preserve the same task",
|
|
assigneeAgentId: agentId,
|
|
status: "in_progress",
|
|
});
|
|
await db.insert(completionContracts).values({
|
|
id: currentExecution.completionContract.id,
|
|
companyId,
|
|
issueId,
|
|
revision: 1,
|
|
schemaVersion: "paperclip.completion-contract.v1",
|
|
policyVersion: "damaged-recovery-v1",
|
|
risk: "standard",
|
|
completionAuthority: "server_arbiter",
|
|
incompleteCriteriaPolicy: "preserve_non_terminal",
|
|
contractJson: { ...currentExecution.completionContract.contract },
|
|
canonicalSha256: currentExecution.completionContract.sha256,
|
|
createdByActorType: "system",
|
|
createdByActorId: "test",
|
|
});
|
|
await db.insert(heartbeatRuns).values([
|
|
{
|
|
id: previousRunId,
|
|
companyId,
|
|
agentId,
|
|
status: "running",
|
|
runtimeMode: "native",
|
|
nativeIssueId: issueId,
|
|
nativeSessionId: normalizedSessionId,
|
|
runnerInstanceId,
|
|
completionContractId: firstExecution.completionContract.id,
|
|
completionContractSha256: firstExecution.completionContract.sha256,
|
|
runnerProfileJson: { nativeExecutionInput: firstExecution },
|
|
},
|
|
{
|
|
id: currentRunId,
|
|
companyId,
|
|
agentId,
|
|
status: "running",
|
|
runtimeMode: "native",
|
|
nativeIssueId: issueId,
|
|
nativeSessionId: normalizedSessionId,
|
|
runnerInstanceId,
|
|
completionContractId: currentExecution.completionContract.id,
|
|
completionContractSha256: currentExecution.completionContract.sha256,
|
|
runnerProfileJson: { nativeExecutionInput: currentExecution },
|
|
},
|
|
]);
|
|
const firstBackend = await createRunnerdBackend({
|
|
db,
|
|
execution: firstExecution,
|
|
runnerInstanceId,
|
|
runnerEnvironment: environment,
|
|
onLog: onRunnerLog,
|
|
onSpawn,
|
|
});
|
|
firstSession = await firstBackend.openSession({
|
|
identity: {
|
|
companyId,
|
|
issueId,
|
|
agentId,
|
|
runId: previousRunId,
|
|
sessionId: normalizedSessionId,
|
|
},
|
|
workingDirectory: workspace,
|
|
});
|
|
ownedSessions.add(firstSession);
|
|
const checkpoint = await firstSession.snapshot();
|
|
expect(checkpoint.identity).toEqual({
|
|
companyId,
|
|
issueId,
|
|
agentId,
|
|
runId: previousRunId,
|
|
sessionId: normalizedSessionId,
|
|
});
|
|
await firstSession.close({
|
|
reason: "Establish a suspended test-only historical root",
|
|
});
|
|
firstSession = undefined;
|
|
const [stateKey] = (await readdir(stateBase)).filter((name) =>
|
|
/^[a-f0-9]{64}$/.test(name),
|
|
);
|
|
expect(stateKey).toBeDefined();
|
|
const root = join(stateBase, stateKey!);
|
|
const runnerPath = join(root, "runner", "runner-state.json");
|
|
const providerPath = join(root, "runner", "codex-provider-state.json");
|
|
const runner = JSON.parse(await readFile(runnerPath, "utf8"));
|
|
const provider = JSON.parse(await readFile(providerPath, "utf8"));
|
|
expect(runner.lifecycle).toBe("suspended");
|
|
|
|
// Reproduce only in a generated disposable root the historical failure:
|
|
// an unacknowledged input whose digest predates bounded sanitization.
|
|
const badPayload = {
|
|
semantic_tool: {
|
|
schema: "paperclip.prp.semantic_tool.v1",
|
|
schemaVersion: 1,
|
|
phase: "input",
|
|
input: { summary: "bounded historical answer" },
|
|
content: { digest: `sha256:${"0".repeat(64)}` },
|
|
},
|
|
};
|
|
const envelope = {
|
|
protocol: "paperclip.runner",
|
|
version: 1,
|
|
kind: "event",
|
|
runnerInstanceId: runner.runnerInstanceId,
|
|
environmentLeaseId: runner.environmentLeaseId,
|
|
runId: previousRunId,
|
|
normalizedSessionId,
|
|
turnId: runner.turnId,
|
|
itemId: runner.itemId,
|
|
payload: {
|
|
schema: "paperclip.prp.event.v1",
|
|
sourceEventId: "historical-bad-input-44",
|
|
sourceSeq: 44,
|
|
sourceInstanceId: runner.runnerInstanceId,
|
|
sourceKind: "runner",
|
|
runId: previousRunId,
|
|
normalizedSessionId,
|
|
turnId: runner.turnId,
|
|
itemId: runner.itemId,
|
|
eventType: "semantic_tool.input",
|
|
schemaVersion: 1,
|
|
priority: 0,
|
|
emittedAt: "2026-09-08T00:00:00.000Z",
|
|
payload: badPayload,
|
|
},
|
|
};
|
|
runner.ackedSourceSeq = 43;
|
|
runner.nextSourceSeq = 45;
|
|
runner.outbox = [
|
|
{
|
|
sourceSeq: 44,
|
|
priority: 0,
|
|
eventType: "semantic_tool.input",
|
|
envelope,
|
|
byteSize: Buffer.byteLength(JSON.stringify(envelope)),
|
|
},
|
|
];
|
|
provider.pendingEvents = [
|
|
{
|
|
executorEventId: "codex_provider_0000000000000044",
|
|
eventType: "semantic_tool.input",
|
|
priority: "p0",
|
|
payload: badPayload,
|
|
},
|
|
];
|
|
provider.nextProviderEventSeq = 45;
|
|
await writeFile(runnerPath, JSON.stringify(runner));
|
|
await writeFile(providerPath, JSON.stringify(provider));
|
|
const damagedRunnerBytes = await readFile(runnerPath);
|
|
const damagedProviderBytes = await readFile(providerPath);
|
|
await expect(
|
|
createRunnerdBackend({
|
|
db,
|
|
execution: currentExecution,
|
|
runnerInstanceId,
|
|
runnerEnvironment: environment,
|
|
}),
|
|
).rejects.toThrow("runner_state_identity_mismatch");
|
|
expect(await readFile(runnerPath, "utf8")).toBe(
|
|
damagedRunnerBytes.toString(),
|
|
);
|
|
expect(await readdir(root)).not.toContain("authority-epochs");
|
|
const source = previousRun({
|
|
nativeExecutionInput: firstExecution,
|
|
sessionCheckpoint: checkpoint,
|
|
});
|
|
await db
|
|
.update(heartbeatRuns)
|
|
.set({ status: "failed", runnerProfileJson: source.runnerProfileJson })
|
|
.where(eq(heartbeatRuns.id, previousRunId));
|
|
const selected = await findNativeSessionResumeRun(db, {
|
|
companyId,
|
|
agentId,
|
|
issueId,
|
|
normalizedSessionId,
|
|
currentRunId,
|
|
beforeCreatedAt: new Date(),
|
|
});
|
|
expect(selected?.id).toBe(previousRunId);
|
|
const rebound = rebindNativeSessionCheckpoint({
|
|
previousRun: selected!,
|
|
currentExecution,
|
|
});
|
|
expect(rebound).toMatchObject({
|
|
cursor: null,
|
|
semanticResult: null,
|
|
activeTurnId: null,
|
|
providerRecoveryPolicy: "allow_replacement_after_resume_failure",
|
|
});
|
|
expect(rebound?.identity).toEqual({
|
|
companyId,
|
|
issueId,
|
|
agentId,
|
|
runId: currentRunId,
|
|
sessionId: normalizedSessionId,
|
|
});
|
|
|
|
const backend = await createRunnerdBackend({
|
|
db,
|
|
execution: currentExecution,
|
|
runnerInstanceId,
|
|
runnerEnvironment: environment,
|
|
onLog: onRunnerLog,
|
|
onSpawn,
|
|
});
|
|
let continuity: Record<string, unknown> | undefined;
|
|
const controlPlaneInstanceId = randomUUID();
|
|
const port = new PaperclipControlPlanePort(db, {
|
|
companyId,
|
|
issueId,
|
|
runId: currentRunId,
|
|
agentId,
|
|
sessionId: normalizedSessionId,
|
|
completionContractId: currentExecution.completionContract.id,
|
|
completionContractSha256: currentExecution.completionContract.sha256,
|
|
sourceInstanceId: runnerInstanceId,
|
|
controlPlaneSourceInstanceId: controlPlaneInstanceId,
|
|
});
|
|
await expect(
|
|
executeNativeSession({
|
|
input: currentExecution,
|
|
backend,
|
|
persistedSession: rebound!,
|
|
runnerInstanceId,
|
|
controlPlaneInstanceId,
|
|
timeoutMs: 20_000,
|
|
// This disposable real-daemon fixture must join full retirement before
|
|
// removing its controller routes and durable state. The production
|
|
// default deliberately permits asynchronous cleanup after a result.
|
|
requireSessionCloseBeforeReturn: true,
|
|
onSession(value) { if (value) ownedSessions.add(value); },
|
|
async onSessionClosed() { executionCloseCompleted = true; },
|
|
controlPlane: port,
|
|
async onContinuityBreak(value) {
|
|
continuity = value;
|
|
},
|
|
}).catch(async (error: unknown) => {
|
|
const eventRows = await db
|
|
.select({
|
|
eventType: heartbeatRunEvents.eventType,
|
|
payload: heartbeatRunEvents.payload,
|
|
})
|
|
.from(heartbeatRunEvents)
|
|
.where(eq(heartbeatRunEvents.runId, currentRunId));
|
|
const eventCodes = eventRows.slice(-32).map((event) => ({
|
|
eventType: event.eventType,
|
|
code: event.payload?.code ?? null,
|
|
}));
|
|
const fakeProvider = await readFile(
|
|
join(root, "codex-home", "fake-state.json"),
|
|
"utf8",
|
|
).then(JSON.parse, () => null);
|
|
throw new Error(
|
|
`${error instanceof Error ? error.message : String(error)}\nDisposable recovery fixture runner diagnostics:\n${runnerDiagnostics.join("").slice(-12_000)}\nPersisted event codes: ${JSON.stringify(eventCodes)}\nFake provider state: ${JSON.stringify(fakeProvider)}`,
|
|
{ cause: error },
|
|
);
|
|
}),
|
|
).resolves.toMatchObject({
|
|
result: {
|
|
summary: "Fixture complete.",
|
|
reportedWorkDisposition: "done",
|
|
},
|
|
terminal: { runTerminalState: "succeeded" },
|
|
normalizedSessionId,
|
|
});
|
|
expect(executionCloseCompleted).toBe(true);
|
|
expect(ownedSpawns.length).toBeGreaterThanOrEqual(2);
|
|
for (const { pid } of ownedSpawns) {
|
|
expect(() => process.kill(pid, 0)).toThrow(expect.objectContaining({ code: "ESRCH" }));
|
|
}
|
|
console.info("Recovery fixture retired owned daemons", ownedSpawns);
|
|
expect(continuity).toMatchObject({
|
|
// The daemon can reject the damaged retained input during startup,
|
|
// before attach gets a chance to reject the unsettled provider session.
|
|
// Both refusal paths must preserve the exact archived evidence below.
|
|
reason: expect.stringMatching(
|
|
/run\.attach requires a settled Codex provider session|semantic tool input content digest does not match its transmitted input/,
|
|
),
|
|
previousDriverSessionId: checkpoint.sessionId,
|
|
});
|
|
const [epoch] = await readdir(join(root, "authority-epochs"));
|
|
const digest = (value: Buffer) =>
|
|
createHash("sha256").update(value).digest("hex");
|
|
expect(
|
|
digest(
|
|
await readFile(
|
|
join(root, "authority-epochs", epoch!, "runner-state.json"),
|
|
),
|
|
),
|
|
).toBe(digest(damagedRunnerBytes));
|
|
const [continuityArchive] = await readdir(
|
|
join(root, "continuity-breaks"),
|
|
);
|
|
const archivedProvider = JSON.parse(
|
|
await readFile(
|
|
join(
|
|
root,
|
|
"continuity-breaks",
|
|
continuityArchive!,
|
|
"runner",
|
|
"codex-provider-state.json",
|
|
),
|
|
"utf8",
|
|
),
|
|
);
|
|
// Restoration may append a lifecycle notice, but must neither consume
|
|
// nor rewrite the old semantic input to make attachment succeed.
|
|
expect(
|
|
archivedProvider.pendingEvents.filter(
|
|
(event: { eventType: string }) =>
|
|
event.eventType === "semantic_tool.input",
|
|
),
|
|
).toEqual(JSON.parse(damagedProviderBytes.toString()).pendingEvents);
|
|
const completedProvider = JSON.parse(
|
|
await readFile(join(root, "codex-home", "fake-state.json"), "utf8"),
|
|
);
|
|
expect(completedProvider.nextTurn).toBe(1);
|
|
expect(completedProvider.turns["final-burst-turn-1"]).toMatchObject({
|
|
status: "completed",
|
|
deltaCount: 16,
|
|
});
|
|
const priorProvider = JSON.parse(
|
|
await readFile(
|
|
join(
|
|
root,
|
|
"continuity-breaks",
|
|
continuityArchive!,
|
|
"codex-home",
|
|
"fake-state.json",
|
|
),
|
|
"utf8",
|
|
),
|
|
);
|
|
expect(priorProvider.nextTurn).toBe(0);
|
|
const results = await db
|
|
.select()
|
|
.from(nativeRunResults)
|
|
.where(eq(nativeRunResults.runId, currentRunId));
|
|
expect(results).toHaveLength(1);
|
|
// Prove the maintenance anchor against an actual completed composition:
|
|
// the DB owns normalized driver events and the accepted result, whereas
|
|
// the retained controller owns distinct raw provider/tool event IDs.
|
|
const [completedRun] = await db
|
|
.select()
|
|
.from(heartbeatRuns)
|
|
.where(eq(heartbeatRuns.id, currentRunId));
|
|
const anchorEvents = await db
|
|
.select()
|
|
.from(heartbeatRunEvents)
|
|
.where(eq(heartbeatRunEvents.runId, currentRunId));
|
|
const control = JSON.parse(
|
|
await readFile(
|
|
join(root, "control-plane", "control-plane-state.json"),
|
|
"utf8",
|
|
),
|
|
);
|
|
const rawIdentity = control.committedEvents
|
|
.map(
|
|
(entry: { envelope: { payload: Record<string, unknown> } }) =>
|
|
entry.envelope.payload,
|
|
)
|
|
.find((event: Record<string, unknown>) =>
|
|
["session.started", "session.resumed"].includes(
|
|
String(event.eventType),
|
|
),
|
|
);
|
|
expect(rawIdentity).toBeDefined();
|
|
expect(
|
|
retainedNativeCleanupJournalMatches({
|
|
run: completedRun!,
|
|
execution: currentExecution,
|
|
accepted: results[0]!,
|
|
control,
|
|
providerSessionId: rawIdentity.payload.providerSessionId,
|
|
providerAccountSessionId:
|
|
rawIdentity.payload.providerAccountSessionId,
|
|
persistedEvents: anchorEvents,
|
|
}),
|
|
).toBe(true);
|
|
expect(results[0]).toMatchObject({
|
|
companyId,
|
|
issueId,
|
|
runId: currentRunId,
|
|
schemaStatus: "accepted",
|
|
resultJson: {
|
|
result: { summary: "Fixture complete." },
|
|
terminal: { runTerminalState: "succeeded" },
|
|
},
|
|
});
|
|
const sourceResults = await db
|
|
.select({ id: nativeRunResults.id })
|
|
.from(nativeRunResults)
|
|
.where(eq(nativeRunResults.runId, previousRunId));
|
|
expect(sourceResults).toHaveLength(0);
|
|
const persistedEvents = await db
|
|
.select({ sourceEventId: heartbeatRunEvents.sourceEventId })
|
|
.from(heartbeatRunEvents)
|
|
.where(eq(heartbeatRunEvents.runId, currentRunId));
|
|
expect(persistedEvents.length).toBeGreaterThan(0);
|
|
expect(persistedEvents.map((event) => event.sourceEventId)).not.toContain(
|
|
"historical-bad-input-44",
|
|
);
|
|
const [task] = await db
|
|
.select({ id: issues.id, assigneeAgentId: issues.assigneeAgentId })
|
|
.from(issues)
|
|
.where(eq(issues.id, issueId));
|
|
expect(task).toEqual({ id: issueId, assigneeAgentId: agentId });
|
|
} finally {
|
|
// onSession(null) quarantines the runtime owner before close finishes;
|
|
// keep every published handle so an assertion/error cannot lose cleanup.
|
|
if (firstSession) ownedSessions.add(firstSession);
|
|
const closed = await Promise.allSettled([...ownedSessions].map((session) =>
|
|
Promise.resolve().then(() => session.close({ reason: "Recovery fixture cleanup" })),
|
|
));
|
|
const closeFailures = closed.filter((result) => result.status === "rejected");
|
|
resolveFixtureCodex.mockRestore();
|
|
runnerPrpWebSocketInternals.resetForTests();
|
|
server.closeAllConnections();
|
|
await new Promise<void>((done) => server.close(() => done()));
|
|
if (previousStateBase === undefined)
|
|
delete process.env.PAPERCLIP_RUNNER_STATE_DIR;
|
|
else process.env.PAPERCLIP_RUNNER_STATE_DIR = previousStateBase;
|
|
await database.cleanup();
|
|
if (closeFailures.length) {
|
|
// Retain exact fixture state if owned retirement cannot be proven.
|
|
throw new AggregateError(closeFailures.map((result) => result.reason), "Recovery fixture session cleanup failed");
|
|
}
|
|
await rm(scratch, { recursive: true, force: true });
|
|
}
|
|
},
|
|
60_000,
|
|
);
|
|
|
|
(embeddedSupport.supported ? describe : describe.skip)(
|
|
"native session recovery database orchestration",
|
|
() => {
|
|
it("uses scoped latest checkpoints, preserves progress barriers, and guards bootstrap persistence after the row lock", async () => {
|
|
const database = await startEmbeddedPostgresTestDatabase(
|
|
"paperclip-native-resume-selection-",
|
|
);
|
|
const db = createDb(database.connectionString);
|
|
try {
|
|
const otherCompany = randomUUID(),
|
|
otherAgent = randomUUID(),
|
|
otherIssue = randomUUID();
|
|
await db.insert(companies).values([
|
|
{ id: companyId, name: "Session recovery", issuePrefix: "NRS" },
|
|
{ id: otherCompany, name: "Other company", issuePrefix: "OTHER" },
|
|
]);
|
|
await db.insert(agents).values([
|
|
{ id: agentId, companyId, name: "Native runner" },
|
|
{ id: otherAgent, companyId: otherCompany, name: "Other agent" },
|
|
]);
|
|
await db.insert(issues).values([
|
|
{ id: issueId, companyId, title: "Retain this task" },
|
|
{ id: otherIssue, companyId, title: "Other task" },
|
|
]);
|
|
const at = (second: number) =>
|
|
new Date(Date.UTC(2026, 8, 8, 0, 0, second));
|
|
const source = {
|
|
...previousRun(),
|
|
runtimeMode: "native",
|
|
nativeIssueId: issueId,
|
|
status: "failed",
|
|
createdAt: at(0),
|
|
runnerInstanceId: randomUUID(),
|
|
};
|
|
const unused = {
|
|
companyId,
|
|
agentId,
|
|
runtimeMode: "native",
|
|
nativeIssueId: issueId,
|
|
nativeSessionId: normalizedSessionId,
|
|
status: "failed",
|
|
runnerProfileJson: {},
|
|
};
|
|
const bootstrapId = randomUUID();
|
|
const futureId = randomUUID();
|
|
await db.insert(heartbeatRuns).values([
|
|
source,
|
|
{ ...unused, id: bootstrapId, createdAt: at(2) },
|
|
{
|
|
...unused,
|
|
id: currentRunId,
|
|
status: "running",
|
|
createdAt: at(10),
|
|
},
|
|
{
|
|
...unused,
|
|
id: randomUUID(),
|
|
companyId: otherCompany,
|
|
createdAt: at(3),
|
|
status: "running",
|
|
},
|
|
{
|
|
...unused,
|
|
id: randomUUID(),
|
|
agentId: otherAgent,
|
|
createdAt: at(4),
|
|
status: "running",
|
|
},
|
|
{
|
|
...unused,
|
|
id: randomUUID(),
|
|
nativeIssueId: otherIssue,
|
|
createdAt: at(5),
|
|
status: "running",
|
|
},
|
|
{
|
|
...unused,
|
|
id: randomUUID(),
|
|
nativeSessionId: randomUUID(),
|
|
createdAt: at(6),
|
|
status: "running",
|
|
},
|
|
{
|
|
...unused,
|
|
id: futureId,
|
|
createdAt: at(11),
|
|
},
|
|
]);
|
|
const selection = {
|
|
companyId,
|
|
agentId,
|
|
issueId,
|
|
normalizedSessionId,
|
|
currentRunId,
|
|
beforeCreatedAt: at(10),
|
|
};
|
|
expect((await findNativeSessionResumeRun(db, selection))?.id).toBe(
|
|
previousRunId,
|
|
);
|
|
await db
|
|
.update(heartbeatRuns)
|
|
.set({ runnerProfileJson: source.runnerProfileJson })
|
|
.where(eq(heartbeatRuns.id, futureId));
|
|
expect(await findNativeSessionResumeRun(db, selection)).toBeNull();
|
|
await db
|
|
.update(heartbeatRuns)
|
|
.set({ runnerProfileJson: {}, status: "running" })
|
|
.where(eq(heartbeatRuns.id, futureId));
|
|
expect(await findNativeSessionResumeRun(db, selection)).toBeNull();
|
|
await db
|
|
.update(heartbeatRuns)
|
|
.set({ status: "failed" })
|
|
.where(eq(heartbeatRuns.id, futureId));
|
|
expect((await findNativeSessionResumeRun(db, selection))?.id).toBe(
|
|
previousRunId,
|
|
);
|
|
await db.insert(heartbeatRunEvents).values({
|
|
companyId,
|
|
agentId,
|
|
runId: bootstrapId,
|
|
seq: 1,
|
|
eventType: "session.started",
|
|
});
|
|
expect(await findNativeSessionResumeRun(db, selection)).toBeNull();
|
|
const incompatibleId = randomUUID();
|
|
await db.insert(heartbeatRuns).values({
|
|
...unused,
|
|
id: incompatibleId,
|
|
createdAt: at(7),
|
|
runnerProfileJson: {
|
|
...source.runnerProfileJson,
|
|
nativeToolContractFingerprint: "stale",
|
|
},
|
|
});
|
|
const incompatible = await findNativeSessionResumeRun(db, selection);
|
|
expect(incompatible?.id).toBe(incompatibleId);
|
|
expect(
|
|
rebindNativeSessionCheckpoint({
|
|
previousRun: incompatible!,
|
|
currentExecution: execution(currentRunId),
|
|
}),
|
|
).toBeNull();
|
|
|
|
const fresh = buildNativeExecutionWithCheckpoint({
|
|
previousRun: null,
|
|
normalizedSessionId,
|
|
buildExecution: ({ normalizedSessionId: selected }) => {
|
|
const current = execution(currentRunId);
|
|
return {
|
|
...current,
|
|
session: { ...current.session, normalizedSessionId: selected },
|
|
};
|
|
},
|
|
});
|
|
await db.transaction(async (tx) => {
|
|
const [locked] = await tx
|
|
.select()
|
|
.from(heartbeatRuns)
|
|
.where(eq(heartbeatRuns.id, currentRunId))
|
|
.for("update");
|
|
const selected = await prepareNativeSessionBootstrapPersistence(tx, {
|
|
run: locked,
|
|
selectedSessionId: fresh.normalizedSessionId,
|
|
execution: fresh.execution,
|
|
restoringCheckpoint: false,
|
|
});
|
|
await tx
|
|
.update(heartbeatRuns)
|
|
.set({
|
|
nativeSessionId: selected,
|
|
runnerProfileJson: { nativeExecutionInput: fresh.execution },
|
|
})
|
|
.where(eq(heartbeatRuns.id, locked.id));
|
|
});
|
|
const [persisted] = await db
|
|
.select()
|
|
.from(heartbeatRuns)
|
|
.where(eq(heartbeatRuns.id, currentRunId));
|
|
expect(persisted.nativeSessionId).toBe(fresh.normalizedSessionId);
|
|
expect(persisted.nativeSessionId).not.toBe(normalizedSessionId);
|
|
await expect(
|
|
db.transaction(async (tx) => {
|
|
const [locked] = await tx
|
|
.select()
|
|
.from(heartbeatRuns)
|
|
.where(eq(heartbeatRuns.id, currentRunId))
|
|
.for("update");
|
|
return prepareNativeSessionBootstrapPersistence(tx, {
|
|
run: locked,
|
|
selectedSessionId: fresh.normalizedSessionId,
|
|
execution: fresh.execution,
|
|
restoringCheckpoint: false,
|
|
});
|
|
}),
|
|
).resolves.toBe(fresh.normalizedSessionId);
|
|
await expect(
|
|
db.transaction(async (tx) => {
|
|
const [locked] = await tx
|
|
.select()
|
|
.from(heartbeatRuns)
|
|
.where(eq(heartbeatRuns.id, currentRunId))
|
|
.for("update");
|
|
const changedExecution = structuredClone(fresh.execution);
|
|
changedExecution.task.prompt = "Changed admission";
|
|
return prepareNativeSessionBootstrapPersistence(tx, {
|
|
run: locked,
|
|
selectedSessionId: fresh.normalizedSessionId,
|
|
execution: changedExecution,
|
|
restoringCheckpoint: false,
|
|
});
|
|
}),
|
|
).rejects.toThrow("native_execution_input_persisted_binding_mismatch");
|
|
const [preserved] = await db
|
|
.select()
|
|
.from(heartbeatRuns)
|
|
.where(eq(heartbeatRuns.id, previousRunId));
|
|
expect(preserved.runnerProfileJson).toEqual(source.runnerProfileJson);
|
|
|
|
const lateId = randomUUID();
|
|
await db
|
|
.insert(heartbeatRuns)
|
|
.values({ ...unused, id: lateId, createdAt: at(11) });
|
|
expect((await nativeSessionProviderEvidence(db, [lateId])).size).toBe(
|
|
0,
|
|
);
|
|
let unlock!: () => void;
|
|
let locked!: () => void;
|
|
const lockAcquired = new Promise<void>((resolve) => {
|
|
locked = resolve;
|
|
});
|
|
const release = new Promise<void>((resolve) => {
|
|
unlock = resolve;
|
|
});
|
|
const writer = db.transaction(async (tx) => {
|
|
await tx
|
|
.select()
|
|
.from(heartbeatRuns)
|
|
.where(eq(heartbeatRuns.id, lateId))
|
|
.for("update");
|
|
locked();
|
|
await release;
|
|
await tx.insert(heartbeatRunEvents).values({
|
|
companyId,
|
|
agentId,
|
|
runId: lateId,
|
|
seq: 1,
|
|
eventType: "session.started",
|
|
});
|
|
});
|
|
let attempt: Promise<unknown> | undefined;
|
|
try {
|
|
await lockAcquired;
|
|
const applicationName = `native-resume-lock-${randomUUID()}`;
|
|
attempt = db
|
|
.transaction(async (tx) => {
|
|
await tx.execute(
|
|
sql`select set_config('application_name', ${applicationName}, true)`,
|
|
);
|
|
const [row] = await tx
|
|
.select()
|
|
.from(heartbeatRuns)
|
|
.where(eq(heartbeatRuns.id, lateId))
|
|
.for("update");
|
|
return prepareNativeSessionBootstrapPersistence(tx, {
|
|
run: row,
|
|
selectedSessionId: randomUUID(),
|
|
execution: execution(lateId),
|
|
restoringCheckpoint: false,
|
|
});
|
|
})
|
|
.then(
|
|
() => null,
|
|
(error: unknown) => error,
|
|
);
|
|
await expect
|
|
.poll(async () => {
|
|
const rows = await db.execute<{ blocked: boolean }>(sql`
|
|
select exists (
|
|
select 1 from pg_stat_activity
|
|
where application_name = ${applicationName}
|
|
and wait_event_type = 'Lock'
|
|
and cardinality(pg_blocking_pids(pid)) > 0
|
|
) as blocked
|
|
`);
|
|
return rows[0]?.blocked;
|
|
})
|
|
.toBe(true);
|
|
unlock();
|
|
await writer;
|
|
expect(await attempt).toMatchObject({
|
|
message: "native_session_bootstrap_identity_conflict",
|
|
});
|
|
const [unchanged] = await db
|
|
.select()
|
|
.from(heartbeatRuns)
|
|
.where(eq(heartbeatRuns.id, lateId));
|
|
expect(unchanged.nativeSessionId).toBe(normalizedSessionId);
|
|
} finally {
|
|
unlock();
|
|
await Promise.all([writer, attempt]);
|
|
}
|
|
} finally {
|
|
await database.cleanup();
|
|
}
|
|
}, 60_000);
|
|
},
|
|
);
|
|
|
|
describe("rebindNativeSessionCheckpoint", () => {
|
|
it("rotates a carried session id when no checkpoint source exists", () => {
|
|
const calls: boolean[] = [];
|
|
const result = buildNativeExecutionWithCheckpoint({
|
|
previousRun: null,
|
|
normalizedSessionId,
|
|
buildExecution: (options) => {
|
|
calls.push(options.resumedSession);
|
|
const current = execution(currentRunId);
|
|
return {
|
|
...current,
|
|
session: {
|
|
...current.session,
|
|
normalizedSessionId: options.normalizedSessionId,
|
|
},
|
|
task: {
|
|
...current.task,
|
|
prompt: options.resumedSession
|
|
? "Compact delta"
|
|
: "Full task context",
|
|
},
|
|
};
|
|
},
|
|
});
|
|
expect(calls.every((resumed) => !resumed)).toBe(true);
|
|
expect(result.checkpoint).toBeNull();
|
|
expect(result.normalizedSessionId).not.toBe(normalizedSessionId);
|
|
expect(result.execution.session.normalizedSessionId).toBe(
|
|
result.normalizedSessionId,
|
|
);
|
|
expect(result.execution.task.prompt).toBe("Full task context");
|
|
expect(
|
|
nativeSessionIdForBootstrapPersistence({
|
|
run: {
|
|
nativeSessionId: normalizedSessionId,
|
|
processPid: null,
|
|
processGroupId: null,
|
|
processStartedAt: null,
|
|
runnerProfileJson: {},
|
|
},
|
|
selectedSessionId: result.normalizedSessionId,
|
|
hasProviderEvents: false,
|
|
}),
|
|
).toBe(result.normalizedSessionId);
|
|
});
|
|
|
|
it("does not rotate an admitted or authority-bearing session id", () => {
|
|
const unused = {
|
|
nativeSessionId: normalizedSessionId,
|
|
processPid: null,
|
|
processGroupId: null,
|
|
processStartedAt: null,
|
|
runnerProfileJson: {},
|
|
};
|
|
for (const run of [
|
|
{
|
|
...unused,
|
|
runnerProfileJson: { nativeExecutionInput: execution(currentRunId) },
|
|
},
|
|
{ ...unused, runnerProfileJson: { sessionCheckpoint: {} } },
|
|
{ ...unused, processPid: 123 },
|
|
{ ...unused, processGroupId: 123 },
|
|
{ ...unused, processStartedAt: new Date() },
|
|
]) {
|
|
expect(() =>
|
|
nativeSessionIdForBootstrapPersistence({
|
|
run,
|
|
selectedSessionId: previousRunId,
|
|
hasProviderEvents: false,
|
|
}),
|
|
).toThrow("native_session_bootstrap_identity_conflict");
|
|
}
|
|
expect(() =>
|
|
nativeSessionIdForBootstrapPersistence({
|
|
run: unused,
|
|
selectedSessionId: previousRunId,
|
|
hasProviderEvents: true,
|
|
}),
|
|
).toThrow("native_session_bootstrap_identity_conflict");
|
|
expect(
|
|
nativeSessionIdForBootstrapPersistence({
|
|
run: {
|
|
...unused,
|
|
runnerProfileJson: { nativeExecutionInput: execution(currentRunId) },
|
|
},
|
|
selectedSessionId: normalizedSessionId,
|
|
hasProviderEvents: true,
|
|
}),
|
|
).toBe(normalizedSessionId);
|
|
});
|
|
|
|
it("recovers the latest exact checkpoint past only unused bootstrap failures", () => {
|
|
const source = {
|
|
...previousRun(),
|
|
nativeIssueId: issueId,
|
|
runtimeMode: "native",
|
|
status: "failed",
|
|
createdAt: new Date(0),
|
|
processPid: null,
|
|
processGroupId: null,
|
|
processStartedAt: null,
|
|
};
|
|
const unusedFailure = {
|
|
...source,
|
|
id: "failed-bootstrap",
|
|
runnerProfileJson: {
|
|
nativeExecutionInput: execution("failed-bootstrap"),
|
|
},
|
|
};
|
|
const original = structuredClone(source);
|
|
const selected = selectNativeSessionResumeRun({
|
|
runs: [unusedFailure, source],
|
|
companyId,
|
|
agentId,
|
|
issueId,
|
|
normalizedSessionId,
|
|
currentRunId,
|
|
beforeCreatedAt: new Date(100),
|
|
providerEvidenceRunIds: new Set(),
|
|
});
|
|
expect(selected).toBe(source);
|
|
expect(
|
|
rebindNativeSessionCheckpoint({
|
|
previousRun: selected!,
|
|
currentExecution: execution(currentRunId),
|
|
}),
|
|
).toMatchObject({
|
|
sessionId: "provider-thread-123",
|
|
identity: { runId: currentRunId, sessionId: normalizedSessionId },
|
|
});
|
|
expect(source).toEqual(original);
|
|
});
|
|
|
|
it("never skips a newer provider authority or incompatible checkpoint", () => {
|
|
const source = {
|
|
...previousRun(),
|
|
nativeIssueId: issueId,
|
|
runtimeMode: "native",
|
|
status: "succeeded",
|
|
createdAt: new Date(0),
|
|
processPid: null as number | null,
|
|
processGroupId: null,
|
|
processStartedAt: null,
|
|
};
|
|
const newer = { ...source, id: "newer", runnerProfileJson: {} };
|
|
const select = (
|
|
candidate: typeof source | typeof newer,
|
|
hasEvents = false,
|
|
) =>
|
|
selectNativeSessionResumeRun({
|
|
runs: [candidate, source],
|
|
companyId,
|
|
agentId,
|
|
issueId,
|
|
normalizedSessionId,
|
|
currentRunId,
|
|
beforeCreatedAt: new Date(100),
|
|
providerEvidenceRunIds: new Set(hasEvents ? [candidate.id] : []),
|
|
});
|
|
expect(select(newer, true)).toBeNull();
|
|
expect(select({ ...newer, processPid: 123 })).toBeNull();
|
|
expect(select({ ...newer, status: "running" })).toBeNull();
|
|
const incompatible = {
|
|
...source,
|
|
id: "newer",
|
|
runnerProfileJson: {
|
|
...source.runnerProfileJson,
|
|
nativeToolContractFingerprint: "stale",
|
|
},
|
|
};
|
|
expect(select(incompatible)).toBe(incompatible);
|
|
expect(
|
|
rebindNativeSessionCheckpoint({
|
|
previousRun: select(incompatible)!,
|
|
currentExecution: execution(currentRunId),
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("does not borrow checkpoints across company, agent, issue, or normalized session", () => {
|
|
const source = {
|
|
...previousRun(),
|
|
nativeIssueId: issueId,
|
|
runtimeMode: "native",
|
|
status: "succeeded",
|
|
createdAt: new Date(0),
|
|
processPid: null,
|
|
processGroupId: null,
|
|
processStartedAt: null,
|
|
};
|
|
for (const changed of [
|
|
{ companyId: currentRunId },
|
|
{ agentId: currentRunId },
|
|
{ nativeIssueId: currentRunId },
|
|
{ nativeSessionId: currentRunId },
|
|
{ id: currentRunId },
|
|
]) {
|
|
expect(
|
|
selectNativeSessionResumeRun({
|
|
runs: [{ ...source, ...changed }],
|
|
companyId,
|
|
agentId,
|
|
issueId,
|
|
normalizedSessionId,
|
|
currentRunId,
|
|
beforeCreatedAt: new Date(100),
|
|
providerEvidenceRunIds: new Set(),
|
|
}),
|
|
).toBeNull();
|
|
}
|
|
});
|
|
|
|
it("does not resume across a work-mode tool-surface change", () => {
|
|
expect(
|
|
rebindNativeSessionCheckpoint({
|
|
previousRun: previousRun(),
|
|
currentExecution: execution(currentRunId, "/workspace", "ask"),
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
it.each(["External chat message received", "issue_comment_added"])(
|
|
"rebuilds full task context when a %s checkpoint is corrupt",
|
|
(reason) => {
|
|
const calls: boolean[] = [];
|
|
const result = buildNativeExecutionWithCheckpoint({
|
|
previousRun: previousRun({ sessionCheckpoint: {} }),
|
|
normalizedSessionId,
|
|
buildExecution: (options) => {
|
|
calls.push(options.resumedSession);
|
|
return buildNativeExecutionInput({
|
|
companyId,
|
|
runId: currentRunId,
|
|
issue: {
|
|
id: issueId,
|
|
identifier: "DOT-2",
|
|
title: "Current task",
|
|
description: "Full task instructions",
|
|
workMode: "standard",
|
|
},
|
|
taskPrompt: options.resumedSession
|
|
? "Compact context"
|
|
: "Full task instructions",
|
|
wakePayload: {
|
|
reason,
|
|
issue: {
|
|
id: issueId,
|
|
title: "Current task",
|
|
workMode: "standard",
|
|
},
|
|
},
|
|
resumedSession: options.resumedSession,
|
|
agentId,
|
|
workspace: {
|
|
id: currentRunId,
|
|
cwd: "/workspace",
|
|
repoUrl: null,
|
|
repoRef: null,
|
|
branchName: null,
|
|
},
|
|
normalizedSessionId: options.normalizedSessionId,
|
|
provider: "codex",
|
|
completionContract: execution(currentRunId).completionContract,
|
|
runtimeContext: nativeRuntimeContextFixture(),
|
|
});
|
|
},
|
|
});
|
|
expect(calls).toEqual([true, false]);
|
|
expect(result.checkpoint).toBeNull();
|
|
expect(result.execution.session.normalizedSessionId).not.toBe(
|
|
normalizedSessionId,
|
|
);
|
|
expect(result.execution.task.prompt).toContain("Full task instructions");
|
|
expect(result.execution.task.prompt).not.toContain(
|
|
"Paperclip Resume Delta",
|
|
);
|
|
expect(result.execution.task.prompt).not.toContain("Compact context");
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
["paperclip.native-execution-input.v4", "paperclip.native-execution-input.v5"],
|
|
["paperclip.native-execution-input.v5", "paperclip.native-execution-input.v4"],
|
|
] as const)("retains a recoverable %s session when the constructor defaults to %s", (priorSchema, currentSchema) => {
|
|
const prior = previousRun();
|
|
const profile = prior.runnerProfileJson as Record<string, unknown>;
|
|
profile.nativeExecutionInput = { ...execution(previousRunId), schema: priorSchema };
|
|
const checkpoint = profile.sessionCheckpoint as Record<string, unknown>;
|
|
checkpoint.goal = { status: "paused", objective: "Preserve the existing durable work" };
|
|
const before = structuredClone(prior);
|
|
const result = buildNativeExecutionWithCheckpoint({
|
|
previousRun: prior,
|
|
normalizedSessionId,
|
|
buildExecution: ({ normalizedSessionId: sessionId, resumedSession }) => parseNativeExecutionInput({
|
|
...execution(currentRunId), schema: currentSchema,
|
|
session: { ...execution(currentRunId).session, normalizedSessionId: sessionId },
|
|
continuationPrompt: resumedSession ? "New direction only; do not replay completed actions" : null,
|
|
}),
|
|
});
|
|
expect(result.execution.schema).toBe(priorSchema);
|
|
expect(result.normalizedSessionId).toBe(normalizedSessionId);
|
|
expect(result.checkpoint).toMatchObject({
|
|
providerSessionId: "provider-thread-123",
|
|
providerRecoveryPolicy: "same_session_only",
|
|
goal: { status: "paused", objective: "Preserve the existing durable work" },
|
|
});
|
|
expect(prior).toEqual(before);
|
|
});
|
|
|
|
it("keeps a valid checkpoint and does not rebuild the resumed task", () => {
|
|
const calls: boolean[] = [];
|
|
const result = buildNativeExecutionWithCheckpoint({
|
|
previousRun: previousRun(),
|
|
normalizedSessionId,
|
|
buildExecution: (options) => {
|
|
calls.push(options.resumedSession);
|
|
return execution(currentRunId);
|
|
},
|
|
});
|
|
expect(calls).toEqual([true]);
|
|
expect(result.checkpoint?.sessionId).toBe("provider-thread-123");
|
|
expect(result.execution.session.normalizedSessionId).toBe(
|
|
normalizedSessionId,
|
|
);
|
|
});
|
|
|
|
it("rebuilds full context when the persisted provider tool contract is stale", () => {
|
|
const calls: boolean[] = [];
|
|
const result = buildNativeExecutionWithCheckpoint({
|
|
previousRun: previousRun({
|
|
nativeToolContractFingerprint: "sha256:stale",
|
|
}),
|
|
normalizedSessionId,
|
|
buildExecution: (options) => {
|
|
calls.push(options.resumedSession);
|
|
const current = execution(currentRunId);
|
|
return {
|
|
...current,
|
|
session: {
|
|
...current.session,
|
|
normalizedSessionId: options.normalizedSessionId,
|
|
},
|
|
task: {
|
|
...current.task,
|
|
prompt: options.resumedSession
|
|
? "Paperclip Resume Delta"
|
|
: "Full task instructions",
|
|
},
|
|
};
|
|
},
|
|
});
|
|
|
|
expect(calls).toEqual([true, false]);
|
|
expect(result.checkpoint).toBeNull();
|
|
expect(result.execution.task.prompt).toBe("Full task instructions");
|
|
expect(result.normalizedSessionId).not.toBe(normalizedSessionId);
|
|
});
|
|
|
|
it.each([
|
|
{ contract: "conditional current-wake reader", retainedFingerprint: "sha256:fe7a2e17ca825228ea2f51f23acdc82db65c91da5ec6add330fefa39b65e1c85" },
|
|
{ contract: "same-conversation attachment reuse", retainedFingerprint: "sha256:2b84f2105c2cea10e0e5f37cf4a6ba9bf7fec90d8c8c19c655a0e21d3a8dab5f" },
|
|
{ contract: "historical file inspection", retainedFingerprint: "sha256:9b558a575dfa4e3dfaf97b7139b47d1676ab3e0307953ec85f2af0b3f443567c" },
|
|
{ contract: "structured human questions", retainedFingerprint: "sha256:1c3cd7e3223f6ed28e1c44a0ba3c961dd4c0c52e52c9c1100a4e41a83384259c" },
|
|
{ contract: "response-wake completion yield", retainedFingerprint: "sha256:09887bbf1f6a3674fcf529d872fe2853cc1d6554376019dd1a9b8d607d7eb7fb" },
|
|
{ contract: "explicit chat-attachment invocation guidance", retainedFingerprint: "sha256:d797c8bc1baef8407a8c80f2c4b28da63b2a4258a47cf52ee9b1fca45535f5e7" },
|
|
{
|
|
contract: "completion descriptions",
|
|
// Deployed v8 / finish_response_wake_concrete_object.v2 local catalog.
|
|
retainedFingerprint:
|
|
"sha256:5b7b302db36f7ed6686f9ea1ba70bbf79ebd7fabf86953b548d966a2bc38b648",
|
|
},
|
|
{
|
|
contract: "native completion tool guidance",
|
|
// Deployed v13 local catalog before canonical finish/block descriptions.
|
|
retainedFingerprint:
|
|
"sha256:68a51d34e091c55ee5d0d2b563153454dd727d72db16e6a27c358d342ae489c9",
|
|
},
|
|
{
|
|
contract: "GitHub working-comment tools",
|
|
// Deployed v14 threads retain the catalog without update_comment.
|
|
retainedFingerprint:
|
|
"sha256:134a7dbd526179aff57f91c261bb653e83db51c20492efab5c26a5ce618792c8",
|
|
},
|
|
{
|
|
contract: "GitHub instruction skill selection",
|
|
// Deployed v15 remote threads omitted configured skills from native turns.
|
|
retainedFingerprint:
|
|
"sha256:f12fe3b1bf5d63b7954d8ab16f57764874806ab49acff7c6582c4de007f4ed4d",
|
|
},
|
|
{
|
|
contract: "task-bound human-input description",
|
|
// Deployed v9 still advertises the generic mock-task question description.
|
|
retainedFingerprint:
|
|
"sha256:b64efcd063a575925aa95dbd2a20953386eaa760f05b0e0e73b4ae04a97679b0",
|
|
},
|
|
])(
|
|
"refreshes retained $contract without changing the Paperclip task or prior history",
|
|
({ contract, retainedFingerprint }) => {
|
|
if (contract === "explicit chat-attachment invocation guidance") {
|
|
expect(LIST_CHAT_ATTACHMENTS_TOOL_DEFINITION.description).toContain("Call with {}");
|
|
expect(LIST_CHAT_ATTACHMENTS_TOOL_DEFINITION.description).toContain("1 through 50");
|
|
expect(REUSE_CHAT_ATTACHMENT_TOOL_DEFINITION.description).toContain("All four arguments are required");
|
|
expect(REUSE_CHAT_ATTACHMENT_TOOL_DEFINITION.description).toContain("same key unchanged on every retry");
|
|
}
|
|
expect(NATIVE_TOOL_CONTRACT_FINGERPRINT).not.toBe(retainedFingerprint);
|
|
const prior = previousRun({
|
|
nativeToolContractFingerprint: retainedFingerprint,
|
|
});
|
|
const priorSnapshot = structuredClone(prior);
|
|
const modes: boolean[] = [];
|
|
const result = buildNativeExecutionWithCheckpoint({
|
|
previousRun: prior,
|
|
normalizedSessionId,
|
|
buildExecution: (options) => {
|
|
modes.push(options.resumedSession);
|
|
const current = execution(currentRunId);
|
|
return {
|
|
...current,
|
|
session: {
|
|
...current.session,
|
|
normalizedSessionId: options.normalizedSessionId,
|
|
},
|
|
task: {
|
|
...current.task,
|
|
prompt: options.resumedSession
|
|
? "Paperclip Resume Delta"
|
|
: "Full context for the same Paperclip task",
|
|
},
|
|
};
|
|
},
|
|
});
|
|
expect(modes).toEqual([true, false]);
|
|
expect(result.checkpoint).toBeNull();
|
|
expect(result.normalizedSessionId).not.toBe(normalizedSessionId);
|
|
expect(result.execution.binding).toMatchObject({
|
|
companyId,
|
|
issueId,
|
|
agentId,
|
|
runId: currentRunId,
|
|
});
|
|
expect(result.execution.workspace).toEqual(
|
|
execution(currentRunId).workspace,
|
|
);
|
|
expect(result.execution.task.prompt).toBe(
|
|
"Full context for the same Paperclip task",
|
|
);
|
|
// No deletion/reset of the existing provider checkpoint or Paperclip history.
|
|
expect(prior).toEqual(priorSnapshot);
|
|
expect(prior.runnerProfileJson.sessionCheckpoint.sessionId).toBe(
|
|
"provider-thread-123",
|
|
);
|
|
},
|
|
);
|
|
|
|
|
|
|
|
it("binds the provider tool catalog to the local or remote execution target", () => {
|
|
const remoteFingerprint = nativeToolContractFingerprintForTarget("remote");
|
|
expect(remoteFingerprint).not.toBe(NATIVE_TOOL_CONTRACT_FINGERPRINT);
|
|
expect(
|
|
rebindNativeSessionCheckpoint({
|
|
previousRun: previousRun(),
|
|
currentExecution: execution(currentRunId),
|
|
executionTargetKind: "remote",
|
|
}),
|
|
).toBeNull();
|
|
expect(
|
|
rebindNativeSessionCheckpoint({
|
|
previousRun: previousRun({
|
|
nativeToolContractFingerprint: remoteFingerprint,
|
|
}),
|
|
currentExecution: execution(currentRunId),
|
|
executionTargetKind: "remote",
|
|
}),
|
|
).not.toBeNull();
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
it("permits legacy retry rebinding only before the replacement acquired authority", () => {
|
|
const source = {
|
|
runtimeMode: "native",
|
|
status: "interrupted",
|
|
nativeSessionId: normalizedSessionId,
|
|
};
|
|
const replacement = {
|
|
processPid: null,
|
|
processGroupId: null,
|
|
processStartedAt: null,
|
|
runnerProfileJson: {},
|
|
};
|
|
expect(
|
|
isUnusedLegacyNativeRetryReplacement({
|
|
source,
|
|
replacement,
|
|
hasProviderEvents: false,
|
|
}),
|
|
).toBe(true);
|
|
expect(
|
|
isUnusedLegacyNativeRetryReplacement({
|
|
source,
|
|
replacement: { ...replacement, processPid: 123 },
|
|
hasProviderEvents: false,
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
isUnusedLegacyNativeRetryReplacement({
|
|
source,
|
|
replacement,
|
|
hasProviderEvents: true,
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
isUnusedLegacyNativeRetryReplacement({
|
|
source,
|
|
replacement: {
|
|
...replacement,
|
|
runnerProfileJson: {
|
|
sessionCheckpoint: { providerSessionId: "claimed" },
|
|
},
|
|
},
|
|
hasProviderEvents: false,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("retains provider identity but clears prior turn and event state", () => {
|
|
const prior = previousRun();
|
|
const profile = prior.runnerProfileJson as Record<string, unknown>;
|
|
const checkpoint = profile.sessionCheckpoint as Record<string, unknown>;
|
|
checkpoint.governedWait = { sourceEvent: { runId: previousRunId, turnId: "old-turn" }, result: { reportedWorkDisposition: "yielded" } };
|
|
const before = structuredClone(prior);
|
|
const rebound = rebindNativeSessionCheckpoint({
|
|
previousRun: prior,
|
|
currentExecution: execution(currentRunId),
|
|
});
|
|
expect(rebound?.governedWait).toBeUndefined();
|
|
expect(prior).toEqual(before);
|
|
expect(rebound).toMatchObject({
|
|
sessionId: "provider-thread-123",
|
|
providerSessionId: "provider-thread-123",
|
|
driverKind: "codex_app_server",
|
|
cursor: null,
|
|
semanticResult: null,
|
|
terminal: null,
|
|
activeTurnId: null,
|
|
terminalTurns: [],
|
|
pendingRuntimeRequests: [],
|
|
providerRecoveryPolicy: "allow_replacement_after_resume_failure",
|
|
identity: {
|
|
runId: currentRunId,
|
|
sessionId: normalizedSessionId,
|
|
companyId,
|
|
issueId,
|
|
agentId,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("allows a provider replacement only after a durable response wake", () => {
|
|
const source = previousRun();
|
|
const profile = source.runnerProfileJson as Record<string, unknown>;
|
|
const checkpoint = profile.sessionCheckpoint as Record<string, unknown>;
|
|
checkpoint.semanticResult = {
|
|
schema: "paperclip.run_result.v1",
|
|
reportedWorkDisposition: "yielded",
|
|
summary: "Waiting for a response.",
|
|
continuation: {
|
|
kind: "response_wake",
|
|
idempotencyKey: "interaction-response:one",
|
|
},
|
|
};
|
|
|
|
expect(
|
|
rebindNativeSessionCheckpoint({
|
|
previousRun: source,
|
|
currentExecution: execution(currentRunId),
|
|
}),
|
|
).toMatchObject({
|
|
providerRecoveryPolicy: "allow_replacement_after_governed_wait",
|
|
semanticResult: null,
|
|
activeTurnId: null,
|
|
});
|
|
});
|
|
|
|
it("requires exact provider continuity for an unfinished session goal", () => {
|
|
const source = previousRun();
|
|
const profile = source.runnerProfileJson as Record<string, unknown>;
|
|
const checkpoint = profile.sessionCheckpoint as Record<string, unknown>;
|
|
checkpoint.goal = {
|
|
threadId: "provider-thread-123",
|
|
objective: "Keep this paused goal on its original provider session.",
|
|
status: "paused",
|
|
tokenBudget: null,
|
|
tokensUsed: 321,
|
|
timeUsedSeconds: 12,
|
|
};
|
|
checkpoint.semanticResult = {
|
|
schema: "paperclip.run_result.v1",
|
|
reportedWorkDisposition: "yielded",
|
|
summary: "Paused at a quiescent boundary.",
|
|
continuation: {
|
|
kind: "same_agent",
|
|
idempotencyKey: "goal:paused:one",
|
|
},
|
|
};
|
|
|
|
expect(
|
|
rebindNativeSessionCheckpoint({
|
|
previousRun: source,
|
|
currentExecution: execution(currentRunId),
|
|
}),
|
|
).toMatchObject({
|
|
providerRecoveryPolicy: "same_session_only",
|
|
semanticResult: null,
|
|
activeTurnId: null,
|
|
goal: {
|
|
objective: "Keep this paused goal on its original provider session.",
|
|
status: "paused",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("refuses to resume when the workspace changes", () => {
|
|
expect(
|
|
rebindNativeSessionCheckpoint({
|
|
previousRun: previousRun(),
|
|
currentExecution: execution(currentRunId, "/different-workspace"),
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("requires the exact managed execution-workspace identity", () => {
|
|
const managedWorkspaceId = "90000000-0000-4000-8000-000000000009";
|
|
const source = previousRun({
|
|
nativeExecutionInput: execution(
|
|
previousRunId,
|
|
"/shared-workspace",
|
|
"standard",
|
|
{ id: managedWorkspaceId },
|
|
),
|
|
});
|
|
expect(
|
|
rebindNativeSessionCheckpoint({
|
|
previousRun: source,
|
|
currentExecution: execution(
|
|
currentRunId,
|
|
"/shared-workspace",
|
|
"standard",
|
|
{ id: managedWorkspaceId },
|
|
),
|
|
}),
|
|
).not.toBeNull();
|
|
expect(
|
|
rebindNativeSessionCheckpoint({
|
|
previousRun: source,
|
|
currentExecution: execution(
|
|
currentRunId,
|
|
"/shared-workspace",
|
|
"standard",
|
|
{ id: "a0000000-0000-4000-8000-00000000000a" },
|
|
),
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("permits per-run projectless placeholders only for an identical workspace descriptor", () => {
|
|
const source = previousRun({
|
|
nativeExecutionInput: execution(
|
|
previousRunId,
|
|
"/projectless",
|
|
"standard",
|
|
{
|
|
repoUrl: "https://example.test/repo.git",
|
|
repoRef: "refs/heads/main",
|
|
branchName: "main",
|
|
},
|
|
),
|
|
});
|
|
const matching = execution(currentRunId, "/projectless", "standard", {
|
|
repoUrl: "https://example.test/repo.git",
|
|
repoRef: "refs/heads/main",
|
|
branchName: "main",
|
|
});
|
|
expect(
|
|
rebindNativeSessionCheckpoint({
|
|
previousRun: source,
|
|
currentExecution: matching,
|
|
}),
|
|
).not.toBeNull();
|
|
expect(
|
|
rebindNativeSessionCheckpoint({
|
|
previousRun: source,
|
|
currentExecution: {
|
|
...matching,
|
|
workspace: { ...matching.workspace, repoRef: "refs/heads/next" },
|
|
},
|
|
}),
|
|
).toBeNull();
|
|
expect(
|
|
rebindNativeSessionCheckpoint({
|
|
previousRun: source,
|
|
currentExecution: execution(currentRunId, "/projectless", "standard", {
|
|
id: "b0000000-0000-4000-8000-00000000000b",
|
|
repoUrl: "https://example.test/repo.git",
|
|
repoRef: "refs/heads/main",
|
|
branchName: "main",
|
|
}),
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("rotates when assigned context changes but permits a fresh run-scoped MCP binding", () => {
|
|
const reboundCredential = execution(currentRunId);
|
|
reboundCredential.runtimeContext.mcp.bindingId = "native-mcp:fresh-run";
|
|
expect(
|
|
rebindNativeSessionCheckpoint({
|
|
previousRun: previousRun(),
|
|
currentExecution: reboundCredential,
|
|
}),
|
|
).not.toBeNull();
|
|
|
|
const changedAssignment = execution(currentRunId);
|
|
const withoutAggregate = {
|
|
prompt: changedAssignment.runtimeContext.prompt,
|
|
instructions: changedAssignment.runtimeContext.instructions,
|
|
skills: changedAssignment.runtimeContext.skills,
|
|
mcp: {
|
|
assignmentSetId: `sha256:${"1".repeat(64)}`,
|
|
digest: "1".repeat(64),
|
|
bindingId: "native-mcp:fresh-run",
|
|
},
|
|
};
|
|
changedAssignment.runtimeContext = {
|
|
...withoutAggregate,
|
|
aggregateDigest: canonicalNativeRuntimeContextDigest(withoutAggregate),
|
|
};
|
|
expect(
|
|
rebindNativeSessionCheckpoint({
|
|
previousRun: previousRun(),
|
|
currentExecution: changedAssignment,
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("refuses a checkpoint whose prior-run binding was rewritten", () => {
|
|
const source = previousRun();
|
|
const profile = source.runnerProfileJson as Record<string, unknown>;
|
|
const checkpoint = profile.sessionCheckpoint as Record<string, unknown>;
|
|
checkpoint.identity = {
|
|
...(checkpoint.identity as Record<string, unknown>),
|
|
runId: currentRunId,
|
|
};
|
|
expect(
|
|
rebindNativeSessionCheckpoint({
|
|
previousRun: source,
|
|
currentExecution: execution(currentRunId),
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("reuses plan mode across canonical revisions but never across a mode change", () => {
|
|
const source = previousRun({
|
|
nativeExecutionInput: planningExecution(previousRunId, "revision-8"),
|
|
});
|
|
expect(
|
|
rebindNativeSessionCheckpoint({
|
|
previousRun: source,
|
|
currentExecution: planningExecution(currentRunId, "revision-9"),
|
|
}),
|
|
).not.toBeNull();
|
|
expect(
|
|
rebindNativeSessionCheckpoint({
|
|
previousRun: source,
|
|
currentExecution: execution(currentRunId),
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("buildNativeExecutionInput wake projection", () => {
|
|
it("uses compact Slack input only after provider resume, preserving fresh bootstrap fallback", () => {
|
|
const base = execution(currentRunId);
|
|
const issue = { id: issueId, identifier: "DOT-2", title: "Old greeting", description: null, workMode: "standard" };
|
|
const message = { id: "wake", body: "NEW_SLACK_MESSAGE", authorType: "user", authorId: "board", sourceTrust: "user", createdByRunId: null };
|
|
const input = buildNativeExecutionInput({
|
|
companyId, runId: currentRunId, agentId, issue,
|
|
taskPrompt: "FULL_BOOTSTRAP_WITH_FILE_AND_AUTHORIZATION_INSTRUCTIONS",
|
|
resumedSession: true,
|
|
previousTurn: { runId: previousRunId, task: issue },
|
|
wakePayload: {
|
|
reason: "External chat message received", issue,
|
|
externalChatProvider: "slack", checkedOutByHarness: true,
|
|
comments: [{ ...message, author: { type: "user", id: "board" } }], commentIds: [message.id], latestCommentId: message.id,
|
|
commentWindow: { requestedCount: 1, includedCount: 1, missingCount: 0 },
|
|
executionContinuation: { version: 1, objective: message.body, messages: [message], resumeDelta: { baseRunId: previousRunId, messages: [message] } },
|
|
},
|
|
workspace: { id: currentRunId, ...base.workspace }, normalizedSessionId,
|
|
provider: "codex", completionContract: base.completionContract,
|
|
runtimeContext: nativeRuntimeContextFixture(),
|
|
});
|
|
expect(input.continuationPrompt).toContain(message.body);
|
|
expect(input.continuationPrompt).not.toContain("FULL_BOOTSTRAP");
|
|
expect(input.task.prompt).toContain("FULL_BOOTSTRAP");
|
|
expect(input.task.prompt).toContain(message.body);
|
|
});
|
|
|
|
it("uses a neutral turn title for authenticated external-chat follow-ups", () => {
|
|
const staleRootTitle = "Reply with exactly STALE-ROOT-MARKER";
|
|
const input = buildNativeExecutionInput({
|
|
companyId,
|
|
runId: currentRunId,
|
|
issue: {
|
|
id: issueId,
|
|
identifier: "CHAT-4",
|
|
title: staleRootTitle,
|
|
description: "Started from Telegram.",
|
|
workMode: "standard",
|
|
},
|
|
taskPrompt: [
|
|
"Paperclip task context:",
|
|
`- Title: ${JSON.stringify(staleRootTitle)}`,
|
|
"Latest wake comment:",
|
|
"```text",
|
|
"Quick question: what is 55 + 8?",
|
|
"```",
|
|
].join("\n"),
|
|
wakePayload: {
|
|
reason: "External chat message received",
|
|
externalChatProvider: "telegram",
|
|
checkedOutByHarness: true,
|
|
issue: {
|
|
id: issueId,
|
|
identifier: "CHAT-4",
|
|
title: staleRootTitle,
|
|
description: "Started from Telegram.",
|
|
descriptionTruncated: false,
|
|
status: "in_progress",
|
|
workMode: "standard",
|
|
},
|
|
commentWindow: {
|
|
requestedCount: 1,
|
|
includedCount: 1,
|
|
missingCount: 0,
|
|
},
|
|
commentIds: ["comment-current"],
|
|
latestCommentId: "comment-current",
|
|
comments: [
|
|
{
|
|
id: "comment-current",
|
|
issueId,
|
|
body: "Quick question: what is 55 + 8?",
|
|
bodyTruncated: false,
|
|
authorType: "user",
|
|
},
|
|
],
|
|
fallbackFetchNeeded: false,
|
|
},
|
|
agentId,
|
|
workspace: {
|
|
id: currentRunId,
|
|
cwd: "/workspace",
|
|
repoUrl: null,
|
|
repoRef: null,
|
|
branchName: null,
|
|
},
|
|
normalizedSessionId,
|
|
provider: "codex",
|
|
completionContract: {
|
|
id: "70000000-0000-4000-8000-000000000007",
|
|
sha256: `sha256:${"a".repeat(64)}`,
|
|
schemaVersion: "paperclip.run-result.v1",
|
|
contract: {
|
|
revision: "2",
|
|
objective: "Respond to the latest comment",
|
|
criteria: [
|
|
{
|
|
id: "objective",
|
|
requirement: "Quick question: what is 55 + 8?",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
runtimeContext: nativeRuntimeContextFixture(),
|
|
});
|
|
|
|
expect(input.task.title).toBe("External chat follow-up");
|
|
expect(input.task.description).toBeNull();
|
|
expect(input.task.prompt).toContain(staleRootTitle);
|
|
expect(input.task.prompt).toContain("Quick question: what is 55 + 8?");
|
|
expect(input.completionContract.contract).toMatchObject({
|
|
objective: "Respond to the latest comment",
|
|
criteria: [
|
|
{
|
|
id: "objective",
|
|
requirement: "Quick question: what is 55 + 8?",
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it("preserves the canonical task title outside the authenticated chat shortcut", () => {
|
|
const input = execution(currentRunId);
|
|
|
|
expect(input.task.title).toBe("Test");
|
|
});
|
|
|
|
it("writes native v4 and pins every provider's complete effective configuration", () => {
|
|
const common = {
|
|
companyId,
|
|
runId: currentRunId,
|
|
issue: {
|
|
id: issueId,
|
|
identifier: "DOT-4",
|
|
title: "Permissions",
|
|
description: null,
|
|
workMode: "standard",
|
|
},
|
|
taskPrompt: "Verify permissions",
|
|
agentId,
|
|
workspace: {
|
|
id: currentRunId,
|
|
cwd: "/workspace",
|
|
repoUrl: null,
|
|
repoRef: null,
|
|
branchName: null,
|
|
},
|
|
normalizedSessionId,
|
|
completionContract: execution(currentRunId).completionContract,
|
|
runtimeContext: nativeRuntimeContextFixture(),
|
|
} as const;
|
|
const codex = buildNativeExecutionInput({
|
|
...common,
|
|
provider: "codex",
|
|
codexApprovalPolicy: "on-request",
|
|
});
|
|
const opencode = buildNativeExecutionInput({
|
|
...common,
|
|
provider: "opencode",
|
|
model: "openrouter/z-ai/glm-5.2",
|
|
opencodePermissionMode: "ask",
|
|
});
|
|
const acpx = buildNativeExecutionInput({
|
|
...common,
|
|
provider: "acpx",
|
|
acpxAgent: "claude",
|
|
model: "claude-sonnet-5",
|
|
acpxPermissionMode: "deny-all",
|
|
});
|
|
const claudeManaged = buildNativeExecutionInput({
|
|
...common,
|
|
provider: "claude_managed",
|
|
model: "claude-sonnet-5",
|
|
managedProfile: {
|
|
profileId: "managed-profile",
|
|
anthropicAgentId: "agent-remote",
|
|
agentVersion: "7",
|
|
environmentId: "environment-remote",
|
|
betaVersion: "managed-agents-2026-04-01",
|
|
},
|
|
maxSessionListCostUsd: 0.75,
|
|
});
|
|
const agentCore = buildNativeExecutionInput({
|
|
...common,
|
|
provider: "aws_agentcore",
|
|
model: "global.anthropic.claude-sonnet-4-6",
|
|
agentCoreProfile: {
|
|
profileId: "agentcore-profile",
|
|
region: "us-east-1",
|
|
accountId: "123456789012",
|
|
harnessArn:
|
|
"arn:aws:bedrock-agentcore:us-east-1:123456789012:harness/h-1",
|
|
harnessVersion: "3",
|
|
endpointArn:
|
|
"arn:aws:bedrock-agentcore:us-east-1:123456789012:endpoint/e-1",
|
|
endpointQualifier: "prod",
|
|
agentRuntimeArn:
|
|
"arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/r-1",
|
|
memoryArn:
|
|
"arn:aws:bedrock-agentcore:us-east-1:123456789012:memory/m-1",
|
|
memoryId: "m-1",
|
|
invocationRoleArn: "arn:aws:iam::123456789012:role/invoke",
|
|
contextBucket: "paperclip-context",
|
|
contextPrefix: "runner/",
|
|
contextKmsKeyArn: "arn:aws:kms:us-east-1:123456789012:key/key-1",
|
|
qualificationRevision: "aws-agentcore-harness-context-v2",
|
|
eventExpiryDays: 90,
|
|
},
|
|
maxEstimatedSessionCostUsd: 1.25,
|
|
invocationLimits: {
|
|
maxIterations: 8,
|
|
maxOutputTokens: 4_096,
|
|
timeoutSeconds: 300,
|
|
},
|
|
});
|
|
const defaultOpenCode = buildNativeExecutionInput({
|
|
...common,
|
|
provider: "opencode",
|
|
model: "openrouter/z-ai/glm-5.2",
|
|
});
|
|
const defaultAcpx = buildNativeExecutionInput({
|
|
...common,
|
|
provider: "acpx",
|
|
model: "gpt-5.6-sol",
|
|
});
|
|
|
|
expect(codex).toMatchObject({
|
|
schema: "paperclip.native-execution-input.v5",
|
|
provider: { kind: "codex", approvalPolicy: "on-request" },
|
|
});
|
|
expect(opencode).toMatchObject({
|
|
schema: "paperclip.native-execution-input.v5",
|
|
provider: { kind: "opencode", permissionMode: "ask" },
|
|
});
|
|
expect(acpx).toMatchObject({
|
|
schema: "paperclip.native-execution-input.v5",
|
|
provider: { kind: "acpx", permissionMode: "deny-all" },
|
|
});
|
|
expect(claudeManaged).toMatchObject({
|
|
session: { driverKind: "claude_managed_agents_api" },
|
|
provider: {
|
|
kind: "claude_managed",
|
|
managedProfile: { profileId: "managed-profile" },
|
|
maxSessionListCostUsd: 0.75,
|
|
},
|
|
});
|
|
expect(agentCore).toMatchObject({
|
|
session: { driverKind: "aws_agentcore_harness_api" },
|
|
provider: {
|
|
kind: "aws_agentcore",
|
|
agentCoreProfile: {
|
|
profileId: "agentcore-profile",
|
|
eventExpiryDays: 90,
|
|
},
|
|
maxEstimatedSessionCostUsd: 1.25,
|
|
},
|
|
});
|
|
expect(defaultOpenCode).toMatchObject({
|
|
provider: { kind: "opencode", permissionMode: "allow" },
|
|
});
|
|
expect(defaultAcpx).toMatchObject({
|
|
provider: {
|
|
kind: "acpx",
|
|
agent: "codex",
|
|
permissionMode: "approve-all",
|
|
},
|
|
});
|
|
expect(
|
|
JSON.stringify([codex, opencode, claudeManaged, agentCore, acpx]),
|
|
).not.toMatch(
|
|
/OPENAI_API_KEY|ANTHROPIC_API_KEY|AWS_SECRET_ACCESS_KEY|PAPERCLIP_API_KEY/,
|
|
);
|
|
});
|
|
|
|
it.each([false, true])("projects wake context with the appropriate execution contract (conversation=%s)", (conversationMode) => {
|
|
const input = buildNativeExecutionInput({
|
|
companyId,
|
|
runId: currentRunId,
|
|
issue: {
|
|
id: issueId,
|
|
identifier: "DOT-146",
|
|
title: "Finish after child handoff",
|
|
description: "Use the child result.",
|
|
workMode: "standard",
|
|
},
|
|
taskPrompt: "Paperclip task context:\n- Issue: DOT-146",
|
|
wakePayload: {
|
|
reason: "issue_children_completed",
|
|
issue: {
|
|
id: issueId,
|
|
identifier: "DOT-146",
|
|
title: "Finish after child handoff",
|
|
description: "Use the child result.",
|
|
status: "in_progress",
|
|
priority: "medium",
|
|
workMode: "standard",
|
|
},
|
|
childIssueSummaries: [
|
|
{
|
|
id: "child-147",
|
|
identifier: "DOT-147",
|
|
title: "Build utility",
|
|
status: "done",
|
|
summary: "Created three files and passed 7/7 tests.",
|
|
},
|
|
],
|
|
childIssueSummaryTruncated: false,
|
|
checkedOutByHarness: true,
|
|
},
|
|
resumedSession: true,
|
|
conversationMode,
|
|
agentId,
|
|
workspace: {
|
|
id: currentRunId,
|
|
cwd: "/workspace",
|
|
repoUrl: null,
|
|
repoRef: null,
|
|
branchName: null,
|
|
},
|
|
normalizedSessionId,
|
|
provider: "opencode",
|
|
model: "openrouter/z-ai/glm-5.2",
|
|
completionContract: {
|
|
id: "70000000-0000-4000-8000-000000000007",
|
|
sha256: `sha256:${"a".repeat(64)}`,
|
|
schemaVersion: "paperclip.run-result.v1",
|
|
contract: {
|
|
revision: "1",
|
|
objective: "Finish after the child",
|
|
criteria: [
|
|
{ id: "objective", requirement: "Report the child result" },
|
|
],
|
|
},
|
|
},
|
|
runtimeContext: nativeRuntimeContextFixture(),
|
|
});
|
|
|
|
expect(input.task.prompt).not.toContain("Execution contract:");
|
|
expect(input.task.prompt).not.toContain("Use child issues");
|
|
// Full bootstrap stays available if provider recovery fails after admission.
|
|
expect(input.task.prompt).toContain("## Paperclip Wake Payload");
|
|
expect(input.task.prompt).toContain("reason: issue_children_completed");
|
|
expect(input.task.prompt).toContain("DOT-147 Build utility (done)");
|
|
expect(input.task.prompt).toContain(
|
|
"Created three files and passed 7/7 tests.",
|
|
);
|
|
expect(input.task.prompt).toContain(
|
|
"Paperclip task context:\n- Issue: DOT-146",
|
|
);
|
|
expect(input.task.prompt).not.toContain("Use the child result.");
|
|
});
|
|
});
|