borgmcp 4.6.0 → 4.6.2
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.map +1 -1
- package/dist/assimilate-cmd.js +1 -0
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +2 -14
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/claude.d.ts +5 -3
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +82 -30
- package/dist/claude.js.map +1 -1
- package/dist/config-utils.d.ts +3 -0
- package/dist/config-utils.d.ts.map +1 -1
- package/dist/config-utils.js +141 -4
- package/dist/config-utils.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/opencode-drone.d.ts +4 -0
- package/dist/opencode-drone.d.ts.map +1 -1
- package/dist/opencode-drone.js +216 -61
- package/dist/opencode-drone.js.map +1 -1
- package/dist/opencode-launch-trust.d.ts +1 -0
- package/dist/opencode-launch-trust.d.ts.map +1 -1
- package/dist/opencode-launch-trust.js +4 -0
- package/dist/opencode-launch-trust.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 +1 -0
- package/src/assimilate-deps.ts +2 -14
- package/src/claude.ts +91 -31
- package/src/config-utils.ts +156 -4
- package/src/index.ts +7 -1
- package/src/opencode-drone.ts +277 -65
- package/src/opencode-launch-trust.ts +5 -0
- package/src/wake-path-health.ts +3 -1
package/src/opencode-drone.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from 'fs';
|
|
15
15
|
import { createHash, randomUUID } from 'crypto';
|
|
16
16
|
import { createServer } from 'node:net';
|
|
17
|
-
import { join } from 'path';
|
|
17
|
+
import { join, resolve } from 'path';
|
|
18
18
|
import { tmpdir } from 'os';
|
|
19
19
|
import {
|
|
20
20
|
borgConfigRoot,
|
|
@@ -44,8 +44,9 @@ 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
|
-
|
|
48
|
-
|
|
47
|
+
// Diagnostic files follow the stable worktree location; launch placeholders
|
|
48
|
+
// must not select a second log before session identity resolves.
|
|
49
|
+
return createHash('sha256').update(resolve(current.directory)).digest('hex').slice(0, 24);
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export function openCodeStartupDiagnosticLogPath(): string {
|
|
@@ -166,6 +167,7 @@ interface OpenCodeDroneState {
|
|
|
166
167
|
directory: string;
|
|
167
168
|
droneLabel: string;
|
|
168
169
|
cubeName: string;
|
|
170
|
+
launchIdentity: string;
|
|
169
171
|
connected: boolean;
|
|
170
172
|
totalEntriesInjected: number;
|
|
171
173
|
totalEntriesRetried: number;
|
|
@@ -204,6 +206,7 @@ interface ConnectDeps {
|
|
|
204
206
|
directory: string;
|
|
205
207
|
droneLabel: string;
|
|
206
208
|
cubeName: string;
|
|
209
|
+
launchIdentity: string;
|
|
207
210
|
}
|
|
208
211
|
|
|
209
212
|
interface OCSession {
|
|
@@ -211,8 +214,6 @@ interface OCSession {
|
|
|
211
214
|
directory: string;
|
|
212
215
|
time: { created: number };
|
|
213
216
|
parentID?: string;
|
|
214
|
-
agent?: string;
|
|
215
|
-
model?: { providerID: string; modelID: string };
|
|
216
217
|
}
|
|
217
218
|
|
|
218
219
|
interface OCMessage {
|
|
@@ -272,7 +273,7 @@ const OPEN_CODE_RECONCILIATION_ATTEMPTS = 20;
|
|
|
272
273
|
const OPEN_CODE_DELIVERY_HISTORY_LIMIT = 256;
|
|
273
274
|
|
|
274
275
|
interface SessionBinding {
|
|
275
|
-
version:
|
|
276
|
+
version: 5;
|
|
276
277
|
sessionId: string;
|
|
277
278
|
sessionCreatedAt: number;
|
|
278
279
|
knownRootSessionIds: string[];
|
|
@@ -280,6 +281,7 @@ interface SessionBinding {
|
|
|
280
281
|
directory: string;
|
|
281
282
|
droneLabel: string;
|
|
282
283
|
cubeName: string;
|
|
284
|
+
launchIdentity: string;
|
|
283
285
|
pendingSubmissions: Array<{
|
|
284
286
|
entryId: string;
|
|
285
287
|
sourceEntryId: string;
|
|
@@ -287,6 +289,13 @@ interface SessionBinding {
|
|
|
287
289
|
}>;
|
|
288
290
|
}
|
|
289
291
|
|
|
292
|
+
interface SessionBindingClaim {
|
|
293
|
+
version: 1;
|
|
294
|
+
directory: string;
|
|
295
|
+
droneLabel: string;
|
|
296
|
+
cubeName: string;
|
|
297
|
+
}
|
|
298
|
+
|
|
290
299
|
export interface OpenCodeLaunchKickoff {
|
|
291
300
|
prompt: string;
|
|
292
301
|
apiPassword: string;
|
|
@@ -325,6 +334,9 @@ export async function connectOpenCodeDrone(deps: ConnectDeps): Promise<void> {
|
|
|
325
334
|
if (!isOpenCode256BitIdentity(deps.apiPassword)) {
|
|
326
335
|
throw new OpenCodeAuthenticationError('OpenCode API password is missing or unverifiable');
|
|
327
336
|
}
|
|
337
|
+
if (!isOpenCode256BitIdentity(deps.launchIdentity)) {
|
|
338
|
+
throw new OpenCodeAuthenticationError('OpenCode launch identity is missing or unverifiable');
|
|
339
|
+
}
|
|
328
340
|
await ensurePrivateBorgConfigRoot(borgConfigRoot());
|
|
329
341
|
abandonOpenCodeDeliveries(state);
|
|
330
342
|
state = {
|
|
@@ -336,6 +348,7 @@ export async function connectOpenCodeDrone(deps: ConnectDeps): Promise<void> {
|
|
|
336
348
|
directory: deps.directory,
|
|
337
349
|
droneLabel: deps.droneLabel,
|
|
338
350
|
cubeName: deps.cubeName,
|
|
351
|
+
launchIdentity: deps.launchIdentity,
|
|
339
352
|
connected: true,
|
|
340
353
|
totalEntriesInjected: 0,
|
|
341
354
|
totalEntriesRetried: 0,
|
|
@@ -461,12 +474,6 @@ function decodeSession(value: unknown): OCSession {
|
|
|
461
474
|
|| typeof value.time.created !== 'number'
|
|
462
475
|
|| !Number.isFinite(value.time.created)
|
|
463
476
|
|| (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
477
|
) {
|
|
471
478
|
throw new OpenCodeResponseError();
|
|
472
479
|
}
|
|
@@ -554,18 +561,28 @@ async function promptSession(id: string, bodyObj: Record<string, unknown>): Prom
|
|
|
554
561
|
|
|
555
562
|
function bindingPath(): string {
|
|
556
563
|
const current = state!;
|
|
557
|
-
const
|
|
564
|
+
const identity = createHash('sha256').update(current.launchIdentity).digest('hex').slice(0, 24);
|
|
565
|
+
const path = join(tmpdir(), `borg-opencode-session-${identity}.json`);
|
|
558
566
|
bindingPathsForTests.add(path);
|
|
559
567
|
return path;
|
|
560
568
|
}
|
|
561
569
|
|
|
570
|
+
function bindingClaimPath(): string {
|
|
571
|
+
return `${bindingPath()}.claim`;
|
|
572
|
+
}
|
|
573
|
+
|
|
562
574
|
function bindingMatchesState(binding: SessionBinding): boolean {
|
|
563
575
|
const current = state!;
|
|
564
|
-
|
|
565
|
-
&& binding.
|
|
576
|
+
const sameResolvedSeat = binding.droneLabel === current.droneLabel
|
|
577
|
+
&& binding.cubeName === current.cubeName;
|
|
578
|
+
// The 256-bit launch identity lets the resolved MCP child claim only its
|
|
579
|
+
// launcher's placeholder binding; resolved ownership still requires labels.
|
|
580
|
+
const unclaimedLaunch = binding.droneLabel === 'opencode'
|
|
581
|
+
&& binding.cubeName === 'borg';
|
|
582
|
+
return binding.version === 5
|
|
566
583
|
&& binding.directory === current.directory
|
|
567
|
-
&& binding.
|
|
568
|
-
&&
|
|
584
|
+
&& binding.launchIdentity === current.launchIdentity
|
|
585
|
+
&& (sameResolvedSeat || unclaimedLaunch)
|
|
569
586
|
&& typeof binding.sessionId === 'string'
|
|
570
587
|
&& typeof binding.sessionCreatedAt === 'number'
|
|
571
588
|
&& Array.isArray(binding.knownRootSessionIds)
|
|
@@ -608,15 +625,14 @@ function clearBinding(): void {
|
|
|
608
625
|
|
|
609
626
|
function writeBinding(binding: SessionBinding): boolean {
|
|
610
627
|
const current = state!;
|
|
611
|
-
current.sessionId = binding.sessionId;
|
|
612
|
-
current.sessionCreatedAt = binding.sessionCreatedAt;
|
|
613
|
-
current.knownRootSessionIds = binding.knownRootSessionIds;
|
|
614
|
-
|
|
615
628
|
try {
|
|
616
629
|
const path = bindingPath();
|
|
617
630
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
618
631
|
writeFileSync(temporary, JSON.stringify(binding), { mode: 0o600 });
|
|
619
632
|
renameSync(temporary, path);
|
|
633
|
+
current.sessionId = binding.sessionId;
|
|
634
|
+
current.sessionCreatedAt = binding.sessionCreatedAt;
|
|
635
|
+
current.knownRootSessionIds = binding.knownRootSessionIds;
|
|
620
636
|
return true;
|
|
621
637
|
} catch (err) {
|
|
622
638
|
log(`session binding write failed: ${err}`);
|
|
@@ -624,10 +640,46 @@ function writeBinding(binding: SessionBinding): boolean {
|
|
|
624
640
|
}
|
|
625
641
|
}
|
|
626
642
|
|
|
643
|
+
function claimPlaceholderBinding(): boolean {
|
|
644
|
+
const current = state!;
|
|
645
|
+
const claim: SessionBindingClaim = {
|
|
646
|
+
version: 1,
|
|
647
|
+
directory: current.directory,
|
|
648
|
+
droneLabel: current.droneLabel,
|
|
649
|
+
cubeName: current.cubeName,
|
|
650
|
+
};
|
|
651
|
+
const path = bindingClaimPath();
|
|
652
|
+
try {
|
|
653
|
+
writeFileSync(path, JSON.stringify(claim), { mode: 0o600, flag: 'wx' });
|
|
654
|
+
return true;
|
|
655
|
+
} catch (error) {
|
|
656
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
|
|
657
|
+
log(`session binding claim failed: ${error}`);
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
try {
|
|
662
|
+
const existing = JSON.parse(readFileSync(path, 'utf8')) as SessionBindingClaim;
|
|
663
|
+
return existing.version === 1
|
|
664
|
+
&& existing.directory === claim.directory
|
|
665
|
+
&& existing.droneLabel === claim.droneLabel
|
|
666
|
+
&& existing.cubeName === claim.cubeName;
|
|
667
|
+
} catch {
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function dropCachedBinding(): void {
|
|
673
|
+
if (!state) return;
|
|
674
|
+
state.sessionId = null;
|
|
675
|
+
state.sessionCreatedAt = null;
|
|
676
|
+
state.knownRootSessionIds = [];
|
|
677
|
+
}
|
|
678
|
+
|
|
627
679
|
function saveBinding(session: OCSession, knownRootSessionIds: string[]): void {
|
|
628
680
|
const current = state!;
|
|
629
681
|
const binding: SessionBinding = {
|
|
630
|
-
version:
|
|
682
|
+
version: 5,
|
|
631
683
|
sessionId: session.id,
|
|
632
684
|
sessionCreatedAt: session.time.created,
|
|
633
685
|
knownRootSessionIds,
|
|
@@ -635,6 +687,7 @@ function saveBinding(session: OCSession, knownRootSessionIds: string[]): void {
|
|
|
635
687
|
directory: current.directory,
|
|
636
688
|
droneLabel: current.droneLabel,
|
|
637
689
|
cubeName: current.cubeName,
|
|
690
|
+
launchIdentity: current.launchIdentity,
|
|
638
691
|
pendingSubmissions: [...current.pendingSubmissions].map(([entryId, pending]) => ({
|
|
639
692
|
entryId,
|
|
640
693
|
sourceEntryId: pending.sourceEntryId,
|
|
@@ -648,7 +701,7 @@ function persistCurrentBinding(): boolean {
|
|
|
648
701
|
const current = state;
|
|
649
702
|
if (!current?.sessionId || current.sessionCreatedAt === null) return false;
|
|
650
703
|
return writeBinding({
|
|
651
|
-
version:
|
|
704
|
+
version: 5,
|
|
652
705
|
sessionId: current.sessionId,
|
|
653
706
|
sessionCreatedAt: current.sessionCreatedAt,
|
|
654
707
|
knownRootSessionIds: current.knownRootSessionIds,
|
|
@@ -656,6 +709,7 @@ function persistCurrentBinding(): boolean {
|
|
|
656
709
|
directory: current.directory,
|
|
657
710
|
droneLabel: current.droneLabel,
|
|
658
711
|
cubeName: current.cubeName,
|
|
712
|
+
launchIdentity: current.launchIdentity,
|
|
659
713
|
pendingSubmissions: [...current.pendingSubmissions].map(([entryId, pending]) => ({
|
|
660
714
|
entryId,
|
|
661
715
|
sourceEntryId: pending.sourceEntryId,
|
|
@@ -667,25 +721,34 @@ function persistCurrentBinding(): boolean {
|
|
|
667
721
|
function restoreBinding(): SessionBinding | null {
|
|
668
722
|
if (!state) return null;
|
|
669
723
|
if (state.sessionId && state.sessionCreatedAt !== null) {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
droneLabel: state.droneLabel,
|
|
678
|
-
cubeName: state.cubeName,
|
|
679
|
-
pendingSubmissions: [...state.pendingSubmissions].map(([entryId, pending]) => ({
|
|
680
|
-
entryId,
|
|
681
|
-
sourceEntryId: pending.sourceEntryId,
|
|
682
|
-
sessionId: pending.sessionId,
|
|
683
|
-
})),
|
|
684
|
-
};
|
|
724
|
+
const persisted = readBinding();
|
|
725
|
+
if (
|
|
726
|
+
persisted?.droneLabel === state.droneLabel
|
|
727
|
+
&& persisted.cubeName === state.cubeName
|
|
728
|
+
) return persisted;
|
|
729
|
+
dropCachedBinding();
|
|
730
|
+
return null;
|
|
685
731
|
}
|
|
686
732
|
|
|
687
|
-
|
|
733
|
+
let binding = readBinding();
|
|
688
734
|
if (!binding) return null;
|
|
735
|
+
if (binding.droneLabel === 'opencode' && binding.cubeName === 'borg') {
|
|
736
|
+
if (!claimPlaceholderBinding()) return null;
|
|
737
|
+
if (!writeBinding({
|
|
738
|
+
...binding,
|
|
739
|
+
droneLabel: state.droneLabel,
|
|
740
|
+
cubeName: state.cubeName,
|
|
741
|
+
})) return null;
|
|
742
|
+
binding = readBinding();
|
|
743
|
+
if (
|
|
744
|
+
!binding
|
|
745
|
+
|| binding.droneLabel !== state.droneLabel
|
|
746
|
+
|| binding.cubeName !== state.cubeName
|
|
747
|
+
) {
|
|
748
|
+
dropCachedBinding();
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
689
752
|
state.sessionId = binding.sessionId;
|
|
690
753
|
state.sessionCreatedAt = binding.sessionCreatedAt;
|
|
691
754
|
state.knownRootSessionIds = binding.knownRootSessionIds;
|
|
@@ -747,35 +810,126 @@ function launchCorrelationMatchCount(messages: OCMessage[], correlationIdentity:
|
|
|
747
810
|
* therefore allowed only when it received this launch's correlation metadata.
|
|
748
811
|
*/
|
|
749
812
|
async function findLaunchSession(correlationIdentity: string): Promise<{
|
|
813
|
+
kind: 'found';
|
|
750
814
|
session: OCSession;
|
|
751
815
|
knownRootSessionIds: string[];
|
|
752
|
-
} |
|
|
816
|
+
} | {
|
|
817
|
+
kind: 'superseded';
|
|
818
|
+
} | {
|
|
819
|
+
kind: 'list-failed';
|
|
820
|
+
failureCode: string;
|
|
821
|
+
errorClass: string;
|
|
822
|
+
httpStatus: number | null;
|
|
823
|
+
} | {
|
|
824
|
+
kind: 'directory-miss';
|
|
825
|
+
listedCount: number;
|
|
826
|
+
directoryCount: 0;
|
|
827
|
+
} | {
|
|
828
|
+
kind: 'message-list-failed';
|
|
829
|
+
listedCount: number;
|
|
830
|
+
directoryCount: number;
|
|
831
|
+
failureCode: string;
|
|
832
|
+
errorClass: string;
|
|
833
|
+
httpStatus: number | null;
|
|
834
|
+
} | {
|
|
835
|
+
kind: 'correlation-mismatch';
|
|
836
|
+
listedCount: number;
|
|
837
|
+
directoryCount: number;
|
|
838
|
+
matchCount: number;
|
|
839
|
+
}> {
|
|
753
840
|
const owner = state!;
|
|
754
841
|
const observationSequence = ++owner.nextObservationSequence;
|
|
842
|
+
let listedSessions: OCSession[];
|
|
755
843
|
try {
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
844
|
+
listedSessions = await listSessions();
|
|
845
|
+
} catch (error) {
|
|
846
|
+
if (state === owner) recordOpenCodeFailure(owner, error, observationSequence);
|
|
847
|
+
return { kind: 'list-failed', ...openCodeFailureDiagnostic(error) };
|
|
848
|
+
}
|
|
849
|
+
if (state !== owner) return { kind: 'superseded' };
|
|
850
|
+
const sessions = listedSessions.filter(
|
|
851
|
+
(session) => session.directory === owner.directory,
|
|
852
|
+
);
|
|
853
|
+
if (sessions.length === 0) {
|
|
854
|
+
return { kind: 'directory-miss', listedCount: listedSessions.length, directoryCount: 0 };
|
|
855
|
+
}
|
|
856
|
+
const knownRootSessionIds = sessions
|
|
857
|
+
.filter(isTopLevelSession)
|
|
858
|
+
.map((session) => session.id);
|
|
859
|
+
let candidates: Array<{ session: OCSession; matchCount: number }>;
|
|
860
|
+
try {
|
|
861
|
+
candidates = await Promise.all(sessions.map(async (session) => ({
|
|
765
862
|
session,
|
|
766
863
|
matchCount: launchCorrelationMatchCount(
|
|
767
864
|
await listSessionMessages(session.id),
|
|
768
865
|
correlationIdentity,
|
|
769
866
|
),
|
|
770
867
|
})));
|
|
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
868
|
} catch (error) {
|
|
777
869
|
if (state === owner) recordOpenCodeFailure(owner, error, observationSequence);
|
|
778
|
-
return
|
|
870
|
+
return {
|
|
871
|
+
kind: 'message-list-failed',
|
|
872
|
+
listedCount: listedSessions.length,
|
|
873
|
+
directoryCount: sessions.length,
|
|
874
|
+
...openCodeFailureDiagnostic(error),
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
if (state !== owner) return { kind: 'superseded' };
|
|
878
|
+
const totalMatches = candidates.reduce((total, candidate) => total + candidate.matchCount, 0);
|
|
879
|
+
if (totalMatches !== 1) {
|
|
880
|
+
return {
|
|
881
|
+
kind: 'correlation-mismatch',
|
|
882
|
+
listedCount: listedSessions.length,
|
|
883
|
+
directoryCount: sessions.length,
|
|
884
|
+
matchCount: totalMatches,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
const matched = candidates.find((candidate) => candidate.matchCount === 1)!;
|
|
888
|
+
return { kind: 'found', session: matched.session, knownRootSessionIds };
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
type LaunchSessionSearchFailure = Exclude<Awaited<ReturnType<typeof findLaunchSession>>, {
|
|
892
|
+
kind: 'found' | 'superseded';
|
|
893
|
+
}>;
|
|
894
|
+
|
|
895
|
+
function logLaunchSessionSearchFailure(
|
|
896
|
+
failure: LaunchSessionSearchFailure,
|
|
897
|
+
attempts: number,
|
|
898
|
+
owner: OpenCodeDroneState,
|
|
899
|
+
): void {
|
|
900
|
+
switch (failure.kind) {
|
|
901
|
+
case 'list-failed':
|
|
902
|
+
log(
|
|
903
|
+
`kickoff: session search list-failed code=${failure.failureCode} `
|
|
904
|
+
+ `class=${failure.errorClass} status=${failure.httpStatus ?? 'none'} `
|
|
905
|
+
+ `listed=unknown directory=unknown matches=unknown attempts=${attempts}`,
|
|
906
|
+
owner,
|
|
907
|
+
);
|
|
908
|
+
break;
|
|
909
|
+
case 'directory-miss':
|
|
910
|
+
log(
|
|
911
|
+
`kickoff: session search directory-miss listed=${failure.listedCount} `
|
|
912
|
+
+ `directory=${failure.directoryCount} matches=0 attempts=${attempts}`,
|
|
913
|
+
owner,
|
|
914
|
+
);
|
|
915
|
+
break;
|
|
916
|
+
case 'message-list-failed':
|
|
917
|
+
log(
|
|
918
|
+
`kickoff: session search messages-failed code=${failure.failureCode} `
|
|
919
|
+
+ `class=${failure.errorClass} status=${failure.httpStatus ?? 'none'} `
|
|
920
|
+
+ `listed=${failure.listedCount} directory=${failure.directoryCount} `
|
|
921
|
+
+ `matches=unknown attempts=${attempts}`,
|
|
922
|
+
owner,
|
|
923
|
+
);
|
|
924
|
+
break;
|
|
925
|
+
case 'correlation-mismatch':
|
|
926
|
+
log(
|
|
927
|
+
`kickoff: session search correlation-mismatch listed=${failure.listedCount} `
|
|
928
|
+
+ `directory=${failure.directoryCount} `
|
|
929
|
+
+ `matches=${failure.matchCount} attempts=${attempts}`,
|
|
930
|
+
owner,
|
|
931
|
+
);
|
|
932
|
+
break;
|
|
779
933
|
}
|
|
780
934
|
}
|
|
781
935
|
|
|
@@ -836,6 +990,18 @@ function openCodeFailureCode(error: unknown): string {
|
|
|
836
990
|
return error instanceof Error && error.name ? error.name : 'unknown';
|
|
837
991
|
}
|
|
838
992
|
|
|
993
|
+
function openCodeFailureDiagnostic(error: unknown): {
|
|
994
|
+
failureCode: string;
|
|
995
|
+
errorClass: string;
|
|
996
|
+
httpStatus: number | null;
|
|
997
|
+
} {
|
|
998
|
+
return {
|
|
999
|
+
failureCode: openCodeFailureCode(error),
|
|
1000
|
+
errorClass: error instanceof Error && error.name ? error.name : 'unknown',
|
|
1001
|
+
httpStatus: error instanceof OpenCodeHttpError ? error.status : null,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
|
|
839
1005
|
function updateLastOpenCodeObservation(
|
|
840
1006
|
owner: OpenCodeDroneState,
|
|
841
1007
|
sequence: number,
|
|
@@ -1185,31 +1351,59 @@ export async function injectInitialKickoff(launch: OpenCodeLaunchKickoff): Promi
|
|
|
1185
1351
|
|
|
1186
1352
|
try {
|
|
1187
1353
|
// Wait for the server.
|
|
1354
|
+
let serverReady = false;
|
|
1355
|
+
let lastServerError: unknown;
|
|
1188
1356
|
for (let i = 0; i < 30; i++) {
|
|
1189
1357
|
try {
|
|
1190
1358
|
await listSessions();
|
|
1359
|
+
if (state !== owner) return false;
|
|
1360
|
+
serverReady = true;
|
|
1191
1361
|
log(`kickoff: server ready (attempt ${i + 1})`, owner);
|
|
1192
1362
|
break;
|
|
1193
|
-
} catch {
|
|
1194
|
-
|
|
1363
|
+
} catch (error) {
|
|
1364
|
+
if (state !== owner) return false;
|
|
1365
|
+
lastServerError = error;
|
|
1195
1366
|
}
|
|
1196
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
1367
|
+
if (i < 29) await new Promise((r) => setTimeout(r, 1000));
|
|
1368
|
+
}
|
|
1369
|
+
if (!serverReady) {
|
|
1370
|
+
const observationSequence = ++owner.nextObservationSequence;
|
|
1371
|
+
recordOpenCodeFailure(owner, lastServerError, observationSequence);
|
|
1372
|
+
const failure = openCodeFailureDiagnostic(lastServerError);
|
|
1373
|
+
log(
|
|
1374
|
+
`kickoff: server unavailable code=${failure.failureCode} `
|
|
1375
|
+
+ `class=${failure.errorClass} status=${failure.httpStatus ?? 'none'} attempts=30`,
|
|
1376
|
+
owner,
|
|
1377
|
+
);
|
|
1378
|
+
return false;
|
|
1197
1379
|
}
|
|
1198
1380
|
|
|
1199
1381
|
// Capture the launch-selected session, including explicit resume/fork
|
|
1200
1382
|
// targets. Unrelated sessions do not contain this launch's metadata identity.
|
|
1383
|
+
let lastSearchFailure: LaunchSessionSearchFailure | null = null;
|
|
1201
1384
|
for (let i = 0; i < 30; i++) {
|
|
1202
|
-
const
|
|
1203
|
-
if (
|
|
1204
|
-
|
|
1205
|
-
saveBinding(
|
|
1206
|
-
log(`kickoff: bound session ${
|
|
1385
|
+
const result = await findLaunchSession(launch.correlationIdentity);
|
|
1386
|
+
if (result.kind === 'superseded') return false;
|
|
1387
|
+
if (result.kind === 'found') {
|
|
1388
|
+
saveBinding(result.session, result.knownRootSessionIds);
|
|
1389
|
+
log(`kickoff: bound session ${result.session.id.slice(0, 8)}…`, owner);
|
|
1207
1390
|
return true;
|
|
1208
1391
|
}
|
|
1209
|
-
|
|
1392
|
+
lastSearchFailure = result;
|
|
1393
|
+
if (i < 29) await new Promise((r) => setTimeout(r, 1000));
|
|
1210
1394
|
}
|
|
1211
1395
|
|
|
1212
|
-
|
|
1396
|
+
if (lastSearchFailure) {
|
|
1397
|
+
logLaunchSessionSearchFailure(lastSearchFailure, 30, owner);
|
|
1398
|
+
if (
|
|
1399
|
+
lastSearchFailure.kind === 'directory-miss'
|
|
1400
|
+
|| lastSearchFailure.kind === 'correlation-mismatch'
|
|
1401
|
+
) {
|
|
1402
|
+
updateLastOpenCodeObservation(owner, ++owner.nextObservationSequence, {
|
|
1403
|
+
lastFailureCode: 'no-target',
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1213
1407
|
return false;
|
|
1214
1408
|
} catch (err) {
|
|
1215
1409
|
log(`kickoff error: ${err}`, owner);
|
|
@@ -1438,11 +1632,24 @@ export function __getOpenCodeDiagnosticLogPathForTests(): string {
|
|
|
1438
1632
|
return diagnosticLogPath(state);
|
|
1439
1633
|
}
|
|
1440
1634
|
|
|
1635
|
+
export function __getOpenCodeBindingPathForTests(): string {
|
|
1636
|
+
if (!state) throw new Error('OpenCode drone is not connected');
|
|
1637
|
+
return bindingPath();
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1441
1640
|
export function __getOpenCodeLastObservationForTests(): OpenCodeLastObservation {
|
|
1442
1641
|
if (!state) throw new Error('OpenCode drone is not connected');
|
|
1443
1642
|
return { ...state.lastObservation };
|
|
1444
1643
|
}
|
|
1445
1644
|
|
|
1645
|
+
export function __decodeOpenCodeSessionForTests(value: unknown): unknown {
|
|
1646
|
+
return decodeSession(value);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
export async function __listOpenCodeSessionsForTests(): Promise<unknown[]> {
|
|
1650
|
+
return listSessions();
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1446
1653
|
export function computeOpenCodePort(droneId: string, base: number = 14096): number {
|
|
1447
1654
|
let hash = 0;
|
|
1448
1655
|
for (let i = 0; i < droneId.length; i++) {
|
|
@@ -1519,6 +1726,11 @@ export function __resetOpenCodeDroneForTests(): void {
|
|
|
1519
1726
|
} catch {
|
|
1520
1727
|
// Already removed.
|
|
1521
1728
|
}
|
|
1729
|
+
try {
|
|
1730
|
+
unlinkSync(`${path}.claim`);
|
|
1731
|
+
} catch {
|
|
1732
|
+
// Already removed.
|
|
1733
|
+
}
|
|
1522
1734
|
}
|
|
1523
1735
|
bindingPathsForTests.clear();
|
|
1524
1736
|
for (const path of diagnosticLogPathsForTests) {
|
|
@@ -44,3 +44,8 @@ export function openCodeApiPasswordFromEnv(env: NodeJS.ProcessEnv): string | nul
|
|
|
44
44
|
const password = env[OPENCODE_SERVER_PASSWORD_ENV];
|
|
45
45
|
return isOpenCode256BitIdentity(password) ? password : null;
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
export function openCodeLaunchCorrelationFromEnv(env: NodeJS.ProcessEnv): string | null {
|
|
49
|
+
const identity = env[BORG_OPENCODE_LAUNCH_CORRELATION_ENV];
|
|
50
|
+
return isOpenCode256BitIdentity(identity) ? identity : null;
|
|
51
|
+
}
|
package/src/wake-path-health.ts
CHANGED