borgmcp 4.5.1 → 4.6.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.
- package/dist/assimilate-cmd.d.ts +4 -0
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +15 -0
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +5 -2
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/opencode-drone.d.ts +2 -0
- package/dist/opencode-drone.d.ts.map +1 -1
- package/dist/opencode-drone.js +116 -34
- package/dist/opencode-drone.js.map +1 -1
- package/dist/seats.d.ts +10 -0
- package/dist/seats.d.ts.map +1 -1
- package/dist/seats.js +16 -0
- package/dist/seats.js.map +1 -1
- package/dist/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js +2 -0
- package/dist/server-handshake.js.map +1 -1
- package/dist/wake-path-health.d.ts.map +1 -1
- package/dist/wake-path-health.js +3 -2
- package/dist/wake-path-health.js.map +1 -1
- package/package.json +1 -1
- package/src/assimilate-cmd.ts +29 -0
- package/src/assimilate-deps.ts +5 -1
- package/src/opencode-drone.ts +166 -36
- package/src/seats.ts +27 -0
- package/src/server-handshake.ts +2 -0
- package/src/wake-path-health.ts +3 -1
package/src/opencode-drone.ts
CHANGED
|
@@ -44,7 +44,7 @@ const OPEN_CODE_DIAGNOSTIC_LOG_MAX_BYTES = 64 * 1024;
|
|
|
44
44
|
const diagnosticLogPathsForTests = new Set<string>();
|
|
45
45
|
|
|
46
46
|
function stateIdentityDigest(current: OpenCodeDroneState): string {
|
|
47
|
-
const key = [current.
|
|
47
|
+
const key = [current.directory, current.cubeName, current.droneLabel].join('\0');
|
|
48
48
|
return createHash('sha256').update(key).digest('hex').slice(0, 24);
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -211,8 +211,6 @@ interface OCSession {
|
|
|
211
211
|
directory: string;
|
|
212
212
|
time: { created: number };
|
|
213
213
|
parentID?: string;
|
|
214
|
-
agent?: string;
|
|
215
|
-
model?: { providerID: string; modelID: string };
|
|
216
214
|
}
|
|
217
215
|
|
|
218
216
|
interface OCMessage {
|
|
@@ -461,12 +459,6 @@ function decodeSession(value: unknown): OCSession {
|
|
|
461
459
|
|| typeof value.time.created !== 'number'
|
|
462
460
|
|| !Number.isFinite(value.time.created)
|
|
463
461
|
|| (value.parentID !== undefined && typeof value.parentID !== 'string')
|
|
464
|
-
|| (value.agent !== undefined && typeof value.agent !== 'string')
|
|
465
|
-
|| (value.model !== undefined && (
|
|
466
|
-
!isRecord(value.model)
|
|
467
|
-
|| typeof value.model.providerID !== 'string'
|
|
468
|
-
|| typeof value.model.modelID !== 'string'
|
|
469
|
-
))
|
|
470
462
|
) {
|
|
471
463
|
throw new OpenCodeResponseError();
|
|
472
464
|
}
|
|
@@ -562,7 +554,6 @@ function bindingPath(): string {
|
|
|
562
554
|
function bindingMatchesState(binding: SessionBinding): boolean {
|
|
563
555
|
const current = state!;
|
|
564
556
|
return binding.version === 4
|
|
565
|
-
&& binding.serverUrl === current.serverUrl
|
|
566
557
|
&& binding.directory === current.directory
|
|
567
558
|
&& binding.droneLabel === current.droneLabel
|
|
568
559
|
&& binding.cubeName === current.cubeName
|
|
@@ -747,35 +738,126 @@ function launchCorrelationMatchCount(messages: OCMessage[], correlationIdentity:
|
|
|
747
738
|
* therefore allowed only when it received this launch's correlation metadata.
|
|
748
739
|
*/
|
|
749
740
|
async function findLaunchSession(correlationIdentity: string): Promise<{
|
|
741
|
+
kind: 'found';
|
|
750
742
|
session: OCSession;
|
|
751
743
|
knownRootSessionIds: string[];
|
|
752
|
-
} |
|
|
744
|
+
} | {
|
|
745
|
+
kind: 'superseded';
|
|
746
|
+
} | {
|
|
747
|
+
kind: 'list-failed';
|
|
748
|
+
failureCode: string;
|
|
749
|
+
errorClass: string;
|
|
750
|
+
httpStatus: number | null;
|
|
751
|
+
} | {
|
|
752
|
+
kind: 'directory-miss';
|
|
753
|
+
listedCount: number;
|
|
754
|
+
directoryCount: 0;
|
|
755
|
+
} | {
|
|
756
|
+
kind: 'message-list-failed';
|
|
757
|
+
listedCount: number;
|
|
758
|
+
directoryCount: number;
|
|
759
|
+
failureCode: string;
|
|
760
|
+
errorClass: string;
|
|
761
|
+
httpStatus: number | null;
|
|
762
|
+
} | {
|
|
763
|
+
kind: 'correlation-mismatch';
|
|
764
|
+
listedCount: number;
|
|
765
|
+
directoryCount: number;
|
|
766
|
+
matchCount: number;
|
|
767
|
+
}> {
|
|
753
768
|
const owner = state!;
|
|
754
769
|
const observationSequence = ++owner.nextObservationSequence;
|
|
770
|
+
let listedSessions: OCSession[];
|
|
755
771
|
try {
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
772
|
+
listedSessions = await listSessions();
|
|
773
|
+
} catch (error) {
|
|
774
|
+
if (state === owner) recordOpenCodeFailure(owner, error, observationSequence);
|
|
775
|
+
return { kind: 'list-failed', ...openCodeFailureDiagnostic(error) };
|
|
776
|
+
}
|
|
777
|
+
if (state !== owner) return { kind: 'superseded' };
|
|
778
|
+
const sessions = listedSessions.filter(
|
|
779
|
+
(session) => session.directory === owner.directory,
|
|
780
|
+
);
|
|
781
|
+
if (sessions.length === 0) {
|
|
782
|
+
return { kind: 'directory-miss', listedCount: listedSessions.length, directoryCount: 0 };
|
|
783
|
+
}
|
|
784
|
+
const knownRootSessionIds = sessions
|
|
785
|
+
.filter(isTopLevelSession)
|
|
786
|
+
.map((session) => session.id);
|
|
787
|
+
let candidates: Array<{ session: OCSession; matchCount: number }>;
|
|
788
|
+
try {
|
|
789
|
+
candidates = await Promise.all(sessions.map(async (session) => ({
|
|
765
790
|
session,
|
|
766
791
|
matchCount: launchCorrelationMatchCount(
|
|
767
792
|
await listSessionMessages(session.id),
|
|
768
793
|
correlationIdentity,
|
|
769
794
|
),
|
|
770
795
|
})));
|
|
771
|
-
if (state !== owner) return null;
|
|
772
|
-
const totalMatches = candidates.reduce((total, candidate) => total + candidate.matchCount, 0);
|
|
773
|
-
if (totalMatches !== 1) return null;
|
|
774
|
-
const matched = candidates.find((candidate) => candidate.matchCount === 1);
|
|
775
|
-
return matched ? { session: matched.session, knownRootSessionIds } : null;
|
|
776
796
|
} catch (error) {
|
|
777
797
|
if (state === owner) recordOpenCodeFailure(owner, error, observationSequence);
|
|
778
|
-
return
|
|
798
|
+
return {
|
|
799
|
+
kind: 'message-list-failed',
|
|
800
|
+
listedCount: listedSessions.length,
|
|
801
|
+
directoryCount: sessions.length,
|
|
802
|
+
...openCodeFailureDiagnostic(error),
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
if (state !== owner) return { kind: 'superseded' };
|
|
806
|
+
const totalMatches = candidates.reduce((total, candidate) => total + candidate.matchCount, 0);
|
|
807
|
+
if (totalMatches !== 1) {
|
|
808
|
+
return {
|
|
809
|
+
kind: 'correlation-mismatch',
|
|
810
|
+
listedCount: listedSessions.length,
|
|
811
|
+
directoryCount: sessions.length,
|
|
812
|
+
matchCount: totalMatches,
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
const matched = candidates.find((candidate) => candidate.matchCount === 1)!;
|
|
816
|
+
return { kind: 'found', session: matched.session, knownRootSessionIds };
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
type LaunchSessionSearchFailure = Exclude<Awaited<ReturnType<typeof findLaunchSession>>, {
|
|
820
|
+
kind: 'found' | 'superseded';
|
|
821
|
+
}>;
|
|
822
|
+
|
|
823
|
+
function logLaunchSessionSearchFailure(
|
|
824
|
+
failure: LaunchSessionSearchFailure,
|
|
825
|
+
attempts: number,
|
|
826
|
+
owner: OpenCodeDroneState,
|
|
827
|
+
): void {
|
|
828
|
+
switch (failure.kind) {
|
|
829
|
+
case 'list-failed':
|
|
830
|
+
log(
|
|
831
|
+
`kickoff: session search list-failed code=${failure.failureCode} `
|
|
832
|
+
+ `class=${failure.errorClass} status=${failure.httpStatus ?? 'none'} `
|
|
833
|
+
+ `listed=unknown directory=unknown matches=unknown attempts=${attempts}`,
|
|
834
|
+
owner,
|
|
835
|
+
);
|
|
836
|
+
break;
|
|
837
|
+
case 'directory-miss':
|
|
838
|
+
log(
|
|
839
|
+
`kickoff: session search directory-miss listed=${failure.listedCount} `
|
|
840
|
+
+ `directory=${failure.directoryCount} matches=0 attempts=${attempts}`,
|
|
841
|
+
owner,
|
|
842
|
+
);
|
|
843
|
+
break;
|
|
844
|
+
case 'message-list-failed':
|
|
845
|
+
log(
|
|
846
|
+
`kickoff: session search messages-failed code=${failure.failureCode} `
|
|
847
|
+
+ `class=${failure.errorClass} status=${failure.httpStatus ?? 'none'} `
|
|
848
|
+
+ `listed=${failure.listedCount} directory=${failure.directoryCount} `
|
|
849
|
+
+ `matches=unknown attempts=${attempts}`,
|
|
850
|
+
owner,
|
|
851
|
+
);
|
|
852
|
+
break;
|
|
853
|
+
case 'correlation-mismatch':
|
|
854
|
+
log(
|
|
855
|
+
`kickoff: session search correlation-mismatch listed=${failure.listedCount} `
|
|
856
|
+
+ `directory=${failure.directoryCount} `
|
|
857
|
+
+ `matches=${failure.matchCount} attempts=${attempts}`,
|
|
858
|
+
owner,
|
|
859
|
+
);
|
|
860
|
+
break;
|
|
779
861
|
}
|
|
780
862
|
}
|
|
781
863
|
|
|
@@ -836,6 +918,18 @@ function openCodeFailureCode(error: unknown): string {
|
|
|
836
918
|
return error instanceof Error && error.name ? error.name : 'unknown';
|
|
837
919
|
}
|
|
838
920
|
|
|
921
|
+
function openCodeFailureDiagnostic(error: unknown): {
|
|
922
|
+
failureCode: string;
|
|
923
|
+
errorClass: string;
|
|
924
|
+
httpStatus: number | null;
|
|
925
|
+
} {
|
|
926
|
+
return {
|
|
927
|
+
failureCode: openCodeFailureCode(error),
|
|
928
|
+
errorClass: error instanceof Error && error.name ? error.name : 'unknown',
|
|
929
|
+
httpStatus: error instanceof OpenCodeHttpError ? error.status : null,
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
839
933
|
function updateLastOpenCodeObservation(
|
|
840
934
|
owner: OpenCodeDroneState,
|
|
841
935
|
sequence: number,
|
|
@@ -1185,31 +1279,59 @@ export async function injectInitialKickoff(launch: OpenCodeLaunchKickoff): Promi
|
|
|
1185
1279
|
|
|
1186
1280
|
try {
|
|
1187
1281
|
// Wait for the server.
|
|
1282
|
+
let serverReady = false;
|
|
1283
|
+
let lastServerError: unknown;
|
|
1188
1284
|
for (let i = 0; i < 30; i++) {
|
|
1189
1285
|
try {
|
|
1190
1286
|
await listSessions();
|
|
1287
|
+
if (state !== owner) return false;
|
|
1288
|
+
serverReady = true;
|
|
1191
1289
|
log(`kickoff: server ready (attempt ${i + 1})`, owner);
|
|
1192
1290
|
break;
|
|
1193
|
-
} catch {
|
|
1194
|
-
|
|
1291
|
+
} catch (error) {
|
|
1292
|
+
if (state !== owner) return false;
|
|
1293
|
+
lastServerError = error;
|
|
1195
1294
|
}
|
|
1196
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
1295
|
+
if (i < 29) await new Promise((r) => setTimeout(r, 1000));
|
|
1296
|
+
}
|
|
1297
|
+
if (!serverReady) {
|
|
1298
|
+
const observationSequence = ++owner.nextObservationSequence;
|
|
1299
|
+
recordOpenCodeFailure(owner, lastServerError, observationSequence);
|
|
1300
|
+
const failure = openCodeFailureDiagnostic(lastServerError);
|
|
1301
|
+
log(
|
|
1302
|
+
`kickoff: server unavailable code=${failure.failureCode} `
|
|
1303
|
+
+ `class=${failure.errorClass} status=${failure.httpStatus ?? 'none'} attempts=30`,
|
|
1304
|
+
owner,
|
|
1305
|
+
);
|
|
1306
|
+
return false;
|
|
1197
1307
|
}
|
|
1198
1308
|
|
|
1199
1309
|
// Capture the launch-selected session, including explicit resume/fork
|
|
1200
1310
|
// targets. Unrelated sessions do not contain this launch's metadata identity.
|
|
1311
|
+
let lastSearchFailure: LaunchSessionSearchFailure | null = null;
|
|
1201
1312
|
for (let i = 0; i < 30; i++) {
|
|
1202
|
-
const
|
|
1203
|
-
if (
|
|
1204
|
-
|
|
1205
|
-
saveBinding(
|
|
1206
|
-
log(`kickoff: bound session ${
|
|
1313
|
+
const result = await findLaunchSession(launch.correlationIdentity);
|
|
1314
|
+
if (result.kind === 'superseded') return false;
|
|
1315
|
+
if (result.kind === 'found') {
|
|
1316
|
+
saveBinding(result.session, result.knownRootSessionIds);
|
|
1317
|
+
log(`kickoff: bound session ${result.session.id.slice(0, 8)}…`, owner);
|
|
1207
1318
|
return true;
|
|
1208
1319
|
}
|
|
1209
|
-
|
|
1320
|
+
lastSearchFailure = result;
|
|
1321
|
+
if (i < 29) await new Promise((r) => setTimeout(r, 1000));
|
|
1210
1322
|
}
|
|
1211
1323
|
|
|
1212
|
-
|
|
1324
|
+
if (lastSearchFailure) {
|
|
1325
|
+
logLaunchSessionSearchFailure(lastSearchFailure, 30, owner);
|
|
1326
|
+
if (
|
|
1327
|
+
lastSearchFailure.kind === 'directory-miss'
|
|
1328
|
+
|| lastSearchFailure.kind === 'correlation-mismatch'
|
|
1329
|
+
) {
|
|
1330
|
+
updateLastOpenCodeObservation(owner, ++owner.nextObservationSequence, {
|
|
1331
|
+
lastFailureCode: 'no-target',
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1213
1335
|
return false;
|
|
1214
1336
|
} catch (err) {
|
|
1215
1337
|
log(`kickoff error: ${err}`, owner);
|
|
@@ -1443,6 +1565,14 @@ export function __getOpenCodeLastObservationForTests(): OpenCodeLastObservation
|
|
|
1443
1565
|
return { ...state.lastObservation };
|
|
1444
1566
|
}
|
|
1445
1567
|
|
|
1568
|
+
export function __decodeOpenCodeSessionForTests(value: unknown): unknown {
|
|
1569
|
+
return decodeSession(value);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
export async function __listOpenCodeSessionsForTests(): Promise<unknown[]> {
|
|
1573
|
+
return listSessions();
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1446
1576
|
export function computeOpenCodePort(droneId: string, base: number = 14096): number {
|
|
1447
1577
|
let hash = 0;
|
|
1448
1578
|
for (let i = 0; i < droneId.length; i++) {
|
package/src/seats.ts
CHANGED
|
@@ -50,6 +50,8 @@ export interface SeatRecord {
|
|
|
50
50
|
sessionId?: string;
|
|
51
51
|
// binding + display (set atomically at FINALIZE; absent while pending)
|
|
52
52
|
worktree?: string;
|
|
53
|
+
commonDir?: string;
|
|
54
|
+
repositoryOrigin?: string;
|
|
53
55
|
name?: string;
|
|
54
56
|
droneLabel?: string;
|
|
55
57
|
roleName?: string;
|
|
@@ -149,6 +151,8 @@ function isValidSeatRecord(ref: string, value: unknown): value is SeatRecord {
|
|
|
149
151
|
if (r.roleClass !== undefined && (typeof r.roleClass !== 'string' || !ROLE_CLASSES.has(r.roleClass))) return false;
|
|
150
152
|
if (r.isHumanSeat !== undefined && typeof r.isHumanSeat !== 'boolean') return false;
|
|
151
153
|
if (r.worktree !== undefined && typeof r.worktree !== 'string') return false;
|
|
154
|
+
if (r.commonDir !== undefined && !isNonEmptyString(r.commonDir)) return false;
|
|
155
|
+
if (r.repositoryOrigin !== undefined && !isNonEmptyString(r.repositoryOrigin)) return false;
|
|
152
156
|
if (r.droneId !== undefined && (typeof r.droneId !== 'string' || !UUID_RE.test(r.droneId))) return false;
|
|
153
157
|
if (r.sessionId !== undefined && (typeof r.sessionId !== 'string' || !UUID_RE.test(r.sessionId))) return false;
|
|
154
158
|
// State-consistency invariants (no inconsistent active|pending).
|
|
@@ -447,6 +451,8 @@ export type ActivateSeatOutcome = 'activated' | 'missing' | 'replaced';
|
|
|
447
451
|
* worktree is decided). Merged atomically with activation by activateAndBindSeat. */
|
|
448
452
|
export interface SeatBinding {
|
|
449
453
|
worktree: string;
|
|
454
|
+
commonDir?: string;
|
|
455
|
+
repositoryOrigin?: string;
|
|
450
456
|
name: string;
|
|
451
457
|
droneLabel: string;
|
|
452
458
|
roleName?: string;
|
|
@@ -475,6 +481,8 @@ export async function activateAndBindSeat(input: {
|
|
|
475
481
|
sessionId: string;
|
|
476
482
|
expectedPendingDigest: string;
|
|
477
483
|
worktree: string;
|
|
484
|
+
commonDir?: string;
|
|
485
|
+
repositoryOrigin?: string;
|
|
478
486
|
name: string;
|
|
479
487
|
droneLabel: string;
|
|
480
488
|
roleName?: string;
|
|
@@ -496,6 +504,8 @@ export async function activateAndBindSeat(input: {
|
|
|
496
504
|
droneId: input.droneId,
|
|
497
505
|
sessionId: input.sessionId,
|
|
498
506
|
worktree: input.worktree,
|
|
507
|
+
...(input.commonDir !== undefined ? { commonDir: input.commonDir } : {}),
|
|
508
|
+
...(input.repositoryOrigin !== undefined ? { repositoryOrigin: input.repositoryOrigin } : {}),
|
|
499
509
|
name: input.name,
|
|
500
510
|
droneLabel: input.droneLabel,
|
|
501
511
|
...(input.roleName !== undefined ? { roleName: input.roleName } : {}),
|
|
@@ -675,6 +685,23 @@ export async function readAllActiveSeats(): Promise<Array<{ worktree: string; re
|
|
|
675
685
|
return out;
|
|
676
686
|
}
|
|
677
687
|
|
|
688
|
+
/** Whether this cube has an active seat on the SAME repository from a different
|
|
689
|
+
* clone family. Repositories without a remote have no identity shared across
|
|
690
|
+
* clones, so their separate clones are undetectable here — see client#264. */
|
|
691
|
+
export async function hasActiveSeatInDifferentCloneFamily(
|
|
692
|
+
cubeId: string,
|
|
693
|
+
repositoryOrigin: string,
|
|
694
|
+
commonDir: string,
|
|
695
|
+
): Promise<boolean> {
|
|
696
|
+
const seats = await readAllActiveSeats();
|
|
697
|
+
return seats.some(({ record }) =>
|
|
698
|
+
record.cubeId === cubeId &&
|
|
699
|
+
record.repositoryOrigin === repositoryOrigin &&
|
|
700
|
+
record.commonDir !== undefined &&
|
|
701
|
+
record.commonDir !== commonDir
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
|
|
678
705
|
/** All valid worktree-bound registry entries, including a PENDING seat whose
|
|
679
706
|
* interrupted finalize preserved its worktree for a later resume. Read-only:
|
|
680
707
|
* pending records remain non-hydratable and getActiveSeatForWorktree stays
|
package/src/server-handshake.ts
CHANGED
|
@@ -410,6 +410,8 @@ export async function sendBorgServerAttach(
|
|
|
410
410
|
sessionId: decoded.session.id,
|
|
411
411
|
expectedPendingDigest: pendingBearerDigest,
|
|
412
412
|
worktree: binding.worktree,
|
|
413
|
+
...(binding.commonDir !== undefined ? { commonDir: binding.commonDir } : {}),
|
|
414
|
+
...(binding.repositoryOrigin !== undefined ? { repositoryOrigin: binding.repositoryOrigin } : {}),
|
|
413
415
|
name: binding.name,
|
|
414
416
|
droneLabel: binding.droneLabel,
|
|
415
417
|
...(binding.roleName !== undefined ? { roleName: binding.roleName } : {}),
|
package/src/wake-path-health.ts
CHANGED