borgmcp 2.2.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.
- package/README.md +36 -0
- package/dist/assimilate-cmd.d.ts +6 -0
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +31 -7
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +2 -0
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +14 -0
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts +2 -0
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +19 -0
- package/dist/cli-help.js.map +1 -1
- package/dist/cli-platform.d.ts +1 -0
- package/dist/cli-platform.d.ts.map +1 -1
- package/dist/cli-platform.js +5 -1
- package/dist/cli-platform.js.map +1 -1
- package/dist/console-prefix.d.ts +9 -10
- package/dist/console-prefix.d.ts.map +1 -1
- package/dist/console-prefix.js +23 -20
- package/dist/console-prefix.js.map +1 -1
- package/dist/cubes.d.ts +11 -0
- package/dist/cubes.d.ts.map +1 -1
- package/dist/cubes.js +10 -0
- package/dist/cubes.js.map +1 -1
- package/dist/display-identity.d.ts +34 -0
- package/dist/display-identity.d.ts.map +1 -0
- package/dist/display-identity.js +88 -0
- package/dist/display-identity.js.map +1 -0
- package/dist/docs-sections.d.ts.map +1 -1
- package/dist/docs-sections.js +9 -0
- package/dist/docs-sections.js.map +1 -1
- package/dist/drone-management.d.ts.map +1 -1
- package/dist/drone-management.js +6 -0
- package/dist/drone-management.js.map +1 -1
- package/dist/inbox-monitor.d.ts.map +1 -1
- package/dist/inbox-monitor.js +12 -0
- package/dist/inbox-monitor.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -16
- package/dist/index.js.map +1 -1
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +41 -15
- package/dist/log-stream.js.map +1 -1
- package/dist/parse-assimilate-args.d.ts +1 -1
- package/dist/parse-assimilate-args.d.ts.map +1 -1
- package/dist/parse-assimilate-args.js +5 -2
- package/dist/parse-assimilate-args.js.map +1 -1
- package/dist/regen-format.d.ts +2 -2
- package/dist/regen-format.d.ts.map +1 -1
- package/dist/regen-format.js +6 -5
- package/dist/regen-format.js.map +1 -1
- package/dist/regen.js +6 -2
- package/dist/regen.js.map +1 -1
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +8 -1
- package/dist/remote-client.js.map +1 -1
- package/dist/reset-local-seat-cmd.d.ts +8 -0
- package/dist/reset-local-seat-cmd.d.ts.map +1 -1
- package/dist/reset-local-seat-cmd.js +21 -2
- package/dist/reset-local-seat-cmd.js.map +1 -1
- package/dist/seat-reattach-guard.d.ts +21 -0
- package/dist/seat-reattach-guard.d.ts.map +1 -0
- package/dist/seat-reattach-guard.js +83 -0
- package/dist/seat-reattach-guard.js.map +1 -0
- package/dist/seats.d.ts +6 -2
- package/dist/seats.d.ts.map +1 -1
- package/dist/seats.js +37 -14
- package/dist/seats.js.map +1 -1
- package/dist/unknown-subcommand.d.ts +1 -1
- package/dist/unknown-subcommand.d.ts.map +1 -1
- package/dist/unknown-subcommand.js +1 -0
- package/dist/unknown-subcommand.js.map +1 -1
- package/dist/update-cmd.d.ts +58 -0
- package/dist/update-cmd.d.ts.map +1 -0
- package/dist/update-cmd.js +801 -0
- package/dist/update-cmd.js.map +1 -0
- package/docs/EXTRACTION_PROVENANCE.md +3 -3
- package/docs/LOCAL_SERVER.md +43 -0
- package/docs/RELEASING.md +145 -4
- package/docs/SEAT_LIFECYCLE.md +242 -0
- package/package.json +6 -3
- package/src/assimilate-cmd.ts +45 -7
- package/src/assimilate-deps.ts +2 -0
- package/src/claude.ts +15 -0
- package/src/cli-help.ts +22 -0
- package/src/cli-platform.ts +5 -2
- package/src/console-prefix.ts +25 -18
- package/src/cubes.ts +16 -0
- package/src/display-identity.ts +130 -0
- package/src/docs-sections.ts +9 -0
- package/src/drone-management.ts +6 -0
- package/src/inbox-monitor.ts +19 -0
- package/src/index.ts +66 -20
- package/src/log-stream.ts +38 -14
- package/src/parse-assimilate-args.ts +4 -2
- package/src/regen-format.ts +6 -5
- package/src/regen.ts +11 -2
- package/src/remote-client.ts +8 -0
- package/src/reset-local-seat-cmd.ts +26 -1
- package/src/seat-reattach-guard.ts +113 -0
- package/src/seats.ts +36 -14
- package/src/unknown-subcommand.ts +1 -0
- package/src/update-cmd.ts +1006 -0
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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(
|
|
411
|
-
cubeName:
|
|
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(
|
|
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: ${
|
|
484
|
+
`# Re-attached to cube: ${displayIdentity.cubeName}`,
|
|
465
485
|
``,
|
|
466
|
-
`**Drone label:** ${
|
|
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(
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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 ${
|
|
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 "${
|
|
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
|
|
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
|
|
473
|
-
// mean nothing for the
|
|
474
|
-
|
|
475
|
-
|
|
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 (
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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) {
|
package/src/regen-format.ts
CHANGED
|
@@ -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,
|
|
116
|
-
* oriented
|
|
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
|
|
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 =
|
|
105
|
+
const identity = renderDisplayIdentity(active);
|
|
97
106
|
process.stdout.write(
|
|
98
107
|
formatLeanOrientation({ ...identity, inboxPath, monitorStateRoot, agentKind, source }) + '\n'
|
|
99
108
|
);
|
package/src/remote-client.ts
CHANGED
|
@@ -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(
|
|
@@ -25,6 +25,7 @@ import { createInterface } from 'node:readline/promises';
|
|
|
25
25
|
import {
|
|
26
26
|
snapshotLocalSeat as cubesSnapshotLocalSeat,
|
|
27
27
|
resetLocalSeatBinding as cubesResetLocalSeatBinding,
|
|
28
|
+
findRemainingActiveSeatForWorktree as cubesFindRemainingActiveSeatForWorktree,
|
|
28
29
|
findProjectRoot as cubesFindProjectRoot,
|
|
29
30
|
type LocalSeatSnapshot,
|
|
30
31
|
type ResetLocalSeatOutcome,
|
|
@@ -39,6 +40,10 @@ export interface ResetLocalSeatFlags {
|
|
|
39
40
|
export interface ResetLocalSeatDeps {
|
|
40
41
|
snapshotLocalSeat: () => Promise<LocalSeatSnapshot | null>;
|
|
41
42
|
resetLocalSeatBinding: (expected: LocalSeatSnapshot) => Promise<ResetLocalSeatOutcome>;
|
|
43
|
+
findRemainingActiveSeat: (worktree: string) => Promise<{
|
|
44
|
+
apiUrl: string;
|
|
45
|
+
operation: { projectRoot: string; kind: 'seat' | 'sibling'; operationKey: string };
|
|
46
|
+
} | null>;
|
|
42
47
|
findProjectRoot: (cwd: string) => string;
|
|
43
48
|
normalizeHost: (host: string) => string;
|
|
44
49
|
cwd: () => string;
|
|
@@ -52,6 +57,10 @@ function reenrollCommand(apiUrl: string): string {
|
|
|
52
57
|
return `\`borg assimilate --host ${apiUrl} --enroll\``;
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
function reattachCommand(apiUrl: string): string {
|
|
61
|
+
return `\`borg assimilate --host ${apiUrl} --here\``;
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
/**
|
|
56
65
|
* Recovery guidance shared by the success + honest-no-op copy. Makes NO
|
|
57
66
|
* server-revocation claim: it recommends the operator issue a LIVE scoped
|
|
@@ -149,7 +158,22 @@ export async function runResetLocalSeat(
|
|
|
149
158
|
`audit: this worktree's saved local seat for ${snapshot.apiUrl} (worktree ${worktree}) ` +
|
|
150
159
|
'was cleared; server, trust anchor, cube, and sibling worktrees unchanged.\n',
|
|
151
160
|
);
|
|
152
|
-
|
|
161
|
+
let remaining: Awaited<ReturnType<ResetLocalSeatDeps['findRemainingActiveSeat']>> = null;
|
|
162
|
+
try {
|
|
163
|
+
remaining = await deps.findRemainingActiveSeat(worktree);
|
|
164
|
+
} catch {
|
|
165
|
+
// The reset already committed. A follow-up read must not turn that success
|
|
166
|
+
// into a false failure; fall back to the universally valid enrollment path.
|
|
167
|
+
}
|
|
168
|
+
if (remaining?.apiUrl === snapshot.apiUrl) {
|
|
169
|
+
deps.stdout(
|
|
170
|
+
`Another saved active seat remains for this worktree on ${snapshot.apiUrl}. ` +
|
|
171
|
+
`Re-attach with ${reattachCommand(snapshot.apiUrl)}; Borg will revalidate that ` +
|
|
172
|
+
'seat with the server before launch.\n',
|
|
173
|
+
);
|
|
174
|
+
} else {
|
|
175
|
+
deps.stdout(recoveryGuidance(snapshot.apiUrl));
|
|
176
|
+
}
|
|
153
177
|
return 0;
|
|
154
178
|
}
|
|
155
179
|
|
|
@@ -175,6 +199,7 @@ export function buildDefaultResetLocalSeatDeps(): ResetLocalSeatDeps {
|
|
|
175
199
|
return {
|
|
176
200
|
snapshotLocalSeat: () => cubesSnapshotLocalSeat(),
|
|
177
201
|
resetLocalSeatBinding: (expected) => cubesResetLocalSeatBinding(expected),
|
|
202
|
+
findRemainingActiveSeat: (worktree) => cubesFindRemainingActiveSeatForWorktree(worktree),
|
|
178
203
|
findProjectRoot: (cwd) => cubesFindProjectRoot(cwd),
|
|
179
204
|
normalizeHost: (host) => normalizeServerEndpoint(host),
|
|
180
205
|
cwd: () => process.cwd(),
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import {
|
|
3
|
+
HEARTBEAT_STALE_MS,
|
|
4
|
+
heartbeatPathFor,
|
|
5
|
+
legacyHeartbeatPathFor,
|
|
6
|
+
legacyPidfilePathFor,
|
|
7
|
+
pidfilePathFor,
|
|
8
|
+
} from './inbox-monitor.js';
|
|
9
|
+
|
|
10
|
+
export type MonitorHeartbeatState = 'fresh' | 'stale' | 'missing';
|
|
11
|
+
|
|
12
|
+
export interface LiveInboxMonitor {
|
|
13
|
+
pid: number;
|
|
14
|
+
heartbeat: MonitorHeartbeatState;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SeatReattachGuardDeps {
|
|
18
|
+
readPidfile: (path: string) => string | null;
|
|
19
|
+
readHeartbeatMtimeMs: (path: string) => number | null;
|
|
20
|
+
isAlive: (pid: number) => boolean;
|
|
21
|
+
now: number;
|
|
22
|
+
heartbeatStaleMs?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseMonitorPid(raw: string | null): number | null {
|
|
26
|
+
if (raw === null) return null;
|
|
27
|
+
const match = /^([1-9]\d*)(?::[^:\s]+)?$/.exec(raw.trim());
|
|
28
|
+
if (!match) return null;
|
|
29
|
+
const pid = Number(match[1]);
|
|
30
|
+
return Number.isSafeInteger(pid) ? pid : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Read-only preflight for reusing a worktree seat. A live PID always blocks:
|
|
35
|
+
* a stale heartbeat may mean the holder is wedged, but only an explicit
|
|
36
|
+
* `--force` decision may launch a second session onto that seat.
|
|
37
|
+
*/
|
|
38
|
+
export function inspectLiveInboxMonitor(
|
|
39
|
+
inboxPath: string,
|
|
40
|
+
monitorStateRoot: string,
|
|
41
|
+
deps: SeatReattachGuardDeps = defaultSeatReattachGuardDeps(),
|
|
42
|
+
): LiveInboxMonitor | null {
|
|
43
|
+
const candidates = [
|
|
44
|
+
{
|
|
45
|
+
pidfile: pidfilePathFor(inboxPath, monitorStateRoot),
|
|
46
|
+
heartbeat: heartbeatPathFor(inboxPath, monitorStateRoot),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
pidfile: legacyPidfilePathFor(inboxPath),
|
|
50
|
+
heartbeat: legacyHeartbeatPathFor(inboxPath),
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
const staleMs = deps.heartbeatStaleMs ?? HEARTBEAT_STALE_MS;
|
|
54
|
+
for (const candidate of candidates) {
|
|
55
|
+
const pid = parseMonitorPid(deps.readPidfile(candidate.pidfile));
|
|
56
|
+
if (pid === null || !deps.isAlive(pid)) continue;
|
|
57
|
+
const heartbeatMtime = deps.readHeartbeatMtimeMs(candidate.heartbeat);
|
|
58
|
+
return {
|
|
59
|
+
pid,
|
|
60
|
+
heartbeat: heartbeatMtime === null
|
|
61
|
+
? 'missing'
|
|
62
|
+
: deps.now - heartbeatMtime < staleMs
|
|
63
|
+
? 'fresh'
|
|
64
|
+
: 'stale',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function formatSeatReattachRefusal(
|
|
71
|
+
holder: LiveInboxMonitor,
|
|
72
|
+
forcedCommand: string,
|
|
73
|
+
): string {
|
|
74
|
+
const heartbeat =
|
|
75
|
+
holder.heartbeat === 'fresh'
|
|
76
|
+
? ''
|
|
77
|
+
: ` Its heartbeat is ${holder.heartbeat}, so the process may be wedged.`;
|
|
78
|
+
return (
|
|
79
|
+
`This worktree's Borg seat already has a live session (inbox monitor pid ${holder.pid}).${heartbeat}\n` +
|
|
80
|
+
'No agent was launched. Stop the existing session or use a fresh worktree with `borg assimilate --worktree <name>`. ' +
|
|
81
|
+
`If the live monitor is wedged, override once with \`${forcedCommand}\`.\n`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function defaultSeatReattachGuardDeps(
|
|
86
|
+
now: number = Date.now(),
|
|
87
|
+
): SeatReattachGuardDeps {
|
|
88
|
+
return {
|
|
89
|
+
readPidfile: (path) => {
|
|
90
|
+
try {
|
|
91
|
+
return readFileSync(path, 'utf8');
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
readHeartbeatMtimeMs: (path) => {
|
|
97
|
+
try {
|
|
98
|
+
return statSync(path).mtimeMs;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
isAlive: (pid) => {
|
|
104
|
+
try {
|
|
105
|
+
process.kill(pid, 0);
|
|
106
|
+
return true;
|
|
107
|
+
} catch (error: any) {
|
|
108
|
+
return error?.code === 'EPERM';
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
now,
|
|
112
|
+
};
|
|
113
|
+
}
|