diff --git a/packages/paperclip-runner/spec/operation-groups/source.json b/packages/paperclip-runner/spec/operation-groups/source.json
index 2d1cecf0f87..571c221d980 100644
--- a/packages/paperclip-runner/spec/operation-groups/source.json
+++ b/packages/paperclip-runner/spec/operation-groups/source.json
@@ -11,6 +11,7 @@
"session": "Provider-neutral session open, resume, reconciliation, close, and failure.",
"turn": "Model turn submission through terminal turn disposition.",
"item": "Provider-neutral model/tool item lifecycle.",
+ "output": "Negotiated, immutable output-body chunks with exact byte offsets and content digests.",
"mcp_app": "MCP App discovery, initialization, tool, action, host-context, and teardown evidence.",
"runtime_request": "Runtime permission/input request lifecycle.",
"interaction": "Issue-thread interaction proposal, materialization, response, delivery, and rejection.",
diff --git a/packages/paperclip-runner/spec/paperclip-agent-operation-groups.md b/packages/paperclip-runner/spec/paperclip-agent-operation-groups.md
index b6bfa691674..31a65d7a84a 100644
--- a/packages/paperclip-runner/spec/paperclip-agent-operation-groups.md
+++ b/packages/paperclip-runner/spec/paperclip-agent-operation-groups.md
@@ -6,7 +6,7 @@ Status: canonical explanatory contract for the Paperclip runner V1 surface.
This document keeps three independent meanings of **group** separate. PRP families describe wire evidence and controller commands; capability placement decides who owns an operation; behavioral eval groups organize the 106 scenario corpus. None of the three axes can be used as a substitute for another.
-The generated totals are **105 PRP events in 31 event families**, **18 controller commands in 7 command families**, **10 control-plane operations**, **48 reconciled semantic operations** (14 always, 34 optional), and **106 scenarios in 16 behavior groups**.
+The generated totals are **106 PRP events in 32 event families**, **18 controller commands in 7 command families**, **10 control-plane operations**, **48 reconciled semantic operations** (14 always, 34 optional), and **106 scenarios in 16 behavior groups**.
## Axis 1: PRP v1 event and command families
@@ -37,6 +37,7 @@ PRP records ordered, replayable execution evidence. It is not the model's Paperc
| `provider` | Redacted provider notices and actionable warnings. | `provider.notice.recorded` | 1 |
| `session` | Provider-neutral session open, resume, reconciliation, close, and failure. | `session.starting`
`session.started`
`session.resuming`
`session.resumed`
`session.reconciled`
`session.updated`
`session.closed`
`session.failed` | 8 |
| `turn` | Model turn submission through terminal turn disposition. | `turn.submitted`
`turn.accepted`
`turn.started`
`turn.completed`
`turn.failed`
`turn.interrupted`
`turn.cancelled` | 7 |
+| `output` | Negotiated, immutable output-body chunks with exact byte offsets and content digests. | `output.body.chunk` | 1 |
| `item` | Provider-neutral model/tool item lifecycle. | `item.started`
`item.delta`
`item.completed`
`item.failed` | 4 |
| `usage` | Provider/model-attributed usage and accounting boundaries. | `usage.reported` | 1 |
| `semantic_tool` | Canonical authorized Paperclip tool input and result evidence. | `semantic_tool.input`
`semantic_tool.result`
`semantic_tool.reconciled` | 3 |
diff --git a/packages/paperclip-runner/src/backends/harness-driver-backend.test.ts b/packages/paperclip-runner/src/backends/harness-driver-backend.test.ts
index 317f80e5a13..6cf9fbbb87b 100644
--- a/packages/paperclip-runner/src/backends/harness-driver-backend.test.ts
+++ b/packages/paperclip-runner/src/backends/harness-driver-backend.test.ts
@@ -4,6 +4,7 @@ import type { HarnessDriver, HarnessSession, PersistedHarnessSession } from "../
import type { PrpEvent, PrpStructuredRunResult, PrpTerminalState } from "../protocol/replay-contract.js";
import { NativeSessionProtocolIntegrityError } from "../contracts/native-session-backend.js";
import { HarnessDriverBackend } from "./harness-driver-backend.js";
+import { authorityJson } from "../control-plane/durable-authority-store.js";
const result: PrpStructuredRunResult = {
schema: "paperclip.run_result.v1",
@@ -97,6 +98,26 @@ const driver: HarnessDriver = {
};
describe("HarnessDriverBackend", () => {
+ it.each([false, true])("recovers a committed terminal after delivery ACK and validates its binding (conflict: %s)", async (conflict) => {
+ class DurableTerminalSession extends FakeHarnessSession {
+ override async *events(): AsyncIterable { /* Already acknowledged before the crash. */ }
+ override async snapshot(): Promise {
+ return {
+ ...(await super.snapshot()), activeTurnId: null,
+ semanticResult: { result, fingerprint: authorityJson(result), turnId: "turn-1" },
+ durableTerminal: { event: { ...prpEvent(2, "turn.completed", { status: "completed" }), ...(conflict ? { normalizedSessionId: "other-session" } : {}) }, semanticFingerprint: authorityJson(result) },
+ };
+ }
+ }
+ const backend = new HarnessDriverBackend({ ...driver, openSession: async () => new DurableTerminalSession() });
+ const session = await backend.openSession({ identity: { runId: "run-1", sessionId: "session-1", companyId: "company-1", issueId: "issue-1", agentId: "agent-1" } });
+ if (conflict) await expect(session.snapshot()).rejects.toBeInstanceOf(NativeSessionProtocolIntegrityError);
+ else {
+ expect((await session.snapshot()).terminal).toMatchObject({ turnTerminalState: "completed", runTerminalState: "succeeded" });
+ expect(await session.result()).toMatchObject({ result, turnId: "turn-1" });
+ }
+ });
+
it.each([false, true])("honors driver steering support even when the transport exposes a steer method (%s)", async supported => {
const steer = vi.fn(async () => { throw new Error("provider does not support steering"); });
const session = Object.assign(new FakeHarnessSession(), { steer });
diff --git a/packages/paperclip-runner/src/backends/harness-driver-backend.ts b/packages/paperclip-runner/src/backends/harness-driver-backend.ts
index 135724c5421..e8a26f2c08a 100644
--- a/packages/paperclip-runner/src/backends/harness-driver-backend.ts
+++ b/packages/paperclip-runner/src/backends/harness-driver-backend.ts
@@ -1,3 +1,4 @@
+import { decodeSourceCursor, encodeSourceCursor, normalizedEventId } from "../control-plane/event-epochs.js";
import type {
HarnessDriver,
HarnessSession,
@@ -17,6 +18,7 @@ import type {
PrpEvent,
PrpTerminalState,
} from "../protocol/replay-contract.js";
+import { validatePrpEvent } from "../protocol/replay-contract.js";
const MAX_RECOVERY_TERMINAL_TURNS = 4_096;
const MAX_RECOVERY_TERMINAL_BYTES = 8 * 1024 * 1024;
@@ -133,6 +135,7 @@ export class HarnessDriverBackend implements NativeSessionBackend {
normalizedSessionId: snapshot.identity.sessionId,
activeTurnId,
lastSourceSequence: parseCursor(snapshot.cursor),
+ ...(decodeSourceCursor(snapshot.cursor).sourceEpoch ? { lastSourceEpoch: decodeSourceCursor(snapshot.cursor).sourceEpoch } : {}),
...(snapshot.semanticResult === undefined ||
snapshot.semanticResult === null
? {}
@@ -393,6 +396,7 @@ class HarnessNativeSession implements NativeSession {
readonly #session: HarnessSession;
#terminal: PrpTerminalState | null = null;
#explicitlyCancelled = false;
+ #lastDriverEventId: string | null = null;
#protocolIntegrityFailure: NativeSessionProtocolIntegrityError | null = null;
#assertProtocolIntegrity(): void {
@@ -412,6 +416,24 @@ class HarnessNativeSession implements NativeSession {
try {
const snapshot = await this.#session.snapshot();
this.#assertProtocolIntegrity();
+ // The indexed driver has an explicit terminal fact even if the previous
+ // process died after its run-log ACK but before orchestration checkpointed.
+ const durable = snapshot.durableTerminal;
+ if (!this.#explicitlyCancelled && this.#terminal === null && durable && snapshot.activeTurnId === null && snapshot.semanticResult) {
+ const event = durable.event;
+ if (!validatePrpEvent(event).ok || event.runId !== this.#input.identity.runId || event.normalizedSessionId !== this.#input.identity.sessionId
+ || event.turnId !== snapshot.semanticResult.turnId || durable.semanticFingerprint !== snapshot.semanticResult.fingerprint
+ || durable.semanticFingerprint !== canonicalJson(snapshot.semanticResult.result)
+ || !["turn.completed", "turn.failed", "turn.interrupted", "turn.cancelled"].includes(event.eventType)) {
+ throw new NativeSessionProtocolIntegrityError("source_event_replay_conflict");
+ }
+ this.#terminal = {
+ schema: "paperclip.prp.terminal.v1",
+ turnTerminalState: event.eventType === "turn.completed" ? "completed" : event.eventType === "turn.failed" ? "failed" : event.eventType === "turn.interrupted" ? "interrupted" : "cancelled",
+ runTerminalState: event.eventType === "turn.completed" ? "succeeded" : event.eventType === "turn.failed" ? "failed" : "cancelled",
+ reportedWorkDisposition: snapshot.semanticResult.result.reportedWorkDisposition,
+ };
+ }
return snapshot;
} catch (error) {
this.#rethrowProtocolIntegrity(error);
@@ -504,6 +526,7 @@ class HarnessNativeSession implements NativeSession {
this.#assertProtocolIntegrity();
let sourceInstanceId: string | null = null;
let lastSourceSequence = 0;
+ let lastSourceEpoch: string | undefined;
let sawTerminal = false;
let synthesizedDurableWait = false;
let streamFailure: unknown = null;
@@ -517,7 +540,8 @@ class HarnessNativeSession implements NativeSession {
event.payload.kind === "interrupt_acknowledgement");
if (this.#explicitlyCancelled && !isCancellationEvent) continue;
sourceInstanceId = event.sourceInstanceId;
- lastSourceSequence = Math.max(lastSourceSequence, event.sourceSeq);
+ lastSourceSequence = event.sourceSeq;
+ lastSourceEpoch = event.sourceEpoch;
if (event.eventType === "runtime_request.created") {
const request = plainRecord(event.payload.request);
if (
@@ -577,6 +601,7 @@ class HarnessNativeSession implements NativeSession {
reportedWorkDisposition: disposition,
};
}
+ this.#lastDriverEventId = event.sourceEventId;
yield structuredClone(event);
}
} catch (error) {
@@ -596,7 +621,8 @@ class HarnessNativeSession implements NativeSession {
let governedWaitTurnId: string | undefined;
for (const request of observedPendingInputs.values()) {
const sourceSeq =
- Math.max(lastSourceSequence, snapshot?.lastSourceSequence ?? 0) + 1;
+ Math.max(lastSourceSequence, snapshot?.lastSourceEpoch === lastSourceEpoch ? snapshot?.lastSourceSequence ?? 0 : 0) + 1;
+ if (!Number.isSafeInteger(sourceSeq)) throw new Error("native_synthetic_event_epoch_required");
lastSourceSequence = sourceSeq;
const requestId = String(request.requestId);
const turnId =
@@ -606,7 +632,8 @@ class HarnessNativeSession implements NativeSession {
typeof request.itemId === "string" ? request.itemId : requestId;
yield {
schema: "paperclip.prp.event.v1",
- sourceEventId: `${sourceInstanceId}:${this.#input.identity.runId}:${sourceSeq}`,
+ sourceEventId: normalizedEventId(sourceInstanceId, this.#input.identity.runId, sourceSeq, lastSourceEpoch),
+ ...(lastSourceEpoch ? { sourceEpoch: lastSourceEpoch } : {}),
sourceSeq,
sourceInstanceId,
sourceKind: "runner",
@@ -635,7 +662,8 @@ class HarnessNativeSession implements NativeSession {
}
if (governedWaitTurnId) {
const sourceSeq =
- Math.max(lastSourceSequence, snapshot?.lastSourceSequence ?? 0) + 1;
+ Math.max(lastSourceSequence, snapshot?.lastSourceEpoch === lastSourceEpoch ? snapshot?.lastSourceSequence ?? 0 : 0) + 1;
+ if (!Number.isSafeInteger(sourceSeq)) throw new Error("native_synthetic_event_epoch_required");
lastSourceSequence = sourceSeq;
synthesizedDurableWait = true;
sawTerminal = true;
@@ -647,7 +675,8 @@ class HarnessNativeSession implements NativeSession {
};
yield {
schema: "paperclip.prp.event.v1",
- sourceEventId: `${sourceInstanceId}:${this.#input.identity.runId}:${sourceSeq}`,
+ sourceEventId: normalizedEventId(sourceInstanceId, this.#input.identity.runId, sourceSeq, lastSourceEpoch),
+ ...(lastSourceEpoch ? { sourceEpoch: lastSourceEpoch } : {}),
sourceSeq,
sourceInstanceId,
sourceKind: "runner",
@@ -665,6 +694,22 @@ class HarnessNativeSession implements NativeSession {
if (streamFailure && !synthesizedDurableWait) throw streamFailure;
}
+ async acknowledgeEvent(event: PrpEvent): Promise {
+ // Backend-synthesized provider-loss facts have their own durable writer;
+ // they were never entries in the driver's normalized outbox.
+ if (this.#lastDriverEventId !== event.sourceEventId) return;
+ await this.#session.acknowledgeEvent?.(event);
+ this.#lastDriverEventId = null;
+ }
+
+ setEventCommitter(commit: (event: PrpEvent) => Promise): void {
+ this.#session.setEventCommitter?.(commit);
+ }
+
+ async flushEventDelivery(): Promise {
+ await this.#session.flushEventDelivery?.();
+ }
+
async startTurn(input: Parameters[0]) {
this.#assertProtocolIntegrity();
// Stop can arrive after session publication but before the first turn.
@@ -801,7 +846,7 @@ class HarnessNativeSession implements NativeSession {
cursor:
snapshot.lastSourceSequence === undefined
? null
- : String(snapshot.lastSourceSequence),
+ : encodeSourceCursor({ sourceSeq: snapshot.lastSourceSequence, sourceEpoch: snapshot.lastSourceEpoch }),
semanticResult: this.#explicitlyCancelled
? null
: (snapshot.semanticResult?.result ?? null),
@@ -849,7 +894,7 @@ export function createHarnessDriverBackend(
function parseCursor(cursor: string | null | undefined): number | undefined {
if (cursor === undefined || cursor === null || cursor === "")
return undefined;
- const parsed = Number(cursor);
+ const parsed = decodeSourceCursor(cursor).sourceSeq;
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : undefined;
}
diff --git a/packages/paperclip-runner/src/catalog/operation-groups-doc.test.ts b/packages/paperclip-runner/src/catalog/operation-groups-doc.test.ts
index 9e3393b9ebb..8e279fa8139 100644
--- a/packages/paperclip-runner/src/catalog/operation-groups-doc.test.ts
+++ b/packages/paperclip-runner/src/catalog/operation-groups-doc.test.ts
@@ -68,7 +68,7 @@ describe("canonical operation-groups specification", () => {
expect(input.source.behaviorGroups).toHaveLength(16);
expect(input.scenarios).toHaveLength(106);
expect(new Set(input.scenarios.map((scenario) => scenario.id)).size).toBe(106);
- expect(input.eventTypes).toHaveLength(105);
+ expect(input.eventTypes).toHaveLength(106);
expect(input.commandTypes).toHaveLength(18);
});
diff --git a/packages/paperclip-runner/src/catalog/operation-groups-doc.ts b/packages/paperclip-runner/src/catalog/operation-groups-doc.ts
index 5f2fcf6514a..c0da43e10a3 100644
--- a/packages/paperclip-runner/src/catalog/operation-groups-doc.ts
+++ b/packages/paperclip-runner/src/catalog/operation-groups-doc.ts
@@ -99,7 +99,7 @@ export interface OperationGroupsInput {
catalogIndexSource: string;
}
-const EXPECTED_EVENT_COUNT = 105;
+const EXPECTED_EVENT_COUNT = 106;
const EXPECTED_COMMAND_COUNT = 18;
const EXPECTED_SCENARIO_COUNT = 106;
const EXPECTED_BEHAVIOR_GROUP_COUNT = 16;
diff --git a/packages/paperclip-runner/src/contracts/control-plane-port.ts b/packages/paperclip-runner/src/contracts/control-plane-port.ts
index d8773168683..c4d845df624 100644
--- a/packages/paperclip-runner/src/contracts/control-plane-port.ts
+++ b/packages/paperclip-runner/src/contracts/control-plane-port.ts
@@ -17,8 +17,9 @@ export interface OpenControlPlaneRunInput {
}
export interface AppendedEventReceipt {
- cursor: number;
+ cursor: number | string;
highestContiguousSourceSeq: number;
+ highestContiguousSourceEpoch?: string;
disposition: "committed" | "duplicate";
}
@@ -42,12 +43,14 @@ export interface ReplayControlPlaneEventsInput {
runId: string;
sourceInstanceId: string;
afterSourceSeq: number;
+ sourceEpoch?: string;
limit: number;
}
export interface ReplayedControlPlaneEvents {
events: PrpEvent[];
highestContiguousSourceSeq: number;
+ highestContiguousSourceEpoch?: string;
}
export interface FinalizeControlPlaneOperationOptions {
diff --git a/packages/paperclip-runner/src/contracts/durable-recovery.ts b/packages/paperclip-runner/src/contracts/durable-recovery.ts
index 6b51c94db65..14f4ed11c89 100644
--- a/packages/paperclip-runner/src/contracts/durable-recovery.ts
+++ b/packages/paperclip-runner/src/contracts/durable-recovery.ts
@@ -92,6 +92,7 @@ export interface DurableRecoveryCoreCommand {
schema: "paperclip.prp.command.v1" | "paperclip.prp.command.v2";
commandId: string;
controllerSeq: number;
+ controllerEpoch?: string;
type: string;
issuedAt: string;
payload: Record;
@@ -106,6 +107,7 @@ export interface DurableRecoveryCoreCommand {
}
export interface DurableRecoveryCommittedEvent {
+ sourceEpoch?: string;
sourceSeq: number;
sourceEventId: string;
eventType: string;
diff --git a/packages/paperclip-runner/src/contracts/harness-driver.ts b/packages/paperclip-runner/src/contracts/harness-driver.ts
index 3400b419c9c..eed4137dd1f 100644
--- a/packages/paperclip-runner/src/contracts/harness-driver.ts
+++ b/packages/paperclip-runner/src/contracts/harness-driver.ts
@@ -470,6 +470,8 @@ export interface PersistedHarnessSession {
activeTurnId?: string | null;
semanticResult?: PersistedHarnessSemanticResult | null;
terminalTurns?: PersistedHarnessTurnTerminal[];
+ /** Indexed reducer fact committed with its normalized delivery outbox. */
+ durableTerminal?: { event: PrpEvent; semanticFingerprint: string | null };
codexUsageBaseline?: { baseline: Record; latest: Record };
/** A result-less terminal task may spend this fail-closed one-shot recovery allowance. */
dispositionOnlyRecoveryConsumed?: boolean;
@@ -479,6 +481,7 @@ export interface PersistedHarnessSession {
goal?: HarnessThreadGoal | null;
lineage?: HarnessThreadLineageEntry[];
lastSourceSequence?: number;
+ lastSourceEpoch?: string;
/** Tagged provider identity used to reject cross-profile recovery. */
providerIdentity?: PersistedHarnessProviderIdentity;
/** Narrow escape hatch for a durable response-wake when a provider cannot reload its prior native session. */
@@ -501,6 +504,10 @@ export interface HarnessSession {
displayId?: string | null;
};
events(): AsyncIterable;
+ /** Called only after the exact normalized event is durable in the run log. */
+ acknowledgeEvent?(event: PrpEvent): Promise;
+ setEventCommitter?(commit: (event: PrpEvent) => Promise): void;
+ flushEventDelivery?(): Promise;
attachRun?(input: { runId: string }): Promise | void;
startTurn(input: {
message: NativeUserMessage;
diff --git a/packages/paperclip-runner/src/contracts/native-execution.ts b/packages/paperclip-runner/src/contracts/native-execution.ts
index c23c2f9ab18..9253111347b 100644
--- a/packages/paperclip-runner/src/contracts/native-execution.ts
+++ b/packages/paperclip-runner/src/contracts/native-execution.ts
@@ -232,6 +232,7 @@ export interface NativeSessionExecutionResult {
driverVersion: string;
nativeEventCount: number;
highestContiguousSourceSeq: number;
+ highestContiguousSourceEpoch?: string;
usage: Record | null;
/** The active durable goal reached a safe turn boundary for run rollover. */
goalRolloverRequired?: boolean;
diff --git a/packages/paperclip-runner/src/contracts/native-session-backend.ts b/packages/paperclip-runner/src/contracts/native-session-backend.ts
index 856d04975af..abb33addb6f 100644
--- a/packages/paperclip-runner/src/contracts/native-session-backend.ts
+++ b/packages/paperclip-runner/src/contracts/native-session-backend.ts
@@ -144,6 +144,9 @@ export interface NativeSession {
/** Relinquish controller authority without suspending provider execution. */
detachControllerForRestart?(): Promise;
events(input?: { afterCursor?: string | null }): AsyncIterable;
+ acknowledgeEvent?(event: PrpEvent): Promise;
+ setEventCommitter?(commit: (event: PrpEvent) => Promise): void;
+ flushEventDelivery?(): Promise;
startTurn(input: {
message: NativeUserMessage;
/** Set by orchestration only after successful provider-session recovery. */
diff --git a/packages/paperclip-runner/src/control-plane/authority-generation.test.ts b/packages/paperclip-runner/src/control-plane/authority-generation.test.ts
new file mode 100644
index 00000000000..61d85a42d72
--- /dev/null
+++ b/packages/paperclip-runner/src/control-plane/authority-generation.test.ts
@@ -0,0 +1,47 @@
+import { describe, expect, it } from "vitest";
+import { authorityDirectlyFollows, authorityGeneration, authorityInteger, validateAuthorityCommit, validateAuthorityWorkPage } from "./durable-authority-store.js";
+
+const first = "r:00000000-0000-4000-8000-000000000001";
+const second = "r:00000000-0000-4000-8000-000000000002";
+const maximum = "9223372036854775807";
+
+describe("exact authority revisions", () => {
+ it("accepts legacy exact boundaries and opaque revisions without converting them to numbers", () => {
+ for (const value of ["0", "9007199254740993", maximum, first]) expect(authorityGeneration(value)).toBe(value);
+ expect(authorityDirectlyFollows({ generation: first, committedFrom: maximum, state: {} }, maximum)).toBe(true);
+ expect(authorityDirectlyFollows({ generation: second, committedFrom: first, state: {} }, first)).toBe(true);
+ validateAuthorityCommit({ expectedGeneration: first, state: {}, records: [] });
+ validateAuthorityWorkPage("process-owner", "", 128, first);
+ });
+
+ it("requires exact immediate predecessor evidence for migration activation", () => {
+ const latest = { generation: second, committedFrom: first, state: {} };
+ expect(authorityDirectlyFollows(latest, maximum)).toBe(false);
+ expect(authorityDirectlyFollows(latest, second)).toBe(false);
+ expect(authorityDirectlyFollows({ generation: first, state: {} }, maximum)).toBe(false);
+ expect(authorityDirectlyFollows({ generation: "2", state: {} }, first)).toBe(false);
+ expect(authorityDirectlyFollows({ generation: first, committedFrom: first, state: {} }, first)).toBe(false);
+ expect(authorityDirectlyFollows({ generation: "9007199254740993", state: {} }, "9007199254740992")).toBe(true);
+ expect(authorityDirectlyFollows({ generation: maximum, state: {} }, maximum)).toBe(false);
+ });
+
+ it.each(["-1", "01", "9223372036854775808", "r:missing", first.toUpperCase(), first.replace("-4000-", "-1000-")])("rejects invalid revision %s", value => {
+ expect(() => authorityGeneration(value)).toThrow("invalid_authority");
+ });
+
+ it("keeps event ordering numeric and separate from opaque state revisions", () => {
+ expect(authorityInteger(maximum)).toBe(9_223_372_036_854_775_807n);
+ expect(() => authorityInteger(first)).toThrow("invalid_authority");
+ expect(() => validateAuthorityCommit({ expectedGeneration: first, state: {}, records: [
+ { epoch: "epoch-1", kind: "event", id: "event-1", sequence: second, body: {} },
+ ] })).toThrow("invalid_authority");
+ });
+
+ it("reserves zero for effects which have exact identities without ordering", () => {
+ const receipt = { epoch: "run-1", kind: "effect" as const, id: "effect", sequence: "0", body: {} };
+ expect(() => validateAuthorityCommit({ expectedGeneration: first, state: {}, records: [receipt] })).not.toThrow();
+ for (const kind of ["command", "event"] as const) {
+ expect(() => validateAuthorityCommit({ expectedGeneration: first, state: {}, records: [{ ...receipt, kind }] })).toThrow("invalid_authority");
+ }
+ });
+});
diff --git a/packages/paperclip-runner/src/control-plane/authority-locator.test.ts b/packages/paperclip-runner/src/control-plane/authority-locator.test.ts
new file mode 100644
index 00000000000..c93f1d0f22d
--- /dev/null
+++ b/packages/paperclip-runner/src/control-plane/authority-locator.test.ts
@@ -0,0 +1,28 @@
+import { closeSync, mkdtempSync, openSync, rmSync, truncateSync, writeSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { resolve } from "node:path";
+import { expect, it } from "vitest";
+import { readDurableControlPlaneState } from "./authority-locator.js";
+import { DURABLE_PRP_CONTROL_PLANE_MAX_STATE_BYTES } from "./durable-prp-control-plane.js";
+
+it("preserves the existing legacy reader window when adding indexed locators", async () => {
+ const root = mkdtempSync(resolve(tmpdir(), "legacy-authority-window-"));
+ const path = resolve(root, "control-plane-state.json");
+ try {
+ const fd = openSync(path, "wx", 0o600);
+ try {
+ writeSync(fd, '{"schema":"paperclip.runner.durable.control-plane-state.v1","history":"');
+ const chunk = Buffer.alloc(1024 * 1024, "x");
+ for (let index = 0; index < 65; index++) writeSync(fd, chunk);
+ writeSync(fd, '"}');
+ } finally { closeSync(fd); }
+ // This valid compatibility file exceeds the accidental 64 MiB locator
+ // reader cap, but is still inside the controller's original 192 MiB bound.
+ const state = await readDurableControlPlaneState(root);
+ expect((state.history as string).length).toBe(65 * 1024 * 1024);
+ // A sparse oversized file is only a metadata-rejection fixture, not growth
+ // qualification: the reader must reject it before allocating its body.
+ truncateSync(path, DURABLE_PRP_CONTROL_PLANE_MAX_STATE_BYTES + 1);
+ await expect(readDurableControlPlaneState(root)).rejects.toThrow("state_unsafe");
+ } finally { rmSync(root, { recursive: true, force: true }); }
+}, 15_000);
diff --git a/packages/paperclip-runner/src/control-plane/authority-locator.ts b/packages/paperclip-runner/src/control-plane/authority-locator.ts
new file mode 100644
index 00000000000..a301fa42dcb
--- /dev/null
+++ b/packages/paperclip-runner/src/control-plane/authority-locator.ts
@@ -0,0 +1,80 @@
+import { constants, lstatSync, readSync, openSync, fstatSync, closeSync } from "node:fs";
+import { basename, resolve } from "node:path";
+import { SqliteAuthorityStore } from "./sqlite-authority-store.js";
+import { assertNoPendingLegacyMigration } from "./legacy-migration-gate.js";
+import { DurableAuthorityStoreError, MAX_LEGACY_AUTHORITY_STATE_BYTES, type AuthoritySnapshot } from "./durable-authority-store.js";
+import { materializeCurrentAuthority } from "./current-authority-evidence.js";
+import { authorityJson } from "./durable-authority-store.js";
+import { createHash } from "node:crypto";
+
+export interface AuthorityLocation {
+ kind: "sqlite" | "postgres";
+ binding: string;
+ file?: string;
+ epoch?: string;
+}
+export interface AuthorityLocator {
+ schema: "paperclip.runner.authority-locator.v1";
+ location: AuthorityLocation;
+}
+export type ExternalAuthorityReader = (location: AuthorityLocation) => Promise;
+
+export function parseAuthorityLocator(value: unknown): AuthorityLocator | null {
+ const record = value as Partial | null;
+ if (!record || record.schema !== "paperclip.runner.authority-locator.v1") return null;
+ const location = record.location;
+ if (!location || !["sqlite", "postgres"].includes(location.kind) || typeof location.binding !== "string" || location.binding.length > 4096 ||
+ (location.kind === "sqlite" && (typeof location.file !== "string" || location.file !== basename(location.file) || !/^[a-zA-Z0-9_.-]+\.sqlite$/.test(location.file))) ||
+ (location.kind === "postgres" && (typeof location.epoch !== "string" || !/^[a-zA-Z0-9_.:-]{1,240}$/.test(location.epoch)))) {
+ throw new DurableAuthorityStoreError("invalid_authority", "invalid authority locator");
+ }
+ return record as AuthorityLocator;
+}
+
+/** Legacy files are bounded compatibility input; indexed locators load one row. */
+export async function readDurableControlPlaneState(directory: string, readExternal?: ExternalAuthorityReader): Promise> {
+ return (await readDurableControlPlaneSnapshot(directory, readExternal)).state;
+}
+
+/** The materialized view and its stored digest are different representations.
+ * Archive fences compare the latter, after checking the former still matches
+ * the authority the caller admitted. External stores own their own fences. */
+export async function readDurableControlPlaneSnapshot(directory: string, readExternal?: ExternalAuthorityReader): Promise<{
+ state: Record; storedSha256: string | null;
+}> {
+ assertNoPendingLegacyMigration(directory);
+ const path = resolve(directory, "control-plane-state.json");
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
+ let value: unknown;
+ try {
+ const stat = fstatSync(fd);
+ if (!stat.isFile() || stat.size > MAX_LEGACY_AUTHORITY_STATE_BYTES) throw new Error("native_runner_control_plane_state_unsafe");
+ const bytes = Buffer.alloc(stat.size);
+ let offset = 0;
+ while (offset < bytes.length) {
+ const count = readSync(fd, bytes, offset, bytes.length - offset, offset);
+ if (!count) throw new Error("native_runner_control_plane_state_changed");
+ offset += count;
+ }
+ const after = fstatSync(fd);
+ if (after.size !== stat.size || after.mtimeMs !== stat.mtimeMs || after.ctimeMs !== stat.ctimeMs) throw new Error("native_runner_control_plane_state_changed");
+ value = JSON.parse(bytes.toString("utf8"));
+ if (parseAuthorityLocator(value) && process.platform !== "win32" && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())) throw new Error("native_runner_control_plane_state_unsafe");
+ } finally { closeSync(fd); }
+ const locator = parseAuthorityLocator(value);
+ if (!locator) return { state: value as Record, storedSha256: null };
+ if (locator.location.kind === "postgres") {
+ if (!readExternal) throw new DurableAuthorityStoreError("storage_unavailable", "Postgres authority reader is required");
+ return { state: (await readExternal(locator.location)).state, storedSha256: null };
+ }
+ const metadata = lstatSync(directory);
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) throw new Error("native_runner_control_plane_state_unsafe");
+ const store = await SqliteAuthorityStore.open({ binding: locator.location.binding, path: resolve(directory, locator.location.file!), create: false, readOnly: true });
+ try {
+ const snapshot = await store.load();
+ if (!snapshot) throw new DurableAuthorityStoreError("storage_unavailable", "activated authority snapshot is missing");
+ const state = await materializeCurrentAuthority(snapshot.state, store.getRecord.bind(store));
+ if ((await store.load())?.generation !== snapshot.generation) throw new DurableAuthorityStoreError("stale_authority", "authority changed while resolving current evidence");
+ return { state, storedSha256: createHash("sha256").update(authorityJson(snapshot.state)).digest("hex") };
+ } finally { await store.close(); }
+}
diff --git a/packages/paperclip-runner/src/control-plane/command-epochs.ts b/packages/paperclip-runner/src/control-plane/command-epochs.ts
new file mode 100644
index 00000000000..4ae04244b96
--- /dev/null
+++ b/packages/paperclip-runner/src/control-plane/command-epochs.ts
@@ -0,0 +1,40 @@
+import { createHash } from "node:crypto";
+/** Ordering is local to a negotiated delivery namespace, never the run lifetime. */
+export const COMMAND_EPOCH_CAPABILITY = "transport.command_epochs.v1";
+export const COMMAND_EPOCH_LIMIT = 1_048_576;
+const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
+export interface CommandEpochTransition {
+ schema: "paperclip.prp.command-epoch.v1";
+ runId: string;
+ transitionId: string;
+ fromEpoch: string | null;
+ nextEpoch: string;
+ finalOrdinal: number;
+}
+export function isCommandEpoch(value: unknown): value is string {
+ return typeof value === "string" && uuid.test(value);
+}
+export function isCommandEpochTransition(value: unknown): value is CommandEpochTransition {
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
+ const v = value as CommandEpochTransition;
+ return Object.keys(v).length === 6 && v.schema === "paperclip.prp.command-epoch.v1"
+ && typeof v.runId === "string" && v.runId.length > 0 && v.runId.length <= 240
+ && isCommandEpoch(v.transitionId) && (v.fromEpoch === null || isCommandEpoch(v.fromEpoch))
+ && isCommandEpoch(v.nextEpoch) && v.nextEpoch !== v.fromEpoch
+ && Number.isSafeInteger(v.finalOrdinal) && v.finalOrdinal > 0;
+}
+/** Current proof slots from older epochs precede this epoch's ordered suffix.
+ * There is at most one old settled receipt per slot; no history traversal. */
+export function compareCurrentCommands(
+ a: { controllerEpoch?: string; controllerSeq: number },
+ b: { controllerEpoch?: string; controllerSeq: number },
+ epoch: string | undefined,
+): number {
+ if (a.controllerEpoch === b.controllerEpoch) return a.controllerSeq - b.controllerSeq;
+ return Number(a.controllerEpoch === epoch) - Number(b.controllerEpoch === epoch);
+}
+
+
+export function commandEpochCloseId(transition: Pick): string {
+ return `command-epoch-from-${createHash("sha256").update(JSON.stringify([transition.runId, transition.fromEpoch])).digest("hex")}`;
+}
diff --git a/packages/paperclip-runner/src/control-plane/current-authority-evidence.test.ts b/packages/paperclip-runner/src/control-plane/current-authority-evidence.test.ts
new file mode 100644
index 00000000000..04b185514f8
--- /dev/null
+++ b/packages/paperclip-runner/src/control-plane/current-authority-evidence.test.ts
@@ -0,0 +1,29 @@
+import { expect, it, vi } from "vitest";
+import { materializeCurrentAuthority, referenceCurrentAuthority } from "./current-authority-evidence.js";
+import type { StoredCoreState } from "./durable-prp-control-plane.js";
+
+it("resolves only exact current references and rejects missing, conflicting or rebound authority", async () => {
+ const command = { commandId: "finish-result", controllerSeq: 12, status: "completed", type: "semantic_tool.result",
+ payload: { operationId: "paperclip_finish", input: { summary: "body".repeat(10000) } }, result: { accepted: true } };
+ const active = { commandId: "pending", controllerSeq: 13, status: "pending", type: "turn.start" };
+ const state = { identity: { runId: "run-1" }, commands: [command, active], committedEvents: [],
+ indexedState: { schema: "paperclip.runner.current-authority.v1", pendingSemanticInputIds: [] } } as unknown as StoredCoreState;
+ const stored = referenceCurrentAuthority(state);
+ expect(stored.commands).toEqual([active]);
+ expect(JSON.stringify(stored).length).toBeLessThan(1024);
+ const receipt = { epoch: "run-1", kind: "command" as const, id: command.commandId, sequence: "12", body: command };
+ const read = vi.fn(async () => receipt);
+ expect(await materializeCurrentAuthority(stored as unknown as Record, read)).toEqual(state);
+ expect(read).toHaveBeenCalledExactlyOnceWith("run-1", "command", "finish-result");
+ for (const bad of [null, { ...receipt, epoch: "foreign" }, { ...receipt, id: "other" }, { ...receipt, sequence: "11" }, { ...receipt, body: { ...command, result: {} } }]) {
+ await expect(materializeCurrentAuthority(stored as unknown as Record, async () => bad)).rejects.toThrow("exact receipt");
+ }
+ const absent = structuredClone(stored);
+ delete absent.indexedState!.recoveryEvidence;
+ await expect(materializeCurrentAuthority(absent as unknown as Record, read)).rejects.toThrow("exact receipt");
+ const rebound = structuredClone(stored);
+ const refs = rebound.indexedState!.recoveryEvidence!.records;
+ refs["command:other"] = refs["command:terminal:paperclip_finish"]!;
+ delete refs["command:terminal:paperclip_finish"];
+ await expect(materializeCurrentAuthority(rebound as unknown as Record, read)).rejects.toThrow("exact receipt");
+});
diff --git a/packages/paperclip-runner/src/control-plane/current-authority-evidence.ts b/packages/paperclip-runner/src/control-plane/current-authority-evidence.ts
new file mode 100644
index 00000000000..877ad9205c8
--- /dev/null
+++ b/packages/paperclip-runner/src/control-plane/current-authority-evidence.ts
@@ -0,0 +1,139 @@
+import { normalizedEpochCloseId } from "./event-epochs.js";
+import { compareCurrentEvents, isEventEpochTransition, eventEpochCloseId } from "./event-epochs.js";
+import { compareCurrentCommands, isCommandEpochTransition, commandEpochCloseId } from "./command-epochs.js";
+import { createHash } from "node:crypto";
+import { authorityJson, DurableAuthorityStoreError, type AuthorityRecord, type DurableAuthorityStore } from "./durable-authority-store.js";
+import type { StoredCoreState } from "./durable-prp-control-plane.js";
+
+export interface CurrentAuthorityReference {
+ epoch: string;
+ kind: "command" | "event";
+ id: string;
+ sequence: string;
+ sha256: string;
+}
+export interface CurrentAuthorityEvidence {
+ schema: "paperclip.current-evidence.v1";
+ records: Record;
+ commandOrder?: string[];
+ eventOrder?: string[];
+}
+const digest = (body: unknown) => createHash("sha256").update(authorityJson(body)).digest("hex");
+const lifecycleEvents = new Set(["session.started", "session.resumed", "session.reconciled", "harness.ready", "turn.accepted", "turn.started", "turn.completed", "turn.failed", "run.terminal"]);
+const terminalOperations = new Set(["paperclip_finish", "paperclip_block"]);
+const object = (v: unknown): Record => v !== null && typeof v === "object" && !Array.isArray(v) ? v as Record : {};
+function invalid(): never { throw new DurableAuthorityStoreError("invalid_authority", "current recovery evidence differs from its exact receipt"); }
+function slot(kind: CurrentAuthorityReference["kind"], body: Record): string | null {
+ if (kind === "command") {
+ if (body.status === "pending" || body.status === "indeterminate") return null;
+ if (typeof body.type !== "string" || body.type.length > 80) invalid();
+ if (body.type !== "semantic_tool.result") return `command:${body.type}`;
+ const operation = String(object(body.payload).operationId);
+ return terminalOperations.has(operation) ? `command:terminal:${operation}` : null;
+ }
+ const operation = String(object(object(object(object(body.envelope).payload).payload).semantic_tool).operationId);
+ if (terminalOperations.has(operation)) return `event:${body.eventType}:${operation}`;
+ return lifecycleEvents.has(String(body.eventType)) ? `event:${body.eventType}` : null;
+}
+
+/** The on-disk current row contains active work and a fixed set of typed slots.
+ * Settled payloads live once in the immutable receipt index. This does not
+ * certify a finish by itself: server acceptance still validates its structured
+ * input, operation result, completion contract and business result receipt. */
+export function referenceCurrentAuthority(state: StoredCoreState): StoredCoreState {
+ const next = structuredClone(state);
+ const current = next.indexedState!;
+ const evidence: CurrentAuthorityEvidence = { schema: "paperclip.current-evidence.v1", records: {}, commandOrder: next.commands.map(c => c.commandId), eventOrder: next.committedEvents.map(e => e.sourceEventId) };
+ const remember = (kind: CurrentAuthorityReference["kind"], body: Record, id: string, sequence: number) => {
+ const key = slot(kind, body);
+ if (!key) return false;
+ evidence.records[key] = { epoch: next.identity.runId, kind, id, sequence: String(sequence), sha256: digest(body) };
+ return true;
+ };
+ next.commands = next.commands.filter(command => !remember("command", command as unknown as Record, command.commandId, command.controllerSeq));
+ next.committedEvents = next.committedEvents.filter(event => current.pendingSemanticInputIds.includes(event.sourceEventId) ||
+ !remember("event", { ...event, deliveryCount: 1 }, event.sourceEventId, event.sourceSeq));
+ if (Object.keys(evidence.records).length > 40) invalid();
+ current.recoveryEvidence = evidence;
+ current.schema = "paperclip.runner.current-authority.v2";
+ return next;
+}
+
+/** Resolves only named current receipts. There is no history scan or fallback
+ * to a nearby event if a receipt is missing. Callers fence snapshot generation
+ * before and after this bounded read when opening an active authority. */
+export async function materializeCurrentAuthority(state: Record, read: DurableAuthorityStore["getRecord"]): Promise> {
+ const current = object(state.indexedState);
+ if (state.indexedState !== undefined && !["paperclip.runner.current-authority.v1", "paperclip.runner.current-authority.v2"].includes(String(current.schema))) invalid();
+ if (current.recoveryEvidence === undefined) {
+ if (current.schema === "paperclip.runner.current-authority.v2") invalid();
+ return state; // Retained v1 current rows and already materialized views.
+ }
+ if (current.controllerEpoch !== undefined || current.lastCommandEpochTransition !== undefined) {
+ const transition = current.lastCommandEpochTransition;
+ if (!isCommandEpochTransition(transition) || transition.nextEpoch !== current.controllerEpoch || transition.runId !== object(state.identity).runId) invalid();
+ for (const id of [`command-epoch-${transition.transitionId}`, commandEpochCloseId(transition)]) {
+ const receipt = await read(transition.runId, "effect", id);
+ if (!receipt || receipt.epoch !== transition.runId || receipt.kind !== "effect" || receipt.id !== id || receipt.sequence !== "0" || authorityJson(receipt.body) !== authorityJson(transition)) invalid();
+ }
+ }
+ if (current.sourceEpoch !== undefined || current.lastEventEpochTransition !== undefined) {
+ const transition = current.lastEventEpochTransition;
+ if (!isEventEpochTransition(transition) || transition.nextEpoch !== current.sourceEpoch || transition.runId !== object(state.identity).runId) invalid();
+ for (const id of [`event-epoch-${transition.transitionId}`, eventEpochCloseId(transition)]) {
+ const receipt = await read(transition.runId, "effect", id);
+ if (!receipt || receipt.epoch !== transition.runId || receipt.kind !== "effect" || receipt.id !== id || receipt.sequence !== "0" || authorityJson(receipt.body) !== authorityJson(transition)) invalid();
+ }
+ }
+ const delivery = object(current.normalizedDelivery);
+ if (delivery.producedEpoch !== undefined || delivery.lastSourceEpochTransition !== undefined) {
+ const t = delivery.lastSourceEpochTransition;
+ const identity = object(state.identity);
+ if (!isEventEpochTransition(t) || t.nextEpoch !== delivery.producedEpoch || t.runId !== identity.runId) invalid();
+ for (const id of [`normalized-epoch-${t.transitionId}`, normalizedEpochCloseId(t.runId, String(identity.runnerInstanceId), t.fromEpoch)]) {
+ const receipt = await read(t.runId, "effect", id);
+ if (!receipt || receipt.sequence !== "0" || authorityJson(receipt.body) !== authorityJson(t)) invalid();
+ }
+ }
+ const raw = object(object(current.normalizedDelivery).raw);
+ if (current.normalizedDelivery && raw.sourceEpoch !== current.sourceEpoch) {
+ const receipt = await read(String(object(state.identity).runId), "effect", eventEpochCloseId({runId: String(object(state.identity).runId), fromEpoch: (raw.sourceEpoch as string | undefined) ?? null}));
+ if (!receipt || !isEventEpochTransition(receipt.body) || receipt.body.fromEpoch !== (raw.sourceEpoch ?? null) || Number(raw.sourceSeq) > receipt.body.finalOrdinal) invalid();
+ }
+ const evidence = object(current.recoveryEvidence), entries = Object.entries(object(evidence.records));
+ if (current.schema !== "paperclip.runner.current-authority.v2" || evidence.schema !== "paperclip.current-evidence.v1" || !evidence.records || Array.isArray(evidence.records) || entries.length > 40 ||
+ !Array.isArray(state.commands) || !Array.isArray(state.committedEvents)) invalid();
+ const result = structuredClone(state) as unknown as StoredCoreState;
+ const ids = new Set();
+ for (const [key, value] of entries) {
+ const ref = value as CurrentAuthorityReference;
+ if (!ref || ref.epoch !== result.identity.runId || !["command", "event"].includes(ref.kind) ||
+ typeof ref.id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,239}$/.test(ref.id) ||
+ !/^[1-9][0-9]{0,15}$/.test(ref.sequence) || !Number.isSafeInteger(Number(ref.sequence)) || !/^[a-f0-9]{64}$/.test(ref.sha256)) invalid();
+ const receipt: AuthorityRecord | null = await read(ref.epoch, ref.kind, ref.id);
+ if (!receipt || receipt.epoch !== ref.epoch || receipt.kind !== ref.kind || receipt.id !== ref.id ||
+ receipt.sequence !== ref.sequence || digest(receipt.body) !== ref.sha256 || slot(ref.kind, receipt.body) !== key || ids.has(`${ref.kind}:${ref.id}`)) invalid();
+ ids.add(`${ref.kind}:${ref.id}`);
+ if (ref.kind === "command") {
+ if (receipt.body.commandId !== ref.id || String(receipt.body.controllerSeq) !== ref.sequence || result.commands.some(c => c.commandId === ref.id)) invalid();
+ result.commands.push(receipt.body as unknown as StoredCoreState["commands"][number]);
+ } else {
+ if (receipt.body.sourceEventId !== ref.id || String(receipt.body.sourceSeq) !== ref.sequence || result.committedEvents.some(e => e.sourceEventId === ref.id)) invalid();
+ result.committedEvents.push(receipt.body as unknown as StoredCoreState["committedEvents"][number]);
+ }
+ }
+ result.commands.sort((a, b) => compareCurrentCommands(a, b, result.indexedState?.controllerEpoch));
+ result.committedEvents.sort((a, b) => compareCurrentEvents(a, b, result.indexedState?.sourceEpoch));
+ for (const [order, records, id] of [
+ [evidence.commandOrder, result.commands, "commandId"], [evidence.eventOrder, result.committedEvents, "sourceEventId"],
+ ] as const) {
+ if (order === undefined) continue; // retained current-evidence v1
+ if (!Array.isArray(order) || order.length !== records.length || new Set(order).size !== order.length) invalid();
+ const position = new Map(order.map((key, index) => [key, index]));
+ if (records.some(record => !position.has((record as unknown as Record)[id]))) invalid();
+ records.sort((a, b) => position.get((a as unknown as Record)[id])! - position.get((b as unknown as Record)[id])!);
+ }
+ delete result.indexedState!.recoveryEvidence;
+ result.indexedState!.schema = "paperclip.runner.current-authority.v1";
+ return result as unknown as Record;
+}
diff --git a/packages/paperclip-runner/src/control-plane/durable-authority-store.ts b/packages/paperclip-runner/src/control-plane/durable-authority-store.ts
new file mode 100644
index 00000000000..313e4f394ef
--- /dev/null
+++ b/packages/paperclip-runner/src/control-plane/durable-authority-store.ts
@@ -0,0 +1,181 @@
+import type { AuthorityLocation } from "./authority-locator.js";
+/** The execution store is independent of the server and its database driver.
+ * History is queried by exact identity or cursor, never returned by load(). */
+export const INDEXED_DURABILITY_CAPABILITY = "durability.indexed_state.v1";
+/** Compatibility readers and writers must admit the same legacy journal size. */
+export const MAX_LEGACY_AUTHORITY_STATE_BYTES = 192 * 1024 * 1024;
+export const MAX_AUTHORITY_STATE_BYTES = 16 * 1024 * 1024;
+export const MAX_AUTHORITY_RECORD_BYTES = 1024 * 1024;
+export const MAX_AUTHORITY_COMMIT_RECORDS = 512;
+
+export interface AuthoritySnapshot {
+ /** Exact store revision. Compare equality; never order or increment it. */
+ generation: string;
+ /** Exact predecessor when the store uses opaque commit identities. */
+ committedFrom?: string;
+ state: Record;
+}
+
+export interface AuthorityRecord {
+ epoch: string;
+ kind: "command" | "event" | "effect";
+ id: string;
+ /** Command/event order belongs to the authenticated run epoch. Effects are
+ * addressed by exact identity and use zero when the store supports it. */
+ sequence: string;
+ /** Absent is the retained numeric namespace; epochs require store capability. */
+ sequenceEpoch?: string;
+ body: Record;
+}
+
+export interface AuthorityCommit {
+ expectedGeneration: string;
+ state: Record;
+ /** Newly settled receipts/events only. Existing records are immutable. */
+ records: readonly AuthorityRecord[];
+ work?: readonly AuthorityWorkChange[];
+}
+
+export interface AuthorityWorkRecord {
+ collection: "process-owner";
+ id: string;
+ body: Record;
+ sha256: string;
+}
+export interface AuthorityWorkChange {
+ collection: AuthorityWorkRecord["collection"];
+ id: string;
+ expectedSha256: string | null;
+ /** Null retires current work; its exact retirement proof belongs in records. */
+ body: Record | null;
+}
+export interface AuthorityWorkPage { records: AuthorityWorkRecord[]; nextAfter: string | null }
+
+export interface AuthorityPage {
+ records: AuthorityRecord[];
+ nextAfter: string | null;
+}
+
+export interface DurableAuthorityStore {
+ readonly binding: string;
+ readonly location: AuthorityLocation;
+ /** Retained v1 stores require a positive ordered effect sequence. */
+ readonly unorderedEffectReceipts?: boolean;
+ readonly commandEpochs?: boolean;
+ readonly eventEpochs?: boolean;
+ load(): Promise;
+ commit(input: AuthorityCommit): Promise;
+ getRecord(epoch: string, kind: AuthorityRecord["kind"], id: string): Promise;
+ /** Session-scoped exact effect lookup survives run/authority rotation. */
+ getSessionEffect(id: string): Promise;
+ getWork(collection: AuthorityWorkRecord["collection"], id: string): Promise;
+ readWorkPage(collection: AuthorityWorkRecord["collection"], after: string, limit: number, expectedGeneration: string): Promise;
+ readEvents(epoch: string, after: string, limit: number, byteBudget: number, sequenceEpoch?: string): Promise;
+ close(): Promise;
+}
+
+export class DurableAuthorityStoreError extends Error {
+ readonly code: "storage_pressure" | "storage_unavailable" | "stale_authority" | "receipt_conflict" | "invalid_authority";
+ constructor(code: DurableAuthorityStoreError["code"], message: string) {
+ super(`${code}: ${message}`);
+ this.name = "DurableAuthorityStoreError";
+ this.code = code;
+ }
+}
+
+export function authorityInteger(value: string): bigint {
+ if (!/^(0|[1-9][0-9]{0,18})$/.test(value) || BigInt(value) > 9_223_372_036_854_775_807n) {
+ throw new DurableAuthorityStoreError("invalid_authority", "invalid exact sequence/generation");
+ }
+ return BigInt(value);
+}
+
+/** Legacy stores expose exact integer revisions. Stores with opaque revisions
+ * retain predecessor evidence for operations which must prove one transition. */
+export function authorityGeneration(value: string): string {
+ if (/^r:[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(value)) return value;
+ authorityInteger(value);
+ return value;
+}
+
+export function authorityDirectlyFollows(snapshot: AuthoritySnapshot, previous: string): boolean {
+ authorityGeneration(previous);
+ authorityGeneration(snapshot.generation);
+ if (snapshot.committedFrom !== undefined) {
+ authorityGeneration(snapshot.committedFrom);
+ return snapshot.committedFrom === previous && snapshot.generation !== previous;
+ }
+ // Numeric v1 stores have no revision wraparound and prove their predecessor
+ // by exact arithmetic. An opaque revision without a commit stamp cannot.
+ if (snapshot.generation.startsWith("r:") || previous.startsWith("r:")) return false;
+ return authorityInteger(snapshot.generation) === authorityInteger(previous) + 1n;
+}
+
+/** Stable encoding for receipt comparison across SQLite and Postgres. */
+export function authorityJson(value: unknown): string {
+ const ancestors = new Set