From adc049b904db095f7515fc44eb12c76115e9ebe0 Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 30 Sep 2026 09:49:27 -0500 Subject: [PATCH] fix(runner): preserve Cursor notice identities and settlement Keep runnerd authority item IDs separate from native notice IDs. Retry optional diagnostic persistence on later state transitions without suppressing authoritative terminal save failures. Co-Authored-By: Paperclip --- .../src/live/cursor-usage-notice.ts | 7 ++- .../src/live/live-session.test.ts | 43 ++++++++++++++++++- .../paperclip-runner/src/live/live-session.ts | 8 +++- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/packages/paperclip-runner/src/live/cursor-usage-notice.ts b/packages/paperclip-runner/src/live/cursor-usage-notice.ts index 0b91417bf57..f31b0c301f4 100644 --- a/packages/paperclip-runner/src/live/cursor-usage-notice.ts +++ b/packages/paperclip-runner/src/live/cursor-usage-notice.ts @@ -31,9 +31,12 @@ export function cursorUsageNotice( || raw.threadId !== owner.threadId || raw.turnId !== owner.turnId || raw.eventType !== "provider.notice.recorded" || typeof raw.itemId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/.test(raw.itemId)) return null; + // Runnerd's envelope item is the durable authority item. The native notice + // keeps its own display identity inside the payload; these are independent. const p = raw.payload; if (!object(p) || !exact(p, ["schema", "noticeId", "severity", "category", "scope", "recoverable", "userActionable", "summary", "details"]) - || p.schema !== "paperclip.provider.notice.v1" || p.noticeId !== raw.itemId + || p.schema !== "paperclip.provider.notice.v1" || typeof p.noticeId !== "string" + || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/.test(p.noticeId) || p.severity !== "info" || p.category !== "cursor_native_usage_observed" || p.scope !== "turn" || p.recoverable !== true || p.userActionable !== false || p.summary !== SUMMARY || !Array.isArray(p.details) || p.details.length < 3 || p.details.length > 11) return null; @@ -84,7 +87,7 @@ export function cursorUsageNotice( } return { eventType: "provider.notice.recorded", itemId: raw.itemId, - payload: { schema: "paperclip.provider.notice.v1", noticeId: raw.itemId, severity: "info", + payload: { schema: "paperclip.provider.notice.v1", noticeId: p.noticeId, severity: "info", category: "cursor_native_usage_observed", scope: "turn", recoverable: true, userActionable: false, summary: SUMMARY, details: retained }, }; diff --git a/packages/paperclip-runner/src/live/live-session.test.ts b/packages/paperclip-runner/src/live/live-session.test.ts index f52980e72ae..bc3195bbcf6 100644 --- a/packages/paperclip-runner/src/live/live-session.test.ts +++ b/packages/paperclip-runner/src/live/live-session.test.ts @@ -1181,7 +1181,9 @@ describe("Capability live runnerd and Codex session", () => { }, }, }, "request-1", "cursor", `${turnId}:cursor-native-usage`)!; - return { method: "paperclip/canonicalProviderEvent", params: { threadId, turnId, ...canonical } }; + // Rust keeps payload.noticeId but replaces the display wrapper itemId + // with its durable authority item (durable/state.rs, runnerd transport). + return { method: "paperclip/canonicalProviderEvent", params: { threadId, turnId, ...canonical, itemId: "item_lab_fixture" } }; } const retained = (snapshot: CapabilityLiveSessionSnapshot) => snapshot.evidence.filter(entry => entry.kind === "provider_event" && entry.data.canonical === true && entry.data.event === "provider.notice.recorded"); @@ -1203,6 +1205,8 @@ describe("Capability live runnerd and Codex session", () => { const rows = retained(result.snapshot); expect(rows).toHaveLength(1); expect(rows[0]?.turnId).toBe(result.turnId); + expect(rows[0]?.data.itemId).toBe("item_lab_fixture"); + expect((rows[0]?.data.payload as Record).noticeId).toBe(`${result.turnId}:cursor-native-usage`); expect(rows[0]?.data.payload).toEqual(notice(state.threadId, result.turnId).params.payload); expect(result.snapshot.usageLedger).toEqual([]); expect(result.snapshot.usageUnavailable).toEqual([expect.objectContaining({ tokenUsage: null, costNanodollars: null, reason: "provider_did_not_report_usage" })]); @@ -1216,12 +1220,49 @@ describe("Capability live runnerd and Codex session", () => { } }); + it("keeps terminal settlement durable after an optional diagnostic save fails", async () => { + const state = providerState(); state.omitReadUsage = true; + state.onUsage = (queue, turnId) => { + queue.push(notice(state.threadId, turnId)); + queue.push({ method: "turn/completed", params: { threadId: state.threadId, turn: { id: turnId, status: "completed" } } }); + }; + const delegate = new InMemoryCapabilityLiveSessionStore(); + let rejectedDiagnosticSaves = 0; + const store: CapabilityLiveSessionStore = { + load: id => delegate.load(id), + delete: id => delegate.delete(id), + save: async snapshot => { + if (retained(snapshot).length > 0 && rejectedDiagnosticSaves === 0) { + rejectedDiagnosticSaves++; + throw new Error("transient optional diagnostic save failure"); + } + await delegate.save(snapshot); + }, + }; + const service = new CapabilityLiveSessionService({ store, transportFactory: fakeTransportFactory(state), transportOptions: { acpxCandidateProfile: "cursor" } }); + const session = await service.create({ provider: "acpx", acpxAgent: "cursor", requestedModel: "exact-model" }); + try { + const result = await session.sendMessage("Orient to this task."); + expect(rejectedDiagnosticSaves).toBe(1); + expect(result.status).toBe("completed"); + const saved = (await store.load(session.id))!; + expect(saved.terminalTurns).toContainEqual(expect.objectContaining({ turnId: result.turnId, status: "completed" })); + expect(retained(saved)).toEqual(retained(result.snapshot)); + expect(retained(saved)).toHaveLength(1); + expect(saved.usageLedger).toEqual([]); + expect(saved.usageUnavailable).toHaveLength(1); + } finally { await service.shutdown(session.id); } + }); + const malformedNotices: Array<(params: Record, payload: Record) => void> = [ params => { params.threadId = "foreign-thread"; }, params => { params.turnId = "previous-turn"; }, params => { delete params.turnId; }, params => { params.itemId = "raw private identity with spaces"; }, params => { params.unexpected = "SECRET_CANARY"; }, + (_params, payload) => { payload.noticeId = "raw private identity with spaces"; }, + (_params, payload) => { payload.noticeId = "x".repeat(161); }, + (_params, payload) => { delete payload.noticeId; }, (_params, payload) => { payload.unexpected = "SECRET_CANARY"; }, (_params, payload) => { payload.summary = "SECRET_CANARY"; }, (_params, payload) => { payload.details.push({ name: "rawProviderText", value: "SECRET_CANARY" }); }, diff --git a/packages/paperclip-runner/src/live/live-session.ts b/packages/paperclip-runner/src/live/live-session.ts index cc6cb36314c..854048859bb 100644 --- a/packages/paperclip-runner/src/live/live-session.ts +++ b/packages/paperclip-runner/src/live/live-session.ts @@ -2771,7 +2771,13 @@ export class CapabilityLiveSession { this.#appendEvidence("provider_event", this.#activeTurnId, { canonicalEventType: notice.eventType, itemId: notice.itemId, payload: notice.payload, }); - await this.#persist(); + try { + await this.#persist(); + } catch { + // These counters are optional diagnostics. Keep the evidence in the + // snapshot so #persist's recovered queue can retry on the next state + // transition. Authoritative terminal saves must still succeed. + } } return; }