@dopamint-fun/open-sdk 0.1.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.
@@ -0,0 +1,519 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { toHex0x } from "./bytes.js";
3
+ import { acceptAndAwaitAdmission, readOffer } from "./offer.js";
4
+ import { decodeLegalActions, decodeParticipantView, encodeAction, } from "./texas.js";
5
+ import { admitAuthorityEvent, equalReceiptRef, freshSessionBoundary, predictionGatePreparationOf, predictionGateTargetReceipt, } from "./sessionCodec.js";
6
+ import { agentReadCapability, openSeatSession, readPublicTable, readTableTalk, turnIsStillOpen, SessionRefusal, afterRefusal, readSittingStatus, } from "./session.js";
7
+ import { decodeContext, decodeCursor, decodeGate, decodeView, encodeContext, encodeCursor, encodeGate, encodeView, } from "./seatState.js";
8
+ /** Whether `view` is a turn this seat has already answered.
9
+ *
10
+ * `act` does not move the cursor, so the next `turn` resumes from before the
11
+ * answered position and is handed it again. A nonce at or before the one
12
+ * `act` recorded is that replay, not a new turn. */
13
+ export function turnAlreadyAnswered(view, state) {
14
+ return (state.answeredNonce != null && view.state.nonce <= BigInt(state.answeredNonce));
15
+ }
16
+ /** The agent this seat plays as, found without asking the caller for it.
17
+ *
18
+ * In order: the id the caller passed, the one an earlier `turn` stored, and
19
+ * the seat's own row on the offer, which names the agent and the key it sits
20
+ * with. The offer is the read that works for every agent, claimed or not; the
21
+ * roster lists an agent under the wallet that claimed it, which a key whose
22
+ * agent registered itself never matches. Null where the seat is not this
23
+ * key's, so the caller can say that rather than guess. */
24
+ export async function seatAgentId(state, agent, given) {
25
+ if (given)
26
+ return given;
27
+ if (state.agentId)
28
+ return state.agentId;
29
+ const offer = await readOffer(state.productUrl, state.offerId);
30
+ const row = offer.seats.find((seat) => seat.seat === state.seat);
31
+ if (!row?.agentId || !row.agentPublicKey)
32
+ return null;
33
+ const own = toHex0x(agent.publicKey).toLowerCase();
34
+ const seated = `0x${row.agentPublicKey.replace(/^0x/i, "")}`.toLowerCase();
35
+ return own === seated ? row.agentId : null;
36
+ }
37
+ /** What a fresh join costs: a nonce this seat has not opened a session with.
38
+ *
39
+ * The session id is `blake2b256(execution ‖ seat ‖ client_nonce)`, so a nonce
40
+ * identifies a session rather than a client. Reusing the stored one to open a
41
+ * NEW session therefore asks the authority for an id it may already hold, and
42
+ * that refusal is permanent for the life of the execution: the collision
43
+ * check in the authority's `join` sits before the code that would supersede
44
+ * the old session, so the id can never be freed by asking again. It is
45
+ * answered with `ServiceUnavailable`, which reads as "busy, try later", and a
46
+ * seat that believed that retried into the same wall until its table timed
47
+ * out waiting for it.
48
+ *
49
+ * So: resume addresses the session the stored nonce names, and every join
50
+ * mints a new one. The first byte carries the seat, as `newSeatState` does it.
51
+ */
52
+ function freshNonce(seat) {
53
+ const nonce = new Uint8Array(randomBytes(32));
54
+ nonce[0] = seat & 0xff;
55
+ return nonce;
56
+ }
57
+ /** Attach to the session: resume where the stored cursor left off, or join if
58
+ * this seat has never opened one.
59
+ *
60
+ * Returns the session it ended up attached to, which is not always the one it
61
+ * was handed: a join needs a nonce of its own (see `freshNonce`), and the
62
+ * nonce is fixed when the session is opened, so joining means reopening. The
63
+ * state it returns carries that nonce, and the caller persists it -- a nonce
64
+ * that opened a session and was not written down is a session nothing can
65
+ * resume.
66
+ *
67
+ * Exported for its test rather than for callers: `reopen` is the seam the
68
+ * nonce rule lives on, and the rule is not observable from `turn`'s output --
69
+ * a seat that reuses a nonce looks identical until the join it cannot make. */
70
+ export async function attach(session, state, reopen) {
71
+ const stored = state.context ? decodeContext(state.context) : null;
72
+ const cursor = decodeCursor(state.cursor);
73
+ /* Hand the stored token back before resuming: the authority authenticates
74
+ the resume itself with it, and only sometimes issues a fresh one. */
75
+ if (state.token)
76
+ session.client.token = state.token;
77
+ const joinFresh = async () => {
78
+ const nonce = freshNonce(state.seat);
79
+ const opened = await reopen(nonce);
80
+ const joined = await opened.client.join();
81
+ return {
82
+ session: opened,
83
+ /* The token and context of the session just opened replace the old
84
+ ones wholesale. Keeping either alongside a new nonce would leave the
85
+ state describing two different sessions. */
86
+ state: {
87
+ ...state,
88
+ clientNonce: toHex0x(nonce),
89
+ context: joined.context ? encodeContext(joined.context) : null,
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,
97
+ },
98
+ context: joined.context ?? null,
99
+ cursor: { sequence: 0n, witnessedReceipt: null },
100
+ messages: joined.messages,
101
+ };
102
+ };
103
+ if (!stored)
104
+ return joinFresh();
105
+ try {
106
+ await session.client.resume(stored, cursor);
107
+ return { session, state, context: stored, cursor, messages: [] };
108
+ }
109
+ catch (error) {
110
+ /* A session the authority no longer knows, or a cursor it will not accept,
111
+ is a rejoin rather than a failure -- the seat is bound to the agent's
112
+ key, not to the process that opened it. A superseded session is not:
113
+ somebody else is holding this seat and two clients on one key is the one
114
+ thing that must not be papered over. */
115
+ if (error instanceof SessionRefusal && afterRefusal(error) === "rejoin")
116
+ return joinFresh();
117
+ throw error;
118
+ }
119
+ }
120
+ const holeFromView = (view) => {
121
+ try {
122
+ const seen = decodeParticipantView(view.participantView);
123
+ return seen.holeCards
124
+ ? [seen.holeCards[0].label, seen.holeCards[1].label]
125
+ : null;
126
+ }
127
+ catch {
128
+ /* A view this build cannot decode is not a reason to refuse the turn: the
129
+ legal actions still say what may be done, and a seat that folds because
130
+ its own cards would not parse has lost more than a decoder bug. */
131
+ return null;
132
+ }
133
+ };
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) {
139
+ return {
140
+ ...state,
141
+ context: context ? encodeContext(context) : state.context,
142
+ token: session.client.token ?? state.token,
143
+ cursor: encodeCursor(cursor),
144
+ view: view ? encodeView(view) : state.view,
145
+ predictionGate: boundary.predictionGate
146
+ ? encodeGate(boundary.predictionGate)
147
+ : null,
148
+ awaitingGatePrefix: boundary.awaitingGatePrefix,
149
+ };
150
+ }
151
+ /** What `turn` answers when it could not open a session.
152
+ *
153
+ * Its own function because the choice is the whole point of the outcome and
154
+ * is not observable from `turn`'s happy path: the authority releases a
155
+ * sitting's session surface once the sitting ends, so the refusal a seat
156
+ * meets after the last hand is the ordinary shape of "finished". Reported as
157
+ * an error it is indistinguishable from a seat that is genuinely stuck, and
158
+ * those want opposite responses -- stop, or keep trying.
159
+ *
160
+ * `unknown` never becomes `terminal`. Telling an agent its match is over when
161
+ * the product merely could not answer would have it walk away from a table it
162
+ * still has chips on. */
163
+ export async function attachFailureOutcome(error, sitting, state) {
164
+ if (sitting.state === "over")
165
+ return { kind: "terminal", state };
166
+ return {
167
+ kind: "unattachable",
168
+ reason: error instanceof Error ? error.message : String(error),
169
+ sitting: sitting.state,
170
+ state,
171
+ };
172
+ }
173
+ /** Read this seat's position, or say why there is nothing to answer.
174
+ *
175
+ * Polls for at most `waitMs`, because a command that blocks until a table
176
+ * moves is a command an agent cannot schedule around. Returning `waiting` is
177
+ * an answer, not a failure. */
178
+ export async function openTurn(args) {
179
+ const fetchImpl = args.fetchImpl ?? fetch;
180
+ /* The seat has to be accepted before there is anything to see, and until
181
+ every seat accepts there is no admission to build a session from. Doing it
182
+ here rather than asking for a separate `accept` command is the difference
183
+ between an agent that reads one instruction and an agent that has to know
184
+ the offer lifecycle: the first `turn` opens the seat, and every `turn`
185
+ after it finds an admission already there and returns immediately.
186
+
187
+ Found by running this against a real authority, which answered "offer is
188
+ not admitted" -- the loop path accepted on the agent's behalf and this
189
+ path had inherited none of that. */
190
+ if (!args.state.context)
191
+ await acceptAndAwaitAdmission(args.state.productUrl, args.state.offerId, args.agentId, args.agent, { timeoutMs: Math.max(30_000, args.waitMs ?? 0) });
192
+ const openWith = (clientNonce) => openSeatSession({
193
+ productUrl: args.state.productUrl,
194
+ offerId: args.state.offerId,
195
+ seat: args.state.seat,
196
+ agent: args.agent,
197
+ agentId: args.agentId,
198
+ clientNonce,
199
+ fetchImpl,
200
+ });
201
+ let session = await openWith(hexBytes(args.state.clientNonce));
202
+ let attached;
203
+ try {
204
+ attached = await attach(session, args.state, openWith);
205
+ }
206
+ catch (error) {
207
+ /* A seat that cannot attach is asking the wrong question if its match is
208
+ over: the authority releases the session surface once a sitting ends, so
209
+ the refusal an agent meets then is the ordinary shape of "finished", not
210
+ a fault. Ask the product which it is before saying anything. */
211
+ return attachFailureOutcome(error, await readSittingStatus(fetchImpl, session.product, session.executionHex), args.state);
212
+ }
213
+ /* `session` and the state are rebound: a join opens its own session under a
214
+ new nonce, and everything below -- the poll, the acknowledge, what gets
215
+ written back -- has to be about that one. */
216
+ session = attached.session;
217
+ args = { ...args, state: attached.state };
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
+ };
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
+ };
267
+ for (;;) {
268
+ for (const message of messages) {
269
+ if (message.type === "error" && !message.retryable)
270
+ throw new SessionRefusal(message);
271
+ /* Acknowledgements, pendings, errors and challenges carry no boundary. */
272
+ if (!("cursor" in message))
273
+ continue;
274
+ if (message.type === "sessionTerminal") {
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. */
279
+ return {
280
+ kind: "terminal",
281
+ /* Carried out rather than dropped: `consent` takes exactly these two,
282
+ and the hand-driven loop had no other way to learn them, so the
283
+ two-command tour had no settle at all. */
284
+ terminalNonce: message.finalState.nonce.toString(),
285
+ terminalCommitment: toHex0x(message.finalState.commitment),
286
+ state: state ?? args.state,
287
+ };
288
+ }
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. */
292
+ const view = "view" in message ? message.view : null;
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)
300
+ continue;
301
+ /* A view with no legal actions is the table moving without this seat.
302
+ One whose deadline has passed is this seat's turn already lost, and
303
+ answering it would sign for a turn that is closed. And one at or before
304
+ the position `act` last answered is a turn this seat has already
305
+ played: the resume replays it, and printing it again as `yours` is how
306
+ the two-command loop answered one turn dozens of times and missed the
307
+ next. */
308
+ if (view.legalActions.length === 0)
309
+ continue;
310
+ if (!turnIsStillOpen(view, Date.now()))
311
+ continue;
312
+ if (turnAlreadyAnswered(view, args.state))
313
+ continue;
314
+ const table = await readPublicTable(fetchImpl, session.product, session.executionHex,
315
+ /* A naming cache of its own: this door is one turn per process, so
316
+ there is no second read for a longer-lived one to save. */
317
+ new Map(),
318
+ /* Signed as this seat, so a private room shows it the table it is
319
+ playing at rather than refusing it as a stranger. */
320
+ agentReadCapability(args.agent, args.agentId)).catch(() => null);
321
+ const tableTalk = table === null
322
+ ? []
323
+ : await readTableTalk(fetchImpl, session.product, session.executionHex, table.handNumber).catch(() => []);
324
+ const mine = table?.seats.find((row) => row.seat === args.state.seat);
325
+ return {
326
+ kind: "your-turn",
327
+ position: {
328
+ seat: args.state.seat,
329
+ executionId: session.executionId,
330
+ hole: holeFromView(view),
331
+ legal: decodeLegalActions(view.legalActions),
332
+ toCall: table && mine
333
+ ? Math.max(0, table.currentWager - mine.streetContribution)
334
+ : null,
335
+ table,
336
+ seats: table?.seats ?? [],
337
+ tableTalk,
338
+ deadlineMs: view.participantDeadlineMs.toString(),
339
+ msRemaining: Number(view.participantDeadlineMs) - Date.now(),
340
+ },
341
+ state,
342
+ };
343
+ }
344
+ if (Date.now() >= until) {
345
+ const waiting = persisted(session, args.state, context, cursor, null, boundary);
346
+ /* Asked only here, where the loop is about to say "waiting" anyway: a
347
+ seat with no chips is not waiting for a turn, it is out, and a caller
348
+ polling `waiting` at the default `--wait 0` would spin at full rate for
349
+ the rest of somebody else's sitting. The stack is on the public record
350
+ rather than the seat's own view, so it costs the one read this branch
351
+ can afford. */
352
+ const busted = await seatIsBusted(fetchImpl, session, args.state.seat);
353
+ return busted
354
+ ? { kind: "eliminated", state: waiting }
355
+ : { kind: "waiting", state: waiting };
356
+ }
357
+ const batch = await session.client.pollEvents();
358
+ if (batch.context)
359
+ context = batch.context;
360
+ messages = batch.messages;
361
+ }
362
+ }
363
+ const hexBytes = (value) => {
364
+ const bare = value.replace(/^0x/i, "");
365
+ return Uint8Array.from(bare.match(/.{2}/g).map((pair) => Number.parseInt(pair, 16)));
366
+ };
367
+ /** Send one move for the position `openTurn` last returned. */
368
+ export async function submitTurn(args) {
369
+ const fetchImpl = args.fetchImpl ?? fetch;
370
+ if (!args.state.view)
371
+ throw new Error("no turn is open; run `turn` first");
372
+ const view = decodeView(args.state.view);
373
+ if (!turnIsStillOpen(view, Date.now()))
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");
387
+ const openWith = (clientNonce) => openSeatSession({
388
+ productUrl: args.state.productUrl,
389
+ offerId: args.state.offerId,
390
+ seat: args.state.seat,
391
+ agent: args.agent,
392
+ agentId: args.agentId,
393
+ clientNonce,
394
+ fetchImpl,
395
+ });
396
+ let session = await openWith(hexBytes(args.state.clientNonce));
397
+ let attached;
398
+ try {
399
+ attached = await attach(session, args.state, openWith);
400
+ }
401
+ catch (error) {
402
+ /* `act` answers with a move or throws, so it cannot carry an outcome the
403
+ way `turn` does. What it can do is say which of the two this is, rather
404
+ than handing on a status code. */
405
+ const sitting = await readSittingStatus(fetchImpl, session.product, session.executionHex);
406
+ const raw = error instanceof Error ? error.message : String(error);
407
+ throw new Error(sitting.state === "over"
408
+ ? `this sitting is ${sitting.detail}; there is no turn left to answer`
409
+ : `this seat cannot attach and the sitting is ${sitting.state === "live" ? "still running" : "of unknown state"}: ${raw}`);
410
+ }
411
+ session = attached.session;
412
+ args = { ...args, state: attached.state };
413
+ const { context } = attached;
414
+ if (!context)
415
+ throw new Error("the session answered with no context");
416
+ const prepared = await session.client.prepareAction(context, view, encodeAction(args.action));
417
+ const submitted = await raceSubmit(session, prepared);
418
+ if (!submitted.ok)
419
+ throw new Error(`action refused (${submitted.status}): ${await submitted.text()}`);
420
+ const said = await sayAtTable(fetchImpl, session, args.agent, args.agentId, args.say);
421
+ /* The cursor moves on the next read, not here: this process is about to
422
+ exit, and a cursor claiming to have seen the commit it has not read would
423
+ resume past it. */
424
+ return {
425
+ committed: true,
426
+ said,
427
+ state: {
428
+ ...args.state,
429
+ token: session.client.token ?? args.state.token,
430
+ view: null,
431
+ answeredNonce: view.state.nonce.toString(),
432
+ },
433
+ };
434
+ }
435
+ /** Submit, answering any seat-authorization challenge that arrives while the
436
+ * submit is in flight. The challenge is not optional: an action whose
437
+ * challenge went unanswered is an action the authority never authorised. */
438
+ /** Whether the public record shows this seat holding no chips.
439
+ *
440
+ * Unknown counts as not busted: a read that did not answer is a reason to keep
441
+ * waiting, not a reason to tell a seat it is out of a sitting it may still be
442
+ * in.
443
+ */
444
+ async function seatIsBusted(fetchImpl, session, seat) {
445
+ const table = await readPublicTable(fetchImpl, session.product, session.executionHex).catch(() => null);
446
+ const mine = table?.seats.find((row) => row.seat === seat);
447
+ return mine !== undefined && mine.stack === 0;
448
+ }
449
+ async function raceSubmit(session, prepared) {
450
+ const submitP = session.client.startSubmit(prepared.wire);
451
+ const abort = new AbortController();
452
+ let pollP = session.client.pollEvents(abort.signal);
453
+ for (;;) {
454
+ const raced = await Promise.race([
455
+ submitP.then((response) => ({ kind: "submit", response })),
456
+ pollP.then((batch) => ({ kind: "events", batch })),
457
+ ]);
458
+ if (raced.kind === "submit") {
459
+ abort.abort();
460
+ const leftover = await pollP.catch(() => null);
461
+ for (const nested of leftover?.messages ?? [])
462
+ if (nested.type === "seatAuthorization")
463
+ await session.client
464
+ .answerSeatAuth(nested.challenge, session.coordinatorKey, session.timeAuthorityKey, prepared.pending)
465
+ .catch(() => undefined);
466
+ return raced.response;
467
+ }
468
+ for (const nested of raced.batch.messages)
469
+ if (nested.type === "seatAuthorization")
470
+ await session.client
471
+ .answerSeatAuth(nested.challenge, session.coordinatorKey, session.timeAuthorityKey, prepared.pending)
472
+ .catch(() => undefined);
473
+ pollP = session.client.pollEvents(abort.signal);
474
+ }
475
+ }
476
+ async function sayAtTable(fetchImpl, session, agent, agentId, say) {
477
+ const trimmed = say.trim();
478
+ if (!trimmed)
479
+ return false;
480
+ const { mintAgentHttpCapability, AGENT_HTTP_CAPABILITY_HEADER } = await import("./agentHttp.js");
481
+ const target = `/open/v1/executions/${session.executionHex}/talk`;
482
+ const body = new TextEncoder().encode(JSON.stringify({ say: trimmed }));
483
+ try {
484
+ const { header } = await mintAgentHttpCapability(agent, agentId, {
485
+ method: "POST",
486
+ requestTarget: target,
487
+ body,
488
+ });
489
+ const response = await fetchImpl(`${session.product}${target}`, {
490
+ method: "POST",
491
+ headers: {
492
+ "content-type": "application/json",
493
+ [AGENT_HTTP_CAPABILITY_HEADER]: header,
494
+ },
495
+ body,
496
+ });
497
+ return response.ok;
498
+ }
499
+ catch {
500
+ return false;
501
+ }
502
+ }
503
+ /** A fresh seat state for an offer this agent has been admitted to. */
504
+ export function newSeatState(productUrl, offerId, seat) {
505
+ const nonce = new Uint8Array(randomBytes(32));
506
+ nonce[0] = seat & 0xff;
507
+ return {
508
+ productUrl,
509
+ offerId,
510
+ seat,
511
+ clientNonce: toHex0x(nonce),
512
+ context: null,
513
+ token: null,
514
+ cursor: { sequence: "0", witnessedReceipt: null },
515
+ view: null,
516
+ predictionGate: null,
517
+ awaitingGatePrefix: false,
518
+ };
519
+ }