borgmcp 5.1.2 → 5.3.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/dist/agent-runtime.d.ts +2 -4
- package/dist/agent-runtime.d.ts.map +1 -1
- package/dist/agent-runtime.js +3 -5
- package/dist/agent-runtime.js.map +1 -1
- package/dist/assimilate-cmd.d.ts +1 -11
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +37 -48
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +1 -4
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +36 -77
- package/dist/claude.js.map +1 -1
- package/dist/codex-app-server.d.ts +4 -6
- package/dist/codex-app-server.d.ts.map +1 -1
- package/dist/codex-app-server.js +4 -25
- package/dist/codex-app-server.js.map +1 -1
- package/dist/codex-app-wake.d.ts +8 -10
- package/dist/codex-app-wake.d.ts.map +1 -1
- package/dist/codex-app-wake.js +49 -66
- package/dist/codex-app-wake.js.map +1 -1
- package/dist/codex-launch.d.ts +15 -24
- package/dist/codex-launch.d.ts.map +1 -1
- package/dist/codex-launch.js +29 -55
- package/dist/codex-launch.js.map +1 -1
- package/dist/codex-remote.d.ts +15 -8
- package/dist/codex-remote.d.ts.map +1 -1
- package/dist/codex-remote.js +71 -70
- package/dist/codex-remote.js.map +1 -1
- package/dist/codex-wake-resolve.d.ts +12 -31
- package/dist/codex-wake-resolve.d.ts.map +1 -1
- package/dist/codex-wake-resolve.js +21 -38
- package/dist/codex-wake-resolve.js.map +1 -1
- package/dist/config-utils.js +2 -2
- package/dist/config-utils.js.map +1 -1
- package/dist/cubes.d.ts +0 -17
- package/dist/cubes.d.ts.map +1 -1
- package/dist/cubes.js +0 -81
- package/dist/cubes.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +4 -1
- package/dist/log-stream.js.map +1 -1
- package/dist/regen-format.d.ts +3 -1
- package/dist/regen-format.d.ts.map +1 -1
- package/dist/regen-format.js +24 -4
- package/dist/regen-format.js.map +1 -1
- package/dist/remote-client.d.ts +4 -3
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +9 -3
- package/dist/remote-client.js.map +1 -1
- package/docs/RELEASING.md +2 -2
- package/package.json +2 -2
- package/src/agent-runtime.ts +3 -5
- package/src/assimilate-cmd.ts +35 -58
- package/src/assimilate-deps.ts +0 -4
- package/src/claude.ts +34 -81
- package/src/codex-app-server.ts +8 -25
- package/src/codex-app-wake.ts +46 -73
- package/src/codex-launch.ts +43 -76
- package/src/codex-remote.ts +90 -82
- package/src/codex-wake-resolve.ts +26 -47
- package/src/config-utils.ts +2 -2
- package/src/cubes.ts +2 -100
- package/src/index.ts +3 -1
- package/src/log-stream.ts +4 -1
- package/src/regen-format.ts +28 -6
- package/src/remote-client.ts +14 -6
package/src/assimilate-cmd.ts
CHANGED
|
@@ -10,20 +10,15 @@ import {
|
|
|
10
10
|
import { validateName } from './name-validator.js';
|
|
11
11
|
import { renderAssimilationWelcome } from './assimilate-welcome.js';
|
|
12
12
|
import { shellEscape } from './shell-escape.js';
|
|
13
|
-
import {
|
|
13
|
+
import { type CodexRemoteLaunch } from './codex-remote.js';
|
|
14
14
|
import {
|
|
15
15
|
buildAgentKickoffPrompt,
|
|
16
16
|
buildKickoffWakePathClause,
|
|
17
|
-
|
|
18
|
-
socketPathFromRemoteArgs,
|
|
17
|
+
buildCodexLaunchArgs,
|
|
19
18
|
} from './codex-launch.js';
|
|
20
19
|
import { perWorktreeBranchName, computeWorktreePath, localBranchExists, isMerged } from './worktree-lifecycle.js';
|
|
21
20
|
import { DroneEvictedError } from './drone-lifecycle.js';
|
|
22
|
-
import { codexBorgSessionConfigArgs } from './launch-gate.js';
|
|
23
21
|
import {
|
|
24
|
-
codexAgentKindConfigArgs,
|
|
25
|
-
codexRemoteWakeConfigArgs,
|
|
26
|
-
codexStateRootConfigArgs,
|
|
27
22
|
withAgentRuntimeEnv,
|
|
28
23
|
} from './agent-runtime.js';
|
|
29
24
|
import {
|
|
@@ -405,13 +400,6 @@ export interface AssimilateDeps {
|
|
|
405
400
|
setCliPreferenceForWorktree: (cli: BorgCli, worktree: string) => Promise<void>;
|
|
406
401
|
resolveCli: (explicit?: BorgCli) => Promise<BorgCli>;
|
|
407
402
|
prepareCodexRemoteLaunch: () => Promise<CodexRemoteLaunch>;
|
|
408
|
-
setCodexWakeTarget: (cubeId: string, droneId: string, target: { threadId: string; socketPath: string }) => Promise<void>;
|
|
409
|
-
findLoadedCodexThread: (options: {
|
|
410
|
-
socketPath: string;
|
|
411
|
-
cwd: string;
|
|
412
|
-
previewIncludes: string;
|
|
413
|
-
updatedAfter: number;
|
|
414
|
-
}) => Promise<string | null>;
|
|
415
403
|
|
|
416
404
|
}
|
|
417
405
|
|
|
@@ -952,13 +940,11 @@ export async function finalizeAssimilationSeat(
|
|
|
952
940
|
export type LaunchDeps = Pick<
|
|
953
941
|
AssimilateDeps,
|
|
954
942
|
| 'exec'
|
|
955
|
-
| 'findLoadedCodexThread'
|
|
956
943
|
| 'getInboxPath'
|
|
957
944
|
| 'isTTY'
|
|
958
945
|
| 'prepareCodexRemoteLaunch'
|
|
959
946
|
| 'probeMcpReady'
|
|
960
947
|
| 'resolveCliApprovals'
|
|
961
|
-
| 'setCodexWakeTarget'
|
|
962
948
|
| 'setTerminalTitle'
|
|
963
949
|
| 'stderr'
|
|
964
950
|
| 'stdout'
|
|
@@ -1019,17 +1005,14 @@ export async function launchAssimilatedAgent(
|
|
|
1019
1005
|
);
|
|
1020
1006
|
}
|
|
1021
1007
|
const inboxPath = deps.getInboxPath(result.cube_id, result.drone_id);
|
|
1022
|
-
const codexWakeNonce = cli === 'codex' ? `borg-wake-${randomUUID()}` : null;
|
|
1023
1008
|
const monitorClause = buildKickoffWakePathClause(
|
|
1024
1009
|
cli,
|
|
1025
1010
|
cli === 'claude' ? inboxPath : null,
|
|
1026
1011
|
cli === 'claude' ? monitorStateRoot : null,
|
|
1027
1012
|
);
|
|
1028
|
-
let codexWakePathClause: string | undefined;
|
|
1029
|
-
let remoteArgs: string[] = [];
|
|
1030
1013
|
let launchArgs: string[];
|
|
1031
|
-
let codexSocketPath: string | null = null;
|
|
1032
1014
|
let codexServerCleanup: (() => void) | null = null;
|
|
1015
|
+
let codexRemote: CodexRemoteLaunch | null = null;
|
|
1033
1016
|
const launchApproval = deps.resolveCliApprovals
|
|
1034
1017
|
? await deps.resolveCliApprovals(cli, agentCwd, { skipOverride: flags.noBorgApprovalOverride })
|
|
1035
1018
|
: { codexArgs: [] };
|
|
@@ -1050,39 +1033,36 @@ export async function launchAssimilatedAgent(
|
|
|
1050
1033
|
for (const key of modelEnv.unset) delete childEnv[key];
|
|
1051
1034
|
|
|
1052
1035
|
if (cli === 'codex') {
|
|
1053
|
-
|
|
1054
|
-
if (
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
} else {
|
|
1059
|
-
codexWakePathClause = 'Codex wake-path capability check passed: remote-control socket established for this session.';
|
|
1036
|
+
codexRemote = await deps.prepareCodexRemoteLaunch();
|
|
1037
|
+
if (!codexRemote.ready) {
|
|
1038
|
+
const stderr = codexRemote.stderr ? `\n${codexRemote.stderr}` : '';
|
|
1039
|
+
deps.stderr(`Codex launch refused: ${codexRemote.reason}${stderr}\n`);
|
|
1040
|
+
return 1;
|
|
1060
1041
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
codexSocketPath = socketPathFromRemoteArgs(remote.args);
|
|
1064
|
-
codexServerCleanup = remote.server?.cleanup ?? null;
|
|
1042
|
+
Object.assign(childEnv, codexRemote.env);
|
|
1043
|
+
codexServerCleanup = codexRemote.server.cleanup;
|
|
1065
1044
|
}
|
|
1066
1045
|
const kickoff = buildAgentKickoffPrompt({
|
|
1067
1046
|
cli,
|
|
1068
|
-
codexWakeNonce,
|
|
1069
1047
|
monitorClause,
|
|
1070
|
-
codexWakePathClause,
|
|
1071
1048
|
});
|
|
1072
1049
|
let openCodeKickoff: ReturnType<typeof createOpenCodeLaunchKickoff> | null = null;
|
|
1073
1050
|
let dronePort: number | undefined;
|
|
1074
1051
|
launchArgs = [kickoff];
|
|
1075
1052
|
if (cli === 'codex') {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1053
|
+
const plan = buildCodexLaunchArgs({
|
|
1054
|
+
remote: codexRemote!,
|
|
1055
|
+
cwd: agentCwd,
|
|
1056
|
+
kickoff,
|
|
1057
|
+
approvalArgs: launchApproval.codexArgs,
|
|
1058
|
+
accessArgs: codexLaunchDirectoryArgs(launchAccessPaths),
|
|
1059
|
+
});
|
|
1060
|
+
if (!plan.ready) {
|
|
1061
|
+
codexServerCleanup?.();
|
|
1062
|
+
deps.stderr(`Codex launch refused: ${plan.reason}\n`);
|
|
1063
|
+
return 1;
|
|
1064
|
+
}
|
|
1065
|
+
launchArgs = plan.args;
|
|
1086
1066
|
} else if (cli === 'opencode') {
|
|
1087
1067
|
dronePort = await allocateOpenCodePort();
|
|
1088
1068
|
childEnv.BORG_OPENCODE_PORT = String(dronePort);
|
|
@@ -1094,17 +1074,12 @@ export async function launchAssimilatedAgent(
|
|
|
1094
1074
|
launchArgs = buildOpenCodeLaunchArgs(agentCwd, dronePort, openCodeKickoff.prompt);
|
|
1095
1075
|
}
|
|
1096
1076
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
socketPath: codexSocketPath,
|
|
1104
|
-
cwd: agentCwd,
|
|
1105
|
-
previewNeedle: codexWakeNonce,
|
|
1106
|
-
launchedAtSeconds: Math.floor(Date.now() / 1000),
|
|
1107
|
-
});
|
|
1077
|
+
let exitPromise: Promise<number>;
|
|
1078
|
+
try {
|
|
1079
|
+
exitPromise = deps.exec(cli, launchArgs, agentCwd, childEnv);
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
codexServerCleanup?.();
|
|
1082
|
+
throw error;
|
|
1108
1083
|
}
|
|
1109
1084
|
if (cli === 'opencode' && openCodeKickoff) {
|
|
1110
1085
|
const launchKickoff = openCodeKickoff;
|
|
@@ -1117,12 +1092,14 @@ export async function launchAssimilatedAgent(
|
|
|
1117
1092
|
launchIdentity: launchKickoff.correlationIdentity,
|
|
1118
1093
|
}).then(() => injectInitialKickoff(launchKickoff)).catch(() => {});
|
|
1119
1094
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1095
|
+
let exitCode: number;
|
|
1096
|
+
try {
|
|
1097
|
+
exitCode = await exitPromise;
|
|
1098
|
+
} finally {
|
|
1122
1099
|
try {
|
|
1123
|
-
codexServerCleanup();
|
|
1100
|
+
codexServerCleanup?.();
|
|
1124
1101
|
} catch {
|
|
1125
|
-
// Best-effort cleanup after
|
|
1102
|
+
// Best-effort cleanup after Codex exits or fails to spawn.
|
|
1126
1103
|
}
|
|
1127
1104
|
}
|
|
1128
1105
|
if (spawnedWorktreePath && originalCwd !== spawnedWorktreePath) {
|
package/src/assimilate-deps.ts
CHANGED
|
@@ -61,7 +61,6 @@ import {
|
|
|
61
61
|
hasPersistedActiveCube as cubesHasPersistedActive,
|
|
62
62
|
inboxPathForDrone,
|
|
63
63
|
setProjectCliPreference,
|
|
64
|
-
setCodexWakeTarget,
|
|
65
64
|
} from './cubes.js';
|
|
66
65
|
import {
|
|
67
66
|
addProjectSessionStartHook,
|
|
@@ -71,7 +70,6 @@ import { findPendingServerEnrollment, listServerCredentialOrigins } from './conf
|
|
|
71
70
|
import { setTerminalTitle as setTitle } from './terminal-title.js';
|
|
72
71
|
import { defaultCliChoiceDeps, resolveCliChoice } from './cli-platform.js';
|
|
73
72
|
import { prepareCodexRemoteLaunch, defaultCodexRemoteDeps } from './codex-remote.js';
|
|
74
|
-
import { findLoadedCodexThread } from './codex-app-server.js';
|
|
75
73
|
import { defaultApprovalIo, resolveLaunchBorgApprovals } from './cli-tool-approval.js';
|
|
76
74
|
import { ensurePrivateBorgConfigRoot } from './private-root.js';
|
|
77
75
|
import {
|
|
@@ -577,7 +575,5 @@ export function buildDefaultAssimilateDeps(
|
|
|
577
575
|
),
|
|
578
576
|
setCliPreferenceForWorktree: (cli, worktree) => setProjectCliPreference(cli, worktree),
|
|
579
577
|
prepareCodexRemoteLaunch: () => prepareCodexRemoteLaunch(defaultCodexRemoteDeps()),
|
|
580
|
-
setCodexWakeTarget,
|
|
581
|
-
findLoadedCodexThread,
|
|
582
578
|
};
|
|
583
579
|
}
|
package/src/claude.ts
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { spawn } from 'child_process';
|
|
19
|
-
import { randomUUID } from 'node:crypto';
|
|
20
19
|
import { realpathSync } from 'node:fs';
|
|
21
20
|
import { homedir } from 'node:os';
|
|
22
21
|
import { basename } from 'node:path';
|
|
@@ -30,8 +29,6 @@ import {
|
|
|
30
29
|
getActiveCube,
|
|
31
30
|
inboxPathForDrone,
|
|
32
31
|
LaunchSeatIdentityChangedError,
|
|
33
|
-
setCodexWakeTarget,
|
|
34
|
-
pruneDeadCodexWakeTargets,
|
|
35
32
|
type ActiveCube,
|
|
36
33
|
type BorgCli,
|
|
37
34
|
} from './cubes.js';
|
|
@@ -82,22 +79,15 @@ import { setTerminalTitle } from './terminal-title.js';
|
|
|
82
79
|
import { initConsolePrefix, consolePrefix } from './console-prefix.js';
|
|
83
80
|
import { initDebugFromArgv } from './debug.js';
|
|
84
81
|
import { configuredCliNames, defaultCliChoiceDeps, detectCliAvailability, detectCliConfiguration, parseCliFlag, resolveCliChoice } from './cli-platform.js';
|
|
85
|
-
import { prepareCodexRemoteLaunch, resolveCodexLaunchCwd,
|
|
82
|
+
import { prepareCodexRemoteLaunch, resolveCodexLaunchCwd, defaultCodexRemoteDeps } from './codex-remote.js';
|
|
86
83
|
import {
|
|
87
|
-
BORG_CODEX_REMOTE_WAKE_ENV,
|
|
88
|
-
codexAgentKindConfigArgs,
|
|
89
|
-
codexRemoteWakeConfigArgs,
|
|
90
|
-
codexStateRootConfigArgs,
|
|
91
84
|
withAgentRuntimeEnv,
|
|
92
85
|
} from './agent-runtime.js';
|
|
93
|
-
import { findLoadedCodexThread } from './codex-app-server.js';
|
|
94
86
|
import {
|
|
95
87
|
buildAgentKickoffPrompt,
|
|
96
88
|
buildKickoffWakePathClause,
|
|
97
|
-
|
|
98
|
-
socketPathFromRemoteArgs,
|
|
89
|
+
buildCodexLaunchArgs,
|
|
99
90
|
} from './codex-launch.js';
|
|
100
|
-
import { codexBorgSessionConfigArgs } from './launch-gate.js';
|
|
101
91
|
import {
|
|
102
92
|
addCodexSessionStartHook,
|
|
103
93
|
addCodexUserPromptSubmitHook,
|
|
@@ -639,9 +629,6 @@ async function main() {
|
|
|
639
629
|
: null
|
|
640
630
|
);
|
|
641
631
|
|
|
642
|
-
const codexWakeNonce = cli === 'codex' ? `borg-wake-${randomUUID()}` : null;
|
|
643
|
-
let codexWakePathClause: string | undefined;
|
|
644
|
-
let remoteArgs: string[] = [];
|
|
645
632
|
// gh#673 P1: mark the agent session as borg-launched. Claude Code's MCP
|
|
646
633
|
// child + hook commands inherit this env, gating the borg activation
|
|
647
634
|
// surface (launch-gate.ts). ACTIVATION-only — never a security gate.
|
|
@@ -654,45 +641,33 @@ async function main() {
|
|
|
654
641
|
if (cli === 'opencode' && launchApproval.openCodePermission) {
|
|
655
642
|
launchEnv.OPENCODE_PERMISSION = launchApproval.openCodePermission;
|
|
656
643
|
}
|
|
657
|
-
let codexSocketPath: string | null = null;
|
|
658
644
|
let codexServerCleanup: (() => void) | null = null;
|
|
659
|
-
|
|
645
|
+
let codexRemote: Awaited<ReturnType<typeof prepareCodexRemoteLaunch>> | null = null;
|
|
646
|
+
if (cli === 'codex') {
|
|
647
|
+
if (passthroughArgs.some((arg) => arg === '--remote' || arg.startsWith('--remote='))) {
|
|
648
|
+
console.error(`${consolePrefix()}${chalk.red('Codex launch refused: Borg owns Codex remote control; remove the passthrough --remote option and retry.')}`);
|
|
649
|
+
process.exit(1);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
660
652
|
console.error(`${consolePrefix()}${chalk.gray('◼ Starting Codex remote-wake app-server…')}`);
|
|
661
|
-
|
|
662
|
-
if (
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
codexWakePathClause =
|
|
668
|
-
`Codex wake-path capability check passed: remote-control socket established for this session.`;
|
|
653
|
+
codexRemote = await prepareCodexRemoteLaunch(defaultCodexRemoteDeps());
|
|
654
|
+
if (!codexRemote.ready) {
|
|
655
|
+
const stderr = codexRemote.stderr ? `\n${codexRemote.stderr}` : '';
|
|
656
|
+
console.error(`${consolePrefix()}${chalk.red(`Codex launch refused: ${codexRemote.reason}${stderr}`)}`);
|
|
657
|
+
process.exit(1);
|
|
658
|
+
return;
|
|
669
659
|
}
|
|
670
|
-
remoteArgs = remote.args;
|
|
671
660
|
launchEnv = {
|
|
672
661
|
...withAgentRuntimeEnv(process.env, cli),
|
|
673
|
-
...
|
|
662
|
+
...codexRemote.env,
|
|
674
663
|
BORG_SESSION: '1',
|
|
675
664
|
};
|
|
676
|
-
|
|
677
|
-
codexServerCleanup = remote.server?.cleanup ?? null;
|
|
678
|
-
} else if (cli === 'codex' && passthroughArgs.includes('--remote')) {
|
|
679
|
-
codexWakePathClause =
|
|
680
|
-
`Codex wake-path capability check: using caller-provided --remote socket; if no wake arrives, run borg_regen manually when returning to the session.`;
|
|
681
|
-
codexSocketPath = socketPathFromRemoteArgs(passthroughArgs);
|
|
682
|
-
if (codexSocketPath) {
|
|
683
|
-
launchEnv = {
|
|
684
|
-
...withAgentRuntimeEnv(process.env, cli),
|
|
685
|
-
[BORG_CODEX_REMOTE_WAKE_ENV]: '1',
|
|
686
|
-
BORG_SESSION: '1',
|
|
687
|
-
};
|
|
688
|
-
}
|
|
665
|
+
codexServerCleanup = codexRemote.server.cleanup;
|
|
689
666
|
}
|
|
690
667
|
|
|
691
668
|
const kickoff = buildAgentKickoffPrompt({
|
|
692
669
|
cli,
|
|
693
|
-
codexWakeNonce,
|
|
694
670
|
monitorClause,
|
|
695
|
-
codexWakePathClause,
|
|
696
671
|
});
|
|
697
672
|
// This stays separate from the shared kickoff so OpenCode can carry
|
|
698
673
|
// launch-only API and correlation identities outside argv and prompt text.
|
|
@@ -700,29 +675,26 @@ async function main() {
|
|
|
700
675
|
let openCodePort: number | undefined;
|
|
701
676
|
let launchArgs: string[];
|
|
702
677
|
if (cli === 'codex') {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
...remoteArgs,
|
|
719
|
-
...withCodexCwdArg([...passthroughArgs, kickoff], process.cwd()),
|
|
720
|
-
];
|
|
678
|
+
const plan = buildCodexLaunchArgs({
|
|
679
|
+
remote: codexRemote!,
|
|
680
|
+
cwd: process.cwd(),
|
|
681
|
+
kickoff,
|
|
682
|
+
approvalArgs: launchApproval.codexArgs,
|
|
683
|
+
seatExpectationArgs: codexLaunchSeatExpectationConfigArgs(),
|
|
684
|
+
passthroughArgs,
|
|
685
|
+
});
|
|
686
|
+
if (!plan.ready) {
|
|
687
|
+
codexServerCleanup?.();
|
|
688
|
+
console.error(`${consolePrefix()}${chalk.red(`Codex launch refused: ${plan.reason}`)}`);
|
|
689
|
+
process.exit(1);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
launchArgs = plan.args;
|
|
721
693
|
} else if (cli === 'opencode') {
|
|
722
694
|
// OpenCode launch: start TUI with the kickoff passed via --prompt
|
|
723
695
|
// (auto-submits it as the first message). BORG_SESSION is pinned in
|
|
724
|
-
// opencode.json.
|
|
725
|
-
//
|
|
696
|
+
// opencode.json. Wake delivery uses HTTP entry injection through OpenCode's local HTTP API;
|
|
697
|
+
// the OS-selected port avoids a deterministic collision space.
|
|
726
698
|
openCodePort = await allocateOpenCodePort();
|
|
727
699
|
installBorgPlugin();
|
|
728
700
|
openCodeKickoff = createOpenCodeLaunchKickoff(kickoff);
|
|
@@ -749,25 +721,6 @@ async function main() {
|
|
|
749
721
|
})).process
|
|
750
722
|
: spawn(cli, launchArgs, { stdio: 'inherit', shell: false, env: launchEnv });
|
|
751
723
|
|
|
752
|
-
// gh#857 WI-2: wake-target recording is codex-only (app-server bridge).
|
|
753
|
-
// OpenCode uses HTTP entry injection; Claude uses the inbox Monitor.
|
|
754
|
-
if (cli === 'codex' && active && codexSocketPath) {
|
|
755
|
-
void recordCodexWakeTarget({
|
|
756
|
-
deps: { setCodexWakeTarget, findLoadedCodexThread },
|
|
757
|
-
cubeId: active.cubeId,
|
|
758
|
-
droneId: active.droneId,
|
|
759
|
-
socketPath: codexSocketPath,
|
|
760
|
-
passthroughArgs,
|
|
761
|
-
previewNeedle: codexWakeNonce ?? kickoff.slice(0, 120),
|
|
762
|
-
cwd: process.cwd(),
|
|
763
|
-
launchedAtSeconds: Math.floor(Date.now() / 1000),
|
|
764
|
-
});
|
|
765
|
-
// gh#855: self-heal the wake-target file — drop entries whose app-server
|
|
766
|
-
// socket is positively dead (crashed prior launches), mirroring the
|
|
767
|
-
// socket-dir pruneStaleSockets. Best-effort; never blocks the launch.
|
|
768
|
-
void pruneDeadCodexWakeTargets((sock) => checkCodexBridgeHealthy(sock));
|
|
769
|
-
}
|
|
770
|
-
|
|
771
724
|
agentProcess.on('error', (err: NodeJS.ErrnoException) => {
|
|
772
725
|
if (codexServerCleanup) {
|
|
773
726
|
try {
|
package/src/codex-app-server.ts
CHANGED
|
@@ -7,6 +7,10 @@ export interface CodexThreadSummary {
|
|
|
7
7
|
preview: string;
|
|
8
8
|
status: { type: string };
|
|
9
9
|
updatedAt: number;
|
|
10
|
+
source?: unknown;
|
|
11
|
+
ephemeral?: boolean;
|
|
12
|
+
threadSource?: string;
|
|
13
|
+
turns?: unknown[];
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
interface PendingRequest {
|
|
@@ -85,6 +89,10 @@ export class CodexAppServerClient {
|
|
|
85
89
|
preview: thread.preview,
|
|
86
90
|
status: thread.status,
|
|
87
91
|
updatedAt: thread.updatedAt,
|
|
92
|
+
source: thread.source,
|
|
93
|
+
ephemeral: thread.ephemeral,
|
|
94
|
+
threadSource: thread.threadSource,
|
|
95
|
+
turns: thread.turns,
|
|
88
96
|
};
|
|
89
97
|
}
|
|
90
98
|
|
|
@@ -159,31 +167,6 @@ export class CodexAppServerClient {
|
|
|
159
167
|
}
|
|
160
168
|
}
|
|
161
169
|
|
|
162
|
-
export async function findLoadedCodexThread(options: {
|
|
163
|
-
socketPath: string;
|
|
164
|
-
cwd: string;
|
|
165
|
-
previewIncludes: string;
|
|
166
|
-
updatedAfter: number;
|
|
167
|
-
}): Promise<string | null> {
|
|
168
|
-
const client = new CodexAppServerClient(options.socketPath);
|
|
169
|
-
await client.connect();
|
|
170
|
-
try {
|
|
171
|
-
const ids = await client.loadedThreadIds();
|
|
172
|
-
let best: CodexThreadSummary | null = null;
|
|
173
|
-
for (const id of ids) {
|
|
174
|
-
const thread = await client.readThread(id);
|
|
175
|
-
if (!thread) continue;
|
|
176
|
-
if (thread.cwd !== options.cwd) continue;
|
|
177
|
-
if (!thread.preview.includes(options.previewIncludes)) continue;
|
|
178
|
-
if (thread.updatedAt < options.updatedAfter) continue;
|
|
179
|
-
if (!best || thread.updatedAt > best.updatedAt) best = thread;
|
|
180
|
-
}
|
|
181
|
-
return best?.id ?? null;
|
|
182
|
-
} finally {
|
|
183
|
-
client.close();
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
170
|
function encodeWebSocketTextFrame(text: string): Buffer {
|
|
188
171
|
const payload = Buffer.from(text);
|
|
189
172
|
let header: Buffer;
|
package/src/codex-app-wake.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getActiveCube,
|
|
3
|
-
getCodexWakeTarget,
|
|
4
|
-
setCodexWakeTarget,
|
|
5
3
|
type ActiveCube,
|
|
6
4
|
} from './cubes.js';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
7
6
|
import { CodexAppServerClient } from './codex-app-server.js';
|
|
8
7
|
import { checkCodexBridgeHealthy } from './codex-remote.js';
|
|
8
|
+
import { debugLog } from './debug.js';
|
|
9
9
|
import { hasPendingWakeActivity, hasPendingWakeEntry } from './remote-client.js';
|
|
10
10
|
import {
|
|
11
11
|
BORG_CODEX_REMOTE_WAKE_ENV,
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
codexAppServerSocketFromEnv,
|
|
16
16
|
pickFreshThread,
|
|
17
|
-
wakeTargetChanged,
|
|
18
17
|
wakeRetryBackoffMs,
|
|
19
18
|
wakeRetryExpired,
|
|
20
19
|
WAKE_RETRY_MAX_ATTEMPTS,
|
|
@@ -69,26 +68,25 @@ export function resolveCodexWakeTarget(env: NodeJS.ProcessEnv = process.env): Co
|
|
|
69
68
|
* (which is false-by-design for codex and falsely flagged them, gh#633).
|
|
70
69
|
*
|
|
71
70
|
* Tri-state (boolean|null; caller maps null→armed for false-deaf-avoidance):
|
|
72
|
-
* - false
|
|
73
|
-
*
|
|
74
|
-
* - true when the
|
|
75
|
-
* - null when the
|
|
76
|
-
* could not resolve) → armed (don't false-flag on uncertainty).
|
|
71
|
+
* - false when this child has no live socket, the socket file is missing, or
|
|
72
|
+
* the app-server pid is dead.
|
|
73
|
+
* - true when the env socket exists and the app-server pid is alive.
|
|
74
|
+
* - null when the pidfile cannot be read or parsed.
|
|
77
75
|
*/
|
|
78
76
|
export async function probeCodexBridgeArmed(
|
|
79
|
-
|
|
77
|
+
_active: { cubeId: string; droneId: string },
|
|
80
78
|
deps: {
|
|
81
|
-
|
|
79
|
+
env?: NodeJS.ProcessEnv;
|
|
80
|
+
socketExists?: (socketPath: string) => boolean;
|
|
82
81
|
checkBridge?: typeof checkCodexBridgeHealthy;
|
|
83
82
|
} = {}
|
|
84
83
|
): Promise<boolean | null> {
|
|
85
84
|
try {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (!target) return false;
|
|
85
|
+
const socketPath = codexAppServerSocketFromEnv(deps.env ?? process.env);
|
|
86
|
+
if (!socketPath) return false;
|
|
87
|
+
if (!(deps.socketExists ?? existsSync)(socketPath)) return false;
|
|
90
88
|
const check = deps.checkBridge ?? checkCodexBridgeHealthy;
|
|
91
|
-
return check(
|
|
89
|
+
return check(socketPath);
|
|
92
90
|
} catch {
|
|
93
91
|
return null;
|
|
94
92
|
}
|
|
@@ -280,9 +278,6 @@ function defaultSleep(ms: number): Promise<void> {
|
|
|
280
278
|
|
|
281
279
|
export interface CodexWakeDeps {
|
|
282
280
|
getActiveCube?: typeof getActiveCube;
|
|
283
|
-
getCodexWakeTarget?: typeof getCodexWakeTarget;
|
|
284
|
-
// gh#855: self-heal write of the freshly-resolved target (write-only-on-change).
|
|
285
|
-
setCodexWakeTarget?: typeof setCodexWakeTarget;
|
|
286
281
|
createClient?: (
|
|
287
282
|
socketPath: string
|
|
288
283
|
) => Pick<CodexAppServerClient, 'connect' | 'readThread' | 'startTurn' | 'loadedThreadIds' | 'close'>;
|
|
@@ -320,12 +315,9 @@ export interface CodexWakeDeps {
|
|
|
320
315
|
/**
|
|
321
316
|
* gh#855: FRESH wake-target resolution. Prefer THIS drone's live app-server
|
|
322
317
|
* socket (pinned into the child's env at spawn) and re-resolve the loaded thread
|
|
323
|
-
* NOW (loadedThreadIds is re-runnable)
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
* stay current. Falls back to the launch-recorded file when the env socket is
|
|
327
|
-
* absent (un-upgraded launch) — no regression. Returns the resolved target, or
|
|
328
|
-
* null (caller skips this wake; the next one retries). Does NOT keep a
|
|
318
|
+
* NOW (loadedThreadIds is re-runnable), so a thread change cannot cause
|
|
319
|
+
* permanent deafness. The live env socket is the sole source. Returns the
|
|
320
|
+
* resolved target, or null when the socket/thread is unavailable. Does NOT keep a
|
|
329
321
|
* connection open — the env path opens a short-lived probe client to re-resolve
|
|
330
322
|
* the thread, then closes it, so the caller can dedup BEFORE opening the wake
|
|
331
323
|
* connection (no reconnect on an already-delivered wake).
|
|
@@ -338,52 +330,31 @@ function makeCodexClient(
|
|
|
338
330
|
}
|
|
339
331
|
|
|
340
332
|
async function resolveFreshCodexWakeTarget(
|
|
341
|
-
|
|
333
|
+
_active: { cubeId: string; droneId: string },
|
|
342
334
|
deps: CodexWakeDeps
|
|
343
335
|
): Promise<{ socketPath: string; threadId: string } | null> {
|
|
344
336
|
const envSocket = codexAppServerSocketFromEnv(deps.env ?? process.env);
|
|
345
|
-
if (envSocket) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
try {
|
|
349
|
-
const ids = await probe.loadedThreadIds();
|
|
350
|
-
const summaries: CodexThreadInfo[] = [];
|
|
351
|
-
for (const id of ids) {
|
|
352
|
-
const t = await probe.readThread(id);
|
|
353
|
-
if (t) summaries.push({ id: t.id, cwd: t.cwd, updatedAt: t.updatedAt });
|
|
354
|
-
}
|
|
355
|
-
const threadId = pickFreshThread(summaries, { cwd: (deps.cwd ?? (() => process.cwd()))() });
|
|
356
|
-
if (!threadId) return null; // no loaded thread yet — next wake retries (no permanent fail)
|
|
357
|
-
await maybePersistWakeTarget(active, { socketPath: envSocket, threadId }, deps);
|
|
358
|
-
return { socketPath: envSocket, threadId };
|
|
359
|
-
} finally {
|
|
360
|
-
probe.close();
|
|
361
|
-
}
|
|
337
|
+
if (!envSocket) {
|
|
338
|
+
debugLog('codex wake target unavailable: BORG_CODEX_APP_SERVER_SOCKET is absent');
|
|
339
|
+
return null;
|
|
362
340
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
// connect needed to resolve, so the caller dedups before opening any socket.
|
|
366
|
-
const target = await (deps.getCodexWakeTarget ?? getCodexWakeTarget)(active.cubeId, active.droneId);
|
|
367
|
-
if (!target) return null;
|
|
368
|
-
return { socketPath: target.socketPath, threadId: target.threadId };
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/** Self-healing cache write — only when the resolved target actually changed. */
|
|
372
|
-
async function maybePersistWakeTarget(
|
|
373
|
-
active: { cubeId: string; droneId: string },
|
|
374
|
-
fresh: { socketPath: string; threadId: string },
|
|
375
|
-
deps: CodexWakeDeps
|
|
376
|
-
): Promise<void> {
|
|
341
|
+
const probe = makeCodexClient(envSocket, deps);
|
|
342
|
+
await probe.connect();
|
|
377
343
|
try {
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
344
|
+
const ids = await probe.loadedThreadIds();
|
|
345
|
+
const summaries: CodexThreadInfo[] = [];
|
|
346
|
+
for (const id of ids) {
|
|
347
|
+
try {
|
|
348
|
+
const thread = await probe.readThread(id);
|
|
349
|
+
if (thread) summaries.push(thread);
|
|
350
|
+
} catch {
|
|
351
|
+
// A loaded thread may disappear before its read; keep other candidates.
|
|
352
|
+
}
|
|
384
353
|
}
|
|
385
|
-
|
|
386
|
-
|
|
354
|
+
const threadId = pickFreshThread(summaries, { cwd: (deps.cwd ?? (() => process.cwd()))() });
|
|
355
|
+
return threadId ? { socketPath: envSocket, threadId } : null;
|
|
356
|
+
} finally {
|
|
357
|
+
probe.close();
|
|
387
358
|
}
|
|
388
359
|
}
|
|
389
360
|
|
|
@@ -396,7 +367,12 @@ export function wakeCodexViaAppServer(
|
|
|
396
367
|
): void {
|
|
397
368
|
const target = resolveCodexWakeTarget(env);
|
|
398
369
|
if (!target.enabled) return;
|
|
399
|
-
pendingWakeRequests.push({
|
|
370
|
+
pendingWakeRequests.push({
|
|
371
|
+
reason,
|
|
372
|
+
deliveryIdentity,
|
|
373
|
+
sourceEntryId,
|
|
374
|
+
deps: deps.env ? deps : { ...deps, env },
|
|
375
|
+
});
|
|
400
376
|
if (wakeInFlight) return;
|
|
401
377
|
|
|
402
378
|
wakeInFlight = true;
|
|
@@ -432,17 +408,14 @@ async function wakeCodexTargeted(
|
|
|
432
408
|
if (!active) return;
|
|
433
409
|
const pendingEntry = deps.hasPendingEntry ?? hasPendingWakeEntry;
|
|
434
410
|
if (sourceEntryId && !(await pendingEntry(active, sourceEntryId))) return;
|
|
435
|
-
//
|
|
436
|
-
// to the launch-recorded file only when the env socket is absent.
|
|
411
|
+
// Resolve fresh from this child’s live env socket and loaded threads.
|
|
437
412
|
const resolved = await resolveFreshCodexWakeTarget(active, deps);
|
|
438
413
|
if (!resolved) {
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
// the deferral live so health reads degraded rather than armed. An
|
|
443
|
-
// unscoped wake carries no authoritative pending signal, so it does not
|
|
444
|
-
// set the marker. Retry behavior is unchanged.
|
|
414
|
+
// A scoped entry passed the pending check but its TUI thread is not loaded
|
|
415
|
+
// yet. Keep it in the coalesced retry-drain instead of waiting for another
|
|
416
|
+
// inbound event or the heartbeat.
|
|
445
417
|
if (sourceEntryId) deliveryDeferred = true;
|
|
418
|
+
scheduleRetryDrain(deps, sourceEntryId);
|
|
446
419
|
return;
|
|
447
420
|
}
|
|
448
421
|
const { socketPath, threadId } = resolved;
|