diff --git a/packages/paperclip-runner/src/protocol/replay-contract.test.ts b/packages/paperclip-runner/src/protocol/replay-contract.test.ts index 2b98ffe4acb..f805104a92f 100644 --- a/packages/paperclip-runner/src/protocol/replay-contract.test.ts +++ b/packages/paperclip-runner/src/protocol/replay-contract.test.ts @@ -386,6 +386,19 @@ it("accepts marked whole-run usage through the runtime standalone event validato expect(result).toMatchObject({ ok: true }); }); +it("rejects an event without normalizedSessionId, which the schemas leave optional", async () => { + const fixture = await readFixture(); + const event = (fixture.events as Array>)[0]!; + expect(validatePrpEvent(event)).toMatchObject({ ok: true }); + + const { normalizedSessionId: _omitted, ...withoutSession } = event; + expect(validatePrpEvent(withoutSession)).toEqual({ + ok: false, + event: null, + issues: [{ code: "schema_validation", path: "/normalizedSessionId", message: "event normalizedSessionId is required" }], + }); +}); + it("declares required incomplete reconciliation evidence in the strict usage contract", () => { const validate = new Ajv2020({ strict: true, allErrors: true }).compile(usageSchema); const measurement = { inputTokens: 120, outputTokens: 12, cacheReadTokens: 0, cacheWriteTokens: 0, activeSeconds: 0, requests: 0, providerCostUsd: 0 }; diff --git a/packages/paperclip-runner/src/protocol/replay-contract.ts b/packages/paperclip-runner/src/protocol/replay-contract.ts index 18ee623889e..9b3a2f6d500 100644 --- a/packages/paperclip-runner/src/protocol/replay-contract.ts +++ b/packages/paperclip-runner/src/protocol/replay-contract.ts @@ -504,6 +504,23 @@ export function validatePrpEvent(value: unknown): EventValidationResult { issues: (eventValidator.errors ?? []).map(ajvIssue), }; } + // The published schemas leave normalizedSessionId optional, but every + // producer sets it (the TypeScript PrpEvent type and the Rust event structs + // both require it), and consumers compare it to the expected session. The + // schema already limits its type and length when it is present. + if (record?.normalizedSessionId === undefined) { + return { + ok: false, + event: null, + issues: [ + { + code: "schema_validation", + path: "/normalizedSessionId", + message: "event normalizedSessionId is required", + }, + ], + }; + } return { ok: true, event: value, issues: [] }; }