fix(acpx): separate restored history from active turn events

Keep history reconstruction without publishing restored text and tools as live output. Version the changed Cursor contract as profile 16 and retain replay compatibility.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-08 12:07:09 -05:00
1 parent 68b1a39459
commit 7d89e304f0
10 files changed
+39 -13

No files matched your search

+1 -1
View File
@@ -33,7 +33,7 @@ COPY cli/package.json ./cli/package.json
# The complete resolved lock (including transitive integrity hashes) is reviewed.
# Reject registry-time drift BEFORE installing packages or running lifecycle code.
# Refresh this digest together with source/provider dependency changes.
ARG PAPERCLIP_RUNNER_LOCK_SHA256=c63e2e731c27744e3de3a7dc8b3e115465ffe8c42e87c2a14e7d8927163f1b13
ARG PAPERCLIP_RUNNER_LOCK_SHA256=f5ee14ee77b1dc7771fe455d619c880fc64b1e62e40a15704addc9f7430e5c50
# pnpm 9 subtracts PNPM_WORKERS from available CPUs; it is not a worker count.
# Subtract all available CPUs to select its minimum (one tarball worker).
RUN export PNPM_WORKERS="$(node -p 'require("node:os").availableParallelism()')" \
+2 -2
View File
@@ -23,12 +23,12 @@
"requiresProviderPolicy": true
},
"cursor": {
"agentProfileVersion": 15,
"agentProfileVersion": 16,
"agentServerPackage": "cursor-agent",
"agentServerVersion": "2026.09.26-dd393fe",
"agentRuntimePackage": null,
"agentRuntimeVersion": null,
"commandDigest": "sha256:aaf20b110eb6e1de8bbea655157682ef0576416f7d80d6a02715805cbbfabde1",
"commandDigest": "sha256:d4d2e4f4542e8b4e1e86698ddbc617bf704221637bacb1f0b779a41bf5c649b9",
"requiresProviderPolicy": true
},
"copilot": {
@@ -1,11 +1,11 @@
{
"schema": "paperclip.cursor.production-contract.v1",
"profileVersion": 15,
"profileVersion": 16,
"sourceSnapshot": "22c78242a4e0c2369fecf0c2dc4e7600fbad6706",
"base": "dd868ed125cd709506dd9b29fca640a44d580501",
"vendor": "2026.09.26-dd393fe",
"nativePatch": "paperclip-cursor-usage-v4",
"acpxPatchSha256": "de7c4a5b35347156ae4a22142afd7a3731200f262ac6e1da3c10c666f50369e6",
"acpxPatchSha256": "94c72d31bf3b9ed656d57a341a353fb79f4028b443def7858b6e1fa33d0b8238",
"publicSetup": "paperclipai runtime setup cursor",
"assets": "public-server-and-provider-pack",
"modes": [
@@ -19,5 +19,5 @@
"planAcceptance": "successful-run-open-task-explicit-continuation",
"otherProviders": "mainline-gated",
"modelSelection": "explicit-provider-verified-startup-and-reconnect",
"commandDigest": "sha256:aaf20b110eb6e1de8bbea655157682ef0576416f7d80d6a02715805cbbfabde1"
"commandDigest": "sha256:d4d2e4f4542e8b4e1e86698ddbc617bf704221637bacb1f0b779a41bf5c649b9"
}
+12
View File
@@ -60,6 +60,11 @@ excluded from recovery copies. Only `hermes/memories` and `hermes/skills` enter
managed agent-file storage. Session databases stay private to a normalized
conversation. Assigned Paperclip skills remain separate protected inputs.
Between turns, authenticated run attachment refreshes the registered agent-file
working copy and assigned tool bindings. The session still pins prompt, bundle,
skill, connection, model, and permission identities. Unknown policy changes and
changes within the same run are rejected.
## Interaction contract
- Reasoning and assistant text use distinct message identities. Native tool
@@ -75,6 +80,7 @@ conversation. Assigned Paperclip skills remain separate protected inputs.
- Steering requires an explicit native acknowledgement. Follow-ups remain in
the durable Paperclip queue. Stop cancels native work and pending input before
bounded provider-process cleanup.
- Restored history is recorded without becoming new assistant text or live tools.
- Restoration requires nonempty native history and follows the native
compaction chain. Missing history and failed persistence are errors.
- Usage is a per-prompt delta. Native price calculations are labeled estimates;
@@ -97,6 +103,12 @@ idempotency key. Listing/inspection use existing authorized read APIs.
Scheduled firings create ordinary Paperclip work with routine provenance and
retain existing ownership, budget, pause, concurrency and duplicate-fire rules.
Provisioning requires uv 0.12.17 and, on macOS, Command Line Tools for
`install_name_tool` plus `codesign`. The Python closure normalizes interpreter
installation paths so local consumers and image builds reproduce the same pin.
Credential cleanup runs after verified exit even when learned-state collection
fails; save failures remain visible through normal runtime errors.
## Qualification
Hermes stays visibly **pending**. Candidate execution is an operator diagnostic
@@ -38,7 +38,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
agent_runtime_package: None,
agent_runtime_version: None,
command_digest:
"sha256:aaf20b110eb6e1de8bbea655157682ef0576416f7d80d6a02715805cbbfabde1",
"sha256:d4d2e4f4542e8b4e1e86698ddbc617bf704221637bacb1f0b779a41bf5c649b9",
requires_provider_policy: true,
},
"copilot" => AcpxReleaseProfile {
@@ -35,12 +35,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
"protocolVersion": 1,
"acpxVersion": "0.13.1",
"agent": "cursor",
"agentProfileVersion": 15,
"agentProfileVersion": 16,
"agentServerPackage": "cursor-agent",
"agentServerVersion": "2026.09.26-dd393fe",
"agentRuntimePackage": null,
"agentRuntimeVersion": null,
"commandDigest": "sha256:aaf20b110eb6e1de8bbea655157682ef0576416f7d80d6a02715805cbbfabde1",
"commandDigest": "sha256:d4d2e4f4542e8b4e1e86698ddbc617bf704221637bacb1f0b779a41bf5c649b9",
"permissionPolicy": "interactive"
},
"copilot": {
@@ -5,6 +5,7 @@ describe("historical ACPX profile decoding", () => {
it("retains each provider's existing revision boundary", () => {
expect(isSupportedAcpxProfileVersion("cursor", 14)).toBe(true);
expect(isSupportedAcpxProfileVersion("cursor", 15)).toBe(true);
expect(isSupportedAcpxProfileVersion("cursor", 16)).toBe(true);
expect(isSupportedAcpxProfileVersion("hermes", 1)).toBe(true);
expect(isSupportedAcpxProfileVersion("hermes", 2)).toBe(false);
for (const agent of ["pi", "claude", "codex", "grok", "copilot"]) {
@@ -12,7 +13,7 @@ describe("historical ACPX profile decoding", () => {
expect(isSupportedAcpxProfileVersion(agent, 6)).toBe(false);
}
});
it.each([0, 16, 1.5, "11", null, undefined, NaN])("rejects invalid revisions: %s", version => {
it.each([0, 17, 1.5, "11", null, undefined, NaN])("rejects invalid revisions: %s", version => {
expect(isSupportedAcpxProfileVersion("cursor", version)).toBe(false);
});
it.each(["unknown", "toString", "__proto__"])("rejects unregistered providers: %s", agent => {
@@ -1,10 +1,10 @@
/** Decodable historical profile revisions, not permission to launch them.
* Exact current profile/package/digest admission is checked separately. */
export type AcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15;
export type AcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16;
const historicalVersions: Readonly<Record<string, readonly AcpxProfileVersion[]>> = {
pi: [1, 2, 3, 4, 5], claude: [1, 2, 3, 4, 5], codex: [1, 2, 3, 4, 5],
grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5],
cursor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
cursor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
hermes: [1],
};
+13
View File
@@ -1097,6 +1097,19 @@ diff --git a/dist/runtime.js b/dist/runtime.js
const active = owner.activeTurn;
if (active) {
const { task, turn } = active;
@@ -833,9 +880,7 @@
if (turn.connectionUpdates) {
turn.connectionUpdates.push(notification);
- this.emitRuntimeTurnEvent(task, {
- jsonrpc: "2.0",
- method: "session/update",
- params: notification
- });
+ // ACP load/resume replays saved history before accepting a prompt.
+ // Retain it in the session projection without publishing it as new
+ // turn output, including tool calls and interruption metadata.
return;
}
@@ -863,6 +910,9 @@
projection.checkpoint.request();
}
+1 -1
View File
@@ -303,7 +303,7 @@ if (invokedPath && import.meta.url === pathToFileURL(invokedPath).href) {
const arguments_ = process.argv.slice(2);
const candidateFlag = arguments_[0];
if (arguments_.length > 1 || (candidateFlag !== undefined && !candidateFlag.startsWith("--candidate-providers="))) {
throw new Error("Expected only --candidate-providers=cursor,copilot,pi");
throw new Error("Expected only --candidate-providers=cursor,copilot,pi,hermes");
}
const candidateProviders = candidateFlag?.slice("--candidate-providers=".length).split(",").filter(Boolean) ?? [];
computeDaytonaImageContentId({ candidateProviders })