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.
@@ -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
- const key = [current.serverUrl, current.directory, current.cubeName, current.droneLabel].join('\0');
48
- return createHash('sha256').update(key).digest('hex').slice(0, 24);
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: 4;
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 path = join(tmpdir(), `borg-opencode-session-${stateIdentityDigest(current)}.json`);
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
- return binding.version === 4
565
- && binding.serverUrl === current.serverUrl
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.droneLabel === current.droneLabel
568
- && binding.cubeName === current.cubeName
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: 4,
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: 4,
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
- return {
671
- version: 4,
672
- sessionId: state.sessionId,
673
- sessionCreatedAt: state.sessionCreatedAt,
674
- knownRootSessionIds: state.knownRootSessionIds,
675
- serverUrl: state.serverUrl,
676
- directory: state.directory,
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
- const binding = readBinding();
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
- } | null> {
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
- const listedSessions = await listSessions();
757
- if (state !== owner) return null;
758
- const sessions = listedSessions.filter(
759
- (session) => session.directory === owner.directory,
760
- );
761
- const knownRootSessionIds = sessions
762
- .filter(isTopLevelSession)
763
- .map((session) => session.id);
764
- const candidates = await Promise.all(sessions.map(async (session) => ({
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 null;
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
- // not ready yet
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 binding = await findLaunchSession(launch.correlationIdentity);
1203
- if (binding) {
1204
- if (state !== owner) return false;
1205
- saveBinding(binding.session, binding.knownRootSessionIds);
1206
- log(`kickoff: bound session ${binding.session.id.slice(0, 8)}…`, owner);
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
- await new Promise((r) => setTimeout(r, 1000));
1392
+ lastSearchFailure = result;
1393
+ if (i < 29) await new Promise((r) => setTimeout(r, 1000));
1210
1394
  }
1211
1395
 
1212
- log('kickoff: no session found', owner);
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
+ }
@@ -49,7 +49,9 @@ export function openCodeWakePathHealthy(
49
49
  ) {
50
50
  return null;
51
51
  }
52
- if (state.sessionId === null) return null;
52
+ if (state.sessionId === null) {
53
+ return state.lastFailureCode === null ? null : false;
54
+ }
53
55
  return true;
54
56
  }
55
57