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; }