@dopamint-fun/open-sdk 0.2.0-dev.0 → 0.2.0-dev.1
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/cli.js +26 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/room.d.ts +29 -7
- package/dist/room.js +38 -8
- package/dist/seatState.d.ts +60 -5
- package/dist/seatState.js +156 -7
- package/dist/seatTurn.d.ts +15 -2
- package/dist/seatTurn.js +98 -21
- package/dist/session.js +90 -46
- package/dist/sessionCodec.d.ts +158 -2
- package/dist/sessionCodec.js +642 -19
- package/dist/sessionWire.d.ts +6 -5
- package/dist/sessionWire.js +7 -6
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -760,6 +760,11 @@ async function commandTurn(args) {
|
|
|
760
760
|
agent,
|
|
761
761
|
agentId: fromHex(agentId),
|
|
762
762
|
waitMs: values.wait === undefined ? 0 : Number(values.wait) * 1000,
|
|
763
|
+
/* Written before each acknowledgement, not once at the end. `turn`
|
|
764
|
+
acknowledges the viewless join and the named prepared/open notices as
|
|
765
|
+
it takes them, and an acknowledgement the file does not record is a
|
|
766
|
+
phase the next process cannot know it is in. */
|
|
767
|
+
checkpoint: (next) => saveSeatState(statePath, next),
|
|
763
768
|
});
|
|
764
769
|
saveSeatState(statePath, outcome.state);
|
|
765
770
|
if (outcome.kind === "your-turn") {
|
|
@@ -1255,6 +1260,12 @@ async function commandRoom(args) {
|
|
|
1255
1260
|
const room = await openRoom(client, seats);
|
|
1256
1261
|
console.log(`table ${room.tableId}`);
|
|
1257
1262
|
console.log(`seats ${room.seatCount}`);
|
|
1263
|
+
/* How full it is and what fills it. A room seats nobody but the agents
|
|
1264
|
+
invited to it: the chairs nobody takes stay empty, so an opener has to
|
|
1265
|
+
know how many are still open to know who to send the invite to. */
|
|
1266
|
+
console.log(`seated 1 of ${room.seatCount}`);
|
|
1267
|
+
console.log("fill the room deals when its last seat is taken, or two minutes after " +
|
|
1268
|
+
"its second agent sits, whichever comes first; untaken seats stay empty");
|
|
1258
1269
|
console.log(`mode ${room.mode}`);
|
|
1259
1270
|
console.log(`settlement ${room.settlement}`);
|
|
1260
1271
|
if (room.executionId)
|
|
@@ -1263,7 +1274,7 @@ async function commandRoom(args) {
|
|
|
1263
1274
|
operator forwards the whole thing rather than the table id alone: an id
|
|
1264
1275
|
without the document is how a guest ends up asking the table routes for
|
|
1265
1276
|
a decision. */
|
|
1266
|
-
console.log(`invite ${roomInvitePrompt(client.productUrl, room.tableId)}`);
|
|
1277
|
+
console.log(`invite ${roomInvitePrompt(client.productUrl, room.tableId, room.seatCount - 1)}`);
|
|
1267
1278
|
/* The room's own page, which exists before any hand does. Not `watch`:
|
|
1268
1279
|
that word is the match link on every door, and `room join` prints it
|
|
1269
1280
|
once the room composes, so two different links under one label was a
|
|
@@ -1276,10 +1287,23 @@ async function commandRoom(args) {
|
|
|
1276
1287
|
fail("--table-id is required: the id the room's opener sent you");
|
|
1277
1288
|
const joined = await joinRoomWhenComposed(client, tableId, {
|
|
1278
1289
|
timeoutMs: Number(values["timeout-ms"]),
|
|
1279
|
-
onWaiting: () =>
|
|
1290
|
+
onWaiting: (found) => {
|
|
1291
|
+
/* What the room is waiting for, said in its own numbers where the join
|
|
1292
|
+
answered with them. An opener's own join is refused rather than
|
|
1293
|
+
answered, so it has none to print. */
|
|
1294
|
+
const seats = found?.seated === undefined || found.seatCount === undefined
|
|
1295
|
+
? ""
|
|
1296
|
+
: ` ${found.seated} of ${found.seatCount} seated;`;
|
|
1297
|
+
const deals = found?.fillAtMs === undefined
|
|
1298
|
+
? " it deals when a second agent sits"
|
|
1299
|
+
: ` it deals by ${new Date(found.fillAtMs).toISOString()} or sooner if its last seat is taken`;
|
|
1300
|
+
console.log(`waiting_for_room table ${tableId} holds this agent's seat;${seats}${deals}`);
|
|
1301
|
+
},
|
|
1280
1302
|
});
|
|
1281
1303
|
console.log(`table ${joined.tableId}`);
|
|
1282
1304
|
console.log(`joined ${joined.joined}`);
|
|
1305
|
+
if (joined.seated !== undefined && joined.seatCount !== undefined)
|
|
1306
|
+
console.log(`seated ${joined.seated} of ${joined.seatCount}`);
|
|
1283
1307
|
if (joined.offerId === undefined) {
|
|
1284
1308
|
/* A mock stack composes no offer, so there is no seat to accept and
|
|
1285
1309
|
nothing for `--play` to drive. Said plainly rather than by an empty
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { DEFAULT_KEY_FILE, generateKeypair, keypairFromSeed, loadKeypair, saveKe
|
|
|
2
2
|
export { deriveAgentId, registerCanonicalPayload, registerPayloadDigest, type RegisterAgentPayload, } from "./registration.js";
|
|
3
3
|
export { canonicalIdentityPayload, nameAgent, parseIdentityFields, type AgentIdentityFields, type NameAgentArgs, } from "./identity.js";
|
|
4
4
|
export { actionSigningBytes, encodeActionFrame, encodeJoinFrame, encodeResumeFrame, joinSigningBytes, resumeSigningBytes, SESSION_VERSION, UnsupportedSessionVersionError, type ActionProposal, type ArtifactReference, type JoinRequest, type ResumeRequest, type SessionContext, } from "./sessionWire.js";
|
|
5
|
-
export { encodeAckFrame, SESSION_ERROR_NAMES, sessionErrorHint, sessionErrorName, } from "./sessionCodec.js";
|
|
5
|
+
export { encodeAckFrame, SESSION_ERROR_NAMES, MAX_PREDICTION_PACING_MS, admitAuthorityEvent, applyPredictionGateStatus, decodeAuthorityMessage, freshSessionBoundary, predictionGateMessage, predictionGatePreparationOf, predictionGateTargetReceipt, sessionErrorHint, sessionErrorName, PredictionGateConflictError, SessionBoundaryError, type AcceptedSessionBoundary, type AuthorityMessage, type PredictionGateMessage, type PredictionGateOpening, type PredictionGatePreparation, type PredictionGateRelease, type PredictionGateStatus, type PredictionWindowRef, type ReceiptRef, type ResumeCursor, type StateRef, type ViewSnapshot, } from "./sessionCodec.js";
|
|
6
6
|
export { type OpenTableView, type PlayReport, type SeatDecision, type SeatDecisionResult, type SeatPosition, SessionClient, SessionRefusal, chooseSeatAction, playSeat, } from "./session.js";
|
|
7
7
|
export { cardFromByte, decodeLegalActions, decodeParticipantView, encodeAction, pickAction, type Card, type Rank, type Suit, type TexasAction, type TexasLegalActions, type TexasSeatView, } from "./texas.js";
|
|
8
8
|
export { authorityOriginFromSessionBase, buildConsentRequest, digestForPrompt, recomputeSettlementDigest, settlementConsentPath, verifyConsentDisclosure, } from "./settlement.js";
|
|
@@ -14,8 +14,8 @@ export { joinRoom, MAX_ROOM_SEATS, MIN_ROOM_SEATS, openRoom, roomInvitePrompt, t
|
|
|
14
14
|
export { AGENT_CLAIM_INVITE_DOMAIN, AGENT_CLAIM_INVITE_MAX_WINDOW_MS, AGENT_CLAIM_INVITE_TOKEN_BYTES, claimInviteCanonicalPayload, claimInviteLink, claimInviteSigningBytes, decodeClaimInvite, encodeClaimInvite, encodeClaimInviteCompact, mintClaimInvite, type ClaimInvite, } from "./claim.js";
|
|
15
15
|
export { baselineDecision, baselineMove, opponentsStillIn, } from "./decide.js";
|
|
16
16
|
export { handEquity, requiredEquity, cardCode, type HandEquity, type HandEquityOptions, } from "./equity.js";
|
|
17
|
-
export {
|
|
17
|
+
export { cardIndex, handCategory, scoreHand, HAND_CATEGORIES, RANK_ORDER, SUIT_ORDER, type BestFive, type HandCategory, } from "./handRank.js";
|
|
18
18
|
export { DEFAULT_SEAT_STATE_FILE, loadSeatState, saveSeatState, type SeatSessionState, } from "./seatState.js";
|
|
19
|
-
export { newSeatState, openTurn, submitTurn, type SeatTurnOutcome, type SeatTurnPosition, } from "./seatTurn.js";
|
|
19
|
+
export { newSeatState, openTurn, submitTurn, type SeatStateCheckpoint, type SeatTurnOutcome, type SeatTurnPosition, } from "./seatTurn.js";
|
|
20
20
|
export { chipAddressOf, joinTransaction, leaveTransaction, listTournaments, planJoin, presentToTournament, readAgentEntry, readPass, readOwner, playsHeldBy, giveBackTransaction, readTournament, sponsorAndExecute, tournamentIdArg, tournamentPossessionSignature, JoinRefused, TournamentRefusal, TOURNAMENT_POSSESSION_SEAT, type JoinPlan, type SponsoredExecution, type TournamentAgentEntry, type TournamentPass, type TournamentPlay, type TournamentOwnerView, type TournamentOwnerAgent, type TournamentChainObjects, type TournamentClient, type TournamentOverview, type TournamentQueueEntry, } from "./openTournament.js";
|
|
21
21
|
export { ClientRefusal, describeNext, refusalLines, refusalMessage, refusalMessageFromText, type RefusalBody, type RefusalNext, } from "./refusal.js";
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ export { DEFAULT_KEY_FILE, generateKeypair, keypairFromSeed, loadKeypair, saveKe
|
|
|
9
9
|
export { deriveAgentId, registerCanonicalPayload, registerPayloadDigest, } from "./registration.js";
|
|
10
10
|
export { canonicalIdentityPayload, nameAgent, parseIdentityFields, } from "./identity.js";
|
|
11
11
|
export { actionSigningBytes, encodeActionFrame, encodeJoinFrame, encodeResumeFrame, joinSigningBytes, resumeSigningBytes, SESSION_VERSION, UnsupportedSessionVersionError, } from "./sessionWire.js";
|
|
12
|
-
export { encodeAckFrame, SESSION_ERROR_NAMES, sessionErrorHint, sessionErrorName, } from "./sessionCodec.js";
|
|
12
|
+
export { encodeAckFrame, SESSION_ERROR_NAMES, MAX_PREDICTION_PACING_MS, admitAuthorityEvent, applyPredictionGateStatus, decodeAuthorityMessage, freshSessionBoundary, predictionGateMessage, predictionGatePreparationOf, predictionGateTargetReceipt, sessionErrorHint, sessionErrorName, PredictionGateConflictError, SessionBoundaryError, } from "./sessionCodec.js";
|
|
13
13
|
export { SessionClient, SessionRefusal, chooseSeatAction, playSeat, } from "./session.js";
|
|
14
14
|
export { cardFromByte, decodeLegalActions, decodeParticipantView, encodeAction, pickAction, } from "./texas.js";
|
|
15
15
|
export { authorityOriginFromSessionBase, buildConsentRequest, digestForPrompt, recomputeSettlementDigest, settlementConsentPath, verifyConsentDisclosure, } from "./settlement.js";
|
|
@@ -26,7 +26,7 @@ export { joinRoom, MAX_ROOM_SEATS, MIN_ROOM_SEATS, openRoom, roomInvitePrompt, }
|
|
|
26
26
|
export { AGENT_CLAIM_INVITE_DOMAIN, AGENT_CLAIM_INVITE_MAX_WINDOW_MS, AGENT_CLAIM_INVITE_TOKEN_BYTES, claimInviteCanonicalPayload, claimInviteLink, claimInviteSigningBytes, decodeClaimInvite, encodeClaimInvite, encodeClaimInviteCompact, mintClaimInvite, } from "./claim.js";
|
|
27
27
|
export { baselineDecision, baselineMove, opponentsStillIn, } from "./decide.js";
|
|
28
28
|
export { handEquity, requiredEquity, cardCode, } from "./equity.js";
|
|
29
|
-
export {
|
|
29
|
+
export { cardIndex, handCategory, scoreHand, HAND_CATEGORIES, RANK_ORDER, SUIT_ORDER, } from "./handRank.js";
|
|
30
30
|
export { DEFAULT_SEAT_STATE_FILE, loadSeatState, saveSeatState, } from "./seatState.js";
|
|
31
31
|
export { newSeatState, openTurn, submitTurn, } from "./seatTurn.js";
|
|
32
32
|
export { chipAddressOf, joinTransaction, leaveTransaction, listTournaments, planJoin, presentToTournament, readAgentEntry, readPass, readOwner, playsHeldBy, giveBackTransaction, readTournament, sponsorAndExecute, tournamentIdArg, tournamentPossessionSignature, JoinRefused, TournamentRefusal, TOURNAMENT_POSSESSION_SEAT, } from "./openTournament.js";
|
package/dist/room.d.ts
CHANGED
|
@@ -8,20 +8,35 @@ export interface RoomClient {
|
|
|
8
8
|
/** A room as the product answers it on create. */
|
|
9
9
|
export interface RoomOpened {
|
|
10
10
|
tableId: string;
|
|
11
|
+
/** The chairs the room was opened for. An upper bound, not a roster: what
|
|
12
|
+
* nobody takes stays empty for the sitting. */
|
|
11
13
|
seatCount: number;
|
|
12
14
|
settlement: string;
|
|
13
15
|
mode: string;
|
|
14
16
|
/** Present once the authority has an execution behind the table. */
|
|
15
17
|
executionId?: string;
|
|
16
18
|
}
|
|
17
|
-
/** A join. `offerId` and `seat` are present
|
|
18
|
-
* where the guest's seat is taken by accepting an offer with its own key.
|
|
19
|
-
*
|
|
19
|
+
/** A join. `offerId` and `seat` are present once the room has composed, which
|
|
20
|
+
* is where the guest's seat is taken by accepting an offer with its own key.
|
|
21
|
+
* A room still filling answers how full it is instead, and when it deals to
|
|
22
|
+
* the agents who are already in it. On a mock stack there is never an offer
|
|
23
|
+
* and nothing to accept. */
|
|
20
24
|
export interface RoomJoined {
|
|
21
25
|
tableId: string;
|
|
22
26
|
joined: boolean;
|
|
23
27
|
offerId?: string;
|
|
24
28
|
seat?: number;
|
|
29
|
+
/** How many chairs are taken, of `seatCount`. Absent once composed — every
|
|
30
|
+
* seat in the offer holds an agent. */
|
|
31
|
+
seated?: number;
|
|
32
|
+
seatCount?: number;
|
|
33
|
+
/** When the room deals to the agents who are here rather than waiting for
|
|
34
|
+
* the rest. Absent while one agent sits alone: a room of one has no sitting
|
|
35
|
+
* to start, and waits indefinitely. */
|
|
36
|
+
fillAtMs?: number;
|
|
37
|
+
/** `authority` or `mock`, so a caller can tell "no offer because this stack
|
|
38
|
+
* composes none" from "no offer yet". */
|
|
39
|
+
mode?: string;
|
|
25
40
|
}
|
|
26
41
|
/** The seat counts a private room can be opened with, both ends included.
|
|
27
42
|
*
|
|
@@ -30,7 +45,12 @@ export interface RoomJoined {
|
|
|
30
45
|
* before the agent tells anybody a room exists. */
|
|
31
46
|
export declare const MIN_ROOM_SEATS = 2;
|
|
32
47
|
export declare const MAX_ROOM_SEATS = 10;
|
|
33
|
-
/** Open a room this agent will host.
|
|
48
|
+
/** Open a room this agent will host.
|
|
49
|
+
*
|
|
50
|
+
* `seatCount` is how many agents may come, including this one. No seat is
|
|
51
|
+
* ever filled for you: the room deals when its last chair is taken, or when
|
|
52
|
+
* its fill window closes with at least two agents in it, and the chairs
|
|
53
|
+
* nobody took stay empty for the sitting. */
|
|
34
54
|
export declare function openRoom(client: RoomClient, seatCount: number): Promise<RoomOpened>;
|
|
35
55
|
/** A join the product turned down, with the code it named. */
|
|
36
56
|
export declare class RoomJoinRefusal extends Error {
|
|
@@ -53,8 +73,10 @@ export declare function joinRoom(client: RoomClient, tableId: string): Promise<R
|
|
|
53
73
|
export declare function joinRoomWhenComposed(client: RoomClient, tableId: string, options?: {
|
|
54
74
|
pollMs?: number;
|
|
55
75
|
timeoutMs?: number;
|
|
56
|
-
/** Called once, the first time the room is found still waiting.
|
|
57
|
-
|
|
76
|
+
/** Called once, the first time the room is found still waiting. Carries
|
|
77
|
+
* the join that found it waiting, where there was one: the opener's own
|
|
78
|
+
* join is refused rather than answered, so it has nothing to carry. */
|
|
79
|
+
onWaiting?: (joined?: RoomJoined) => void;
|
|
58
80
|
sleep?: (ms: number) => Promise<void>;
|
|
59
81
|
now?: () => number;
|
|
60
82
|
}): Promise<RoomJoined>;
|
|
@@ -63,4 +85,4 @@ export declare function joinRoomWhenComposed(client: RoomClient, tableId: string
|
|
|
63
85
|
* A prompt rather than a URL: what the guest needs is the document that says
|
|
64
86
|
* what to do and the id to do it with, and an operator pasting two things in
|
|
65
87
|
* the right order is how a room went unjoined. */
|
|
66
|
-
export declare function roomInvitePrompt(productUrl: string, tableId: string): string;
|
|
88
|
+
export declare function roomInvitePrompt(productUrl: string, tableId: string, seatsOpen?: number): string;
|
package/dist/room.js
CHANGED
|
@@ -46,7 +46,12 @@ async function signedFetch(client, method, target, body) {
|
|
|
46
46
|
* before the agent tells anybody a room exists. */
|
|
47
47
|
export const MIN_ROOM_SEATS = 2;
|
|
48
48
|
export const MAX_ROOM_SEATS = 10;
|
|
49
|
-
/** Open a room this agent will host.
|
|
49
|
+
/** Open a room this agent will host.
|
|
50
|
+
*
|
|
51
|
+
* `seatCount` is how many agents may come, including this one. No seat is
|
|
52
|
+
* ever filled for you: the room deals when its last chair is taken, or when
|
|
53
|
+
* its fill window closes with at least two agents in it, and the chairs
|
|
54
|
+
* nobody took stay empty for the sitting. */
|
|
50
55
|
export async function openRoom(client, seatCount) {
|
|
51
56
|
if (!Number.isInteger(seatCount) ||
|
|
52
57
|
seatCount < MIN_ROOM_SEATS ||
|
|
@@ -105,6 +110,10 @@ export async function joinRoom(client, tableId) {
|
|
|
105
110
|
joined: json.joined === true,
|
|
106
111
|
offerId: json.offerId ?? undefined,
|
|
107
112
|
seat: json.seat ?? undefined,
|
|
113
|
+
seated: json.seated ?? undefined,
|
|
114
|
+
seatCount: json.seatCount ?? undefined,
|
|
115
|
+
fillAtMs: json.fillAtMs ?? undefined,
|
|
116
|
+
mode: json.mode ?? undefined,
|
|
108
117
|
};
|
|
109
118
|
}
|
|
110
119
|
/** Join, and wait for the room to fill when this agent already holds a seat
|
|
@@ -125,22 +134,37 @@ export async function joinRoomWhenComposed(client, tableId, options = {}) {
|
|
|
125
134
|
const now = options.now ?? Date.now;
|
|
126
135
|
const deadline = now() + timeoutMs;
|
|
127
136
|
let told = false;
|
|
137
|
+
const stillWaiting = () => {
|
|
138
|
+
if (now() >= deadline)
|
|
139
|
+
throw new Error(`room ${tableId} had not composed after ${timeoutMs} ms; ` +
|
|
140
|
+
"send the invite line to the guests you are still waiting for, ask its " +
|
|
141
|
+
"owner to start the room with whoever is already in it, or open a room again later");
|
|
142
|
+
};
|
|
128
143
|
for (;;) {
|
|
129
144
|
try {
|
|
130
|
-
|
|
145
|
+
const joined = await joinRoom(client, tableId);
|
|
146
|
+
/* Joined, and the room is still filling: this agent holds a chair and
|
|
147
|
+
there is no offer to accept until the room deals. A mock stack
|
|
148
|
+
composes no offer at all, so its join is the answer rather than a step
|
|
149
|
+
towards one. */
|
|
150
|
+
if (joined.offerId !== undefined || joined.mode === "mock")
|
|
151
|
+
return joined;
|
|
152
|
+
stillWaiting();
|
|
153
|
+
if (!told) {
|
|
154
|
+
told = true;
|
|
155
|
+
options.onWaiting?.(joined);
|
|
156
|
+
}
|
|
131
157
|
}
|
|
132
158
|
catch (error) {
|
|
133
159
|
if (!(error instanceof RoomJoinRefusal) || error.code !== "already_seated")
|
|
134
160
|
throw error;
|
|
135
|
-
|
|
136
|
-
throw new Error(`room ${tableId} still had nobody in its other seat after ${timeoutMs} ms; ` +
|
|
137
|
-
"send the invite line to the guest, or open the room again later");
|
|
161
|
+
stillWaiting();
|
|
138
162
|
if (!told) {
|
|
139
163
|
told = true;
|
|
140
164
|
options.onWaiting?.();
|
|
141
165
|
}
|
|
142
|
-
await sleep(pollMs);
|
|
143
166
|
}
|
|
167
|
+
await sleep(pollMs);
|
|
144
168
|
}
|
|
145
169
|
}
|
|
146
170
|
/** The one line an opener hands its guest, which is the whole invitation.
|
|
@@ -148,7 +172,13 @@ export async function joinRoomWhenComposed(client, tableId, options = {}) {
|
|
|
148
172
|
* A prompt rather than a URL: what the guest needs is the document that says
|
|
149
173
|
* what to do and the id to do it with, and an operator pasting two things in
|
|
150
174
|
* the right order is how a room went unjoined. */
|
|
151
|
-
export function roomInvitePrompt(productUrl, tableId) {
|
|
175
|
+
export function roomInvitePrompt(productUrl, tableId, seatsOpen) {
|
|
152
176
|
const base = productUrl.replace(/\/$/, "");
|
|
153
|
-
|
|
177
|
+
/* How many can still come, when the caller knows. A room deals to whoever
|
|
178
|
+
is in it when its window closes, so a guest reading this is being told
|
|
179
|
+
both what to do and how much room is left to do it in. */
|
|
180
|
+
const room = seatsOpen === undefined
|
|
181
|
+
? ""
|
|
182
|
+
: ` (${seatsOpen} ${seatsOpen === 1 ? "seat" : "seats"} open)`;
|
|
183
|
+
return `read ${base}/skills/private-room.md and join table ${tableId}${room}`;
|
|
154
184
|
}
|
package/dist/seatState.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SessionContext } from "./sessionWire.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type PredictionGateStatus, type ResumeCursor, type ViewSnapshot } from "./sessionCodec.js";
|
|
3
3
|
/** Written beside the key file, as `.dopa-keypair` is. */
|
|
4
4
|
export declare const DEFAULT_SEAT_STATE_FILE = ".dopa-seat";
|
|
5
5
|
export interface SeatSessionState {
|
|
@@ -37,6 +37,8 @@ export interface SeatSessionState {
|
|
|
37
37
|
* the documented `act` refused its first turn. Absent in a file written
|
|
38
38
|
* before it existed. */
|
|
39
39
|
agentId?: string | null;
|
|
40
|
+
predictionGate: SerialisedGate | null;
|
|
41
|
+
awaitingGatePrefix: boolean;
|
|
40
42
|
}
|
|
41
43
|
interface SerialisedContext {
|
|
42
44
|
wireVersion: number;
|
|
@@ -70,22 +72,75 @@ interface SerialisedView {
|
|
|
70
72
|
participantDeadlineMs: string;
|
|
71
73
|
latestReceipt: SerialisedReceipt | null;
|
|
72
74
|
}
|
|
75
|
+
interface SerialisedWindow {
|
|
76
|
+
windowId: string;
|
|
77
|
+
marketId: string;
|
|
78
|
+
contract: "pokerActionV1";
|
|
79
|
+
}
|
|
80
|
+
interface SerialisedPreparation {
|
|
81
|
+
window: SerialisedWindow;
|
|
82
|
+
actingSeat: number;
|
|
83
|
+
state: SerialisedState;
|
|
84
|
+
receipt: SerialisedReceipt | null;
|
|
85
|
+
originalDeadlineMs: string;
|
|
86
|
+
preparedAtMs: string;
|
|
87
|
+
}
|
|
88
|
+
interface SerialisedOpening {
|
|
89
|
+
preparation: SerialisedPreparation;
|
|
90
|
+
openedAtMs: string;
|
|
91
|
+
closesAtMs: string;
|
|
92
|
+
}
|
|
93
|
+
interface SerialisedRelease {
|
|
94
|
+
window: SerialisedWindow;
|
|
95
|
+
actingSeat: number;
|
|
96
|
+
state: SerialisedState;
|
|
97
|
+
receipt: SerialisedReceipt | null;
|
|
98
|
+
terminal: "locked" | "cancelled";
|
|
99
|
+
originalDeadlineMs: string;
|
|
100
|
+
arrivalMs: string;
|
|
101
|
+
lockedAtMs: string;
|
|
102
|
+
budgetMs: string;
|
|
103
|
+
}
|
|
104
|
+
type SerialisedGate = {
|
|
105
|
+
phase: "prepared";
|
|
106
|
+
preparation: SerialisedPreparation;
|
|
107
|
+
} | {
|
|
108
|
+
phase: "open";
|
|
109
|
+
opening: SerialisedOpening;
|
|
110
|
+
} | {
|
|
111
|
+
phase: "released";
|
|
112
|
+
release: SerialisedRelease;
|
|
113
|
+
};
|
|
73
114
|
export declare function encodeContext(context: SessionContext): SerialisedContext;
|
|
74
115
|
export declare function decodeContext(stored: SerialisedContext): SessionContext;
|
|
75
116
|
export declare function encodeCursor(cursor: ResumeCursor): SerialisedCursor;
|
|
76
117
|
export declare function decodeCursor(stored: SerialisedCursor): ResumeCursor;
|
|
77
118
|
export declare function encodeView(view: ViewSnapshot): SerialisedView;
|
|
78
119
|
export declare function decodeView(stored: SerialisedView): ViewSnapshot;
|
|
120
|
+
/** The accepted gate phase, in the file's own vocabulary. */
|
|
121
|
+
export declare function encodeGate(gate: PredictionGateStatus): SerialisedGate;
|
|
122
|
+
export declare function decodeGate(stored: SerialisedGate): PredictionGateStatus;
|
|
79
123
|
/** Read the seat's state, or null where it has not been opened yet.
|
|
80
124
|
*
|
|
81
125
|
* A state file whose retained context names a session version this build
|
|
82
126
|
* cannot speak is refused here, before any network call or signature: the
|
|
83
127
|
* sitting it was written for ended under the previous contract, and its
|
|
84
128
|
* cursor and token do not carry over. The file is left untouched, so an
|
|
85
|
-
* operator can still read what the seat was doing.
|
|
129
|
+
* operator can still read what the seat was doing.
|
|
130
|
+
*
|
|
131
|
+
* A file that does name this version and omits the gate boundary is refused
|
|
132
|
+
* too. Reading a missing `awaitingGatePrefix` as "not awaiting" would have a
|
|
133
|
+
* restarted seat answer the private view it acknowledged a viewless join
|
|
134
|
+
* for, which is the one thing the flag exists to prevent. */
|
|
86
135
|
export declare function loadSeatState(path: string): SeatSessionState | null;
|
|
87
|
-
/** Write it back, whole. Read-modify-write, never append: a
|
|
88
|
-
* cursor is a session that cannot resume, and the fix for that
|
|
89
|
-
* that costs the seat every turn in between.
|
|
136
|
+
/** Write it back, whole, and durably. Read-modify-write, never append: a
|
|
137
|
+
* half-written cursor is a session that cannot resume, and the fix for that
|
|
138
|
+
* is a rejoin that costs the seat every turn in between.
|
|
139
|
+
*
|
|
140
|
+
* Same-directory temporary file, fsync, rename, then fsync the directory -
|
|
141
|
+
* the repository's durable-private-file convention
|
|
142
|
+
* (`arena_authority::recovery_anchor::write_durable_private_file`). `turn`
|
|
143
|
+
* acknowledges an event only after this returns, so a torn file here would
|
|
144
|
+
* be a phase the authority believes was received and the seat cannot see. */
|
|
90
145
|
export declare function saveSeatState(path: string, state: SeatSessionState): void;
|
|
91
146
|
export {};
|
package/dist/seatState.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, openSync, closeSync, fsyncSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
2
3
|
import { fromHex, toHex0x } from "./bytes.js";
|
|
3
4
|
import { requireSessionVersion } from "./sessionWire.js";
|
|
5
|
+
import { bindPredictionGateOpening, bindPredictionGatePreparation, bindPredictionGateRelease, } from "./sessionCodec.js";
|
|
4
6
|
/* What a seat has to remember between one command and the next.
|
|
5
7
|
*
|
|
6
8
|
* `playSeat` is a loop: it joins, plays every turn and exits at a terminal, so
|
|
@@ -112,26 +114,173 @@ export function decodeView(stored) {
|
|
|
112
114
|
latestReceipt: decodeReceipt(stored.latestReceipt),
|
|
113
115
|
};
|
|
114
116
|
}
|
|
117
|
+
const encodeWindow = (window) => ({
|
|
118
|
+
windowId: window.windowId.toString(),
|
|
119
|
+
marketId: window.marketId.toString(),
|
|
120
|
+
contract: window.contract,
|
|
121
|
+
});
|
|
122
|
+
function decodeWindow(stored) {
|
|
123
|
+
if (stored.contract !== "pokerActionV1")
|
|
124
|
+
throw new Error(`stored prediction window names contract ${stored.contract}`);
|
|
125
|
+
return {
|
|
126
|
+
windowId: BigInt(stored.windowId),
|
|
127
|
+
marketId: BigInt(stored.marketId),
|
|
128
|
+
contract: "pokerActionV1",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function encodePreparation(preparation) {
|
|
132
|
+
return {
|
|
133
|
+
window: encodeWindow(preparation.window),
|
|
134
|
+
actingSeat: preparation.actingSeat,
|
|
135
|
+
state: encodeState(preparation.state),
|
|
136
|
+
receipt: encodeReceipt(preparation.receipt),
|
|
137
|
+
originalDeadlineMs: preparation.originalDeadlineMs.toString(),
|
|
138
|
+
preparedAtMs: preparation.preparedAtMs.toString(),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function decodePreparation(stored) {
|
|
142
|
+
return bindPredictionGatePreparation({
|
|
143
|
+
window: decodeWindow(stored.window),
|
|
144
|
+
actingSeat: stored.actingSeat,
|
|
145
|
+
state: decodeState(stored.state),
|
|
146
|
+
receipt: decodeReceipt(stored.receipt),
|
|
147
|
+
originalDeadlineMs: BigInt(stored.originalDeadlineMs),
|
|
148
|
+
preparedAtMs: BigInt(stored.preparedAtMs),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function encodeOpening(opening) {
|
|
152
|
+
return {
|
|
153
|
+
preparation: encodePreparation(opening.preparation),
|
|
154
|
+
openedAtMs: opening.openedAtMs.toString(),
|
|
155
|
+
closesAtMs: opening.closesAtMs.toString(),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** The accepted gate phase, in the file's own vocabulary. */
|
|
159
|
+
export function encodeGate(gate) {
|
|
160
|
+
switch (gate.phase) {
|
|
161
|
+
case "prepared":
|
|
162
|
+
return { phase: "prepared", preparation: encodePreparation(gate.preparation) };
|
|
163
|
+
case "open":
|
|
164
|
+
return { phase: "open", opening: encodeOpening(gate.opening) };
|
|
165
|
+
case "released":
|
|
166
|
+
return {
|
|
167
|
+
phase: "released",
|
|
168
|
+
release: {
|
|
169
|
+
window: encodeWindow(gate.release.window),
|
|
170
|
+
actingSeat: gate.release.actingSeat,
|
|
171
|
+
state: encodeState(gate.release.state),
|
|
172
|
+
receipt: encodeReceipt(gate.release.receipt),
|
|
173
|
+
terminal: gate.release.terminal,
|
|
174
|
+
originalDeadlineMs: gate.release.originalDeadlineMs.toString(),
|
|
175
|
+
arrivalMs: gate.release.arrivalMs.toString(),
|
|
176
|
+
lockedAtMs: gate.release.lockedAtMs.toString(),
|
|
177
|
+
budgetMs: gate.release.budgetMs.toString(),
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
export function decodeGate(stored) {
|
|
183
|
+
switch (stored.phase) {
|
|
184
|
+
case "prepared":
|
|
185
|
+
return {
|
|
186
|
+
phase: "prepared",
|
|
187
|
+
preparation: decodePreparation(stored.preparation),
|
|
188
|
+
};
|
|
189
|
+
case "open":
|
|
190
|
+
return {
|
|
191
|
+
phase: "open",
|
|
192
|
+
opening: bindPredictionGateOpening({
|
|
193
|
+
preparation: decodePreparation(stored.opening.preparation),
|
|
194
|
+
openedAtMs: BigInt(stored.opening.openedAtMs),
|
|
195
|
+
closesAtMs: BigInt(stored.opening.closesAtMs),
|
|
196
|
+
}),
|
|
197
|
+
};
|
|
198
|
+
case "released":
|
|
199
|
+
return {
|
|
200
|
+
phase: "released",
|
|
201
|
+
release: bindPredictionGateRelease({
|
|
202
|
+
window: decodeWindow(stored.release.window),
|
|
203
|
+
actingSeat: stored.release.actingSeat,
|
|
204
|
+
state: decodeState(stored.release.state),
|
|
205
|
+
receipt: decodeReceipt(stored.release.receipt),
|
|
206
|
+
terminal: stored.release.terminal,
|
|
207
|
+
originalDeadlineMs: BigInt(stored.release.originalDeadlineMs),
|
|
208
|
+
arrivalMs: BigInt(stored.release.arrivalMs),
|
|
209
|
+
lockedAtMs: BigInt(stored.release.lockedAtMs),
|
|
210
|
+
budgetMs: BigInt(stored.release.budgetMs),
|
|
211
|
+
}),
|
|
212
|
+
};
|
|
213
|
+
default:
|
|
214
|
+
throw new Error("stored prediction gate names no phase");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
115
217
|
/** Read the seat's state, or null where it has not been opened yet.
|
|
116
218
|
*
|
|
117
219
|
* A state file whose retained context names a session version this build
|
|
118
220
|
* cannot speak is refused here, before any network call or signature: the
|
|
119
221
|
* sitting it was written for ended under the previous contract, and its
|
|
120
222
|
* cursor and token do not carry over. The file is left untouched, so an
|
|
121
|
-
* operator can still read what the seat was doing.
|
|
223
|
+
* operator can still read what the seat was doing.
|
|
224
|
+
*
|
|
225
|
+
* A file that does name this version and omits the gate boundary is refused
|
|
226
|
+
* too. Reading a missing `awaitingGatePrefix` as "not awaiting" would have a
|
|
227
|
+
* restarted seat answer the private view it acknowledged a viewless join
|
|
228
|
+
* for, which is the one thing the flag exists to prevent. */
|
|
122
229
|
export function loadSeatState(path) {
|
|
123
230
|
if (!existsSync(path))
|
|
124
231
|
return null;
|
|
125
232
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
126
233
|
if (!parsed.offerId || typeof parsed.seat !== "number")
|
|
127
234
|
throw new Error(`${path} is not a seat state file`);
|
|
128
|
-
if (parsed.context)
|
|
235
|
+
if (parsed.context) {
|
|
129
236
|
requireSessionVersion(parsed.context.sessionVersion);
|
|
237
|
+
if (typeof parsed.awaitingGatePrefix !== "boolean" ||
|
|
238
|
+
parsed.predictionGate === undefined)
|
|
239
|
+
throw new Error(`${path} retains a session but no prediction-gate boundary; it cannot be resumed safely`);
|
|
240
|
+
if (parsed.predictionGate !== null)
|
|
241
|
+
decodeGate(parsed.predictionGate);
|
|
242
|
+
}
|
|
130
243
|
return parsed;
|
|
131
244
|
}
|
|
132
|
-
/**
|
|
133
|
-
*
|
|
134
|
-
|
|
245
|
+
/** Distinguishes concurrent writers' temporary files, as the Rust convention
|
|
246
|
+
* does with its process-scoped sequence. */
|
|
247
|
+
let nextTemporary = 0;
|
|
248
|
+
/** Write it back, whole, and durably. Read-modify-write, never append: a
|
|
249
|
+
* half-written cursor is a session that cannot resume, and the fix for that
|
|
250
|
+
* is a rejoin that costs the seat every turn in between.
|
|
251
|
+
*
|
|
252
|
+
* Same-directory temporary file, fsync, rename, then fsync the directory -
|
|
253
|
+
* the repository's durable-private-file convention
|
|
254
|
+
* (`arena_authority::recovery_anchor::write_durable_private_file`). `turn`
|
|
255
|
+
* acknowledges an event only after this returns, so a torn file here would
|
|
256
|
+
* be a phase the authority believes was received and the seat cannot see. */
|
|
135
257
|
export function saveSeatState(path, state) {
|
|
136
|
-
|
|
258
|
+
const directory = dirname(path) || ".";
|
|
259
|
+
mkdirSync(directory, { recursive: true });
|
|
260
|
+
const temporary = `${path}.tmp-${process.pid}-${nextTemporary++}`;
|
|
261
|
+
try {
|
|
262
|
+
writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, {
|
|
263
|
+
mode: 0o600,
|
|
264
|
+
flag: "wx",
|
|
265
|
+
});
|
|
266
|
+
const file = openSync(temporary, "r+");
|
|
267
|
+
try {
|
|
268
|
+
fsyncSync(file);
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
closeSync(file);
|
|
272
|
+
}
|
|
273
|
+
renameSync(temporary, path);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
rmSync(temporary, { force: true });
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
const parent = openSync(directory, "r");
|
|
280
|
+
try {
|
|
281
|
+
fsyncSync(parent);
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
closeSync(parent);
|
|
285
|
+
}
|
|
137
286
|
}
|
package/dist/seatTurn.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { AgentKeypair } from "./keypair.js";
|
|
2
2
|
import type { TexasAction, TexasLegalActions } from "./texas.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type AuthorityMessage, type ResumeCursor } from "./sessionCodec.js";
|
|
4
4
|
import type { SessionContext } from "./sessionWire.js";
|
|
5
|
-
import type { ResumeCursor } from "./sessionCodec.js";
|
|
6
5
|
import { type OpenTableSeatView, type OpenTableTalkLine, type OpenTableView, type SeatSession, type OpenSittingStatus } from "./session.js";
|
|
7
6
|
import { type SeatSessionState } from "./seatState.js";
|
|
8
7
|
export interface SeatTurnArgs {
|
|
@@ -11,6 +10,19 @@ export interface SeatTurnArgs {
|
|
|
11
10
|
agentId: Uint8Array;
|
|
12
11
|
fetchImpl?: typeof fetch;
|
|
13
12
|
}
|
|
13
|
+
/** Persist the accepted boundary before it is acknowledged.
|
|
14
|
+
*
|
|
15
|
+
* `openTurn` acknowledges events, and an acknowledgement is a promise that
|
|
16
|
+
* this seat has the event: the authority may drop it from the replay window
|
|
17
|
+
* on the strength of it. Two commands are two processes, so the promise has
|
|
18
|
+
* to be on disk before it is made -- otherwise a restart lands on a seat that
|
|
19
|
+
* acknowledged a viewless join or a prepared notice and has no record of
|
|
20
|
+
* either, which is exactly the seat that would answer a view it must refuse.
|
|
21
|
+
*
|
|
22
|
+
* Supplied by the CLI from `saveSeatState`. A rejection means no
|
|
23
|
+
* acknowledgement and no decision: the previously persisted boundary stays
|
|
24
|
+
* valid, and the authority will replay from it. */
|
|
25
|
+
export type SeatStateCheckpoint = (state: SeatSessionState) => void | Promise<void>;
|
|
14
26
|
/** What this seat is looking at, in the shape a reader can act on. */
|
|
15
27
|
export interface SeatTurnPosition {
|
|
16
28
|
seat: number;
|
|
@@ -121,6 +133,7 @@ export declare function attachFailureOutcome(error: unknown, sitting: OpenSittin
|
|
|
121
133
|
* an answer, not a failure. */
|
|
122
134
|
export declare function openTurn(args: SeatTurnArgs & {
|
|
123
135
|
waitMs?: number;
|
|
136
|
+
checkpoint: SeatStateCheckpoint;
|
|
124
137
|
}): Promise<SeatTurnOutcome>;
|
|
125
138
|
export interface SubmitTurnResult {
|
|
126
139
|
committed: boolean;
|