borgmcp 2.3.0 → 2.4.0

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.
Files changed (99) hide show
  1. package/README.md +3 -0
  2. package/dist/assimilate-cmd.d.ts +6 -0
  3. package/dist/assimilate-cmd.d.ts.map +1 -1
  4. package/dist/assimilate-cmd.js +31 -7
  5. package/dist/assimilate-cmd.js.map +1 -1
  6. package/dist/assimilate-deps.d.ts.map +1 -1
  7. package/dist/assimilate-deps.js +2 -0
  8. package/dist/assimilate-deps.js.map +1 -1
  9. package/dist/claude.d.ts.map +1 -1
  10. package/dist/claude.js +10 -0
  11. package/dist/claude.js.map +1 -1
  12. package/dist/cli-help.d.ts.map +1 -1
  13. package/dist/cli-help.js +2 -0
  14. package/dist/cli-help.js.map +1 -1
  15. package/dist/cli-platform.d.ts +1 -0
  16. package/dist/cli-platform.d.ts.map +1 -1
  17. package/dist/cli-platform.js +5 -1
  18. package/dist/cli-platform.js.map +1 -1
  19. package/dist/console-prefix.d.ts +9 -10
  20. package/dist/console-prefix.d.ts.map +1 -1
  21. package/dist/console-prefix.js +23 -20
  22. package/dist/console-prefix.js.map +1 -1
  23. package/dist/cubes.d.ts +11 -0
  24. package/dist/cubes.d.ts.map +1 -1
  25. package/dist/cubes.js +10 -0
  26. package/dist/cubes.js.map +1 -1
  27. package/dist/display-identity.d.ts +34 -0
  28. package/dist/display-identity.d.ts.map +1 -0
  29. package/dist/display-identity.js +88 -0
  30. package/dist/display-identity.js.map +1 -0
  31. package/dist/docs-sections.d.ts.map +1 -1
  32. package/dist/docs-sections.js +9 -0
  33. package/dist/docs-sections.js.map +1 -1
  34. package/dist/drone-management.d.ts.map +1 -1
  35. package/dist/drone-management.js +6 -0
  36. package/dist/drone-management.js.map +1 -1
  37. package/dist/inbox-monitor.d.ts.map +1 -1
  38. package/dist/inbox-monitor.js +12 -0
  39. package/dist/inbox-monitor.js.map +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +58 -16
  42. package/dist/index.js.map +1 -1
  43. package/dist/log-stream.d.ts.map +1 -1
  44. package/dist/log-stream.js +41 -15
  45. package/dist/log-stream.js.map +1 -1
  46. package/dist/parse-assimilate-args.d.ts +1 -1
  47. package/dist/parse-assimilate-args.d.ts.map +1 -1
  48. package/dist/parse-assimilate-args.js +5 -2
  49. package/dist/parse-assimilate-args.js.map +1 -1
  50. package/dist/regen-format.d.ts +2 -2
  51. package/dist/regen-format.d.ts.map +1 -1
  52. package/dist/regen-format.js +6 -5
  53. package/dist/regen-format.js.map +1 -1
  54. package/dist/regen.js +6 -2
  55. package/dist/regen.js.map +1 -1
  56. package/dist/remote-client.d.ts.map +1 -1
  57. package/dist/remote-client.js +8 -1
  58. package/dist/remote-client.js.map +1 -1
  59. package/dist/reset-local-seat-cmd.d.ts +8 -0
  60. package/dist/reset-local-seat-cmd.d.ts.map +1 -1
  61. package/dist/reset-local-seat-cmd.js +21 -2
  62. package/dist/reset-local-seat-cmd.js.map +1 -1
  63. package/dist/seat-reattach-guard.d.ts +21 -0
  64. package/dist/seat-reattach-guard.d.ts.map +1 -0
  65. package/dist/seat-reattach-guard.js +83 -0
  66. package/dist/seat-reattach-guard.js.map +1 -0
  67. package/dist/seats.d.ts +6 -2
  68. package/dist/seats.d.ts.map +1 -1
  69. package/dist/seats.js +37 -14
  70. package/dist/seats.js.map +1 -1
  71. package/dist/update-cmd.d.ts.map +1 -1
  72. package/dist/update-cmd.js +1 -37
  73. package/dist/update-cmd.js.map +1 -1
  74. package/docs/EXTRACTION_PROVENANCE.md +3 -3
  75. package/docs/LOCAL_SERVER.md +4 -0
  76. package/docs/RELEASING.md +23 -4
  77. package/docs/SEAT_LIFECYCLE.md +242 -0
  78. package/package.json +1 -1
  79. package/src/assimilate-cmd.ts +45 -7
  80. package/src/assimilate-deps.ts +2 -0
  81. package/src/claude.ts +11 -0
  82. package/src/cli-help.ts +2 -0
  83. package/src/cli-platform.ts +5 -2
  84. package/src/console-prefix.ts +25 -18
  85. package/src/cubes.ts +16 -0
  86. package/src/display-identity.ts +130 -0
  87. package/src/docs-sections.ts +9 -0
  88. package/src/drone-management.ts +6 -0
  89. package/src/inbox-monitor.ts +19 -0
  90. package/src/index.ts +66 -20
  91. package/src/log-stream.ts +38 -14
  92. package/src/parse-assimilate-args.ts +4 -2
  93. package/src/regen-format.ts +6 -5
  94. package/src/regen.ts +11 -2
  95. package/src/remote-client.ts +8 -0
  96. package/src/reset-local-seat-cmd.ts +26 -1
  97. package/src/seat-reattach-guard.ts +113 -0
  98. package/src/seats.ts +36 -14
  99. package/src/update-cmd.ts +0 -34
@@ -0,0 +1,130 @@
1
+ import type { ActiveCube } from './cubes.js';
2
+
3
+ const LAST_CONFIRMED = ' (last confirmed)';
4
+
5
+ type DisplayField = {
6
+ value: string | null;
7
+ uncertain: boolean;
8
+ };
9
+
10
+ type DisplayState = {
11
+ seatKey: string;
12
+ cubeName: DisplayField;
13
+ droneLabel: DisplayField;
14
+ roleName: DisplayField;
15
+ };
16
+
17
+ export type DisplayIdentity = {
18
+ cubeName: string;
19
+ droneLabel: string;
20
+ roleName: string | null;
21
+ };
22
+
23
+ export type ServerDisplayIdentity = Partial<DisplayIdentity>;
24
+
25
+ let state: DisplayState | null = null;
26
+
27
+ function seatKey(active: Pick<ActiveCube, 'cubeId' | 'droneId'>): string {
28
+ return `${active.cubeId}\0${active.droneId}`;
29
+ }
30
+
31
+ function initialState(active: ActiveCube): DisplayState {
32
+ return {
33
+ seatKey: seatKey(active),
34
+ cubeName: { value: active.name, uncertain: false },
35
+ droneLabel: { value: active.droneLabel, uncertain: false },
36
+ roleName: { value: active.roleName ?? null, uncertain: false },
37
+ };
38
+ }
39
+
40
+ function ensureState(active: ActiveCube): DisplayState {
41
+ const key = seatKey(active);
42
+ if (state === null || state.seatKey !== key) {
43
+ state = initialState(active);
44
+ }
45
+ return state;
46
+ }
47
+
48
+ function confirmField(field: DisplayField, value: string | null | undefined): void {
49
+ if (value === undefined || value === null) return;
50
+ field.value = value;
51
+ field.uncertain = false;
52
+ }
53
+
54
+ function renderField(field: DisplayField): string | null {
55
+ if (field.value === null) return null;
56
+ return field.uncertain ? `${field.value}${LAST_CONFIRMED}` : field.value;
57
+ }
58
+
59
+ /** Seed the invocation-local display source from the exact seat selected by #63. */
60
+ export function seedDisplayIdentity(active: ActiveCube): void {
61
+ ensureState(active);
62
+ }
63
+
64
+ /** Apply server-authoritative fields and clear uncertainty only for those fields. */
65
+ export function confirmDisplayIdentity(
66
+ active: ActiveCube,
67
+ identity: ServerDisplayIdentity,
68
+ ): DisplayIdentity {
69
+ const current = ensureState(active);
70
+ confirmField(current.cubeName, identity.cubeName);
71
+ confirmField(current.droneLabel, identity.droneLabel);
72
+ confirmField(current.roleName, identity.roleName);
73
+ return renderDisplayIdentity(active);
74
+ }
75
+
76
+ /** Mark the current seat identity as last-confirmed after an identity read fails. */
77
+ export function markDisplayIdentityReadFailed(active: ActiveCube): void {
78
+ const current = ensureState(active);
79
+ current.cubeName.uncertain = true;
80
+ current.droneLabel.uncertain = true;
81
+ if (current.roleName.value !== null) current.roleName.uncertain = true;
82
+ }
83
+
84
+ export function renderDisplayIdentity(active: ActiveCube): DisplayIdentity {
85
+ const current = ensureState(active);
86
+ return {
87
+ cubeName: renderField(current.cubeName)!,
88
+ droneLabel: renderField(current.droneLabel)!,
89
+ roleName: renderField(current.roleName),
90
+ };
91
+ }
92
+
93
+ /** Synchronous view for console-prefix after initConsolePrefix seeds the seat. */
94
+ export function currentDisplayIdentity(): DisplayIdentity | null {
95
+ if (state === null) return null;
96
+ return {
97
+ cubeName: renderField(state.cubeName)!,
98
+ droneLabel: renderField(state.droneLabel)!,
99
+ roleName: renderField(state.roleName),
100
+ };
101
+ }
102
+
103
+ export function identityFromRegen(result: {
104
+ cube?: { name?: string | null };
105
+ drone?: { label?: string | null };
106
+ role?: { name?: string | null };
107
+ }): ServerDisplayIdentity {
108
+ return {
109
+ cubeName: result.cube?.name ?? undefined,
110
+ droneLabel: result.drone?.label ?? undefined,
111
+ roleName: result.role?.name ?? undefined,
112
+ };
113
+ }
114
+
115
+ export function withRenderedRegenIdentity<T extends {
116
+ cube?: Record<string, unknown>;
117
+ drone?: Record<string, unknown>;
118
+ role?: Record<string, unknown>;
119
+ }>(result: T, identity: DisplayIdentity): T {
120
+ return {
121
+ ...result,
122
+ cube: { ...result.cube, name: identity.cubeName },
123
+ drone: { ...result.drone, label: identity.droneLabel },
124
+ role: { ...result.role, name: identity.roleName },
125
+ };
126
+ }
127
+
128
+ export function _resetDisplayIdentityForTests(): void {
129
+ state = null;
130
+ }
@@ -16,6 +16,7 @@ export const DOCS_BASE_URL = "https://github.com/Byte-Ventures/borg-mcp-client";
16
16
 
17
17
  const README_URL = `${DOCS_BASE_URL}#readme`;
18
18
  const LOCAL_SERVER_URL = `${DOCS_BASE_URL}/blob/main/docs/LOCAL_SERVER.md`;
19
+ const SEAT_LIFECYCLE_URL = `${DOCS_BASE_URL}/blob/main/docs/SEAT_LIFECYCLE.md`;
19
20
 
20
21
  export interface DocsSection {
21
22
  /** logical topic key */
@@ -71,6 +72,14 @@ export const DOCS_SECTIONS: DocsSection[] = [
71
72
  summary: "Connect a client to a self-hosted server: invitations, assimilate --host --enroll, credentials.",
72
73
  keywords: ["enroll", "enrollment", "invitation", "invite", "assimilate", "--host", "credential", "credentials", "owner", "join server"],
73
74
  },
75
+ {
76
+ slug: "seat-lifecycle",
77
+ title: "Seat lifecycle and recovery",
78
+ url: SEAT_LIFECYCLE_URL,
79
+ page: "docs/SEAT_LIFECYCLE.md",
80
+ summary: "Saved-seat states, re-attach and reset recovery, duplicate-session guards, and deterministic multi-seat selection.",
81
+ keywords: ["seat", "lifecycle", "reattach", "re-attach", "reset-local-seat", "evicted", "revoked", "superseded", "inbox monitor", "multiple seats", "silent deafness"],
82
+ },
74
83
  {
75
84
  slug: "self-hosting",
76
85
  title: "Self-hosting operations",
@@ -17,6 +17,7 @@ import {
17
17
  reassignDrone,
18
18
  type LocalManageOperation,
19
19
  } from './remote-client.js';
20
+ import { confirmDisplayIdentity } from './display-identity.js';
20
21
 
21
22
  interface ManagedCube {
22
23
  id: string;
@@ -95,6 +96,11 @@ export async function runReassignDroneTool(
95
96
 
96
97
  let warning = '';
97
98
  if (drone.id === active.droneId) {
99
+ confirmDisplayIdentity(active, {
100
+ cubeName: cube.name,
101
+ droneLabel: drone.label,
102
+ roleName: role.name,
103
+ });
98
104
  try {
99
105
  const refreshed = await deps.refreshActiveCubeMetadata(activeCubeWithFreshRegenIdentity(active, {
100
106
  cube,
@@ -753,6 +753,23 @@ export function parseMonitorInvocation(argv: string[]): MonitorInvocation | null
753
753
  return null;
754
754
  }
755
755
 
756
+ function liveMonitorYieldLine(
757
+ inboxPath: string,
758
+ holderPidfilePath: string,
759
+ deps: Pick<InboxLockDeps, 'read' | 'isAlive'>
760
+ ): string {
761
+ const raw = deps.read(holderPidfilePath);
762
+ const parsed = raw === null ? null : parsePidfileContent(raw.trim());
763
+ const livePid = parsed && Number.isSafeInteger(parsed.pid) && parsed.pid > 0 && deps.isAlive(parsed.pid)
764
+ ? parsed.pid
765
+ : null;
766
+ const holder = livePid === null ? 'another monitor instance' : `a live instance (pid ${livePid})`;
767
+ return (
768
+ `borg-inbox-monitor: seat inbox ${JSON.stringify(inboxPath)} is already monitored by ${holder}; ` +
769
+ 'yielding — another session likely holds this seat.'
770
+ );
771
+ }
772
+
756
773
  function main(): void {
757
774
  const invocation = parseMonitorInvocation(process.argv.slice(2));
758
775
  if (!invocation) {
@@ -812,6 +829,7 @@ function main(): void {
812
829
  if (claimResult === 'legacy-live') {
813
830
  // An already-running old monitor still owns the wake path; yield without
814
831
  // touching its inbox-adjacent state.
832
+ console.log(liveMonitorYieldLine(inboxPath, legacyPidfilePathFor(inboxPath), lockDeps));
815
833
  process.exit(0);
816
834
  }
817
835
  if (claimResult === 'legacy-blocked') {
@@ -822,6 +840,7 @@ function main(): void {
822
840
  process.exit(1);
823
841
  }
824
842
  if (claimResult === 'modern-live') {
843
+ console.log(liveMonitorYieldLine(inboxPath, pidfilePath, lockDeps));
825
844
  process.exit(0);
826
845
  }
827
846
 
package/src/index.ts CHANGED
@@ -112,6 +112,14 @@ import {
112
112
  import { gateAllowsActivation, borgSessionToolNotice } from './launch-gate.js';
113
113
  import { renderSyncRolesResult } from './sync-roles-render.js';
114
114
  import { initConsolePrefix, consolePrefix } from './console-prefix.js';
115
+ import {
116
+ confirmDisplayIdentity,
117
+ identityFromRegen,
118
+ markDisplayIdentityReadFailed,
119
+ renderDisplayIdentity,
120
+ seedDisplayIdentity,
121
+ withRenderedRegenIdentity,
122
+ } from './display-identity.js';
115
123
  import {
116
124
  resolveSessionAgentKind,
117
125
  } from './codex-app-wake.js';
@@ -369,16 +377,25 @@ export async function main() {
369
377
  ],
370
378
  };
371
379
  }
380
+ seedDisplayIdentity(active);
372
381
  const since = typeof args?.since === 'string' ? args.since : undefined;
373
382
  const mode = args?.mode === 'lite' ? 'lite' : 'full';
374
383
  const reportedModel = typeof args?.model === 'string' ? args.model : undefined;
375
- const result = await regen(active.sessionToken, active.apiUrl, {
376
- since,
377
- reportedModel,
378
- agentKind: resolveReportableSessionAgentKind(),
379
- workingRepo: resolveWorkingRepo(),
380
- serverTrustIdentity: active.serverTrustIdentity,
381
- });
384
+ let result: Awaited<ReturnType<typeof regen>>;
385
+ try {
386
+ result = await regen(active.sessionToken, active.apiUrl, {
387
+ since,
388
+ reportedModel,
389
+ agentKind: resolveReportableSessionAgentKind(),
390
+ workingRepo: resolveWorkingRepo(),
391
+ serverTrustIdentity: active.serverTrustIdentity,
392
+ });
393
+ } catch (error) {
394
+ markDisplayIdentityReadFailed(active);
395
+ throw error;
396
+ }
397
+ const displayIdentity = confirmDisplayIdentity(active, identityFromRegen(result));
398
+ const displayedResult = withRenderedRegenIdentity(result, displayIdentity);
382
399
  const freshActive = activeCubeWithFreshRegenIdentity(active, result);
383
400
  if (freshActive !== active) {
384
401
  await refreshActiveCubeMetadata(freshActive);
@@ -407,8 +424,8 @@ export async function main() {
407
424
  ? formatWakePathPrefix({
408
425
  inboxPath,
409
426
  monitorStateRoot,
410
- droneLabel: regenWakePathDroneLabel(result, freshActive.droneLabel),
411
- cubeName: freshActive.name,
427
+ droneLabel: regenWakePathDroneLabel(displayedResult, displayIdentity.droneLabel),
428
+ cubeName: displayIdentity.cubeName,
412
429
  })
413
430
  : '';
414
431
 
@@ -427,7 +444,7 @@ export async function main() {
427
444
  }
428
445
  } catch { /* never break regen */ }
429
446
 
430
- return { content: [{ type: 'text', text: versionHeader + prefix + formatRegenMarkdown(result, { mode }) }] };
447
+ return { content: [{ type: 'text', text: versionHeader + prefix + formatRegenMarkdown(displayedResult, { mode }) }] };
431
448
  }
432
449
 
433
450
  case 'borg_assimilate': {
@@ -451,27 +468,31 @@ export async function main() {
451
468
  // server-validated regen path (SR cond-4: no fabricated success
452
469
  // — an evicted/revoked seat FAILS server-side and is surfaced).
453
470
  try {
471
+ seedDisplayIdentity(active!);
454
472
  const result = await regen(active!.sessionToken, active!.apiUrl, {
455
473
  agentKind: resolveReportableSessionAgentKind(),
456
474
  workingRepo: resolveWorkingRepo(),
457
475
  serverTrustIdentity: active!.serverTrustIdentity,
458
476
  });
477
+ const displayIdentity = confirmDisplayIdentity(active!, identityFromRegen(result));
478
+ const displayedResult = withRenderedRegenIdentity(result, displayIdentity);
459
479
  const freshActive = activeCubeWithFreshRegenIdentity(active!, result);
460
480
  if (freshActive !== active) {
461
481
  await refreshActiveCubeMetadata(freshActive);
462
482
  }
463
483
  const header = [
464
- `# Re-attached to cube: ${freshActive.name}`,
484
+ `# Re-attached to cube: ${displayIdentity.cubeName}`,
465
485
  ``,
466
- `**Drone label:** ${freshActive.droneLabel}`,
486
+ `**Drone label:** ${displayIdentity.droneLabel}`,
467
487
  `**Seat:** existing identity reused — no new drone minted (gh#780)`,
468
488
  ``,
469
489
  ``,
470
490
  ].join('\n');
471
491
  return {
472
- content: [{ type: 'text', text: header + formatRegenMarkdown(result, { mode: 'full' }) }],
492
+ content: [{ type: 'text', text: header + formatRegenMarkdown(displayedResult, { mode: 'full' }) }],
473
493
  };
474
494
  } catch (err: any) {
495
+ markDisplayIdentityReadFailed(active!);
475
496
  const failure = reattachFailureMessage(err ?? {});
476
497
  return { content: [{ type: 'text', text: failure }], isError: true };
477
498
  }
@@ -506,11 +527,34 @@ export async function main() {
506
527
 
507
528
  case 'borg_whoami': {
508
529
  const active = await requireActiveCube();
509
- const result = await whoami(
510
- active.sessionToken,
511
- active.apiUrl,
512
- active.serverTrustIdentity,
513
- );
530
+ seedDisplayIdentity(active);
531
+ let result: Awaited<ReturnType<typeof whoami>>;
532
+ try {
533
+ result = await whoami(
534
+ active.sessionToken,
535
+ active.apiUrl,
536
+ active.serverTrustIdentity,
537
+ );
538
+ } catch (error) {
539
+ markDisplayIdentityReadFailed(active);
540
+ throw error;
541
+ }
542
+ confirmDisplayIdentity(active, {
543
+ cubeName: result.cube_name,
544
+ droneLabel: result.drone_label,
545
+ roleName: result.role_name,
546
+ });
547
+ try {
548
+ await refreshActiveCubeMetadata({
549
+ ...active,
550
+ name: result.cube_name,
551
+ droneLabel: result.drone_label,
552
+ roleName: result.role_name,
553
+ });
554
+ } catch {
555
+ // The invocation-local server truth remains authoritative even if
556
+ // the display-only persistence refresh cannot be written.
557
+ }
514
558
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
515
559
  }
516
560
 
@@ -729,6 +773,8 @@ export async function main() {
729
773
  if (!message || typeof message !== 'string') throw new Error('message is required');
730
774
  const active = await getActiveCube();
731
775
  if (!active) throw new Error('Not assimilated to a cube. Use borg_assimilate <cube-name> first.');
776
+ seedDisplayIdentity(active);
777
+ const displayIdentity = renderDisplayIdentity(active);
732
778
  const lifecycleSignal = lifecycleSignalForMessage(message);
733
779
  if (lifecycleSignal) {
734
780
  const decision = await shouldSuppressLifecycleLog(active, message);
@@ -739,7 +785,7 @@ export async function main() {
739
785
  content: [
740
786
  {
741
787
  type: 'text',
742
- text: `Suppressed duplicate ${decision.signal?.toUpperCase()} lifecycle log for ${active.droneLabel}; recent cube log already contains this signal.`,
788
+ text: `Suppressed duplicate ${decision.signal?.toUpperCase()} lifecycle log for ${displayIdentity.droneLabel}; recent cube log already contains this signal.`,
743
789
  },
744
790
  ],
745
791
  };
@@ -773,7 +819,7 @@ export async function main() {
773
819
  .map((r) => r.label)
774
820
  .join(', ')}. Message delivered — they'll read it when they return.`
775
821
  : '';
776
- const text = `Logged to cube "${active.name}" as ${active.droneLabel}. (entry id: ${result.entry.id})${echo}${unreachable}`;
822
+ const text = `Logged to cube "${displayIdentity.cubeName}" as ${displayIdentity.droneLabel}. (entry id: ${result.entry.id})${echo}${unreachable}`;
777
823
  return { content: [{ type: 'text', text }] };
778
824
  }
779
825
 
package/src/log-stream.ts CHANGED
@@ -55,6 +55,7 @@ import {
55
55
  } from './codex-app-wake.js';
56
56
  import { readBoundedResponseBody } from './server-response.js';
57
57
  import { BorgServerError } from './server-errors.js';
58
+ import { markSeatRejected } from './seats.js';
58
59
  import {
59
60
  acquireStreamLease,
60
61
  readOwnershipSnapshot,
@@ -441,7 +442,7 @@ async function runLoop(testDeps: RunLoopTestDeps = {}): Promise<void> {
441
442
 
442
443
  let attempt = 0;
443
444
  let lastEventId: string | null = null;
444
- let currentCubeId: string | null = null;
445
+ let currentStreamKey: string | null = null;
445
446
  let lease: StreamLease | null = null;
446
447
  let leaseKey: string | null = null;
447
448
 
@@ -469,10 +470,11 @@ async function runLoop(testDeps: RunLoopTestDeps = {}): Promise<void> {
469
470
  // teardown (cube-cleared / dead app-server socket).
470
471
  ensureCodexHeartbeatStarted();
471
472
 
472
- // Reset resume cursor on cube switch — entries from a prior cube
473
- // mean nothing for the new cube's stream.
474
- if (active.cubeId !== currentCubeId) {
475
- currentCubeId = active.cubeId;
473
+ // Reset resume cursor on cube/drone switch — entries from a prior seat
474
+ // mean nothing for the newly selected stream.
475
+ const activeStreamKey = `${active.cubeId}:${active.droneId}`;
476
+ if (activeStreamKey !== currentStreamKey) {
477
+ currentStreamKey = activeStreamKey;
476
478
  lastEventId = null;
477
479
  }
478
480
 
@@ -560,18 +562,36 @@ async function runLoop(testDeps: RunLoopTestDeps = {}): Promise<void> {
560
562
  // loop quiesces cleanly. The agent's graceful shutdown (TaskStop Monitor,
561
563
  // no /loop reschedule) is driven separately by the EVICTED tool-result it
562
564
  // already received on the authed call that produced this verdict.
563
- if (err instanceof DroneEvictedError) {
564
- if (lease) await lease.release().catch(() => {});
565
- lease = null;
566
- leaseKey = null;
567
- streamState.connected = false;
568
- streamState.ownership = await readOwnershipSnapshot(active.cubeId, active.droneId);
569
- process.stderr.write(
570
- `[borg-mcp log stream] drone evicted — stream terminated (no reconnect).\n`
565
+ if (err instanceof DroneEvictedError || err instanceof BorgServerError) {
566
+ if (active.localSessionCredentialRef) {
567
+ markSeatRejected(active.localSessionCredentialRef);
568
+ }
569
+ const replacement = await _getActiveCube().catch(() => null);
570
+ const replacementChanged = replacement !== null && (
571
+ replacement.localSessionCredentialRef !== active.localSessionCredentialRef ||
572
+ replacement.cubeId !== active.cubeId ||
573
+ replacement.droneId !== active.droneId
571
574
  );
575
+ if (replacementChanged) {
576
+ if (lease) await lease.release().catch(() => {});
577
+ lease = null;
578
+ leaseKey = null;
579
+ streamState.connected = false;
580
+ streamState.ownership = await readOwnershipSnapshot(active.cubeId, active.droneId);
581
+ continue;
582
+ }
583
+ if (err instanceof DroneEvictedError) {
584
+ if (lease) await lease.release().catch(() => {});
585
+ lease = null;
586
+ leaseKey = null;
587
+ streamState.connected = false;
588
+ streamState.ownership = await readOwnershipSnapshot(active.cubeId, active.droneId);
589
+ process.stderr.write(
590
+ `[borg-mcp log stream] drone evicted — stream terminated (no reconnect).\n`
591
+ );
592
+ }
572
593
  throw new TerminalStreamError();
573
594
  }
574
- if (err instanceof BorgServerError) throw new TerminalStreamError();
575
595
  streamState.connected = false;
576
596
  const delay =
577
597
  Math.min(RECONNECT_MIN_MS * 2 ** attempt, RECONNECT_MAX_MS) +
@@ -883,11 +903,14 @@ export async function streamOnce(
883
903
  code = undefined;
884
904
  }
885
905
  if (code === ErrorCode.SESSION_REJECTED) {
906
+ if (active.localSessionCredentialRef) markSeatRejected(active.localSessionCredentialRef);
886
907
  throw new BorgServerError('SESSION_REJECTED', 'Borg server session was rejected');
887
908
  }
888
909
  if (code === ErrorCode.SESSION_REVOKED) {
910
+ if (active.localSessionCredentialRef) markSeatRejected(active.localSessionCredentialRef);
889
911
  throw new BorgServerError('SESSION_REVOKED', 'Borg server session was revoked');
890
912
  }
913
+ if (active.localSessionCredentialRef) markSeatRejected(active.localSessionCredentialRef);
891
914
  throw new BorgServerError('CREDENTIAL_REJECTED', 'Borg server rejected the stream credential');
892
915
  }
893
916
  if (response.status === 410) {
@@ -899,6 +922,7 @@ export async function streamOnce(
899
922
  ).catch(() => '');
900
923
  const code = errorCodeFromBody(body);
901
924
  if (code === DRONE_EVICTED_CODE) {
925
+ if (active.localSessionCredentialRef) markSeatRejected(active.localSessionCredentialRef);
902
926
  throw new DroneEvictedError();
903
927
  }
904
928
  // client#42: an expired resume cursor is RECOVERABLE, not terminal. The
@@ -12,7 +12,7 @@ export type ParseResult =
12
12
 
13
13
  /**
14
14
  * Parse argv for `borg assimilate [role] [--worktree <n>] [--template <n>]
15
- * [--no-template] [--cube-name <n>] [--host <host>] [--enroll] [--here] [--yes]`. The `assimilate`
15
+ * [--no-template] [--cube-name <n>] [--host <host>] [--enroll] [--here] [--force] [--yes]`. The `assimilate`
16
16
  * subcommand token must already be stripped by the caller.
17
17
  */
18
18
  export function parseAssimilateArgs(rawArgs: string[]): ParseResult {
@@ -46,6 +46,8 @@ export function parseAssimilateArgs(rawArgs: string[]): ParseResult {
46
46
  i += 1;
47
47
  } else if (arg === '--here') {
48
48
  flags.here = true;
49
+ } else if (arg === '--force') {
50
+ flags.force = true;
49
51
  } else if (arg === '--host') {
50
52
  const next = rawArgs[i + 1];
51
53
  if (typeof next !== 'string' || next.length === 0 || next.startsWith('-')) {
@@ -99,7 +101,7 @@ export function parseAssimilateArgs(rawArgs: string[]): ParseResult {
99
101
  } else if (arg.startsWith('--')) {
100
102
  return {
101
103
  ok: false,
102
- error: `unknown flag: ${arg}. Supported: --worktree, --template, --no-template, --cube-name, --host, --enroll, --here, --yes, --cli, --model`,
104
+ error: `unknown flag: ${arg}. Supported: --worktree, --template, --no-template, --cube-name, --host, --enroll, --here, --force, --yes, --cli, --model`,
103
105
  };
104
106
  } else {
105
107
  if (role !== undefined) {
@@ -112,17 +112,18 @@ export function wakePathArming(
112
112
  * network `regen()` result and falling back per-field to the local
113
113
  * `getActiveCube` state. When `result` is null — the net-free fallback path
114
114
  * taken on a `regen()` network failure — identity comes entirely from local
115
- * state, so a weak drone that hits a SessionStart network blip still gets
116
- * oriented (with its wake-path arming) instead of left dormant.
115
+ * state, explicitly qualified as last-confirmed so a weak drone still gets
116
+ * oriented without presenting a failed identity read as current server truth.
117
117
  */
118
118
  export function resolveLeanIdentity(
119
119
  active: { name: string; droneLabel: string; roleName?: string | null },
120
120
  result: { cube?: any; drone?: any; role?: any } | null
121
121
  ): { cubeName: string; droneLabel: string; roleName: string | null } {
122
+ const qualifier = result === null ? ' (last confirmed)' : '';
122
123
  return {
123
- cubeName: result?.cube?.name ?? active.name,
124
- droneLabel: result?.drone?.label ?? active.droneLabel,
125
- roleName: result?.role?.name ?? active.roleName ?? null,
124
+ cubeName: result?.cube?.name ?? `${active.name}${qualifier}`,
125
+ droneLabel: result?.drone?.label ?? `${active.droneLabel}${qualifier}`,
126
+ roleName: result?.role?.name ?? (active.roleName ? `${active.roleName}${qualifier}` : null),
126
127
  };
127
128
  }
128
129
 
package/src/regen.ts CHANGED
@@ -22,7 +22,6 @@ import { monitorStateRootForWorktree } from './inbox-monitor.js';
22
22
  import {
23
23
  parseHookSource,
24
24
  formatLeanOrientation,
25
- resolveLeanIdentity,
26
25
  type AgentKind,
27
26
  } from './regen-format.js';
28
27
  import { resolveSessionAgentKind } from './codex-app-wake.js';
@@ -30,6 +29,13 @@ import { resolveReportableSessionAgentKind } from './agent-runtime.js';
30
29
  import { handleVersionFlag } from './version.js';
31
30
  import { gateAllowsActivation } from './launch-gate.js';
32
31
  import { resolveWorkingRepo } from './working-repo.js';
32
+ import {
33
+ confirmDisplayIdentity,
34
+ identityFromRegen,
35
+ markDisplayIdentityReadFailed,
36
+ renderDisplayIdentity,
37
+ seedDisplayIdentity,
38
+ } from './display-identity.js';
33
39
 
34
40
  /**
35
41
  * Drain the SessionStart hook's stdin payload (best-effort). Mirrors
@@ -68,6 +74,7 @@ async function main(): Promise<void> {
68
74
  await emitUnassimilatedNotice();
69
75
  return;
70
76
  }
77
+ seedDisplayIdentity(active);
71
78
 
72
79
  const inboxPath = inboxPathForDrone(active.cubeId, active.droneId);
73
80
  const monitorStateRoot = monitorStateRootForWorktree(findProjectRoot());
@@ -90,10 +97,12 @@ async function main(): Promise<void> {
90
97
  workingRepo: resolveWorkingRepo(),
91
98
  serverTrustIdentity: active.serverTrustIdentity,
92
99
  });
100
+ confirmDisplayIdentity(active, identityFromRegen(result));
93
101
  } catch {
102
+ markDisplayIdentityReadFailed(active);
94
103
  result = null;
95
104
  }
96
- const identity = resolveLeanIdentity(active, result);
105
+ const identity = renderDisplayIdentity(active);
97
106
  process.stdout.write(
98
107
  formatLeanOrientation({ ...identity, inboxPath, monitorStateRoot, agentKind, source }) + '\n'
99
108
  );
@@ -47,6 +47,7 @@ import {
47
47
  LocalManageRequiredError,
48
48
  } from './server-errors.js';
49
49
  import { getActiveCube, type ActiveCube } from './cubes.js';
50
+ import { markSeatRejected } from './seats.js';
50
51
  import {
51
52
  advanceLocalServerCursor,
52
53
  getLocalServerCursor,
@@ -241,6 +242,7 @@ async function localServerRequest<T>(
241
242
  method,
242
243
  signal,
243
244
  droneSession: active.sessionToken,
245
+ localSessionCredentialRef: active.localSessionCredentialRef,
244
246
  apiUrl: active.apiUrl,
245
247
  serverTrustIdentity: active.serverTrustIdentity,
246
248
  redirect: 'error',
@@ -587,6 +589,7 @@ async function authedFetch(
587
589
  apiUrl?: string;
588
590
  authToken?: string;
589
591
  serverTrustIdentity?: string;
592
+ localSessionCredentialRef?: string;
590
593
  } = {}
591
594
  ): Promise<Response> {
592
595
  const {
@@ -594,6 +597,7 @@ async function authedFetch(
594
597
  apiUrl,
595
598
  authToken,
596
599
  serverTrustIdentity: suppliedTrustIdentity,
600
+ localSessionCredentialRef,
597
601
  headers,
598
602
  ...rest
599
603
  } = init;
@@ -673,6 +677,9 @@ async function authedFetch(
673
677
  } catch {
674
678
  rejectedCode = undefined;
675
679
  }
680
+ if (droneSession !== undefined && localSessionCredentialRef !== undefined) {
681
+ markSeatRejected(localSessionCredentialRef);
682
+ }
676
683
  if (droneSession !== undefined && rejectedCode === ErrorCode.SESSION_REJECTED) {
677
684
  throw new BorgServerError(
678
685
  'SESSION_REJECTED',
@@ -732,6 +739,7 @@ async function authedFetch(
732
739
  }
733
740
  debugLog(`✗ ${response.status} ${method} ${path}`);
734
741
  if (droneSession !== undefined && response.status === 410 && code === DRONE_EVICTED_CODE) {
742
+ if (localSessionCredentialRef !== undefined) markSeatRejected(localSessionCredentialRef);
735
743
  throw new DroneEvictedError();
736
744
  }
737
745
  throw new BorgServerHttpError(