@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/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,179 @@ 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
|
+
terminalDigest: gate.release.terminalDigest
|
|
179
|
+
? toHex0x(gate.release.terminalDigest)
|
|
180
|
+
: null,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export function decodeGate(stored) {
|
|
186
|
+
switch (stored.phase) {
|
|
187
|
+
case "prepared":
|
|
188
|
+
return {
|
|
189
|
+
phase: "prepared",
|
|
190
|
+
preparation: decodePreparation(stored.preparation),
|
|
191
|
+
};
|
|
192
|
+
case "open":
|
|
193
|
+
return {
|
|
194
|
+
phase: "open",
|
|
195
|
+
opening: bindPredictionGateOpening({
|
|
196
|
+
preparation: decodePreparation(stored.opening.preparation),
|
|
197
|
+
openedAtMs: BigInt(stored.opening.openedAtMs),
|
|
198
|
+
closesAtMs: BigInt(stored.opening.closesAtMs),
|
|
199
|
+
}),
|
|
200
|
+
};
|
|
201
|
+
case "released":
|
|
202
|
+
return {
|
|
203
|
+
phase: "released",
|
|
204
|
+
release: bindPredictionGateRelease({
|
|
205
|
+
window: decodeWindow(stored.release.window),
|
|
206
|
+
actingSeat: stored.release.actingSeat,
|
|
207
|
+
state: decodeState(stored.release.state),
|
|
208
|
+
receipt: decodeReceipt(stored.release.receipt),
|
|
209
|
+
terminal: stored.release.terminal,
|
|
210
|
+
originalDeadlineMs: BigInt(stored.release.originalDeadlineMs),
|
|
211
|
+
arrivalMs: BigInt(stored.release.arrivalMs),
|
|
212
|
+
lockedAtMs: BigInt(stored.release.lockedAtMs),
|
|
213
|
+
budgetMs: BigInt(stored.release.budgetMs),
|
|
214
|
+
terminalDigest: stored.release.terminalDigest
|
|
215
|
+
? fromHex(stored.release.terminalDigest)
|
|
216
|
+
: null,
|
|
217
|
+
}),
|
|
218
|
+
};
|
|
219
|
+
default:
|
|
220
|
+
throw new Error("stored prediction gate names no phase");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
115
223
|
/** Read the seat's state, or null where it has not been opened yet.
|
|
116
224
|
*
|
|
117
225
|
* A state file whose retained context names a session version this build
|
|
118
226
|
* cannot speak is refused here, before any network call or signature: the
|
|
119
227
|
* sitting it was written for ended under the previous contract, and its
|
|
120
228
|
* cursor and token do not carry over. The file is left untouched, so an
|
|
121
|
-
* operator can still read what the seat was doing.
|
|
229
|
+
* operator can still read what the seat was doing.
|
|
230
|
+
*
|
|
231
|
+
* A file that does name this version and omits the gate boundary is refused
|
|
232
|
+
* too. Reading a missing `awaitingGatePrefix` as "not awaiting" would have a
|
|
233
|
+
* restarted seat answer the private view it acknowledged a viewless join
|
|
234
|
+
* for, which is the one thing the flag exists to prevent. */
|
|
122
235
|
export function loadSeatState(path) {
|
|
123
236
|
if (!existsSync(path))
|
|
124
237
|
return null;
|
|
125
238
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
126
239
|
if (!parsed.offerId || typeof parsed.seat !== "number")
|
|
127
240
|
throw new Error(`${path} is not a seat state file`);
|
|
128
|
-
if (parsed.context)
|
|
241
|
+
if (parsed.context) {
|
|
129
242
|
requireSessionVersion(parsed.context.sessionVersion);
|
|
243
|
+
if (typeof parsed.awaitingGatePrefix !== "boolean" ||
|
|
244
|
+
parsed.predictionGate === undefined)
|
|
245
|
+
throw new Error(`${path} retains a session but no prediction-gate boundary; it cannot be resumed safely`);
|
|
246
|
+
if (parsed.predictionGate !== null)
|
|
247
|
+
decodeGate(parsed.predictionGate);
|
|
248
|
+
}
|
|
130
249
|
return parsed;
|
|
131
250
|
}
|
|
132
|
-
/**
|
|
133
|
-
*
|
|
134
|
-
|
|
251
|
+
/** Distinguishes concurrent writers' temporary files, as the Rust convention
|
|
252
|
+
* does with its process-scoped sequence. */
|
|
253
|
+
let nextTemporary = 0;
|
|
254
|
+
/** Write it back, whole, and durably. Read-modify-write, never append: a
|
|
255
|
+
* half-written cursor is a session that cannot resume, and the fix for that
|
|
256
|
+
* is a rejoin that costs the seat every turn in between.
|
|
257
|
+
*
|
|
258
|
+
* Same-directory temporary file, fsync, rename, then fsync the directory -
|
|
259
|
+
* the repository's durable-private-file convention
|
|
260
|
+
* (`arena_authority::recovery_anchor::write_durable_private_file`). `turn`
|
|
261
|
+
* acknowledges an event only after this returns, so a torn file here would
|
|
262
|
+
* be a phase the authority believes was received and the seat cannot see. */
|
|
135
263
|
export function saveSeatState(path, state) {
|
|
136
|
-
|
|
264
|
+
const directory = dirname(path) || ".";
|
|
265
|
+
mkdirSync(directory, { recursive: true });
|
|
266
|
+
const temporary = `${path}.tmp-${process.pid}-${nextTemporary++}`;
|
|
267
|
+
try {
|
|
268
|
+
writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, {
|
|
269
|
+
mode: 0o600,
|
|
270
|
+
flag: "wx",
|
|
271
|
+
});
|
|
272
|
+
const file = openSync(temporary, "r+");
|
|
273
|
+
try {
|
|
274
|
+
fsyncSync(file);
|
|
275
|
+
}
|
|
276
|
+
finally {
|
|
277
|
+
closeSync(file);
|
|
278
|
+
}
|
|
279
|
+
renameSync(temporary, path);
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
rmSync(temporary, { force: true });
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
const parent = openSync(directory, "r");
|
|
286
|
+
try {
|
|
287
|
+
fsyncSync(parent);
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
closeSync(parent);
|
|
291
|
+
}
|
|
137
292
|
}
|
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;
|
package/dist/seatTurn.js
CHANGED
|
@@ -2,8 +2,9 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { toHex0x } from "./bytes.js";
|
|
3
3
|
import { acceptAndAwaitAdmission, readOffer } from "./offer.js";
|
|
4
4
|
import { decodeLegalActions, decodeParticipantView, encodeAction, } from "./texas.js";
|
|
5
|
+
import { admitAuthorityEvent, equalReceiptRef, freshSessionBoundary, predictionGatePreparationOf, predictionGateTargetReceipt, } from "./sessionCodec.js";
|
|
5
6
|
import { agentReadCapability, openSeatSession, readPublicTable, readTableTalk, turnIsStillOpen, SessionRefusal, afterRefusal, readSittingStatus, } from "./session.js";
|
|
6
|
-
import { decodeContext, decodeCursor, decodeView, encodeContext, encodeCursor, encodeView, } from "./seatState.js";
|
|
7
|
+
import { decodeContext, decodeCursor, decodeGate, decodeView, encodeContext, encodeCursor, encodeGate, encodeView, } from "./seatState.js";
|
|
7
8
|
/** Whether `view` is a turn this seat has already answered.
|
|
8
9
|
*
|
|
9
10
|
* `act` does not move the cursor, so the next `turn` resumes from before the
|
|
@@ -87,6 +88,12 @@ export async function attach(session, state, reopen) {
|
|
|
87
88
|
clientNonce: toHex0x(nonce),
|
|
88
89
|
context: joined.context ? encodeContext(joined.context) : null,
|
|
89
90
|
token: opened.client.token ?? null,
|
|
91
|
+
/* A fresh admission resets the gate boundary with everything else:
|
|
92
|
+
the phase belonged to the session that just ended. */
|
|
93
|
+
cursor: { sequence: "0", witnessedReceipt: null },
|
|
94
|
+
view: null,
|
|
95
|
+
predictionGate: null,
|
|
96
|
+
awaitingGatePrefix: false,
|
|
90
97
|
},
|
|
91
98
|
context: joined.context ?? null,
|
|
92
99
|
cursor: { sequence: 0n, witnessedReceipt: null },
|
|
@@ -124,13 +131,21 @@ const holeFromView = (view) => {
|
|
|
124
131
|
return null;
|
|
125
132
|
}
|
|
126
133
|
};
|
|
127
|
-
|
|
134
|
+
/** The file as it stands after one accepted event: the session it belongs to,
|
|
135
|
+
* the boundary that was accepted, and the position to answer if there is one.
|
|
136
|
+
* The gate phase travels with the cursor because they are one promise - a
|
|
137
|
+
* cursor that outran its phase is a seat that forgot it was held. */
|
|
138
|
+
function persisted(session, state, context, cursor, view, boundary) {
|
|
128
139
|
return {
|
|
129
140
|
...state,
|
|
130
141
|
context: context ? encodeContext(context) : state.context,
|
|
131
142
|
token: session.client.token ?? state.token,
|
|
132
143
|
cursor: encodeCursor(cursor),
|
|
133
144
|
view: view ? encodeView(view) : state.view,
|
|
145
|
+
predictionGate: boundary.predictionGate
|
|
146
|
+
? encodeGate(boundary.predictionGate)
|
|
147
|
+
: null,
|
|
148
|
+
awaitingGatePrefix: boundary.awaitingGatePrefix,
|
|
134
149
|
};
|
|
135
150
|
}
|
|
136
151
|
/** What `turn` answers when it could not open a session.
|
|
@@ -201,18 +216,66 @@ export async function openTurn(args) {
|
|
|
201
216
|
session = attached.session;
|
|
202
217
|
args = { ...args, state: attached.state };
|
|
203
218
|
let { context, cursor, messages } = attached;
|
|
219
|
+
/* What this seat has already accepted, restored from the file: the sequence
|
|
220
|
+
and receipt floor its cursor names, the gate phase it holds, and whether a
|
|
221
|
+
viewless admission still owes its prepared prefix. The view is not
|
|
222
|
+
restored - a restart drops it exactly as
|
|
223
|
+
`ParticipantSessionState::restore` does, and the committed floor carries
|
|
224
|
+
what the boundary still needs. */
|
|
225
|
+
let boundary = {
|
|
226
|
+
...freshSessionBoundary(),
|
|
227
|
+
context,
|
|
228
|
+
sequence: cursor.sequence,
|
|
229
|
+
receiptFloor: cursor.witnessedReceipt,
|
|
230
|
+
predictionGate: args.state.predictionGate
|
|
231
|
+
? decodeGate(args.state.predictionGate)
|
|
232
|
+
: null,
|
|
233
|
+
awaitingGatePrefix: args.state.awaitingGatePrefix,
|
|
234
|
+
};
|
|
204
235
|
const until = Date.now() + (args.waitMs ?? 0);
|
|
236
|
+
/* Accept, persist, acknowledge - in that order, for every event this door
|
|
237
|
+
takes. The acknowledgement tells the authority this seat holds the event,
|
|
238
|
+
and it may drop the event from the replay window on the strength of it; a
|
|
239
|
+
promise made before the file is on disk is one a restart cannot keep.
|
|
240
|
+
|
|
241
|
+
A refusal from `admitAuthorityEvent` throws out of here with nothing
|
|
242
|
+
written and nothing acknowledged, which is what leaves the previously
|
|
243
|
+
persisted boundary resumable. A re-delivery answers `null`: it was
|
|
244
|
+
persisted and acknowledged by the process that first applied it, and
|
|
245
|
+
writing its cursor again would walk this seat's boundary backwards. */
|
|
246
|
+
const accept = async (message, view) => {
|
|
247
|
+
const admitted = admitAuthorityEvent(boundary, message);
|
|
248
|
+
if (admitted.disposition !== "applied")
|
|
249
|
+
return null;
|
|
250
|
+
const originToken = session.client.token;
|
|
251
|
+
if (!originToken)
|
|
252
|
+
throw new Error("session token missing");
|
|
253
|
+
/* A fresh join is a new session: the view the previous one printed
|
|
254
|
+
describes a turn that no longer exists. */
|
|
255
|
+
const previous = message.type === "sessionJoined"
|
|
256
|
+
? { ...args.state, view: null }
|
|
257
|
+
: args.state;
|
|
258
|
+
const state = persisted(session, previous, message.context, message.cursor, view, admitted.boundary);
|
|
259
|
+
await args.checkpoint(state);
|
|
260
|
+
boundary = admitted.boundary;
|
|
261
|
+
context = message.context;
|
|
262
|
+
cursor = message.cursor;
|
|
263
|
+
args = { ...args, state };
|
|
264
|
+
await session.client.acknowledge(message.context, message.cursor, originToken);
|
|
265
|
+
return state;
|
|
266
|
+
};
|
|
205
267
|
for (;;) {
|
|
206
268
|
for (const message of messages) {
|
|
207
269
|
if (message.type === "error" && !message.retryable)
|
|
208
270
|
throw new SessionRefusal(message);
|
|
209
|
-
|
|
210
|
-
|
|
271
|
+
/* Acknowledgements, pendings, errors and challenges carry no boundary. */
|
|
272
|
+
if (!("cursor" in message))
|
|
273
|
+
continue;
|
|
211
274
|
if (message.type === "sessionTerminal") {
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
275
|
+
const state = await accept(message, null);
|
|
276
|
+
/* A re-delivered terminal is this file's own last event: a resume at
|
|
277
|
+
the terminal cursor repeats it, so there is nothing to write or
|
|
278
|
+
promise again - and the sitting is still over. */
|
|
216
279
|
return {
|
|
217
280
|
kind: "terminal",
|
|
218
281
|
/* Carried out rather than dropped: `consent` takes exactly these two,
|
|
@@ -220,21 +283,21 @@ export async function openTurn(args) {
|
|
|
220
283
|
two-command tour had no settle at all. */
|
|
221
284
|
terminalNonce: message.finalState.nonce.toString(),
|
|
222
285
|
terminalCommitment: toHex0x(message.finalState.commitment),
|
|
223
|
-
state:
|
|
286
|
+
state: state ?? args.state,
|
|
224
287
|
};
|
|
225
288
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
throw new Error("session token missing");
|
|
230
|
-
await session.client.acknowledge(message.context, message.cursor, originToken);
|
|
231
|
-
cursor = message.cursor;
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
289
|
+
/* A named notice and a viewless admission are acknowledged and waited
|
|
290
|
+
on: no private view arrived, so there is nothing here to answer and
|
|
291
|
+
nothing to decide. The phase they carry is persisted first. */
|
|
234
292
|
const view = "view" in message ? message.view : null;
|
|
235
|
-
|
|
293
|
+
const state = await accept(message, view);
|
|
294
|
+
/* A re-delivery is not a turn: the view it carries was answered or
|
|
295
|
+
waited out by the process that applied it, and nothing was written
|
|
296
|
+
here to hand back. */
|
|
297
|
+
if (!state)
|
|
298
|
+
continue;
|
|
299
|
+
if (!view)
|
|
236
300
|
continue;
|
|
237
|
-
cursor = message.cursor;
|
|
238
301
|
/* A view with no legal actions is the table moving without this seat.
|
|
239
302
|
One whose deadline has passed is this seat's turn already lost, and
|
|
240
303
|
answering it would sign for a turn that is closed. And one at or before
|
|
@@ -275,11 +338,11 @@ export async function openTurn(args) {
|
|
|
275
338
|
deadlineMs: view.participantDeadlineMs.toString(),
|
|
276
339
|
msRemaining: Number(view.participantDeadlineMs) - Date.now(),
|
|
277
340
|
},
|
|
278
|
-
state
|
|
341
|
+
state,
|
|
279
342
|
};
|
|
280
343
|
}
|
|
281
344
|
if (Date.now() >= until) {
|
|
282
|
-
const waiting = persisted(session, args.state, context, cursor, null);
|
|
345
|
+
const waiting = persisted(session, args.state, context, cursor, null, boundary);
|
|
283
346
|
/* Asked only here, where the loop is about to say "waiting" anyway: a
|
|
284
347
|
seat with no chips is not waiting for a turn, it is out, and a caller
|
|
285
348
|
polling `waiting` at the default `--wait 0` would spin at full rate for
|
|
@@ -309,6 +372,18 @@ export async function submitTurn(args) {
|
|
|
309
372
|
const view = decodeView(args.state.view);
|
|
310
373
|
if (!turnIsStillOpen(view, Date.now()))
|
|
311
374
|
throw new Error("the deadline for that turn has passed; run `turn` again for the next one");
|
|
375
|
+
/* A retained gate that still holds this seat's own turn means the authority
|
|
376
|
+
has not released it: answering the stored position would be inferring on
|
|
377
|
+
a turn nobody may act in yet. `turn` refuses to hand such a view out, so
|
|
378
|
+
this only fires on a stored position the gate later reclaimed. */
|
|
379
|
+
const retained = args.state.predictionGate
|
|
380
|
+
? decodeGate(args.state.predictionGate)
|
|
381
|
+
: null;
|
|
382
|
+
if (retained &&
|
|
383
|
+
retained.phase !== "released" &&
|
|
384
|
+
equalReceiptRef(predictionGateTargetReceipt(retained), view.latestReceipt) &&
|
|
385
|
+
predictionGatePreparationOf(retained)?.actingSeat === args.state.seat)
|
|
386
|
+
throw new Error("this turn is held behind a prediction gate; run `turn` again once it releases");
|
|
312
387
|
const openWith = (clientNonce) => openSeatSession({
|
|
313
388
|
productUrl: args.state.productUrl,
|
|
314
389
|
offerId: args.state.offerId,
|
|
@@ -403,7 +478,7 @@ async function sayAtTable(fetchImpl, session, agent, agentId, say) {
|
|
|
403
478
|
if (!trimmed)
|
|
404
479
|
return false;
|
|
405
480
|
const { mintAgentHttpCapability, AGENT_HTTP_CAPABILITY_HEADER } = await import("./agentHttp.js");
|
|
406
|
-
const target = `/
|
|
481
|
+
const target = `/v1/executions/${session.executionHex}/talk`;
|
|
407
482
|
const body = new TextEncoder().encode(JSON.stringify({ say: trimmed }));
|
|
408
483
|
try {
|
|
409
484
|
const { header } = await mintAgentHttpCapability(agent, agentId, {
|
|
@@ -438,5 +513,7 @@ export function newSeatState(productUrl, offerId, seat) {
|
|
|
438
513
|
token: null,
|
|
439
514
|
cursor: { sequence: "0", witnessedReceipt: null },
|
|
440
515
|
view: null,
|
|
516
|
+
predictionGate: null,
|
|
517
|
+
awaitingGatePrefix: false,
|
|
441
518
|
};
|
|
442
519
|
}
|
package/dist/session.d.ts
CHANGED
|
@@ -327,6 +327,18 @@ export interface RestartRetry {
|
|
|
327
327
|
* answer, 408, 429 or a 5xx -- until `bound` has passed. Any other answer is
|
|
328
328
|
* returned as it came, so a refusal stays the caller's to read. */
|
|
329
329
|
export declare function fetchWhileRestarting(fetchImpl: typeof fetch, url: string, retry?: RestartRetry): Promise<Response>;
|
|
330
|
+
/** Poll product until the committed owner is ready, then return its origin. */
|
|
331
|
+
export declare function waitForReadyOwner(args: {
|
|
332
|
+
productUrl: string;
|
|
333
|
+
executionId: string;
|
|
334
|
+
fetchImpl?: typeof fetch;
|
|
335
|
+
timeoutMs?: number;
|
|
336
|
+
pauseMs?: number;
|
|
337
|
+
sleep?: (ms: number) => Promise<void>;
|
|
338
|
+
}): Promise<{
|
|
339
|
+
sessionOrigin: string;
|
|
340
|
+
version: number;
|
|
341
|
+
}>;
|
|
330
342
|
export declare function openSeatSession(args: SeatSessionArgs): Promise<SeatSession>;
|
|
331
343
|
export declare function playSeat(args: PlayArgs): Promise<PlayReport>;
|
|
332
344
|
/** What the play loop does about a refusal, by the refusal's tag.
|