borgmcp 5.0.3 → 5.1.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.
@@ -23,6 +23,8 @@ import { OPENCODE_WAKE_PATH_GUIDANCE } from './opencode-wake-copy.js';
23
23
  import { isBorgSession } from './launch-gate.js';
24
24
  import type { AgentKind } from './agent-runtime.js';
25
25
 
26
+ export type HandoverMode = 'origin' | 'local';
27
+
26
28
  /**
27
29
  * Extract the SessionStart `source` from a Claude Code hook payload (gh#926).
28
30
  *
@@ -245,10 +247,16 @@ export function markArrivalAnnouncedThisProcess(): void {
245
247
  // The playbook below points drones to `borg_docs {topic}` for user questions
246
248
  // about how Borg MCP works; this marker pins the param so the #490/#529 guard
247
249
  // verifies borg_docs exposes `topic`.
248
- export function getDronePlaybook(): string {
250
+ export function getDronePlaybook(handoverMode: HandoverMode = 'origin'): string {
249
251
  const arrivalInstruction = arrivalAnnouncedThisProcess
250
252
  ? ''
251
253
  : `\n**Required one-time startup setup:** If this instruction is visible, immediately call \`borg_log message="ARRIVAL: <your-label> (<your-role>) online on ${osHostname()}" to="broadcast"\` before unread-log triage. This is the sole exception to no-work log silence. Do not infer your startup state from peers' \`ARRIVAL:\` entries. After the post succeeds, the client suppresses this instruction until the MCP process restarts; an explicit \`/mcp\` reconnect may show it again.\n`;
254
+ const reviewReadyRefs = handoverMode === 'local'
255
+ ? '["HEAD"]'
256
+ : '["HEAD","origin/<branch>","origin/main"]';
257
+ const stagedDiffPurpose = handoverMode === 'local'
258
+ ? 'Catches deleted files / anomalous -LOC / wrong paths before handover.'
259
+ : 'Catches deleted files / anomalous -LOC / wrong paths pre-push.';
252
260
  return `## How to operate as a Drone
253
261
 
254
262
  You're a Drone in a Cube. Coordinate with other drones through the activity log.
@@ -262,14 +270,14 @@ You're a Drone in a Cube. Coordinate with other drones through the activity log.
262
270
  - \`borg_roster\` — see who else is connected
263
271
  - \`borg_read-log unread_only=true [limit]\` — drain unread log entries from your server-side cursor
264
272
  - \`borg_read-entry entry_id=<id>\` — read one known complete entry without moving the unread cursor
265
- - \`borg_log message="<message>" to="broadcast"|["<selector>"] refs=["HEAD","origin/<branch>","origin/main"]\` — append with an explicit audience and optional mechanically resolved Git provenance; REVIEW-READY requires \`refs\`
273
+ - \`borg_log message="<message>" to="broadcast"|["<selector>"] refs=${reviewReadyRefs}\` — append with an explicit audience and optional mechanically resolved Git provenance; REVIEW-READY requires \`refs\`
266
274
  - \`borg_assimilate <cube>\` — switch to a different cube
267
275
 
268
276
  **How coordination works:** the Cube gives primitives, not workflows. Your role's \`detailed_description\` (above) is your playbook — its conventions + signals come from there, not the system. The log is the coordination channel. Different cubes, different conventions. Every \`borg_log\` call must choose its audience with \`to: "broadcast"\` or a non-empty selector array; omission, message text, prefixes, and classes never choose recipients.
269
277
 
270
278
  **Communication discipline for non-human seats:**
271
279
  - **Console:** write nothing except harness-required output. Surface something to the operator only when blocked and needing unblocking; do not narrate plans, progress, method, or results.
272
- - **Log:** a post must change what another seat does. Otherwise, do not write it. Keep posts short: lifecycle signal + mechanically resolved SHA and nothing else; defect + location/evidence; correction to your live claim; or a genuine blocking question. Post REVIEW-READY with \`refs: ["HEAD","origin/<branch>","origin/main"]\`.
280
+ - **Log:** a post must change what another seat does. Otherwise, do not write it. Keep posts short: lifecycle signal + mechanically resolved SHA and nothing else; defect + location/evidence; correction to your live claim; or a genuine blocking question. Post REVIEW-READY with \`refs: ${reviewReadyRefs}\`.
273
281
  - **Do not post:** plans, work-in-progress/progress narration, method or reasoning, restatements/agreement/credit, self-examination, framing phrases, or coordination commentary.
274
282
  - **Evidence boundary:** state what a verdict did not exercise and any unavailable control in the same short clause. The human seat is excluded so its dispatches can explain constraints without being misapplied.
275
283
 
@@ -313,7 +321,7 @@ ${arrivalInstruction}
313
321
  One seat uses one stable worktree. Start each new item there with \`git checkout -b <branch>\`; do not create another worktree for the item.
314
322
  A reviewing seat uses \`git checkout --detach <SHA>\` in its own worktree. Keep scratch work under \`~/.borg/scratch/<seat>/\`; load \`borg_playbook\` for the full mechanism.
315
323
 
316
- Any drone that commits code: run \`git diff --staged --stat\` before \`git commit\` to verify file count + LOC direction + paths match your intent. Catches deleted files / anomalous -LOC / wrong paths pre-push. Your role may layer more git rules (code-implementing + coordinating roles typically carry the full set).`;
324
+ Any drone that commits code: run \`git diff --staged --stat\` before \`git commit\` to verify file count + LOC direction + paths match your intent. ${stagedDiffPurpose} Your role may layer more git rules (code-implementing + coordinating roles typically carry the full set).`;
317
325
  }
318
326
 
319
327
  /**
@@ -329,7 +337,31 @@ Any drone that commits code: run \`git diff --staged --stat\` before \`git commi
329
337
  // `borg_decisions {topic}`; this marker pins the param so the #490/#529
330
338
  // copy-guard verifies borg_decisions exposes `topic`
331
339
  // (client/__tests__/copy-mechanism-guard.test.ts).
332
- export function getDronePlaybookChapter(): string {
340
+ export function getDronePlaybookChapter(handoverMode: HandoverMode = 'origin'): string {
341
+ const codeStateRef = handoverMode === 'local'
342
+ ? 'a named local branch or commit'
343
+ : '`origin/main`, PR head, branch, merge-SHA, or tag';
344
+ const proposalRef = handoverMode === 'local'
345
+ ? 'If the proposal cites a named local branch or commit, grep that ref via `git show <ref>:<path> | grep`'
346
+ : 'If the proposal cites current `origin/main` or a branch/SHA, grep that ref via `git show <ref>:<path> | grep`';
347
+ const commentRef = handoverMode === 'local'
348
+ ? 'If the comment describes a named local branch or commit, grep that ref via `git show <ref>:<path> | grep`'
349
+ : 'If the comment describes a merged/base/PR-head state, grep the named ref via `git show <ref>:<path> | grep`';
350
+ const worktreeMechanism = handoverMode === 'local'
351
+ ? `- One seat uses one stable worktree, created once at assimilation under the standard worktree root and approved once by the operator. All seats for a repository use worktrees from the same clone family, sharing its object database and refs.
352
+ - Start each new work item in that stable worktree with \`git checkout -b <branch>\`. Do not create a new worktree or folder for each item.
353
+ - A commit on a named branch is the handover artifact. Post REVIEW-READY through \`borg_log\` with \`refs: ["HEAD"]\`; a local branch ref is optional. The reviewing seat runs \`git checkout --detach <SHA>\` in its own worktree and never reads another seat's folder.
354
+ - Treat branch history as shared: another seat may have the branch checked out, so never rewrite it.
355
+ - A commit is the only checkpoint; repository backup is the operator's concern.
356
+ - Put detached review checkouts, clean-environment rigs, fake HOMEs, unpacked artifacts, and throwaway worktrees under \`~/.borg/scratch/<your-seat-label>/\`. Never use \`/tmp\` or an ad-hoc path. Scratch contents are disposable and must be cleaned up with the work.`
357
+ : `- One seat uses one stable worktree, created once at assimilation under the standard worktree root and approved once by the operator. All seats for a repository use worktrees from the same clone family, sharing its object database and refs.
358
+ - Start each new work item in that stable worktree with \`git checkout -b <branch>\`. Do not create a new worktree or folder for each item.
359
+ - Hand a branch to another seat only through an explicit log event.
360
+ - Treat branch history as shared: another seat may have the branch checked out or fetched, so never rebase or force-push it.
361
+ - Hand over refs and exact commit SHAs, never a filesystem path. Post REVIEW-READY through \`borg_log\` with \`refs: ["HEAD","origin/<branch>","origin/main"]\`; a reviewing seat checks out the resolved SHA in its own worktree with \`git checkout --detach <SHA>\` and never reads another seat's folder.
362
+ - With no hosted remote, the commit is the durable handover artifact because clone-family worktrees share refs; omit the push step. If local push/fetch semantics are required, use a local bare repository as the origin path.
363
+ - Put detached review checkouts, clean-environment rigs, fake HOMEs, unpacked artifacts, and throwaway worktrees under \`~/.borg/scratch/<your-seat-label>/\`. Never use \`/tmp\` or an ad-hoc path. Scratch contents are disposable and must be cleaned up with the work.
364
+ - When an origin exists, synchronize with merge-only history using \`git fetch origin && git merge origin/main\`.`;
333
365
  return `## Operating playbook — full disciplines (borg_playbook chapter)
334
366
 
335
367
  This is the on-demand detail behind the rule-spine in your regen. Load it ONCE per session; it is static — do not re-fetch on every wake.
@@ -346,7 +378,7 @@ Any time you make a factual claim that could be verified — "this shipped as ve
346
378
  - Version attribution → \`git tag --contains <sha>\` or \`git log --oneline <tag>\`
347
379
  - Code state → match the grep surface to the claim surface:
348
380
  - Local uncommitted claim → \`grep -n "<symbol>" <file>\` or direct file read in the working tree
349
- - \`origin/main\`, PR head, branch, merge-SHA, or tag claim → \`git show <ref>:<path>\` followed by a symbol search in the returned source
381
+ - ${codeStateRef} claim → \`git show <ref>:<path>\` followed by a symbol search in the returned source
350
382
  - Prod state → \`curl https://<endpoint>\` or \`wrangler tail --env production\`
351
383
  - npm registry state → \`npm view <package>@<version>\` or \`npm view <package>@latest\`
352
384
  - DB state → query through the existing \`db\` interface; never trust a doc claim about row counts / column values
@@ -359,8 +391,8 @@ Any time you make a factual claim that could be verified — "this shipped as ve
359
391
 
360
392
  The discipline applies at FOUR surfaces. Catches at the surface closest to origin are cheapest; catches at later surfaces have already propagated through earlier consumers:
361
393
 
362
- - **Surface 1 (brainstorm-proposal time)**: when a brainstorm contribution names specific code identifiers / API field names / enum values / column names / function signatures, the PROPOSING drone source-grep's the referenced file BEFORE composing the proposal. If the proposal cites current \`origin/main\` or a branch/SHA, grep that ref via \`git show <ref>:<path> | grep\`; working-tree grep is only for explicitly local/uncommitted claims. Cheapest catch surface; one drone catches one error.
363
- - **Surface 2 (comment/JSDoc/docstring writing time)**: when an implementation comment cites cross-file invariants (other modules' thresholds, schema columns, enum values, semantic contracts), the WRITING drone source-grep's the referenced file BEFORE writing the comment. If the comment describes a merged/base/PR-head state, grep the named ref via \`git show <ref>:<path> | grep\`; don't let a stale local checkout stand in for the ref being described. Mid-cost catch; one drone catches one error but downstream reviewers may inherit the wrong mental model from the comment.
394
+ - **Surface 1 (brainstorm-proposal time)**: when a brainstorm contribution names specific code identifiers / API field names / enum values / column names / function signatures, the PROPOSING drone source-grep's the referenced file BEFORE composing the proposal. ${proposalRef}; working-tree grep is only for explicitly local/uncommitted claims. Cheapest catch surface; one drone catches one error.
395
+ - **Surface 2 (comment/JSDoc/docstring writing time)**: when an implementation comment cites cross-file invariants (other modules' thresholds, schema columns, enum values, semantic contracts), the WRITING drone source-grep's the referenced file BEFORE writing the comment. ${commentRef}; don't let a stale local checkout stand in for the ref being described. Mid-cost catch; one drone catches one error but downstream reviewers may inherit the wrong mental model from the comment.
364
396
  - **Surface 3 (review-time verification)**: the existing review-class discipline (Code Reviewer formal gates + Security Auditor SR gates + PM/UX/QA courtesy reviews). Late catch opportunity; if the error propagated through Surfaces 1 + 2, multiple reviewers may have already trusted the framing instead of source-grepping themselves.
365
397
  - **Surface 4 (durable-tracking-artifact-writing time)**: when filing a deferred-tracking issue from a cube event payload, the FILING drone fetches the originating entry's full body with \`borg_read-entry entry_id=<id>\` BEFORE composing the issue body. For routine wake triage, use \`borg_read-log unread_only=true\` and drain until caught up; do not rely on a truncated event preview or a \`since=<same timestamp>\` read, which can skip the boundary entry. Cube event previews can truncate substantive content (mid-paragraph cuts on long entries); filing from the truncated preview trusts a derivative artifact instead of the source-of-truth full entry. Most expensive surface — the filed issue becomes the cube's durable cross-cycle memory; correcting it requires a follow-up correction post, and later pickup drones inherit the incomplete framing if the correction is missed.
366
398
 
@@ -368,14 +400,7 @@ The discipline applies at FOUR surfaces. Catches at the surface closest to origi
368
400
 
369
401
  **Worktree and git mechanism:**
370
402
 
371
- - One seat uses one stable worktree, created once at assimilation under the standard worktree root and approved once by the operator. All seats for a repository use worktrees from the same clone family, sharing its object database and refs.
372
- - Start each new work item in that stable worktree with \`git checkout -b <branch>\`. Do not create a new worktree or folder for each item.
373
- - Hand a branch to another seat only through an explicit log event.
374
- - Treat branch history as shared: another seat may have the branch checked out or fetched, so never rebase or force-push it.
375
- - Hand over refs and exact commit SHAs, never a filesystem path. Post REVIEW-READY through \`borg_log\` with \`refs: ["HEAD","origin/<branch>","origin/main"]\`; a reviewing seat checks out the resolved SHA in its own worktree with \`git checkout --detach <SHA>\` and never reads another seat's folder.
376
- - With no hosted remote, the commit is the durable handover artifact because clone-family worktrees share refs; omit the push step. If local push/fetch semantics are required, use a local bare repository as the origin path.
377
- - Put detached review checkouts, clean-environment rigs, fake HOMEs, unpacked artifacts, and throwaway worktrees under \`~/.borg/scratch/<your-seat-label>/\`. Never use \`/tmp\` or an ad-hoc path. Scratch contents are disposable and must be cleaned up with the work.
378
- - When an origin exists, synchronize with merge-only history using \`git fetch origin && git merge origin/main\`.`;
403
+ ${worktreeMechanism}`;
379
404
  }
380
405
 
381
406
  /**
@@ -531,7 +556,7 @@ export function formatRegenMarkdown(
531
556
  // mid-session restatement moments. Absent on a pre-gh#740 worker → omitted.
532
557
  decisions?: any[];
533
558
  },
534
- opts: { mode?: RegenMode } = {}
559
+ opts: { mode?: RegenMode; handoverMode?: HandoverMode } = {}
535
560
  ): string {
536
561
  const mode = opts.mode ?? 'full';
537
562
  const roleOverview = result.roles
@@ -676,7 +701,7 @@ export function formatRegenMarkdown(
676
701
  );
677
702
 
678
703
  if (shouldEmitPlaybook) {
679
- lines.push('', getDronePlaybook());
704
+ lines.push('', getDronePlaybook(opts.handoverMode));
680
705
  boilerplateEmittedThisSession = true;
681
706
  }
682
707
  if (shouldEmitRoleText && roleTextHash != null) {
@@ -3,8 +3,9 @@
3
3
  * seat reset from the ratified client-seat-reset-state-model (Option W).
4
4
  *
5
5
  * Attach is PURE DIAGNOSIS on a pin-matched SESSION_REJECTED (it mutates
6
- * nothing and points here). This command is the ONLY writer that intentionally
7
- * clears a worktree's saved local seat. It performs ZERO network I/O: it never
6
+ * nothing and points here). This is the ONLY command that explicitly clears a
7
+ * worktree's saved local seat; a successful consented bind can also retire its
8
+ * predecessor atomically. The command performs ZERO network I/O: it never
8
9
  * contacts the server, so it makes NO server-revocation claim. It clears ONLY
9
10
  * this worktree's saved local seat — its credential and cube binding together —
10
11
  * from the local seat store (keyed on findProjectRoot()); server, trust anchor,
package/src/seats.ts CHANGED
@@ -45,6 +45,8 @@ export interface SeatRecord {
45
45
  // credential
46
46
  credential: string;
47
47
  state: 'pending' | 'active';
48
+ /** Monotonic store-local order assigned when this record is bound active. */
49
+ bindingOrder?: number;
48
50
  // server metadata (active only)
49
51
  droneId?: string;
50
52
  sessionId?: string;
@@ -144,6 +146,8 @@ function isValidSeatRecord(ref: string, value: unknown): value is SeatRecord {
144
146
  return false;
145
147
  }
146
148
  if (r.state !== 'pending' && r.state !== 'active') return false;
149
+ if (r.bindingOrder !== undefined &&
150
+ (typeof r.bindingOrder !== 'number' || !Number.isSafeInteger(r.bindingOrder) || r.bindingOrder < 1)) return false;
147
151
  // Optional display/typed fields, validated when present.
148
152
  if (r.name !== undefined && typeof r.name !== 'string') return false;
149
153
  if (r.droneLabel !== undefined && typeof r.droneLabel !== 'string') return false;
@@ -169,7 +173,7 @@ function isValidSeatRecord(ref: string, value: unknown): value is SeatRecord {
169
173
  }
170
174
  } else {
171
175
  // A PENDING record must NOT carry active-only server session fields.
172
- if (r.sessionId !== undefined) return false;
176
+ if (r.sessionId !== undefined || r.bindingOrder !== undefined) return false;
173
177
  }
174
178
  // The map key must equal the record's derived ref (no cross-key aliasing).
175
179
  return seatRef(value as SeatRecord) === ref;
@@ -231,12 +235,56 @@ function parseStore(raw: string): SeatsFile | null {
231
235
  }
232
236
  if (!isValidSeatRecord(ref, record)) return null;
233
237
  }
234
- return { version: SEATS_VERSION, seats: seats as Record<string, SeatRecord> };
238
+ const store = { version: SEATS_VERSION, seats: seats as Record<string, SeatRecord> };
239
+ repairOrderedDuplicateBindings(store);
240
+ return store;
235
241
  }
236
242
  }
237
243
  return null;
238
244
  }
239
245
 
246
+ /**
247
+ * Remove superseded worktree bindings when persisted ordering proves which bind
248
+ * is newest. The mutation is persisted by the next transaction that commits.
249
+ * Legacy duplicates without a complete, unique order stay intact and fail
250
+ * closed when that worktree is resolved.
251
+ */
252
+ function repairOrderedDuplicateBindings(store: SeatsFile): void {
253
+ const byWorktree = new Map<string, Array<[string, SeatRecord]>>();
254
+ for (const entry of Object.entries(store.seats)) {
255
+ const [, record] = entry;
256
+ if (record.state !== 'active' || record.worktree === undefined) continue;
257
+ const candidates = byWorktree.get(record.worktree) ?? [];
258
+ candidates.push(entry);
259
+ byWorktree.set(record.worktree, candidates);
260
+ }
261
+ for (const candidates of byWorktree.values()) {
262
+ if (candidates.length < 2) continue;
263
+ const ordered = candidates.filter(
264
+ (entry): entry is [string, SeatRecord & { bindingOrder: number }] =>
265
+ entry[1].bindingOrder !== undefined,
266
+ );
267
+ if (ordered.length === 0) continue;
268
+ const orders = ordered.map(([, record]) => record.bindingOrder);
269
+ if (new Set(orders).size !== orders.length) continue;
270
+ const newest = ordered.reduce((left, right) =>
271
+ left[1].bindingOrder > right[1].bindingOrder ? left : right);
272
+ for (const [ref] of candidates) {
273
+ if (ref !== newest[0]) delete store.seats[ref];
274
+ }
275
+ }
276
+ }
277
+
278
+ export class AmbiguousSeatBindingError extends Error {
279
+ constructor(worktree: string) {
280
+ super(
281
+ `Borg found multiple active seats for worktree ${worktree}, but their bind order is unavailable. ` +
282
+ 'No identity or inbox path was selected. Run `borg reset-local-connection` to clear the ambiguous local connection.',
283
+ );
284
+ this.name = 'AmbiguousSeatBindingError';
285
+ }
286
+ }
287
+
240
288
  async function readStore(): Promise<SeatsFile> {
241
289
  const raw = await readStoreFile(SEATS_FILE);
242
290
  // CR4 fail-closed: ENOENT alone initializes empty. A present-but-malformed /
@@ -498,9 +546,28 @@ export async function activateAndBindSeat(input: {
498
546
  const record = txn.data.seats[ref];
499
547
  if (!recordMatches(record, ref, binding)) return 'missing';
500
548
  if (digestOf(record.credential) !== input.expectedPendingDigest) return 'replaced';
549
+ const highestOrder = Object.values(txn.data.seats).reduce(
550
+ (highest, candidate) => Math.max(highest, candidate.bindingOrder ?? 0),
551
+ 0,
552
+ );
553
+ const bindingOrder = record.state === 'active' && record.worktree === input.worktree &&
554
+ record.bindingOrder !== undefined
555
+ ? record.bindingOrder
556
+ : highestOrder + 1;
557
+ if (!Number.isSafeInteger(bindingOrder)) {
558
+ throw new Error('Borg private seat store bind order is exhausted; refusing to activate a seat');
559
+ }
560
+ const retiredRefs: string[] = [];
561
+ for (const [candidateRef, candidate] of Object.entries(txn.data.seats)) {
562
+ if (candidateRef !== ref && candidate.state === 'active' && candidate.worktree === input.worktree) {
563
+ delete txn.data.seats[candidateRef];
564
+ retiredRefs.push(candidateRef);
565
+ }
566
+ }
501
567
  txn.data.seats[ref] = {
502
568
  ...record,
503
569
  state: 'active',
570
+ bindingOrder,
504
571
  droneId: input.droneId,
505
572
  sessionId: input.sessionId,
506
573
  worktree: input.worktree,
@@ -513,6 +580,7 @@ export async function activateAndBindSeat(input: {
513
580
  ...(input.isHumanSeat !== undefined ? { isHumanSeat: input.isHumanSeat } : {}),
514
581
  };
515
582
  await txn.commit();
583
+ for (const retiredRef of retiredRefs) rejectedSeatRefs.delete(retiredRef);
516
584
  rejectedSeatRefs.delete(ref);
517
585
  return 'activated';
518
586
  });
@@ -584,25 +652,19 @@ export async function bindPendingSeatToWorktree(input: {
584
652
  // ─── Hydration / enumeration (scan by worktree) ──────────────────────────────
585
653
 
586
654
  /** The preferred ACTIVE seat bound to `worktree`, or null. A pending record (no
587
- * worktree, or non-active) is NEVER surfaced as a live binding. Candidates use
588
- * one total order across every process: unrejected before rejected, a sibling
589
- * finalized into this worktree before an older in-place binding, then seat ref. */
655
+ * worktree, or non-active) is NEVER surfaced as a live binding. Persisted bind
656
+ * order is authoritative across processes; an unorderable legacy duplicate
657
+ * fails closed rather than selecting an identity or inbox path. */
590
658
  export async function getActiveSeatForWorktree(worktree: string): Promise<SeatRecord | null> {
591
659
  const store = await readStore();
592
660
  const candidates = Object.entries(store.seats)
593
661
  .filter(([ref, record]) =>
594
- record.state === 'active' && record.worktree === worktree && seatRef(record) === ref)
595
- .sort(([leftRef, left], [rightRef, right]) => {
596
- const rejectionOrder = Number(rejectedSeatRefs.has(leftRef)) - Number(rejectedSeatRefs.has(rightRef));
597
- if (rejectionOrder !== 0) return rejectionOrder;
598
- const operationOrder = Number(left.operation.kind === 'seat') - Number(right.operation.kind === 'seat');
599
- if (operationOrder !== 0) return operationOrder;
600
- return leftRef < rightRef ? -1 : leftRef > rightRef ? 1 : 0;
601
- });
662
+ record.state === 'active' && record.worktree === worktree && seatRef(record) === ref);
663
+ if (candidates.length > 1) throw new AmbiguousSeatBindingError(worktree);
602
664
  return candidates[0]?.[1] ?? null;
603
665
  }
604
666
 
605
- /** Deprioritize an exact seat after a definitive server auth or eviction verdict. */
667
+ /** Remember a definitive server auth or eviction verdict in this process only. */
606
668
  export function markSeatRejected(ref: string): void {
607
669
  if (REF_RE.test(ref)) rejectedSeatRefs.add(ref);
608
670
  }