@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/index.d.ts CHANGED
@@ -2,10 +2,10 @@ 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";
6
- export { type OpenTableView, type PlayReport, type SeatDecision, type SeatDecisionResult, type SeatPosition, SessionClient, SessionRefusal, chooseSeatAction, playSeat, } from "./session.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
+ export { type OpenTableView, type PlayReport, type SeatDecision, type SeatDecisionResult, type SeatPosition, SessionClient, SessionRefusal, chooseSeatAction, playSeat, waitForReadyOwner, } 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
- export { authorityOriginFromSessionBase, buildConsentRequest, digestForPrompt, recomputeSettlementDigest, settlementConsentPath, verifyConsentDisclosure, } from "./settlement.js";
8
+ export { buildConsentRequest, DEFAULT_OPEN_API_BASE_URL, digestForPrompt, recomputeSettlementDigest, settlementConsentPath, verifyConsentDisclosure, } from "./settlement.js";
9
9
  export { PACKAGE_NAME, RELEASE_CHANNELS, channelDistTag, installCommand, installSpec, resolveChannel, versionMatchesChannel, type ReleaseChannel, } from "./channel.js";
10
10
  export { fromHex, toHex } from "./bytes.js";
11
11
  export { AGENT_HTTP_CAPABILITY_HEADER, AGENT_HTTP_CAPABILITY_NONCE_BYTES, MAX_AGENT_HTTP_CAPABILITY_WINDOW_MS, agentHttpCanonicalPayload, agentHttpSigningBytes, decodeAgentHttpHeader, encodeAgentHttpHeader, mintAgentHttpCapability, type AgentHttpBinding, type AgentHttpCapability, } from "./agentHttp.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 { bestFive, cardIndex, handCategory, scoreHand, HAND_CATEGORIES, RANK_ORDER, SUIT_ORDER, type BestFive, type HandCategory, } from "./handRank.js";
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";
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
- export { ClientRefusal, describeNext, refusalLines, refusalMessage, refusalMessageFromText, type RefusalBody, type RefusalNext, } from "./refusal.js";
19
+ export { newSeatState, openTurn, submitTurn, type SeatStateCheckpoint, type SeatTurnOutcome, type SeatTurnPosition, } from "./seatTurn.js";
20
+ export { chipAddressOf, joinTransaction, leaveTransaction, listTournaments, planJoin, presentToTournament, readAgentEntry, readAgentEntrySettled, BOOK_CATCHES_UP_MS, 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
+ export { ClientRefusal, describeNext, ownershipFencePoll, ownershipFenced, refusalLines, refusalMessage, refusalMessageFromText, type RefusalBody, type RefusalNext, } from "./refusal.js";
package/dist/index.js CHANGED
@@ -3,16 +3,16 @@
3
3
  * Generate and hold an ed25519 keypair (Sui format, `.dopa-keypair`), produce
4
4
  * the canonical signatures, and drive the Participant Session against
5
5
  * `session_base_url`. Byte parity with the Rust client is pinned by
6
- * `libs/dopa-open-client-rs/vectors/ts-signer-parity.json`.
6
+ * `libs/dopa-open/client-rs/vectors/ts-signer-parity.json`.
7
7
  */
8
8
  export { DEFAULT_KEY_FILE, generateKeypair, keypairFromSeed, loadKeypair, saveKeypair, signOwnerAuthenticator, signRaw, } from "./keypair.js";
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";
13
- export { SessionClient, SessionRefusal, chooseSeatAction, playSeat, } from "./session.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
+ export { SessionClient, SessionRefusal, chooseSeatAction, playSeat, waitForReadyOwner, } from "./session.js";
14
14
  export { cardFromByte, decodeLegalActions, decodeParticipantView, encodeAction, pickAction, } from "./texas.js";
15
- export { authorityOriginFromSessionBase, buildConsentRequest, digestForPrompt, recomputeSettlementDigest, settlementConsentPath, verifyConsentDisclosure, } from "./settlement.js";
15
+ export { buildConsentRequest, DEFAULT_OPEN_API_BASE_URL, digestForPrompt, recomputeSettlementDigest, settlementConsentPath, verifyConsentDisclosure, } from "./settlement.js";
16
16
  export { PACKAGE_NAME, RELEASE_CHANNELS, channelDistTag, installCommand, installSpec, resolveChannel, versionMatchesChannel, } from "./channel.js";
17
17
  export { fromHex, toHex } from "./bytes.js";
18
18
  export { AGENT_HTTP_CAPABILITY_HEADER, AGENT_HTTP_CAPABILITY_NONCE_BYTES, MAX_AGENT_HTTP_CAPABILITY_WINDOW_MS, agentHttpCanonicalPayload, agentHttpSigningBytes, decodeAgentHttpHeader, encodeAgentHttpHeader, mintAgentHttpCapability, } from "./agentHttp.js";
@@ -26,8 +26,8 @@ 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 { bestFive, cardIndex, handCategory, scoreHand, HAND_CATEGORIES, RANK_ORDER, SUIT_ORDER, } from "./handRank.js";
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
- export { chipAddressOf, joinTransaction, leaveTransaction, listTournaments, planJoin, presentToTournament, readAgentEntry, readPass, readOwner, playsHeldBy, giveBackTransaction, readTournament, sponsorAndExecute, tournamentIdArg, tournamentPossessionSignature, JoinRefused, TournamentRefusal, TOURNAMENT_POSSESSION_SEAT, } from "./openTournament.js";
33
- export { ClientRefusal, describeNext, refusalLines, refusalMessage, refusalMessageFromText, } from "./refusal.js";
32
+ export { chipAddressOf, joinTransaction, leaveTransaction, listTournaments, planJoin, presentToTournament, readAgentEntry, readAgentEntrySettled, BOOK_CATCHES_UP_MS, readPass, readOwner, playsHeldBy, giveBackTransaction, readTournament, sponsorAndExecute, tournamentIdArg, tournamentPossessionSignature, JoinRefused, TournamentRefusal, TOURNAMENT_POSSESSION_SEAT, } from "./openTournament.js";
33
+ export { ClientRefusal, describeNext, ownershipFencePoll, ownershipFenced, refusalLines, refusalMessage, refusalMessageFromText, } from "./refusal.js";
package/dist/offer.js CHANGED
@@ -52,7 +52,7 @@ function record(raw) {
52
52
  };
53
53
  }
54
54
  export async function readOffer(productUrl, offerId) {
55
- const { status, json } = await readJson(`${productUrl.replace(/\/$/, "")}/open/v1/playground/matches/${offerId}`);
55
+ const { status, json } = await readJson(`${productUrl.replace(/\/$/, "")}/v1/playground/matches/${offerId}`);
56
56
  if (status !== 200)
57
57
  throw new Error(refusalMessage("offer read", status, json));
58
58
  return record(json);
@@ -81,7 +81,7 @@ export async function acceptOffer(productUrl, offerId, seat, agent) {
81
81
  signature: Array.from(signed.signature),
82
82
  seat_possession_signature: Array.from(signed.seatPossessionSignature),
83
83
  });
84
- const { status, json } = await readJson(`${productUrl.replace(/\/$/, "")}/open/v1/playground/matches/${offerId}/acceptances`, { method: "POST", headers: { "content-type": "application/json" }, body });
84
+ const { status, json } = await readJson(`${productUrl.replace(/\/$/, "")}/v1/playground/matches/${offerId}/acceptances`, { method: "POST", headers: { "content-type": "application/json" }, body });
85
85
  if (status === 200 || status === 201)
86
86
  return;
87
87
  if (status === 409)
@@ -89,7 +89,7 @@ export async function acceptOffer(productUrl, offerId, seat, agent) {
89
89
  throw new Error(refusalMessage("acceptance", status, json));
90
90
  }
91
91
  async function admit(productUrl, offerId, agentId, agent) {
92
- const target = `/open/v1/playground/matches/${offerId}/admit`;
92
+ const target = `/v1/playground/matches/${offerId}/admit`;
93
93
  const { header } = await mintAgentHttpCapability(agent, agentId, {
94
94
  method: "POST",
95
95
  requestTarget: target,
@@ -143,6 +143,28 @@ export declare function readTournamentMatches(productUrl: string, tournamentId:
143
143
  export declare function disputeHolding(productUrl: string, tournamentId: string, chipAddress: string, fetchImpl?: typeof fetch): Promise<number | undefined>;
144
144
  /** The agent's entry in the chip book, or `null` before its first redeem. */
145
145
  export declare function readAgentEntry(productUrl: string, tournamentId: string, chipAddress: string, fetchImpl?: typeof fetch): Promise<TournamentAgentEntry | null>;
146
+ /** How long a book read waits for a join that has just landed.
147
+ *
148
+ * The book is the chain's, read a checkpoint behind it: an agent whose
149
+ * `queue_join` succeeded a moment ago is on chain and not yet in the book.
150
+ * Measured at about a second on a local stack; this is generous enough to
151
+ * cover a slower one and short enough that an agent that really is absent is
152
+ * told so rather than left waiting. */
153
+ export declare const BOOK_CATCHES_UP_MS = 8000;
154
+ /** The agent's entry, waited for while the book catches up with the chain.
155
+ *
156
+ * For a caller that has just been told its join landed. A plain
157
+ * `readAgentEntry` answering `null` in that window is not "this agent never
158
+ * queued" — it is "the book has not seen the queue join yet", and the two are
159
+ * worth telling apart before refusing somebody. Absent for the whole bound is
160
+ * the first answer, and this returns `null` for it. */
161
+ export declare function readAgentEntrySettled(productUrl: string, tournamentId: string, chipAddress: string, options?: {
162
+ boundMs?: number;
163
+ pollMs?: number;
164
+ fetchImpl?: typeof fetch;
165
+ sleep?: (ms: number) => Promise<void>;
166
+ now?: () => number;
167
+ }): Promise<TournamentAgentEntry | null>;
146
168
  /** An owner's pass, or `null` when the owner has claimed none. */
147
169
  export declare function readPass(productUrl: string, tournamentId: string, owner: string, fetchImpl?: typeof fetch): Promise<TournamentPass | null>;
148
170
  /** An owner's side: its pass and every agent it has claimed, each with the
@@ -26,7 +26,7 @@ function base(productUrl) {
26
26
  return productUrl.replace(/\/$/, "");
27
27
  }
28
28
  function tournamentPath(tournamentId, rest = "") {
29
- return `/open/v1/tournaments/${tournamentId}${rest}`;
29
+ return `/v1/tournaments/${tournamentId}${rest}`;
30
30
  }
31
31
  async function readJson(fetchImpl, url) {
32
32
  const response = await fetchImpl(url);
@@ -57,7 +57,7 @@ export async function readTournament(productUrl, tournamentId, fetchImpl = fetch
57
57
  }
58
58
  /** The open-entry tournaments the product runs. */
59
59
  export async function listTournaments(productUrl, fetchImpl = fetch) {
60
- const { status, json } = await readJson(fetchImpl, `${base(productUrl)}/open/v1/tournaments`);
60
+ const { status, json } = await readJson(fetchImpl, `${base(productUrl)}/v1/tournaments`);
61
61
  if (status === 404)
62
62
  return [];
63
63
  if (status !== 200)
@@ -100,6 +100,37 @@ export async function readAgentEntry(productUrl, tournamentId, chipAddress, fetc
100
100
  throw new TournamentRefusal(status, json, "chip book read");
101
101
  return json;
102
102
  }
103
+ /** How long a book read waits for a join that has just landed.
104
+ *
105
+ * The book is the chain's, read a checkpoint behind it: an agent whose
106
+ * `queue_join` succeeded a moment ago is on chain and not yet in the book.
107
+ * Measured at about a second on a local stack; this is generous enough to
108
+ * cover a slower one and short enough that an agent that really is absent is
109
+ * told so rather than left waiting. */
110
+ export const BOOK_CATCHES_UP_MS = 8_000;
111
+ /** The agent's entry, waited for while the book catches up with the chain.
112
+ *
113
+ * For a caller that has just been told its join landed. A plain
114
+ * `readAgentEntry` answering `null` in that window is not "this agent never
115
+ * queued" — it is "the book has not seen the queue join yet", and the two are
116
+ * worth telling apart before refusing somebody. Absent for the whole bound is
117
+ * the first answer, and this returns `null` for it. */
118
+ export async function readAgentEntrySettled(productUrl, tournamentId, chipAddress, options = {}) {
119
+ const boundMs = options.boundMs ?? BOOK_CATCHES_UP_MS;
120
+ const pollMs = options.pollMs ?? 1_000;
121
+ const now = options.now ?? Date.now;
122
+ const sleep = options.sleep ??
123
+ ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
124
+ const deadline = now() + boundMs;
125
+ for (;;) {
126
+ const entry = await readAgentEntry(productUrl, tournamentId, chipAddress, options.fetchImpl ?? fetch);
127
+ if (entry)
128
+ return entry;
129
+ if (now() >= deadline)
130
+ return null;
131
+ await sleep(pollMs);
132
+ }
133
+ }
103
134
  /** An owner's pass, or `null` when the owner has claimed none. */
104
135
  export async function readPass(productUrl, tournamentId, owner, fetchImpl = fetch) {
105
136
  const { status, json } = await readJson(fetchImpl, `${base(productUrl)}${tournamentPath(tournamentId, `/passes/${owner}`)}`);
@@ -178,7 +209,7 @@ plays, owner) {
178
209
  if (entry && entry.state !== "idle")
179
210
  throw new JoinRefused(`this agent is already ${entry.state}${entry.state === "seated" ? " at a table" : ""}; an agent plays one table at a time`, {
180
211
  action: "wait",
181
- poll: `/open/v1/tournaments/${overview.tournamentId}/agents/${entry.chipAddress}`,
212
+ poll: `/v1/tournaments/${overview.tournamentId}/agents/${entry.chipAddress}`,
182
213
  });
183
214
  const booked = entry?.balance ?? 0;
184
215
  const wallet = entry?.walletBalance ?? booked;
package/dist/refusal.d.ts CHANGED
@@ -29,6 +29,11 @@ export interface RefusalBody {
29
29
  docs?: string;
30
30
  }
31
31
  /** `next …`, as one line an agent can act on. */
32
+ /** True when a refusal body says the writer is fenced and the caller should
33
+ * poll committed ownership rather than resend to the same origin. */
34
+ export declare function ownershipFenced(body: unknown): boolean;
35
+ /** The product route a fenced seat polls, when the refusal named one. */
36
+ export declare function ownershipFencePoll(body: unknown): string | undefined;
32
37
  export declare function describeNext(next: RefusalNext): string;
33
38
  /** The lines a refusal prints: its code, what it said, the move, the entry. */
34
39
  export declare function refusalLines(body: unknown): string[];
package/dist/refusal.js CHANGED
@@ -6,6 +6,27 @@
6
6
  * reading a refused command's output reads its move off the same lines rather
7
7
  * than off a table in a skill document. */
8
8
  /** `next …`, as one line an agent can act on. */
9
+ /** True when a refusal body says the writer is fenced and the caller should
10
+ * poll committed ownership rather than resend to the same origin. */
11
+ export function ownershipFenced(body) {
12
+ if (typeof body !== "object" || body === null)
13
+ return false;
14
+ const refusal = body;
15
+ if (refusal.code !== "ownership_fenced")
16
+ return false;
17
+ return (isNext(refusal.next) &&
18
+ refusal.next.action === "wait" &&
19
+ typeof refusal.next.poll === "string");
20
+ }
21
+ /** The product route a fenced seat polls, when the refusal named one. */
22
+ export function ownershipFencePoll(body) {
23
+ if (!ownershipFenced(body))
24
+ return undefined;
25
+ const next = body.next;
26
+ if (next && next.action === "wait")
27
+ return next.poll;
28
+ return undefined;
29
+ }
9
30
  export function describeNext(next) {
10
31
  switch (next.action) {
11
32
  case "retry":
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 on an authority stack, which is
18
- * where the guest's seat is taken by accepting an offer with its own key. On
19
- * a mock stack the room is joined and there is nothing to accept. */
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. Seats above the two taken are house. */
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
- onWaiting?: () => void;
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,13 +46,18 @@ 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. Seats above the two taken are house. */
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 ||
53
58
  seatCount > MAX_ROOM_SEATS)
54
59
  throw new Error(`a private room seats ${MIN_ROOM_SEATS} to ${MAX_ROOM_SEATS}, not ${seatCount}`);
55
- const { status, json } = await signedFetch(client, "POST", "/open/v1/tables/private-rooms", { seatCount });
60
+ const { status, json } = await signedFetch(client, "POST", "/v1/tables/private-rooms", { seatCount });
56
61
  if (status !== 201) {
57
62
  if (json?.code === "agent_not_claimed")
58
63
  throw notClaimed();
@@ -90,7 +95,7 @@ export class RoomJoinRefusal extends Error {
90
95
  }
91
96
  /** Join a room by the id its opener handed out. */
92
97
  export async function joinRoom(client, tableId) {
93
- const target = `/open/v1/tables/private-rooms/${tableId}/join`;
98
+ const target = `/v1/tables/private-rooms/${tableId}/join`;
94
99
  const { status, json } = await signedFetch(client, "POST", target);
95
100
  if (status !== 201) {
96
101
  if (json?.code === "agent_not_claimed")
@@ -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
- return await joinRoom(client, tableId);
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
- if (now() >= deadline)
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
- return `read ${base}/skills/private-room.md and join table ${tableId}`;
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
  }
@@ -1,5 +1,5 @@
1
1
  import type { SessionContext } from "./sessionWire.js";
2
- import type { ResumeCursor, ViewSnapshot } from "./sessionCodec.js";
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,76 @@ 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
+ terminalDigest: string | null;
104
+ }
105
+ type SerialisedGate = {
106
+ phase: "prepared";
107
+ preparation: SerialisedPreparation;
108
+ } | {
109
+ phase: "open";
110
+ opening: SerialisedOpening;
111
+ } | {
112
+ phase: "released";
113
+ release: SerialisedRelease;
114
+ };
73
115
  export declare function encodeContext(context: SessionContext): SerialisedContext;
74
116
  export declare function decodeContext(stored: SerialisedContext): SessionContext;
75
117
  export declare function encodeCursor(cursor: ResumeCursor): SerialisedCursor;
76
118
  export declare function decodeCursor(stored: SerialisedCursor): ResumeCursor;
77
119
  export declare function encodeView(view: ViewSnapshot): SerialisedView;
78
120
  export declare function decodeView(stored: SerialisedView): ViewSnapshot;
121
+ /** The accepted gate phase, in the file's own vocabulary. */
122
+ export declare function encodeGate(gate: PredictionGateStatus): SerialisedGate;
123
+ export declare function decodeGate(stored: SerialisedGate): PredictionGateStatus;
79
124
  /** Read the seat's state, or null where it has not been opened yet.
80
125
  *
81
126
  * A state file whose retained context names a session version this build
82
127
  * cannot speak is refused here, before any network call or signature: the
83
128
  * sitting it was written for ended under the previous contract, and its
84
129
  * cursor and token do not carry over. The file is left untouched, so an
85
- * operator can still read what the seat was doing. */
130
+ * operator can still read what the seat was doing.
131
+ *
132
+ * A file that does name this version and omits the gate boundary is refused
133
+ * too. Reading a missing `awaitingGatePrefix` as "not awaiting" would have a
134
+ * restarted seat answer the private view it acknowledged a viewless join
135
+ * for, which is the one thing the flag exists to prevent. */
86
136
  export declare function loadSeatState(path: string): SeatSessionState | null;
87
- /** Write it back, whole. Read-modify-write, never append: a half-written
88
- * cursor is a session that cannot resume, and the fix for that is a rejoin
89
- * that costs the seat every turn in between. */
137
+ /** Write it back, whole, and durably. Read-modify-write, never append: a
138
+ * half-written cursor is a session that cannot resume, and the fix for that
139
+ * is a rejoin that costs the seat every turn in between.
140
+ *
141
+ * Same-directory temporary file, fsync, rename, then fsync the directory -
142
+ * the repository's durable-private-file convention
143
+ * (`arena_authority::recovery_anchor::write_durable_private_file`). `turn`
144
+ * acknowledges an event only after this returns, so a torn file here would
145
+ * be a phase the authority believes was received and the seat cannot see. */
90
146
  export declare function saveSeatState(path: string, state: SeatSessionState): void;
91
147
  export {};