@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/README.md +3 -3
- package/dist/agentHttp.js +12 -15
- package/dist/bytes.js +1 -1
- package/dist/claim.js +38 -22
- package/dist/cli.js +79 -56
- package/dist/identity.js +19 -37
- package/dist/index.d.ts +7 -7
- package/dist/index.js +7 -7
- package/dist/offer.js +3 -3
- package/dist/openTournament.d.ts +22 -0
- package/dist/openTournament.js +34 -3
- package/dist/refusal.d.ts +5 -0
- package/dist/refusal.js +21 -0
- package/dist/room.d.ts +29 -7
- package/dist/room.js +40 -10
- package/dist/seatState.d.ts +61 -5
- package/dist/seatState.js +162 -7
- package/dist/seatTurn.d.ts +15 -2
- package/dist/seatTurn.js +99 -22
- package/dist/session.d.ts +12 -0
- package/dist/session.js +153 -55
- package/dist/sessionCodec.d.ts +162 -2
- package/dist/sessionCodec.js +658 -20
- package/dist/sessionWire.d.ts +6 -5
- package/dist/sessionWire.js +8 -7
- package/dist/settlement.d.ts +5 -1
- package/dist/settlement.js +6 -7
- package/dist/tour.js +4 -4
- package/package.json +2 -2
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
|
|
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}/
|
|
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
|
-
|
|
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 === "
|
|
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
|
-
|
|
646
|
-
|
|
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
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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 (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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 (
|
|
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 = `/
|
|
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}/
|
|
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 = `/
|
|
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 /
|
|
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}/
|
|
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}/
|
|
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 = `/
|
|
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, {
|
package/dist/sessionCodec.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|