@dopamint-fun/open-sdk 0.2.0-dev.0 → 0.2.0-dev.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/session.js CHANGED
@@ -3,10 +3,11 @@ import { blake2b256 } from "./crypto.js";
3
3
  import { equalBytes, fromHex, textBytes, toHex0x } from "./bytes.js";
4
4
  import { AGENT_HTTP_CAPABILITY_HEADER, mintAgentHttpCapability, } from "./agentHttp.js";
5
5
  import { signRaw } from "./keypair.js";
6
- import { MAX_TRANSPORT_FRAME_BYTES, decodeAuthorityMessage, decodeEnvelope, encodeAckFrame, encodeEnvelope, encodeSeatAuthSuccessFrame, sessionErrorHint, sessionErrorName, } from "./sessionCodec.js";
6
+ import { MAX_TRANSPORT_FRAME_BYTES, PredictionGateConflictError, SessionBoundaryError, admitAuthorityEvent, decodeAuthorityMessage, freshSessionBoundary, decodeEnvelope, encodeAckFrame, encodeEnvelope, encodeSeatAuthSuccessFrame, sessionErrorHint, sessionErrorName, } from "./sessionCodec.js";
7
7
  import { actionSigningBytes, encodeActionFrame, encodeJoinFrame, encodeResumeFrame, joinSigningBytes, requireSessionVersion, resumeSigningBytes, SESSION_VERSION, UnsupportedSessionVersionError, } from "./sessionWire.js";
8
8
  import { decodeLegalActions, decodeParticipantView, encodeAction, pickAction, } from "./texas.js";
9
9
  import { authorizeSeatChallenge } from "./seatAuth.js";
10
+ import { ownershipFenced } from "./refusal.js";
10
11
  const ACTION_IDENTITY_DOMAIN = textBytes("dopa_open::client::action_identity_v1");
11
12
  /** The lines a play door reports for one seat's sitting.
12
13
  *
@@ -245,7 +246,7 @@ export class SessionClient {
245
246
  typeof ceiling !== "number" ||
246
247
  !Number.isFinite(ceiling) ||
247
248
  ceiling < MAX_TRANSPORT_FRAME_BYTES)
248
- throw new Error("authority discovery does not support participant session V2");
249
+ throw new Error("authority discovery does not support participant session V3");
249
250
  }
250
251
  async answeredDiscovery() {
251
252
  const { boundMs, pauseMs, sleep } = this.discoveryRetry;
@@ -528,13 +529,50 @@ export async function fetchWhileRestarting(fetchImpl, url, retry = {
528
529
  await sleep(retry.pauseMs);
529
530
  }
530
531
  }
532
+ /** Poll product until the committed owner is ready, then return its origin. */
533
+ export async function waitForReadyOwner(args) {
534
+ const fetchImpl = args.fetchImpl ?? fetch;
535
+ const sleep = args.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
536
+ const product = args.productUrl.replace(/\/$/, "");
537
+ const until = Date.now() + (args.timeoutMs ?? 30_000);
538
+ const pause = args.pauseMs ?? 200;
539
+ const id = args.executionId.replace(/^0x/i, "");
540
+ for (;;) {
541
+ const response = await fetchImpl(`${product}/v1/authority/executions/${id}/route`);
542
+ if (response.ok) {
543
+ const body = (await response.json());
544
+ if (body.ready && body.sessionOrigin)
545
+ return { sessionOrigin: body.sessionOrigin, version: body.version ?? 0 };
546
+ }
547
+ else {
548
+ await response.body?.cancel();
549
+ }
550
+ if (Date.now() + pause > until)
551
+ throw new Error("committed owner did not become ready");
552
+ await sleep(pause);
553
+ }
554
+ }
555
+ function httpFailure(error) {
556
+ if (!(error instanceof Error))
557
+ return null;
558
+ const match = /^[a-z-]+ failed \((\d+)\): (.*)$/is.exec(error.message);
559
+ if (!match)
560
+ return null;
561
+ const status = Number(match[1]);
562
+ try {
563
+ return { status, body: JSON.parse(match[2] ?? "") };
564
+ }
565
+ catch {
566
+ return { status, body: match[2] ?? "" };
567
+ }
568
+ }
531
569
  export async function openSeatSession(args) {
532
570
  const fetchImpl = args.fetchImpl ?? fetch;
533
571
  const product = args.productUrl.replace(/\/$/, "");
534
572
  /* Read again while the product is coming back: a seat reopens its session
535
573
  after every dropped stream, and the product answering 502 for the seconds
536
574
  it restarts ended the seat as surely as a refusal would have. */
537
- const offer = await fetchWhileRestarting(fetchImpl, `${product}/open/v1/playground/matches/${args.offerId}`, args.restartRetry);
575
+ const offer = await fetchWhileRestarting(fetchImpl, `${product}/v1/playground/matches/${args.offerId}`, args.restartRetry);
538
576
  if (!offer.ok)
539
577
  throw new Error(`offer read failed (${offer.status}): ${await offer.text()}`);
540
578
  const record = (await offer.json());
@@ -581,7 +619,7 @@ export async function openSeatSession(args) {
581
619
  }
582
620
  export async function playSeat(args) {
583
621
  const fetchImpl = args.fetchImpl ?? fetch;
584
- const { client, product, executionHex, coordinatorKey, timeAuthorityKey, executionId, } = await openSeatSession(args);
622
+ let { client, product, executionHex, coordinatorKey, timeAuthorityKey, executionId, } = await openSeatSession(args);
585
623
  const record = {
586
624
  admission: {
587
625
  execution_id: executionId,
@@ -611,6 +649,11 @@ export async function playSeat(args) {
611
649
  let pending = null;
612
650
  let lastContext = null;
613
651
  let lastCursor = { sequence: 0n, witnessedReceipt: null };
652
+ /* What this seat has accepted: the event sequence, the receipt floor, the
653
+ view it was last shown, and the named gate phase it holds. Every event
654
+ goes through it before anything is acknowledged, so the two doors -- this
655
+ loop and `openTurn` -- refuse the same streams for the same reasons. */
656
+ let accepted = freshSessionBoundary();
614
657
  const inbox = [];
615
658
  let joined = await client.join();
616
659
  if (joined.context)
@@ -634,7 +677,17 @@ export async function playSeat(args) {
634
677
  if (message.type === "actionPending" ||
635
678
  message.type === "actionAcknowledged")
636
679
  return null;
637
- if (message.type === "predictionGateReleased") {
680
+ if (message.type === "predictionGatePrepared" ||
681
+ message.type === "predictionGateOpened" ||
682
+ message.type === "predictionGateReleased") {
683
+ /* A named notice moves no receipt and carries no view: it is accepted,
684
+ acknowledged, and waited on. Only the real view that follows a
685
+ release is a turn to decide. */
686
+ const admitted = admitAuthorityEvent(accepted, message);
687
+ if (admitted.disposition !== "applied")
688
+ return null;
689
+ accepted = admitted.boundary;
690
+ lastContext = message.context;
638
691
  await client.acknowledge(message.context, message.cursor, originToken);
639
692
  lastCursor = message.cursor;
640
693
  return null;
@@ -642,8 +695,16 @@ export async function playSeat(args) {
642
695
  if ("context" in message)
643
696
  lastContext = message.context;
644
697
  if (message.type === "sessionTerminal") {
645
- await client.acknowledge(message.context, message.cursor, originToken);
646
- lastCursor = message.cursor;
698
+ const admitted = admitAuthorityEvent(accepted, message);
699
+ /* A re-delivered terminal is this boundary's own last event - a resume
700
+ at the terminal cursor repeats it. There is nothing to apply and its
701
+ acknowledgement is spent, but the sitting is over either way and the
702
+ caller still needs what `consent` takes. */
703
+ if (admitted.disposition === "applied") {
704
+ accepted = admitted.boundary;
705
+ await client.acknowledge(message.context, message.cursor, originToken);
706
+ lastCursor = message.cursor;
707
+ }
647
708
  return {
648
709
  outcome: "terminal",
649
710
  committedActions,
@@ -660,52 +721,61 @@ export async function playSeat(args) {
660
721
  }
661
722
  let view = null;
662
723
  let cursor = null;
663
- if (message.type === "sessionJoined") {
664
- view = message.view;
665
- cursor = message.cursor;
666
- }
667
- else if (message.type === "participantView") {
724
+ if (message.type === "sessionJoined" ||
725
+ message.type === "participantView" ||
726
+ message.type === "actionCommitted" ||
727
+ message.type === "actionRejected" ||
728
+ message.type === "sessionResumed") {
668
729
  view = message.view;
669
730
  cursor = message.cursor;
670
731
  }
671
- else if (message.type === "actionCommitted") {
672
- pending = null;
673
- committedActions += 1;
674
- view = message.view;
675
- cursor = message.cursor;
676
- }
677
- else if (message.type === "actionRejected") {
678
- pending = null;
679
- view = message.view;
680
- cursor = message.cursor;
681
- }
682
- else if (message.type === "sessionResumed") {
683
- view = message.view;
684
- cursor = message.cursor;
685
- }
686
- if (view &&
687
- view.legalActions.length === 0 &&
688
- !elimination.eliminated &&
689
- Date.now() - lastIdleTableReadMs >= IDLE_TABLE_READ_MS) {
690
- /* Nothing to decide: somebody else's turn, a hand this seat folded, or
691
- a sitting this seat is out of. Read now and then, so the hand lines go
692
- on and a seat that has lost its last chip says so once. */
693
- lastIdleTableReadMs = Date.now();
694
- const idle = await readPublicTable(fetchImpl, product, executionHex, names, tableCapability);
695
- if (idle && idle.handNumber !== lastHandSeen) {
696
- lastHandSeen = idle.handNumber;
697
- args.onHand?.({
698
- number: idle.handNumber,
699
- stack: idle.seats.find((entry) => entry.seat === args.seat)?.stack ?? null,
700
- });
732
+ if (cursor !== null && lastContext) {
733
+ /* Accepted before anything else happens: a refusal here leaves the
734
+ retained boundary untouched and nothing acknowledged. */
735
+ const admitted = admitAuthorityEvent(accepted, message);
736
+ /* A re-delivery is the suffix a resume repeats. It was applied and
737
+ acknowledged once, so nothing here moves for it: no view, no pending
738
+ resolution, no commit count, no table read, no decision, no
739
+ acknowledgement. */
740
+ if (admitted.disposition !== "applied")
741
+ return null;
742
+ accepted = admitted.boundary;
743
+ /* Resolved only for the applied event, so a replayed commit cannot
744
+ count twice or drop a proposal that is still in flight. */
745
+ if (message.type === "actionCommitted") {
746
+ pending = null;
747
+ committedActions += 1;
701
748
  }
702
- if (elimination.observe(idle))
703
- args.onEliminated?.();
704
- }
705
- if (view && cursor !== null && lastContext) {
706
- const nonceKey = view.state.nonce.toString();
749
+ else if (message.type === "actionRejected")
750
+ pending = null;
751
+ if (view &&
752
+ view.legalActions.length === 0 &&
753
+ !elimination.eliminated &&
754
+ Date.now() - lastIdleTableReadMs >= IDLE_TABLE_READ_MS) {
755
+ /* Nothing to decide: somebody else's turn, a hand this seat folded, or
756
+ a sitting this seat is out of. Read now and then, so the hand lines go
757
+ on and a seat that has lost its last chip says so once. */
758
+ lastIdleTableReadMs = Date.now();
759
+ const idle = await readPublicTable(fetchImpl, product, executionHex, names, tableCapability);
760
+ if (idle && idle.handNumber !== lastHandSeen) {
761
+ lastHandSeen = idle.handNumber;
762
+ args.onHand?.({
763
+ number: idle.handNumber,
764
+ stack: idle.seats.find((entry) => entry.seat === args.seat)?.stack ??
765
+ null,
766
+ });
767
+ }
768
+ if (elimination.observe(idle))
769
+ args.onEliminated?.();
770
+ }
771
+ const nonceKey = view?.state.nonce.toString() ?? "";
707
772
  const now = BigInt(Date.now());
708
- if (view.legalActions.length > 0 &&
773
+ if (
774
+ /* A viewless admission is acknowledged and waited on: a held seat has
775
+ nothing to answer, and nothing to infer on, until its gate releases
776
+ and the real view arrives. */
777
+ view !== null &&
778
+ view.legalActions.length > 0 &&
709
779
  !acted.has(nonceKey) &&
710
780
  view.participantDeadlineMs > now) {
711
781
  const legal = decodeLegalActions(view.legalActions);
@@ -876,6 +946,12 @@ export async function playSeat(args) {
876
946
  look successful while the seat cannot read the gated stream. */
877
947
  if (error instanceof UnsupportedSessionVersionError)
878
948
  throw error;
949
+ /* Nor is a refused projection: the event the authority sent is one this
950
+ contract forbids, and resuming would fetch the same bytes again while
951
+ the loop reported it as flaky transport. */
952
+ if (error instanceof SessionBoundaryError ||
953
+ error instanceof PredictionGateConflictError)
954
+ throw error;
879
955
  if (!lastContext || reconnects >= 5)
880
956
  throw error;
881
957
  /* Three refusals, three answers. A gone session (`UnknownSession`, or a
@@ -885,6 +961,22 @@ export async function playSeat(args) {
885
961
  client joined this seat with this key; fighting it back would evict
886
962
  a reconnect that may be the operator's own, so this one steps aside
887
963
  and says so. Everything else is transient, and resume is right. */
964
+ const failed = httpFailure(error);
965
+ if (failed?.status === 409 && ownershipFenced(failed.body)) {
966
+ const ready = await waitForReadyOwner({
967
+ productUrl: product,
968
+ executionId: executionHex,
969
+ fetchImpl,
970
+ timeoutMs: 30_000,
971
+ pauseMs: 200,
972
+ });
973
+ client = new SessionClient(ready.sessionOrigin, args.agent, args.seat, args.agentId, client.executionId, client.executionManifestDigest, client.clientNonce, fetchImpl);
974
+ client.token = originToken;
975
+ await client.resume(lastContext, lastCursor);
976
+ originToken = client.token ?? originToken;
977
+ reconnects += 1;
978
+ continue;
979
+ }
888
980
  const answer = afterRefusal(error);
889
981
  if (answer === "step-aside")
890
982
  return {
@@ -907,6 +999,12 @@ export async function playSeat(args) {
907
999
  if (joined.context)
908
1000
  lastContext = joined.context;
909
1001
  lastCursor = { sequence: 0n, witnessedReceipt: null };
1002
+ /* A rejoin is a new session: nothing the old one retained - its phase,
1003
+ its floor, its sequence - describes this one, and only a boundary
1004
+ this loop opened itself may be reset. The stale inbox belongs to
1005
+ the session that is gone. */
1006
+ accepted = freshSessionBoundary();
1007
+ inbox.length = 0;
910
1008
  inbox.push(...joined.messages);
911
1009
  rejoins += 1;
912
1010
  reconnects += 1;
@@ -960,7 +1058,7 @@ export function normaliseCardCode(code) {
960
1058
  export async function readDisclosedEntitlement(fetchImpl, product, executionId, seat, capability, options = {}) {
961
1059
  const attempts = options.attempts ?? 15;
962
1060
  const pauseMs = options.pauseMs ?? 1_000;
963
- const target = `/open/v1/spectator/executions/${executionId}`;
1061
+ const target = `/v1/spectator/executions/${executionId}`;
964
1062
  for (let attempt = 0; attempt < attempts; attempt += 1) {
965
1063
  try {
966
1064
  const header = capability ? await capability("GET", target) : null;
@@ -988,7 +1086,7 @@ export async function readDisclosedEntitlement(fetchImpl, product, executionId,
988
1086
  * gone -- which is precisely the moment this question is being asked. */
989
1087
  export async function readSittingStatus(fetchImpl, product, executionHex) {
990
1088
  try {
991
- const response = await fetchImpl(`${product}/open/v1/history/matches/${executionHex}`);
1089
+ const response = await fetchImpl(`${product}/v1/history/matches/${executionHex}`);
992
1090
  if (!response.ok)
993
1091
  return { state: "unknown" };
994
1092
  const wire = (await response.json());
@@ -1029,7 +1127,7 @@ export function agentReadCapability(agent, agentId) {
1029
1127
  export async function readPublicTable(fetchImpl, product, executionHex, names = new Map(), capability) {
1030
1128
  /* Bound to the target the server reconstructs from the request, which is the
1031
1129
  origin-form path and not the absolute URL the fetch is given. */
1032
- const target = `/open/v1/spectator/executions/${executionHex}`;
1130
+ const target = `/v1/spectator/executions/${executionHex}`;
1033
1131
  try {
1034
1132
  const header = capability ? await capability("GET", target) : null;
1035
1133
  const response = await fetchImpl(`${product}${target}`, header === null
@@ -1090,14 +1188,14 @@ export async function readPublicTable(fetchImpl, product, executionHex, names =
1090
1188
  return null;
1091
1189
  }
1092
1190
  }
1093
- /** What an agent is called, from `GET /open/v1/agents/{id}/custody`, read
1191
+ /** What an agent is called, from `GET /v1/agents/{id}/custody`, read
1094
1192
  * once and remembered in `names`. Null where the read did not answer. */
1095
1193
  async function readAgentNaming(fetchImpl, product, agentId, names) {
1096
1194
  const known = names.get(agentId);
1097
1195
  if (known !== undefined)
1098
1196
  return known;
1099
1197
  try {
1100
- const response = await fetchImpl(`${product}/open/v1/agents/${agentId}/custody`);
1198
+ const response = await fetchImpl(`${product}/v1/agents/${agentId}/custody`);
1101
1199
  if (!response.ok) {
1102
1200
  names.set(agentId, null);
1103
1201
  return null;
@@ -1120,7 +1218,7 @@ async function readAgentNaming(fetchImpl, product, agentId, names) {
1120
1218
  * table: an empty list where the read did not answer. */
1121
1219
  export async function readTableTalk(fetchImpl, product, executionHex, handIndex) {
1122
1220
  try {
1123
- const response = await fetchImpl(`${product}/open/v1/executions/${executionHex}/talk`);
1221
+ const response = await fetchImpl(`${product}/v1/executions/${executionHex}/talk`);
1124
1222
  if (!response.ok)
1125
1223
  return [];
1126
1224
  const wire = (await response.json());
@@ -1156,7 +1254,7 @@ function potChips(pot) {
1156
1254
  /** File one line at the table, signed as this seat's agent. True when the
1157
1255
  * product accepted it. */
1158
1256
  async function sayAtTable(fetchImpl, product, executionHex, args, say) {
1159
- const target = `/open/v1/executions/${executionHex}/talk`;
1257
+ const target = `/v1/executions/${executionHex}/talk`;
1160
1258
  const body = textBytes(JSON.stringify({ say }));
1161
1259
  try {
1162
1260
  const { header } = await mintAgentHttpCapability(args.agent, args.agentId, {
@@ -11,6 +11,9 @@ export declare const SESSION_ERROR_TAG = 13;
11
11
  export declare const SEAT_AUTHORIZATION_CHALLENGE_TAG = 15;
12
12
  export declare const SEAT_AUTHORIZATION_RESPONSE_TAG = 16;
13
13
  export declare const PREDICTION_GATE_RELEASED_TAG = 17;
14
+ export declare const PREDICTION_GATE_PREPARED_TAG = 18;
15
+ export declare const PREDICTION_GATE_OPENED_TAG = 19;
16
+ export declare const PREDICTION_GATE_RELEASED_V4_TAG = 20;
14
17
  export declare const MAX_TRANSPORT_FRAME_BYTES: number;
15
18
  export interface WireEnvelope {
16
19
  message: string;
@@ -57,12 +60,83 @@ export interface SeatAuthChallenge {
57
60
  coordinatorProof: Uint8Array;
58
61
  coordinatorPublicKey: Uint8Array;
59
62
  }
63
+ /** ADR-0096's ceiling on added pacing for one next-action prediction window,
64
+ * as `arena_session::prediction_gate::MAX_PREDICTION_PACING_MS`. */
65
+ export declare const MAX_PREDICTION_PACING_MS = 25000n;
66
+ /** The in-play window a gate names. The session reuses this identity; it
67
+ * never mints a second one. */
68
+ export interface PredictionWindowRef {
69
+ windowId: bigint;
70
+ marketId: bigint;
71
+ /** The only contract kind a gate can hold a seat behind (wire tag 1). */
72
+ contract: "pokerActionV1";
73
+ }
74
+ /** A durably committed preparation: the frozen window identity, the target
75
+ * the acting seat is held on, its original committed deadline, and the
76
+ * persisted instant publication started. */
77
+ export interface PredictionGatePreparation {
78
+ window: PredictionWindowRef;
79
+ actingSeat: number;
80
+ state: StateRef;
81
+ receipt: ReceiptRef | null;
82
+ originalDeadlineMs: bigint;
83
+ preparedAtMs: bigint;
84
+ }
85
+ /** Acknowledged publication. `closesAtMs` is the service-committed close
86
+ * instant, never the action deadline. */
87
+ export interface PredictionGateOpening {
88
+ preparation: PredictionGatePreparation;
89
+ openedAtMs: bigint;
90
+ closesAtMs: bigint;
91
+ }
92
+ /** The terminal overlay: how the gate closed, and the bounded same-receipt
93
+ * deadline it admits. `lockCappedMs` and `participantDeadlineMs` are derived
94
+ * the way the shared crate derives them, so a recipient never has to repeat
95
+ * the arithmetic to know which later deadline is legal. */
96
+ export interface PredictionGateRelease {
97
+ window: PredictionWindowRef;
98
+ actingSeat: number;
99
+ state: StateRef;
100
+ receipt: ReceiptRef | null;
101
+ terminal: "locked" | "cancelled";
102
+ originalDeadlineMs: bigint;
103
+ arrivalMs: bigint;
104
+ lockedAtMs: bigint;
105
+ lockCappedMs: bigint;
106
+ budgetMs: bigint;
107
+ participantDeadlineMs: bigint;
108
+ /** Digest of the protected LLM terminal decision, when bound. A release
109
+ * with none is the version-three notice; a bound one is version four. */
110
+ terminalDigest: Uint8Array | null;
111
+ }
112
+ /** The gate facts a participant retains for one revision. */
113
+ export type PredictionGateStatus = {
114
+ phase: "prepared";
115
+ preparation: PredictionGatePreparation;
116
+ } | {
117
+ phase: "open";
118
+ opening: PredictionGateOpening;
119
+ } | {
120
+ phase: "released";
121
+ release: PredictionGateRelease;
122
+ };
123
+ /** One named lifecycle step, as a value: what the fold below applies. */
124
+ export type PredictionGateMessage = {
125
+ kind: "prepared";
126
+ preparation: PredictionGatePreparation;
127
+ } | {
128
+ kind: "opened";
129
+ opening: PredictionGateOpening;
130
+ } | {
131
+ kind: "released";
132
+ release: PredictionGateRelease;
133
+ };
60
134
  export type AuthorityMessage = {
61
135
  type: "sessionJoined";
62
136
  context: SessionContext;
63
137
  sequence: bigint;
64
138
  policy: SessionPolicy;
65
- view: ViewSnapshot;
139
+ view: ViewSnapshot | null;
66
140
  cursor: ResumeCursor;
67
141
  } | {
68
142
  type: "participantView";
@@ -98,7 +172,10 @@ export type AuthorityMessage = {
98
172
  type: "sessionResumed";
99
173
  context: SessionContext;
100
174
  sequence: bigint;
101
- view: ViewSnapshot;
175
+ /** The boundary the authority replayed from. It must precede the event
176
+ itself, or the resume names a suffix it cannot have replayed. */
177
+ replayFrom: bigint;
178
+ view: ViewSnapshot | null;
102
179
  cursor: ResumeCursor;
103
180
  } | {
104
181
  type: "sessionTerminal";
@@ -108,10 +185,23 @@ export type AuthorityMessage = {
108
185
  finalState: StateRef;
109
186
  cursor: ResumeCursor;
110
187
  finalReceipt: ReceiptRef | null;
188
+ } | {
189
+ type: "predictionGatePrepared";
190
+ context: SessionContext;
191
+ sequence: bigint;
192
+ preparation: PredictionGatePreparation;
193
+ cursor: ResumeCursor;
194
+ } | {
195
+ type: "predictionGateOpened";
196
+ context: SessionContext;
197
+ sequence: bigint;
198
+ opening: PredictionGateOpening;
199
+ cursor: ResumeCursor;
111
200
  } | {
112
201
  type: "predictionGateReleased";
113
202
  context: SessionContext;
114
203
  sequence: bigint;
204
+ release: PredictionGateRelease;
115
205
  cursor: ResumeCursor;
116
206
  } | {
117
207
  type: "error";
@@ -131,6 +221,19 @@ export declare function sessionErrorName(tag: number): string;
131
221
  * join supersedes the older binding, and the older client's next message
132
222
  * finds its session gone. Both used to read as transport flakiness. */
133
223
  export declare function sessionErrorHint(tag: number): string | null;
224
+ export declare function equalStateRef(left: StateRef, right: StateRef): boolean;
225
+ export declare function equalReceiptRef(left: ReceiptRef | null, right: ReceiptRef | null): boolean;
226
+ export declare function equalPredictionGatePreparation(left: PredictionGatePreparation, right: PredictionGatePreparation): boolean;
227
+ export declare function equalPredictionGateRelease(left: PredictionGateRelease, right: PredictionGateRelease): boolean;
228
+ /** The structural refusals of `PredictionGatePreparation::new`, made wherever
229
+ * a preparation enters this process - the wire, or a retained seat state. */
230
+ export declare function bindPredictionGatePreparation(fields: PredictionGatePreparation): PredictionGatePreparation;
231
+ /** The structural refusals of `PredictionGateOpening::new`. */
232
+ export declare function bindPredictionGateOpening(fields: PredictionGateOpening): PredictionGateOpening;
233
+ /** The structural refusals of `PredictionGateRelease::new`, with the bounded
234
+ * overlay derived here rather than trusted from a sender or a stored file:
235
+ * the lock instant capped at the pacing ceiling, plus the budget. */
236
+ export declare function bindPredictionGateRelease(fields: Omit<PredictionGateRelease, "lockCappedMs" | "participantDeadlineMs">): PredictionGateRelease;
134
237
  export declare function decodeAuthorityMessage(bytes: Uint8Array): AuthorityMessage;
135
238
  export declare function encodeSeatAuthSuccessFrame(context: SessionContext, actionId: Uint8Array, signature: Uint8Array): Uint8Array;
136
239
  export declare function extractProtocolInput(payload: Uint8Array): Uint8Array;
@@ -143,3 +246,60 @@ export declare function takePrincipalProof(bytes: Uint8Array): {
143
246
  signature: Uint8Array;
144
247
  rest: Uint8Array;
145
248
  };
249
+ /** A refused phase transition, named as the shared crate names it. */
250
+ export declare class PredictionGateConflictError extends Error {
251
+ readonly conflict: "PredictionGatePhaseConflict" | "PredictionGateReleaseConflict";
252
+ constructor(conflict: "PredictionGatePhaseConflict" | "PredictionGateReleaseConflict", detail: string);
253
+ }
254
+ /** The lifecycle step an authority message carries, or null where it carries
255
+ * none. The fold takes the value, not the envelope. */
256
+ export declare function predictionGateMessage(message: AuthorityMessage): PredictionGateMessage | null;
257
+ /** The committed preparation behind any phase, or null once released - a
258
+ * release carries the identity but not the persisted preparation instant. */
259
+ export declare function predictionGatePreparationOf(status: PredictionGateStatus): PredictionGatePreparation | null;
260
+ /** The receipt the gate targets. A recipient behind it still receives every
261
+ * notice, because each notice cursor names that recipient's own floor. */
262
+ export declare function predictionGateTargetReceipt(status: PredictionGateStatus): ReceiptRef | null;
263
+ /** Whether a newly named preparation supersedes the retained facts.
264
+ *
265
+ * Only a released gate can be superseded: a held gate's turn has not closed,
266
+ * so a second preparation contradicts it. Once released, the next selected
267
+ * turn always targets a later committed state and therefore its own receipt.
268
+ * A seat that is itself withheld for that next turn never witnesses the view
269
+ * that would otherwise retire the closed gate - which is exactly the second
270
+ * window of a hand, held on the seat that was a bystander for the first. A
271
+ * preparation naming the same revision is the same-target conflict, not a
272
+ * supersession. */
273
+ export declare function predictionGateSupersededByPreparation(status: PredictionGateStatus, nextTurn: PredictionGatePreparation): boolean;
274
+ export declare function applyPredictionGateStatus(current: PredictionGateStatus | null, message: PredictionGateMessage): PredictionGateStatus;
275
+ export interface AcceptedSessionBoundary {
276
+ context: SessionContext | null;
277
+ /** The last accepted authority event sequence; zero before the first. */
278
+ sequence: bigint;
279
+ receiptFloor: ReceiptRef | null;
280
+ /** The view last disclosed to this seat, or null while it is withheld. */
281
+ view: ViewSnapshot | null;
282
+ predictionGate: PredictionGateStatus | null;
283
+ /** Whether a viewless admission still owes its named prepared prefix. */
284
+ awaitingGatePrefix: boolean;
285
+ }
286
+ /** A refused event, named as `SessionSemanticError` names it. */
287
+ export declare class SessionBoundaryError extends Error {
288
+ readonly reason: string;
289
+ constructor(reason: string, detail: string);
290
+ }
291
+ /** The boundary a seat that has accepted nothing holds. */
292
+ export declare function freshSessionBoundary(): AcceptedSessionBoundary;
293
+ /** What the seam did with an event.
294
+ *
295
+ * `applied` is the only disposition that moved the boundary, and so the only
296
+ * one a consumer may act on: a re-delivery was applied and acknowledged once
297
+ * already, and acting on it a second time would decide a turn that is over,
298
+ * count a commit twice, or walk the acknowledged cursor backwards. */
299
+ export type AuthorityEventDisposition = "applied" | "replay" | "boundaryless";
300
+ export interface AdmittedAuthorityEvent {
301
+ disposition: AuthorityEventDisposition;
302
+ /** The boundary to retain. Unchanged unless the event was applied. */
303
+ boundary: AcceptedSessionBoundary;
304
+ }
305
+ export declare function admitAuthorityEvent(accepted: AcceptedSessionBoundary, message: AuthorityMessage): AdmittedAuthorityEvent;