mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 18:19:05 +02:00
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 <noreply@paperclip.ing>
This commit is contained in:
1 parent
024776da24
commit
adc049b904
3 files changed
+54
-4
No files matched your search
@@ -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 },
|
||||
};
|
||||
|
||||
@@ -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<string, unknown>).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<string, any>, payload: Record<string, any>) => 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" }); },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user