pokertools-arena 0.16.2 → 0.17.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.
@@ -548,7 +548,7 @@ label input, label select { margin-top: 5px; font-weight: 450; }
548
548
  .split-footer { justify-content: space-between; }
549
549
  .seat-footer-actions, .replay-share-actions { display: flex; gap: 7px; }
550
550
  .seat-editor-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
551
- .seat-model-field { grid-column: 1 / -1; }
551
+ .seat-model-field, .seat-effort-field { grid-column: 1 / -1; }
552
552
  .model-input-wrap { margin-top: 6px; display: grid; grid-template-columns: minmax(0, 1fr) 38px; gap: 8px; }
553
553
  .model-input-wrap input { margin: 0; }
554
554
  .field-icon-button, .dialog-body .remove-connection { width: 38px; height: 38px; min-height: 38px; border-radius: 8px; }
@@ -1041,6 +1041,11 @@ label input, label select { margin-top: 5px; font-weight: 450; }
1041
1041
  <span class="checkbox-field"><input id="seatCaptureReasoning" type="checkbox" /> Capture raw model reasoning</span>
1042
1042
  <span class="field-help seat-reasoning-help hidden">This model is not a reasoning model by default. Capturing its reasoning can be far slower (Gemma measured ~2s → 27–48s per decision) and is outside the standard benchmark track. Raise the action clock before using it.</span>
1043
1043
  </label>
1044
+ <label class="seat-effort-field">
1045
+ <span class="field-label-row">Reasoning effort <button class="info-button inline-info" type="button" aria-label="About reasoning effort" data-tip="How much hidden thinking the model may spend. Higher effort is smarter but slower; a slow reasoner can exhaust the action clock and be forced to fold. Options come from the OpenRouter model catalogue, and an unset value keeps the arena's fixed reasoning cap.">i</button></span>
1046
+ <select id="seatReasoningEffort"></select>
1047
+ <span id="seatReasoningEffortHelp" class="field-help"></span>
1048
+ </label>
1044
1049
  </div>
1045
1050
  </div>
1046
1051
  <div id="seatError" class="setup-error hidden"></div>
@@ -115591,8 +115596,15 @@ async function fetchStreamingJson(url, options, { maxRetries = 1, onDelta = null
115591
115596
  throw new ArenaRequestError(message, { status: response.status, category: requestErrorCategory(response.status), payload, incidents });
115592
115597
  }
115593
115598
  }
115599
+ function requestErrorDetail(err) {
115600
+ const parts = [summarizeError(err)];
115601
+ const nested = err?.payload?.error?.metadata?.raw ?? err?.payload?.metadata?.raw;
115602
+ if (typeof nested === "string") parts.push(nested);
115603
+ else if (nested) parts.push(String(JSON.stringify(nested)));
115604
+ return parts.join(" ");
115605
+ }
115594
115606
  function isUnsupportedToolChoiceError(err) {
115595
- const message = summarizeError(err);
115607
+ const message = requestErrorDetail(err);
115596
115608
  return [400, 404, 422].includes(Number(err?.status)) && /tool[_ -]?choice|tool call|tools?.*(?:unsupported|support)|no endpoints?.*support/i.test(message);
115597
115609
  }
115598
115610
  function mapGet(mapish, key, fallback = 0) {
@@ -115618,13 +115630,23 @@ function jsonSafe(value) {
115618
115630
  }
115619
115631
  var protocolCapabilityCache = /* @__PURE__ */ new Map();
115620
115632
  var modelSupportedParameters = /* @__PURE__ */ new Map();
115633
+ var modelReasoningCapabilities = /* @__PURE__ */ new Map();
115621
115634
  var DEFAULT_TEMPERATURE = 0.3;
115622
115635
  function registerModelCapabilities(models = []) {
115623
115636
  for (const entry of models ?? []) {
115624
115637
  const id2 = typeof entry === "string" ? entry : entry?.id;
115638
+ if (!id2) continue;
115625
115639
  const parameters = typeof entry === "object" && entry ? entry.supported_parameters : null;
115626
- if (!id2 || !Array.isArray(parameters)) continue;
115627
- modelSupportedParameters.set(String(id2), new Set(parameters.map(String)));
115640
+ if (Array.isArray(parameters)) modelSupportedParameters.set(String(id2), new Set(parameters.map(String)));
115641
+ const reasoning = typeof entry === "object" && entry ? entry.reasoning : null;
115642
+ if (!reasoning || typeof reasoning !== "object") continue;
115643
+ const efforts = Array.isArray(reasoning.supported_efforts) ? reasoning.supported_efforts.map((value) => String(value).trim().toLowerCase()).filter(Boolean) : [];
115644
+ const declaredDefault = String(reasoning.default_effort || "").trim().toLowerCase();
115645
+ modelReasoningCapabilities.set(String(id2), {
115646
+ mandatory: reasoning.mandatory === true,
115647
+ supportedEfforts: efforts,
115648
+ defaultEffort: efforts.includes(declaredDefault) ? declaredDefault : null
115649
+ });
115628
115650
  }
115629
115651
  }
115630
115652
  function modelSupportsParameter(model, parameter) {
@@ -115632,6 +115654,34 @@ function modelSupportsParameter(model, parameter) {
115632
115654
  if (!supported) return null;
115633
115655
  return supported.has(String(parameter));
115634
115656
  }
115657
+ function reasoningCapabilities(model) {
115658
+ return modelReasoningCapabilities.get(String(model || "")) ?? null;
115659
+ }
115660
+ function reasoningEffortsFor(model) {
115661
+ return reasoningCapabilities(model)?.supportedEfforts ?? [];
115662
+ }
115663
+ function defaultReasoningEffort(model) {
115664
+ return reasoningCapabilities(model)?.defaultEffort ?? null;
115665
+ }
115666
+ function requiresReasoning(model) {
115667
+ return reasoningCapabilities(model)?.mandatory === true;
115668
+ }
115669
+ function resolveReasoningEffort(model, requested) {
115670
+ const value = String(requested || "").trim().toLowerCase();
115671
+ if (!value) return null;
115672
+ const capabilities = reasoningCapabilities(model);
115673
+ if (!capabilities) return value;
115674
+ return capabilities.supportedEfforts.includes(value) ? value : null;
115675
+ }
115676
+ function reasoningOverrideFor(agent) {
115677
+ const effort = resolveReasoningEffort(agent?.model, agent?.reasoningEffort);
115678
+ return effort ? { effort } : { max_tokens: 256, exclude: false };
115679
+ }
115680
+ function truncationReasoningOverride(model) {
115681
+ const capabilities = reasoningCapabilities(model);
115682
+ if (!capabilities) return { effort: "low" };
115683
+ return capabilities.supportedEfforts.includes("low") ? { effort: "low" } : { max_tokens: 4096, exclude: false };
115684
+ }
115635
115685
  function resolveTemperature(model, requested) {
115636
115686
  if (modelSupportsParameter(model, "temperature") === false) return void 0;
115637
115687
  return Number.isFinite(Number(requested)) ? Number(requested) : DEFAULT_TEMPERATURE;
@@ -115670,7 +115720,7 @@ function isReasoningModel(model) {
115670
115720
  return /(?:^|[\/._:-])(qwen3|qwq|deepseek-[rv]\d|magistral|glm-(?:\d+[a-z]*|latest|flash-latest)|gpt-oss|nemotron|reason(?:ing)?|thinking|o1|o3|o4)(?:$|[\/._:-])/.test(id2);
115671
115721
  }
115672
115722
  function wantsReasoning(agent) {
115673
- return isReasoningModel(agent?.model) || agent?.captureReasoning === true;
115723
+ return isReasoningModel(agent?.model) || agent?.captureReasoning === true || reasoningCapabilities(agent?.model)?.mandatory === true || Boolean(resolveReasoningEffort(agent?.model, agent?.reasoningEffort));
115674
115724
  }
115675
115725
  function effectiveProtocol(agent, connection) {
115676
115726
  if (isOpenRouterConnection(connection) && isJevModel(agent?.model)) return "jev_decisions";
@@ -115776,6 +115826,13 @@ function playerCards(player) {
115776
115826
  function playerStack(player) {
115777
115827
  return asNumber(player?.stack ?? player?.chips ?? 0);
115778
115828
  }
115829
+ function playersStillInTournament(players, eliminatedPlayerIds = []) {
115830
+ const eliminated = new Set(eliminatedPlayerIds ?? []);
115831
+ return (players ?? []).filter((player) => player?.id && !eliminated.has(player.id));
115832
+ }
115833
+ function playersWithChips(players) {
115834
+ return (players ?? []).filter((player) => player && playerStack(player) > 0);
115835
+ }
115779
115836
  function currentBet(state, seat) {
115780
115837
  return asNumber(mapGet(state?.currentBets, seat, state?.players?.[seat]?.bet ?? 0));
115781
115838
  }
@@ -116023,7 +116080,7 @@ function assertDecisionState(state) {
116023
116080
  }
116024
116081
  }
116025
116082
  const expectedRemaining = 1 + state.opponents.length;
116026
- if (Number(state.tournament?.playersRemaining) !== expectedRemaining) throw new Error(`playersRemaining mismatch: expected ${expectedRemaining}, got ${state.tournament?.playersRemaining}`);
116083
+ if (Number(state.tournament?.playersRemaining) !== expectedRemaining) throw new Error(`playersRemaining mismatch: expected ${expectedRemaining} (hero plus ${state.opponents.length} non-eliminated opponents), got ${state.tournament?.playersRemaining}`);
116027
116084
  return state;
116028
116085
  }
116029
116086
  function extractTextContent(message) {
@@ -116446,7 +116503,7 @@ Respond with one of: ${criteriaKeys.map((k) => `${k} = ${criteria[k]}`).join(";
116446
116503
  }
116447
116504
  if (isOpenRouter) {
116448
116505
  body.provider = agent.provider ? { only: [agent.provider], allow_fallbacks: false, require_parameters: true } : { allow_fallbacks: true, require_parameters: true };
116449
- if (wantsReasoning(agent)) body.reasoning = reasoning ?? { max_tokens: 256, exclude: false };
116506
+ if (wantsReasoning(agent)) body.reasoning = reasoning ?? reasoningOverrideFor(agent);
116450
116507
  }
116451
116508
  if (stream) {
116452
116509
  body.stream = true;
@@ -116509,7 +116566,7 @@ async function decideOpenAICompatible({ agent, connection, state, legalActions,
116509
116566
  result = await execute(actualProtocol);
116510
116567
  } else if (err?.truncated) {
116511
116568
  truncationRetry = true;
116512
- result = await execute(actualProtocol, { maxTokens: 4096, reasoning: { effort: "low" } });
116569
+ result = await execute(actualProtocol, { maxTokens: 4096, reasoning: truncationReasoningOverride(agent.model) });
116513
116570
  } else throw err;
116514
116571
  }
116515
116572
  const normalized = normalizeDecisionObject(result.obj, legalActions, decisionId);
@@ -116685,7 +116742,7 @@ async function requestChatStage({ agent, connection, schema, toolName, systemPro
116685
116742
  }
116686
116743
  if (isOpenRouter) {
116687
116744
  body.provider = agent.provider ? { only: [agent.provider], allow_fallbacks: false, require_parameters: true } : { allow_fallbacks: true, require_parameters: true };
116688
- if (wantsReasoning(agent)) body.reasoning = override.reasoning ?? { max_tokens: 256, exclude: false };
116745
+ if (wantsReasoning(agent)) body.reasoning = override.reasoning ?? reasoningOverrideFor(agent);
116689
116746
  }
116690
116747
  if (onDelta) {
116691
116748
  body.stream = true;
@@ -116729,7 +116786,7 @@ async function requestChatStage({ agent, connection, schema, toolName, systemPro
116729
116786
  return { ...result, protocolFallback: "tool\u2192json_schema" };
116730
116787
  }
116731
116788
  if (err?.truncated) {
116732
- const result = await execute(actual, { maxTokens: 4096, reasoning: { effort: "low" } });
116789
+ const result = await execute(actual, { maxTokens: 4096, reasoning: truncationReasoningOverride(agent.model) });
116733
116790
  return { ...result, protocolFallback: "truncated\u2192retry" };
116734
116791
  }
116735
116792
  throw err;
@@ -117002,6 +117059,8 @@ var els = {
117002
117059
  seatProtocol: $("#seatProtocol"),
117003
117060
  seatProvider: $("#seatProvider"),
117004
117061
  seatCaptureReasoning: $("#seatCaptureReasoning"),
117062
+ seatReasoningEffort: $("#seatReasoningEffort"),
117063
+ seatReasoningEffortHelp: $("#seatReasoningEffortHelp"),
117005
117064
  seatError: $("#seatError"),
117006
117065
  removeSeatBtn: $("#removeSeatBtn"),
117007
117066
  cancelSeatBtn: $("#cancelSeatBtn"),
@@ -117052,6 +117111,7 @@ var WINNER_REVIEW_MS = 2e3;
117052
117111
  var seatAssignments = Array(MAX_LOBBY_SEATS).fill(null);
117053
117112
  var editingSeatIndex = null;
117054
117113
  var seatNameAuto = false;
117114
+ var seatReasoningEffortDraft = "";
117055
117115
  var lobbyVisible = true;
117056
117116
  var pendingAutostart = false;
117057
117117
  var arenaMaxDecisions = 0;
@@ -117357,7 +117417,8 @@ function injectedEnvironmentConfig() {
117357
117417
  model: String(player.model || ""),
117358
117418
  protocol: String(player.protocol || "tool"),
117359
117419
  provider: String(player.provider || ""),
117360
- captureReasoning: Boolean(player.captureReasoning)
117420
+ captureReasoning: Boolean(player.captureReasoning),
117421
+ reasoningEffort: String(player.reasoningEffort || "").trim().toLowerCase()
117361
117422
  }))
117362
117423
  };
117363
117424
  }
@@ -117674,7 +117735,7 @@ function normalizeConfig(input = {}) {
117674
117735
  if (!connIds.has(raw.connectionId)) throw new Error(`Connection missing for ${name}`);
117675
117736
  const model = String(raw.model || "").trim();
117676
117737
  if (!model) throw new Error(`Model missing for ${name}`);
117677
- return { id: `player-${lobbySeat + 1}`, seat: index, lobbySeat, name, connectionId: raw.connectionId, model, protocol: raw.protocol || "tool", provider: String(raw.provider || "").trim(), captureReasoning: raw.captureReasoning === true, temperature: clamp(Number(raw.temperature) || 0.3, 0, 2) };
117738
+ return { id: `player-${lobbySeat + 1}`, seat: index, lobbySeat, name, connectionId: raw.connectionId, model, protocol: raw.protocol || "tool", provider: String(raw.provider || "").trim(), captureReasoning: raw.captureReasoning === true, reasoningEffort: String(raw.reasoningEffort || "").trim().toLowerCase(), temperature: clamp(Number(raw.temperature) || 0.3, 0, 2) };
117678
117739
  })
117679
117740
  };
117680
117741
  }
@@ -117750,7 +117811,7 @@ var TournamentDirector = class {
117750
117811
  for (const player of this.config.players) {
117751
117812
  this.engine.sit(player.seat, player.id, player.name, this.config.startingStack);
117752
117813
  this.timeBanks[player.id] = this.config.timeBankSeconds * 1e3;
117753
- this.stats[player.id] = { decisions: 0, invalid: 0, modelErrors: 0, providerErrors: 0, rateLimits: 0, timeouts: 0, autoFallbacks: 0, protocolFallbacks: 0, retries: 0, totalLatencyMs: 0, lastAction: null, lastActionType: null, lastActionAmount: null, lastReason: "" };
117814
+ this.stats[player.id] = { decisions: 0, invalid: 0, modelErrors: 0, providerErrors: 0, rateLimits: 0, timeouts: 0, autoFallbacks: 0, contextErrors: 0, protocolFallbacks: 0, retries: 0, totalLatencyMs: 0, lastAction: null, lastActionType: null, lastActionAmount: null, lastReason: "" };
117754
117815
  }
117755
117816
  this.logEvent("TOURNAMENT_START", { config: this.publicConfig() });
117756
117817
  this.broadcast();
@@ -117794,8 +117855,15 @@ var TournamentDirector = class {
117794
117855
  while (this.status === "PAUSED") await new Promise((resolve) => this.pauseResolvers.push(resolve));
117795
117856
  if (this.status === "STOPPED") throw new Error("Tournament stopped");
117796
117857
  }
117858
+ // Decision-context/table count: seated players that are not eliminated. Must
117859
+ // equal `1 + non-eliminated opponents` in serializeForAgent, so it must NOT be
117860
+ // derived from stacks — an all-in player has stack 0 mid-hand and is still in.
117797
117861
  playersRemaining() {
117798
- return (this.engine?.state?.players ?? []).map((p, seat) => ({ p, seat })).filter(({ p }) => p && playerStack(p) > 0);
117862
+ return playersStillInTournament(this.engine?.state?.players, this.eliminations.map((e) => e.playerId));
117863
+ }
117864
+ // Tournament-end / place accounting: seats that can still contest chips.
117865
+ playersWithChips() {
117866
+ return playersWithChips(this.engine?.state?.players);
117799
117867
  }
117800
117868
  tournamentMeta() {
117801
117869
  const startingPlayers = this.config?.players.length ?? 0;
@@ -117862,9 +117930,9 @@ var TournamentDirector = class {
117862
117930
  async run() {
117863
117931
  while (["RUNNING", "PAUSED"].includes(this.status)) {
117864
117932
  await this.waitIfPaused();
117865
- const remaining = this.playersRemaining();
117933
+ const remaining = this.playersWithChips();
117866
117934
  if (remaining.length <= 1) {
117867
- this.finish(remaining[0]?.p ?? null);
117935
+ this.finish(remaining[0] ?? null);
117868
117936
  return;
117869
117937
  }
117870
117938
  if (this.handNumber > 0 && this.handNumber % this.config.handsPerLevel === 0) {
@@ -117875,10 +117943,18 @@ var TournamentDirector = class {
117875
117943
  this.logEvent("BLINDS_UP_FAILED", { error: summarizeError(err) });
117876
117944
  }
117877
117945
  }
117878
- await this.startHand();
117879
- await this.playHand();
117880
- this.completeHand();
117881
- this.broadcast();
117946
+ try {
117947
+ await this.startHand();
117948
+ await this.playHand();
117949
+ this.completeHand();
117950
+ this.broadcast();
117951
+ this.handErrorStreak = 0;
117952
+ } catch (err) {
117953
+ if (this.status === "STOPPED" || err?.name === "AbortError") throw err;
117954
+ await this.recoverHand(err);
117955
+ this.handErrorStreak = (this.handErrorStreak || 0) + 1;
117956
+ if (this.handErrorStreak >= 3) throw err;
117957
+ }
117882
117958
  if (this.budgetReached) {
117883
117959
  this.logEvent("DECISION_BUDGET_REACHED", { decisions: this.decisionCount, budget: this.decisionBudget });
117884
117960
  this.stop();
@@ -117888,6 +117964,52 @@ var TournamentDirector = class {
117888
117964
  await sleep(Math.max(WINNER_REVIEW_MS, this.config.betweenHandsMs));
117889
117965
  }
117890
117966
  }
117967
+ // Last-resort safety net for a hand. A model failure or a decision-context
117968
+ // failure is already handled inside the hand loop; anything else that escapes
117969
+ // (a future regression) would otherwise abort the whole tournament mid-game.
117970
+ // Here the hand is finished with deterministic safe actions so the tournament
117971
+ // continues. Recovery is bounded by the hand-action guard and by the caller's
117972
+ // streak counter, so a genuinely broken engine still surfaces as an error
117973
+ // instead of looping forever.
117974
+ async recoverHand(triggerError) {
117975
+ this.currentDecision = null;
117976
+ if (this.handComplete()) {
117977
+ this.broadcast();
117978
+ return;
117979
+ }
117980
+ this.logEvent("HAND_RECOVERY", { handNumber: this.handNumber, street: this.engine?.state?.street ?? null, error: summarizeError(triggerError) });
117981
+ this.broadcast();
117982
+ let guard = 0;
117983
+ while (!this.handComplete()) {
117984
+ if (++guard > 500) throw new Error("Hand recovery guard exceeded 500 actions");
117985
+ await this.waitIfPaused();
117986
+ const state = this.engine.state, seat = state.actionTo;
117987
+ if (seat == null) {
117988
+ if (Array.isArray(state.winners) && state.winners.length) break;
117989
+ await sleep(10);
117990
+ continue;
117991
+ }
117992
+ const player = state.players?.[seat];
117993
+ if (!player) break;
117994
+ if (state.street === "SHOWDOWN") {
117995
+ const show = { type: ACTION.SHOW, playerId: player.id, cardIndices: [0, 1] };
117996
+ if (this.engine.validate(show)?.valid) {
117997
+ this.engine.act(show);
117998
+ continue;
117999
+ }
118000
+ const muck = { type: ACTION.MUCK, playerId: player.id };
118001
+ if (this.engine.validate(muck)?.valid) {
118002
+ this.engine.act(muck);
118003
+ continue;
118004
+ }
118005
+ }
118006
+ if (!this.forceSafeAction(seat, { error: triggerError, errorCategory: "recovery" })) break;
118007
+ this.broadcast();
118008
+ }
118009
+ if (!this.handComplete()) throw triggerError;
118010
+ this.completeHand();
118011
+ this.broadcast();
118012
+ }
117891
118013
  pruneBustedSeats() {
117892
118014
  const failures = [];
117893
118015
  for (const p of this.engine?.state?.players ?? []) {
@@ -117968,22 +118090,31 @@ var TournamentDirector = class {
117968
118090
  }
117969
118091
  async takeDecision(agent, seat, legalActions) {
117970
118092
  const decisionId = id(`d-h${this.handNumber}-s${seat + 1}`), baseMs = this.config.actionSeconds * 1e3, bankBefore = this.timeBanks[agent.id] ?? 0, totalMs = baseMs + bankBefore;
118093
+ const stats = this.stats[agent.id];
118094
+ const connection = this.config.connections.find((c) => c.id === agent.connectionId);
118095
+ const architecture = this.config.decisionArchitecture === DECISION_ARCHITECTURES.FLAT ? "flat" : "hierarchical";
117971
118096
  const recentHands = buildPublicTournamentMemory(this.events, this.handNumber);
117972
118097
  const publicPlayerStats = buildPublicPlayerStats(this.events, this.config.players, this.handNumber);
117973
118098
  const actionHistory = buildCurrentHandPublicActions(this.events, this.handNumber);
117974
- const baseState2 = serializeForAgent(this.engine, seat, this.tournamentMeta(), legalActions, recentHands, publicPlayerStats, actionHistory);
117975
- const stateForAgent = assertDecisionState(applyBenchmarkMode(baseState2, this.config.benchmarkMode)), startedAt = Date.now();
117976
- const connection = this.config.connections.find((c) => c.id === agent.connectionId);
117977
- const architecture = this.config.decisionArchitecture === DECISION_ARCHITECTURES.FLAT ? "flat" : "hierarchical";
117978
118099
  const sizesForFamily = (family) => legalAggressiveSizes(this.engine, seat, family);
117979
- let hierarchy = null;
117980
- if (architecture === "hierarchical") {
117981
- hierarchy = buildHierarchicalDecision(stateForAgent, { legalActions: stateForAgent.legalActions });
117982
- if (hierarchy.aggressiveFamily) {
117983
- const engineSizes = sizesForFamily(hierarchy.aggressiveFamily);
117984
- hierarchy.stage2 = { ...hierarchy.stage2, sizes: engineSizes, criteria: sizeCriteria(engineSizes) };
118100
+ let stateForAgent = null, hierarchy = null;
118101
+ try {
118102
+ stateForAgent = assertDecisionState(applyBenchmarkMode(
118103
+ serializeForAgent(this.engine, seat, this.tournamentMeta(), legalActions, recentHands, publicPlayerStats, actionHistory),
118104
+ this.config.benchmarkMode
118105
+ ));
118106
+ if (architecture === "hierarchical") {
118107
+ hierarchy = buildHierarchicalDecision(stateForAgent, { legalActions: stateForAgent.legalActions });
118108
+ if (hierarchy.aggressiveFamily) {
118109
+ const engineSizes = sizesForFamily(hierarchy.aggressiveFamily);
118110
+ hierarchy.stage2 = { ...hierarchy.stage2, sizes: engineSizes, criteria: sizeCriteria(engineSizes) };
118111
+ }
117985
118112
  }
118113
+ } catch (contextError) {
118114
+ if (!this.forceSafeAction(seat, { error: contextError, errorCategory: "context", decisionId })) throw contextError;
118115
+ return;
117986
118116
  }
118117
+ const startedAt = Date.now();
117987
118118
  this.currentDecision = {
117988
118119
  id: decisionId,
117989
118120
  playerId: agent.id,
@@ -117992,6 +118123,7 @@ var TournamentDirector = class {
117992
118123
  model: agent.model,
117993
118124
  connection: connection?.name ?? agent.connectionId,
117994
118125
  provider: agent.provider || "auto",
118126
+ reasoningEffort: agent.reasoningEffort || null,
117995
118127
  protocol: effectiveProtocol(agent, connection),
117996
118128
  handNumber: this.handNumber,
117997
118129
  street: this.engine.state.street,
@@ -118009,7 +118141,6 @@ var TournamentDirector = class {
118009
118141
  };
118010
118142
  this.logEvent("DECISION_START", this.currentDecision);
118011
118143
  this.broadcast();
118012
- const stats = this.stats[agent.id];
118013
118144
  let result = null, error = null, errorCategory = null, elapsed = 0;
118014
118145
  const pausedTotal = () => (this.currentDecision?.pausedMs || 0) + (this.currentDecision?.pausedAt ? Math.max(0, Date.now() - this.currentDecision.pausedAt) : 0);
118015
118146
  const recordIncident = (incident) => {
@@ -118140,6 +118271,7 @@ var TournamentDirector = class {
118140
118271
  configuredModel: agent.model,
118141
118272
  resolvedModel: result?.model ?? agent.model,
118142
118273
  provider: agent.provider || "auto",
118274
+ reasoningEffort: agent.reasoningEffort || null,
118143
118275
  action: { id: chosen.id, type: chosen.type, amount: chosen.amount, description: chosen.description },
118144
118276
  forced,
118145
118277
  legalActions: legalActions.map((a) => ({ id: a.id, type: a.type, amount: a.amount ?? null, description: a.description })),
@@ -118182,6 +118314,72 @@ var TournamentDirector = class {
118182
118314
  }
118183
118315
  this.currentDecision = null;
118184
118316
  }
118317
+ // Deterministic safe action for one seat, used whenever a decision cannot be
118318
+ // produced (context failure or emergency hand recovery). It always picks a
118319
+ // legal engine action, records a normal DECISION event so public action
118320
+ // history, replay and stats stay complete, and marks the row `forced`. It
118321
+ // never consults a model and never sees masked-out information.
118322
+ // Returns the chosen action, or null when the seat has no legal action.
118323
+ forceSafeAction(seat, { error = null, errorCategory = "context", decisionId = null } = {}) {
118324
+ const state = this.engine.state;
118325
+ const player = state.players?.[seat];
118326
+ if (!player) return null;
118327
+ const legalActions = legalActionCandidates(this.engine, seat);
118328
+ const chosen = fallbackAction(legalActions);
118329
+ if (!chosen) return null;
118330
+ this.engine.act(chosen.engineAction);
118331
+ const agent = (this.config?.players ?? []).find((p) => p.id === player.id) ?? null;
118332
+ const connection = agent ? this.config.connections.find((c) => c.id === agent.connectionId) ?? null : null;
118333
+ const stats = this.stats?.[player.id];
118334
+ if (stats) {
118335
+ stats.decisions++;
118336
+ stats.autoFallbacks++;
118337
+ stats.contextErrors = (stats.contextErrors || 0) + 1;
118338
+ stats.lastAction = chosen.description;
118339
+ stats.lastActionType = chosen.type;
118340
+ stats.lastActionAmount = chosen.amount ?? null;
118341
+ stats.lastReason = `Automatic ${chosen.description} after ${errorCategory} error.`;
118342
+ }
118343
+ this.decisionCount++;
118344
+ if (this.decisionBudget > 0 && this.decisionCount >= this.decisionBudget) this.budgetReached = true;
118345
+ this.currentDecision = null;
118346
+ const protocol = agent ? effectiveProtocol(agent, connection) : "none";
118347
+ this.logEvent("DECISION", {
118348
+ decisionId: decisionId ?? id(`d-h${this.handNumber}-s${seat + 1}`),
118349
+ handNumber: this.handNumber,
118350
+ street: state.street,
118351
+ position: positionForSeat(state, seat),
118352
+ potBefore: totalPot(state),
118353
+ playerId: player.id,
118354
+ playerName: player.name,
118355
+ connection: connection?.name ?? agent?.connectionId ?? null,
118356
+ protocol,
118357
+ requestedProtocol: protocol,
118358
+ protocolFallback: null,
118359
+ configuredModel: agent?.model ?? null,
118360
+ resolvedModel: agent?.model ?? null,
118361
+ provider: agent?.provider || "auto",
118362
+ reasoningEffort: agent?.reasoningEffort || null,
118363
+ action: { id: chosen.id, type: chosen.type, amount: chosen.amount, description: chosen.description },
118364
+ forced: true,
118365
+ legalActions: legalActions.map((a) => ({ id: a.id, type: a.type, amount: a.amount ?? null, description: a.description })),
118366
+ latencyMs: 0,
118367
+ primaryDecisionLatencyMs: 0,
118368
+ timeBankUsedMs: 0,
118369
+ timeBankRemainingMs: this.timeBanks?.[player.id] ?? 0,
118370
+ publicReason: stats?.lastReason ?? "",
118371
+ usage: null,
118372
+ decisionMeta: { method: "safe-fallback", decisionArchitecture: "safe-fallback-v1", reason: errorCategory, error: error ? summarizeError(error) : null },
118373
+ reasoning: null,
118374
+ benchmarkMode: this.config?.benchmarkMode ?? DEFAULT_BENCHMARK_MODE,
118375
+ decisionArchitecture: this.config?.decisionArchitecture ?? DEFAULT_DECISION_ARCHITECTURE,
118376
+ representation: this.config?.representation ?? DEFAULT_REPRESENTATION_MODE,
118377
+ replay: null,
118378
+ errorCategory,
118379
+ error: error ? summarizeError(error) : null
118380
+ });
118381
+ return chosen;
118382
+ }
118185
118383
  enqueueSpectatorExplanation({ decisionId, agent, connection, stateForAgent, chosen }) {
118186
118384
  if (!connection || !["openai", "openrouter"].includes(connection.kind) || isJevModel(agent.model)) return this.explanationChain;
118187
118385
  this.explanationChain = this.explanationChain.then(async () => {
@@ -118222,7 +118420,7 @@ var TournamentDirector = class {
118222
118420
  const already = this.eliminations.some((e) => e.playerId === player.id);
118223
118421
  return p && playerStack(p) <= 0 && !already ? { player, seat, startStack: asNumber(this.handStartStacks?.[player.id]) } : null;
118224
118422
  }).filter(Boolean).sort((a, b) => b.startStack - a.startStack || a.seat - b.seat);
118225
- const survivors = this.playersRemaining().length;
118423
+ const survivors = this.playersWithChips().length;
118226
118424
  newlyEliminated.forEach((row, index) => {
118227
118425
  const elimination = { playerId: row.player.id, playerName: row.player.name, place: Math.max(2, survivors + 1 + index), handNumber: this.handNumber, at: Date.now() };
118228
118426
  this.eliminations.push(elimination);
@@ -118380,6 +118578,7 @@ function renderModelOptions(models, currentValue = "") {
118380
118578
  if (currentValue && !byId.has(currentValue)) byId.set(currentValue, { id: currentValue, name: currentValue });
118381
118579
  const rows = [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
118382
118580
  els.seatModelOptions.innerHTML = rows.map((model) => `<option value="${escapeHtml(model.id)}" label="${escapeHtml(model.name || model.id)}"></option>`).join("");
118581
+ updateSeatReasoningEffortOptions();
118383
118582
  }
118384
118583
  async function refreshSeatModelCatalog({ force = false } = {}) {
118385
118584
  const connection = fullConnectionById(els.seatConnection?.value);
@@ -118433,9 +118632,41 @@ function defaultSeatDraft(seatIndex) {
118433
118632
  model: conn?.kind === "typesafe" ? "jev-latest" : "",
118434
118633
  protocol: conn?.kind === "typesafe" ? "jev_native" : "tool",
118435
118634
  provider: "",
118436
- captureReasoning: false
118635
+ captureReasoning: false,
118636
+ reasoningEffort: ""
118437
118637
  };
118438
118638
  }
118639
+ function updateSeatReasoningEffortOptions(selected) {
118640
+ const select = els.seatReasoningEffort;
118641
+ if (!select) return;
118642
+ const openRouter = isOpenRouterConnection(connectionById(els.seatConnection.value));
118643
+ const model = els.seatModel.value.trim();
118644
+ const efforts = openRouter ? reasoningEffortsFor(model) : [];
118645
+ const modelDefault = efforts.length ? defaultReasoningEffort(model) : null;
118646
+ const current = String(selected ?? seatReasoningEffortDraft ?? "").trim().toLowerCase();
118647
+ select.innerHTML = [
118648
+ '<option value="">Arena default (capped)</option>',
118649
+ ...efforts.map((effort) => `<option value="${escapeHtml(effort)}">${escapeHtml(effort)}${effort === modelDefault ? " \xB7 model default" : ""}</option>`)
118650
+ ].join("");
118651
+ if (efforts.includes(current)) {
118652
+ select.value = current;
118653
+ seatReasoningEffortDraft = current;
118654
+ } else {
118655
+ select.value = "";
118656
+ if (!current) seatReasoningEffortDraft = "";
118657
+ }
118658
+ select.disabled = !efforts.length;
118659
+ const help = els.seatReasoningEffortHelp;
118660
+ if (!help) return;
118661
+ help.dataset.tone = "";
118662
+ if (!openRouter) help.textContent = "Reasoning effort is available for OpenRouter models that advertise it.";
118663
+ else if (!efforts.length) help.textContent = "This model does not advertise reasoning efforts; the capped default is used.";
118664
+ else if (!select.value && requiresReasoning(model)) {
118665
+ help.textContent = `This model always reasons and ignores the capped default, so it can exceed the action clock and auto-fold. Pick an effort \u2014 ${efforts.includes("minimal") ? "minimal" : efforts[0]} is usually fastest.`;
118666
+ help.dataset.tone = "bad";
118667
+ } else if (modelDefault) help.textContent = `Model default is ${modelDefault}. Lower it when a slow reasoner runs out of action clock and auto-folds.`;
118668
+ else help.textContent = "Lower it when a slow reasoner runs out of action clock and auto-folds.";
118669
+ }
118439
118670
  function applySeatProtocolRules() {
118440
118671
  const conn = connectionById(els.seatConnection.value);
118441
118672
  const kind = conn?.kind || "openrouter";
@@ -118444,6 +118675,7 @@ function applySeatProtocolRules() {
118444
118675
  const jevSeat = kind === "typesafe" || jevViaOpenRouter;
118445
118676
  document.querySelector(".seat-provider-field")?.classList.toggle("hidden", !openRouter || jevViaOpenRouter);
118446
118677
  document.querySelector(".seat-reasoning-field")?.classList.toggle("hidden", jevSeat);
118678
+ document.querySelector(".seat-effort-field")?.classList.toggle("hidden", jevSeat);
118447
118679
  if (els.seatCaptureReasoning) {
118448
118680
  els.seatCaptureReasoning.disabled = jevSeat;
118449
118681
  const alreadyReasoning = isReasoningModel(els.seatModel.value.trim());
@@ -118464,6 +118696,7 @@ function applySeatProtocolRules() {
118464
118696
  els.seatProvider.disabled = !openRouter;
118465
118697
  if (els.seatModel.value === "jev-latest") els.seatModel.value = "";
118466
118698
  }
118699
+ updateSeatReasoningEffortOptions();
118467
118700
  }
118468
118701
  function updateSeatSummary() {
118469
118702
  const count = seatAssignments.filter(Boolean).length;
@@ -118490,13 +118723,16 @@ function openSeatEditor(seatIndex) {
118490
118723
  els.seatProtocol.value = draft.protocol || "tool";
118491
118724
  els.seatProvider.value = draft.provider || "";
118492
118725
  if (els.seatCaptureReasoning) els.seatCaptureReasoning.checked = Boolean(draft.captureReasoning);
118726
+ seatReasoningEffortDraft = String(draft.reasoningEffort || "").trim().toLowerCase();
118493
118727
  applySeatProtocolRules();
118728
+ updateSeatReasoningEffortOptions(seatReasoningEffortDraft);
118494
118729
  syncSeatNameFromModel();
118495
118730
  void refreshSeatModelCatalog();
118496
118731
  els.seatError.classList.add("hidden");
118497
118732
  els.seatLockNotice.classList.toggle("hidden", !locked);
118498
118733
  for (const control of [els.seatName, els.seatConnection, els.seatModel, els.seatProtocol, els.seatProvider]) control.disabled = locked || control === els.seatProvider && (!isOpenRouterConnection(connectionById(els.seatConnection.value)) || isJevModel(els.seatModel.value));
118499
118734
  if (els.seatCaptureReasoning) els.seatCaptureReasoning.disabled = locked || els.seatCaptureReasoning.disabled;
118735
+ if (els.seatReasoningEffort) els.seatReasoningEffort.disabled = locked || els.seatReasoningEffort.disabled;
118500
118736
  els.saveSeatBtn.disabled = locked;
118501
118737
  els.removeSeatBtn.disabled = locked || !seatAssignments[seatIndex];
118502
118738
  if (!els.seatDialog.open) els.seatDialog.showModal();
@@ -118514,7 +118750,8 @@ function readSeatDraft() {
118514
118750
  model: els.seatModel.value.trim(),
118515
118751
  protocol: els.seatProtocol.value,
118516
118752
  provider: els.seatProvider.disabled ? "" : els.seatProvider.value.trim(),
118517
- captureReasoning: Boolean(els.seatCaptureReasoning && !els.seatCaptureReasoning.disabled && els.seatCaptureReasoning.checked)
118753
+ captureReasoning: Boolean(els.seatCaptureReasoning && !els.seatCaptureReasoning.disabled && els.seatCaptureReasoning.checked),
118754
+ reasoningEffort: els.seatReasoningEffort && !els.seatReasoningEffort.disabled ? els.seatReasoningEffort.value.trim().toLowerCase() : ""
118518
118755
  };
118519
118756
  }
118520
118757
  function readSeatPlayers() {
@@ -118543,7 +118780,8 @@ function restoreSeatAssignments(rows = []) {
118543
118780
  model: player.model || "",
118544
118781
  protocol: player.protocol || "tool",
118545
118782
  provider: player.provider || "",
118546
- captureReasoning: player.captureReasoning === true
118783
+ captureReasoning: player.captureReasoning === true,
118784
+ reasoningEffort: String(player.reasoningEffort || "").trim().toLowerCase()
118547
118785
  };
118548
118786
  });
118549
118787
  updateSeatSummary();
@@ -119269,13 +119507,13 @@ function renderStats(s) {
119269
119507
  const pc = (value) => `${Math.round(Number(value || 0) * 100)}%`;
119270
119508
  return `<div class="stat-card"><div class="stat-top"><div><div class="stat-name">${escapeHtml(visiblePlayerName(p.name, p.model))}</div><div class="stat-model mono">${escapeHtml(p.model)} \xB7 ${escapeHtml(effectiveProtocol(p, s.config.connections.find((c) => c.id === p.connectionId)))}</div></div><strong>${fmt(tableP?.stack || 0)}</strong></div>
119271
119509
  <div class="poker-profile"><div title="Voluntarily put chips in pot"><b>${pc(poker.vpipPct)}</b><span>VPIP</span></div><div title="Preflop raise"><b>${pc(poker.pfrPct)}</b><span>PFR</span></div><div title="Aggression frequency"><b>${pc(poker.aggressionPct)}</b><span>AFq</span></div><div title="Fold when folding was legal"><b>${pc(poker.foldPct)}</b><span>FOLD</span></div><div><b>${poker.sampleHands || 0}</b><span>HANDS</span></div></div>
119272
- <div class="stat-values"><div class="metric"><b>${st.decisions || 0}</b><span>moves</span></div><div class="metric"><b>${avg}ms</b><span>avg</span></div><div class="metric"><b>${st.autoFallbacks || 0}</b><span>auto</span></div></div><div class="stat-reliability"><span><b>${st.modelErrors || 0}</b> model</span><span><b>${st.providerErrors || 0}</b> provider</span><span><b>${st.rateLimits || 0}</b> rate</span><span><b>${st.timeouts || 0}</b> timeout</span><span><b>${st.protocolFallbacks || 0}</b> protocol</span><span><b>${st.retries || 0}</b> retry</span></div></div>`;
119510
+ <div class="stat-values"><div class="metric"><b>${st.decisions || 0}</b><span>moves</span></div><div class="metric"><b>${avg}ms</b><span>avg</span></div><div class="metric"><b>${st.autoFallbacks || 0}</b><span>auto</span></div></div><div class="stat-reliability"><span><b>${st.modelErrors || 0}</b> model</span><span><b>${st.providerErrors || 0}</b> provider</span><span><b>${st.rateLimits || 0}</b> rate</span><span><b>${st.timeouts || 0}</b> timeout</span><span><b>${st.contextErrors || 0}</b> context</span><span><b>${st.protocolFallbacks || 0}</b> protocol</span><span><b>${st.retries || 0}</b> retry</span></div></div>`;
119273
119511
  }).join("");
119274
119512
  }
119275
119513
  function tableRenderSignature(s) {
119276
119514
  const running = ["RUNNING", "PAUSED"].includes(s?.status);
119277
119515
  if (lobbyVisible && !running) {
119278
- return JSON.stringify(["lobby", seatAssignments.map((p) => p ? [p.name, p.model, p.protocol, p.connectionId, p.provider, Boolean(p.captureReasoning)] : null)]);
119516
+ return JSON.stringify(["lobby", seatAssignments.map((p) => p ? [p.name, p.model, p.protocol, p.connectionId, p.provider, Boolean(p.captureReasoning), p.reasoningEffort || ""] : null)]);
119279
119517
  }
119280
119518
  const t = s?.table;
119281
119519
  if (!t) return JSON.stringify(["empty", s?.status || "IDLE"]);
@@ -120659,6 +120897,9 @@ els.seatName.addEventListener("input", () => {
120659
120897
  });
120660
120898
  els.seatProtocol.addEventListener("change", applySeatProtocolRules);
120661
120899
  els.seatCaptureReasoning?.addEventListener("change", applySeatProtocolRules);
120900
+ els.seatReasoningEffort?.addEventListener("change", () => {
120901
+ seatReasoningEffortDraft = els.seatReasoningEffort.value;
120902
+ });
120662
120903
  els.refreshModelsBtn?.addEventListener("click", () => void refreshSeatModelCatalog({ force: true }));
120663
120904
  els.removeSeatBtn.addEventListener("click", () => {
120664
120905
  if (editingSeatIndex == null || director && ["RUNNING", "PAUSED"].includes(director.status)) return;
package/dist/styles.css CHANGED
@@ -492,7 +492,7 @@ label input, label select { margin-top: 5px; font-weight: 450; }
492
492
  .split-footer { justify-content: space-between; }
493
493
  .seat-footer-actions, .replay-share-actions { display: flex; gap: 7px; }
494
494
  .seat-editor-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
495
- .seat-model-field { grid-column: 1 / -1; }
495
+ .seat-model-field, .seat-effort-field { grid-column: 1 / -1; }
496
496
  .model-input-wrap { margin-top: 6px; display: grid; grid-template-columns: minmax(0, 1fr) 38px; gap: 8px; }
497
497
  .model-input-wrap input { margin: 0; }
498
498
  .field-icon-button, .dialog-body .remove-connection { width: 38px; height: 38px; min-height: 38px; border-radius: 8px; }
@@ -1,4 +1,4 @@
1
- # pokertools-arena 0.16.2 — benchmark methodology & results
1
+ # pokertools-arena 0.17.1 — benchmark methodology & results
2
2
 
3
3
  Generated: 2026-09-19T18:19:58.352Z
4
4
  Methodology: paired fixed-state corpus; deterministic interleaving with recorded experiment seed.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.16.2",
3
+ "version": "0.17.1",
4
4
  "generatedAt": "2026-09-19T18:19:58.352Z",
5
5
  "experiments": [
6
6
  "paired"