borgmcp 5.2.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-runtime.d.ts +3 -4
- package/dist/agent-runtime.d.ts.map +1 -1
- package/dist/agent-runtime.js +9 -5
- package/dist/agent-runtime.js.map +1 -1
- package/dist/agent-session-identity.d.ts +13 -0
- package/dist/agent-session-identity.d.ts.map +1 -0
- package/dist/agent-session-identity.js +106 -0
- package/dist/agent-session-identity.js.map +1 -0
- 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 +10 -5
- package/dist/index.js.map +1 -1
- package/dist/lifecycle-log-guard.d.ts +6 -8
- package/dist/lifecycle-log-guard.d.ts.map +1 -1
- package/dist/lifecycle-log-guard.js +74 -22
- package/dist/lifecycle-log-guard.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/opencode-drone.d.ts +2 -0
- package/dist/opencode-drone.d.ts.map +1 -1
- package/dist/opencode-drone.js +16 -0
- package/dist/opencode-drone.js.map +1 -1
- package/dist/regen-format.js +1 -1
- package/dist/regen-format.js.map +1 -1
- package/dist/regen.js +11 -1
- package/dist/regen.js.map +1 -1
- package/docs/SESSION_IDENTITY.md +15 -0
- package/package.json +1 -1
- package/src/agent-runtime.ts +9 -5
- package/src/agent-session-identity.ts +98 -0
- 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 +10 -5
- package/src/lifecycle-log-guard.ts +70 -26
- package/src/log-stream.ts +4 -1
- package/src/opencode-drone.ts +14 -0
- package/src/regen-format.ts +1 -1
- package/src/regen.ts +7 -1
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;
|
package/src/codex-launch.ts
CHANGED
|
@@ -2,6 +2,13 @@ import type { BorgCli } from './cubes.js';
|
|
|
2
2
|
import type { AgentKind } from './agent-runtime.js';
|
|
3
3
|
import { wakePathArming } from './regen-format.js';
|
|
4
4
|
import { OPENCODE_WAKE_PATH_GUIDANCE } from './opencode-wake-copy.js';
|
|
5
|
+
import { withCodexCwdArg, type CodexRemoteReadyLaunch } from './codex-remote.js';
|
|
6
|
+
import { codexBorgSessionConfigArgs } from './launch-gate.js';
|
|
7
|
+
import {
|
|
8
|
+
codexAgentKindConfigArgs,
|
|
9
|
+
codexRemoteWakeConfigArgs,
|
|
10
|
+
codexStateRootConfigArgs,
|
|
11
|
+
} from './agent-runtime.js';
|
|
5
12
|
|
|
6
13
|
/**
|
|
7
14
|
* The claude kickoff prompt's wake-path section (gh#929) — the SAME shared
|
|
@@ -29,25 +36,9 @@ export function buildKickoffWakePathClause(
|
|
|
29
36
|
return '';
|
|
30
37
|
}
|
|
31
38
|
|
|
32
|
-
export interface CodexWakeTargetDeps {
|
|
33
|
-
setCodexWakeTarget: (
|
|
34
|
-
cubeId: string,
|
|
35
|
-
droneId: string,
|
|
36
|
-
target: { threadId: string; socketPath: string }
|
|
37
|
-
) => Promise<void>;
|
|
38
|
-
findLoadedCodexThread: (options: {
|
|
39
|
-
socketPath: string;
|
|
40
|
-
cwd: string;
|
|
41
|
-
previewIncludes: string;
|
|
42
|
-
updatedAfter: number;
|
|
43
|
-
}) => Promise<string | null>;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
39
|
export function buildAgentKickoffPrompt(options: {
|
|
47
40
|
cli: BorgCli;
|
|
48
|
-
codexWakeNonce: string | null;
|
|
49
41
|
monitorClause: string;
|
|
50
|
-
codexWakePathClause?: string;
|
|
51
42
|
}): string {
|
|
52
43
|
// gh#929: compacted to the load-bearing launch essentials (lean/explicit/
|
|
53
44
|
// imperative, #914 treatment). STRIPPED: the read-log-triage paragraph (the
|
|
@@ -55,13 +46,9 @@ export function buildAgentKickoffPrompt(options: {
|
|
|
55
46
|
// clause (Coordinator/Queen-only; belongs in role-text, not injected for
|
|
56
47
|
// ALL). KEPT: core call + MCP-disconnect recovery + the wake-path arming
|
|
57
48
|
// (claude via the shared monitorClause = buildKickoffWakePathClause; codex
|
|
58
|
-
// via
|
|
59
|
-
const codexNonceClause = options.codexWakeNonce
|
|
60
|
-
? `Wake target nonce: ${options.codexWakeNonce}. `
|
|
61
|
-
: '';
|
|
49
|
+
// via the Borg-owned remote socket clause) + the cli-specific branching.
|
|
62
50
|
const codexWakePathClause =
|
|
63
|
-
|
|
64
|
-
`Codex Borg wakeups use remote-control when available; if no wake arrives, run borg_regen manually when returning to the session.`;
|
|
51
|
+
'Codex Borg wakeups use the Borg-owned remote-control socket for this session.';
|
|
65
52
|
const opencodeWakePathClause = OPENCODE_WAKE_PATH_GUIDANCE;
|
|
66
53
|
const wakeClause = options.cli === 'claude'
|
|
67
54
|
? options.monitorClause
|
|
@@ -70,7 +57,6 @@ export function buildAgentKickoffPrompt(options: {
|
|
|
70
57
|
: opencodeWakePathClause;
|
|
71
58
|
return (
|
|
72
59
|
`Call borg_regen and follow the playbook in its response. ` +
|
|
73
|
-
codexNonceClause +
|
|
74
60
|
`Note: at session start the borg MCP server is still spinning up in ` +
|
|
75
61
|
`parallel — if a system reminder claims "MCP server disconnected" or ` +
|
|
76
62
|
`the borg tools are not yet registered, do NOT bail. Make one recovery attempt via ` +
|
|
@@ -82,61 +68,42 @@ export function buildAgentKickoffPrompt(options: {
|
|
|
82
68
|
);
|
|
83
69
|
}
|
|
84
70
|
|
|
85
|
-
export
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const value = args[index + 1];
|
|
89
|
-
if (!value?.startsWith('unix://')) return null;
|
|
90
|
-
return value.slice('unix://'.length);
|
|
91
|
-
}
|
|
71
|
+
export type CodexLaunchArgsResult =
|
|
72
|
+
| { ready: true; args: string[] }
|
|
73
|
+
| { ready: false; reason: string };
|
|
92
74
|
|
|
93
|
-
export function
|
|
94
|
-
|
|
95
|
-
const resumeIndex = args.indexOf('--resume');
|
|
96
|
-
if (resumeIndex >= 0 && args[resumeIndex + 1]) return args[resumeIndex + 1];
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export async function recordCodexWakeTarget(options: {
|
|
101
|
-
deps: CodexWakeTargetDeps;
|
|
102
|
-
cubeId: string;
|
|
103
|
-
droneId: string;
|
|
104
|
-
socketPath: string;
|
|
75
|
+
export function buildCodexLaunchArgs(options: {
|
|
76
|
+
remote: CodexRemoteReadyLaunch;
|
|
105
77
|
cwd: string;
|
|
106
|
-
|
|
107
|
-
|
|
78
|
+
kickoff: string;
|
|
79
|
+
approvalArgs?: string[];
|
|
80
|
+
accessArgs?: string[];
|
|
81
|
+
seatExpectationArgs?: string[];
|
|
108
82
|
passthroughArgs?: string[];
|
|
109
|
-
}):
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const deadline = Date.now() + 15_000;
|
|
123
|
-
while (Date.now() < deadline) {
|
|
124
|
-
const threadId = await options.deps.findLoadedCodexThread({
|
|
125
|
-
socketPath: options.socketPath,
|
|
126
|
-
cwd: options.cwd,
|
|
127
|
-
previewIncludes: options.previewNeedle,
|
|
128
|
-
updatedAfter: options.launchedAtSeconds - 5,
|
|
129
|
-
});
|
|
130
|
-
if (threadId) {
|
|
131
|
-
await options.deps.setCodexWakeTarget(options.cubeId, options.droneId, {
|
|
132
|
-
threadId,
|
|
133
|
-
socketPath: options.socketPath,
|
|
134
|
-
});
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
138
|
-
}
|
|
139
|
-
} catch {
|
|
140
|
-
// Best-effort mapping: launch still succeeds and manual regen remains available.
|
|
83
|
+
}): CodexLaunchArgsResult {
|
|
84
|
+
const passthroughArgs = options.passthroughArgs ?? [];
|
|
85
|
+
if (passthroughArgs.some((arg) => arg === '--remote' || arg.startsWith('--remote='))) {
|
|
86
|
+
return {
|
|
87
|
+
ready: false,
|
|
88
|
+
reason: 'Borg owns Codex remote control; remove the passthrough --remote option and retry.',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const remoteValue = `unix://${options.remote.server.socketPath}`;
|
|
92
|
+
if (options.remote.args.length !== 2 || options.remote.args[0] !== '--remote' || options.remote.args[1] !== remoteValue) {
|
|
93
|
+
return { ready: false, reason: 'The Borg-owned Codex remote socket is unavailable.' };
|
|
141
94
|
}
|
|
95
|
+
return {
|
|
96
|
+
ready: true,
|
|
97
|
+
args: [
|
|
98
|
+
...(options.accessArgs ?? []),
|
|
99
|
+
...(options.approvalArgs ?? []),
|
|
100
|
+
...codexBorgSessionConfigArgs(),
|
|
101
|
+
...codexAgentKindConfigArgs(),
|
|
102
|
+
...codexRemoteWakeConfigArgs(),
|
|
103
|
+
...codexStateRootConfigArgs(),
|
|
104
|
+
...(options.seatExpectationArgs ?? []),
|
|
105
|
+
...options.remote.args,
|
|
106
|
+
...withCodexCwdArg([...passthroughArgs, options.kickoff], options.cwd),
|
|
107
|
+
],
|
|
108
|
+
};
|
|
142
109
|
}
|
package/src/codex-remote.ts
CHANGED
|
@@ -35,14 +35,22 @@ export interface CodexAppServerHandle {
|
|
|
35
35
|
cleanup: () => void;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export interface
|
|
38
|
+
export interface CodexRemoteReadyLaunch {
|
|
39
|
+
ready: true;
|
|
39
40
|
args: string[];
|
|
40
41
|
env: Record<string, string>;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
server?: CodexAppServerHandle;
|
|
42
|
+
/** Borg owns this per-launch app-server and must clean it up on TUI exit. */
|
|
43
|
+
server: CodexAppServerHandle;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export interface CodexRemoteLaunchRefusal {
|
|
47
|
+
ready: false;
|
|
48
|
+
reason: string;
|
|
49
|
+
stderr: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type CodexRemoteLaunch = CodexRemoteReadyLaunch | CodexRemoteLaunchRefusal;
|
|
53
|
+
|
|
46
54
|
export interface CodexChild {
|
|
47
55
|
pid: number | undefined;
|
|
48
56
|
kill: () => void;
|
|
@@ -69,15 +77,17 @@ export interface PrepareCodexRemoteDeps {
|
|
|
69
77
|
runtimeDir?: string;
|
|
70
78
|
/** Unique socket id generator (default 32-hex). Injected for deterministic tests. */
|
|
71
79
|
socketId?: () => string;
|
|
72
|
-
/** Readiness timeout (default
|
|
80
|
+
/** Readiness timeout (default 60000ms) + poll interval (default 250ms). */
|
|
73
81
|
readyTimeoutMs?: number;
|
|
74
82
|
pollIntervalMs?: number;
|
|
83
|
+
/** Wall clock used to enforce the readiness budget. */
|
|
84
|
+
now?: () => number;
|
|
75
85
|
/** Whether a pid is alive (default process.kill(pid, 0)). Injected for tests. */
|
|
76
86
|
isAlive?: (pid: number) => boolean;
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
export const DEFAULT_CODEX_REMOTE_DIR = join(borgConfigRoot(), 'codex-remote');
|
|
80
|
-
const DEFAULT_CODEX_REMOTE_READY_TIMEOUT_MS =
|
|
90
|
+
const DEFAULT_CODEX_REMOTE_READY_TIMEOUT_MS = 60_000;
|
|
81
91
|
const MAX_CODEX_APP_SERVER_STDERR_CHARS = 16_384;
|
|
82
92
|
|
|
83
93
|
/**
|
|
@@ -266,8 +276,8 @@ function pruneStaleSockets(runtimeDir: string, isAlive: (pid: number) => boolean
|
|
|
266
276
|
}
|
|
267
277
|
}
|
|
268
278
|
|
|
269
|
-
function
|
|
270
|
-
return {
|
|
279
|
+
function refuse(reason: string, stderr = ''): CodexRemoteLaunchRefusal {
|
|
280
|
+
return { ready: false, reason, stderr: sanitizeCodexDiagnostic(stderr).trim() };
|
|
271
281
|
}
|
|
272
282
|
|
|
273
283
|
function formatExitReason(diagnostics: CodexChildDiagnostics): string {
|
|
@@ -277,16 +287,10 @@ function formatExitReason(diagnostics: CodexChildDiagnostics): string {
|
|
|
277
287
|
return 'unknown exit status';
|
|
278
288
|
}
|
|
279
289
|
|
|
280
|
-
function formatStderr(stderr: string): string {
|
|
281
|
-
const trimmed = sanitizeCodexDiagnostic(stderr).trim();
|
|
282
|
-
return trimmed ? ` Stderr: ${trimmed}` : '';
|
|
283
|
-
}
|
|
284
|
-
|
|
285
290
|
/**
|
|
286
291
|
* Start a borg-owned per-launch Codex app-server, probe it for readiness, and
|
|
287
|
-
* return the `--remote` launch args + an owned handle
|
|
288
|
-
*
|
|
289
|
-
* TUI exit.
|
|
292
|
+
* return the `--remote` launch args + an owned handle, or a typed refusal after
|
|
293
|
+
* one retry. The caller MUST call `result.server.cleanup()` on TUI exit.
|
|
290
294
|
*/
|
|
291
295
|
export async function prepareCodexRemoteLaunch(
|
|
292
296
|
deps: PrepareCodexRemoteDeps
|
|
@@ -295,6 +299,7 @@ export async function prepareCodexRemoteLaunch(
|
|
|
295
299
|
const isAlive = deps.isAlive ?? defaultIsAlive;
|
|
296
300
|
const readyTimeoutMs = deps.readyTimeoutMs ?? DEFAULT_CODEX_REMOTE_READY_TIMEOUT_MS;
|
|
297
301
|
const pollIntervalMs = deps.pollIntervalMs ?? 250;
|
|
302
|
+
const now = deps.now ?? Date.now;
|
|
298
303
|
|
|
299
304
|
// 1. 0700 owned dir + prune crashed prior sockets (concurrent-safe via pid liveness).
|
|
300
305
|
try {
|
|
@@ -302,89 +307,92 @@ export async function prepareCodexRemoteLaunch(
|
|
|
302
307
|
chmodSync(runtimeDir, 0o700); // enforce 0700 even if it pre-existed with looser perms
|
|
303
308
|
pruneStaleSockets(runtimeDir, isAlive);
|
|
304
309
|
} catch (err: any) {
|
|
305
|
-
return
|
|
306
|
-
`Codex remote-wake disabled: could not prepare ${runtimeDir} (${err?.message ?? err}); run borg_regen manually.`
|
|
307
|
-
);
|
|
310
|
+
return refuse(`could not prepare the Codex app-server runtime directory (${sanitizeCodexDiagnostic(String(err?.message ?? err))})`);
|
|
308
311
|
}
|
|
309
312
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
try {
|
|
318
|
-
child = deps.spawnAppServer(socketPath);
|
|
319
|
-
} catch (err: any) {
|
|
320
|
-
safeRm(socketPath);
|
|
321
|
-
return failLoud(
|
|
322
|
-
`Codex remote-wake disabled: could not start \`codex app-server\` (${sanitizeCodexDiagnostic(String(err?.message ?? err))}) — ` +
|
|
323
|
-
`confirm the Codex executable is installed and available on PATH. ` +
|
|
324
|
-
`Remote wake is unavailable for this session; run borg_regen manually to catch up.`
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
if (child.pid != null) {
|
|
313
|
+
let lastRefusal = refuse('the Codex app-server did not become ready');
|
|
314
|
+
for (let launchAttempt = 0; launchAttempt < 2; launchAttempt += 1) {
|
|
315
|
+
// Each retry owns a fresh, non-predictable socket and pidfile.
|
|
316
|
+
const id = (deps.socketId ?? (() => randomBytes(16).toString('hex')))();
|
|
317
|
+
const socketPath = join(runtimeDir, `${id}.sock`);
|
|
318
|
+
const pidPath = join(runtimeDir, `${id}.pid`);
|
|
319
|
+
let child: CodexChild;
|
|
328
320
|
try {
|
|
329
|
-
|
|
330
|
-
} catch {
|
|
331
|
-
|
|
321
|
+
child = deps.spawnAppServer(socketPath);
|
|
322
|
+
} catch (err: any) {
|
|
323
|
+
safeRm(socketPath);
|
|
324
|
+
safeRm(pidPath);
|
|
325
|
+
lastRefusal = refuse(
|
|
326
|
+
`could not start \`codex app-server\` (${sanitizeCodexDiagnostic(String(err?.message ?? err))}); confirm the Codex executable is installed and available on PATH`
|
|
327
|
+
);
|
|
328
|
+
continue;
|
|
332
329
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
// best-effort
|
|
330
|
+
if (child.pid != null) {
|
|
331
|
+
try {
|
|
332
|
+
writeFileSync(pidPath, String(child.pid));
|
|
333
|
+
} catch {
|
|
334
|
+
// pidfile is only for stale-prune; launch still proceeds.
|
|
335
|
+
}
|
|
340
336
|
}
|
|
341
|
-
safeRm(socketPath);
|
|
342
|
-
safeRm(pidPath);
|
|
343
|
-
};
|
|
344
337
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
338
|
+
const cleanup = () => {
|
|
339
|
+
try {
|
|
340
|
+
child.kill();
|
|
341
|
+
} catch {
|
|
342
|
+
// best-effort
|
|
343
|
+
}
|
|
344
|
+
safeRm(socketPath);
|
|
345
|
+
safeRm(pidPath);
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// Readiness is bounded by elapsed wall time, so slow protocol probes count
|
|
349
|
+
// against the same cold-start budget as the poll sleeps.
|
|
350
|
+
const startedAt = now();
|
|
351
|
+
let ready = false;
|
|
352
|
+
let exitDiagnostics: CodexChildDiagnostics | undefined;
|
|
353
|
+
for (;;) {
|
|
354
|
+
try {
|
|
355
|
+
ready = await deps.probeReady(socketPath);
|
|
356
|
+
} catch {
|
|
357
|
+
ready = false;
|
|
358
|
+
}
|
|
359
|
+
if (ready) break;
|
|
356
360
|
const diagnostics = child.diagnostics?.();
|
|
357
361
|
if (diagnostics?.exited) {
|
|
358
362
|
exitDiagnostics = diagnostics;
|
|
359
363
|
break;
|
|
360
364
|
}
|
|
365
|
+
const remainingMs = readyTimeoutMs - (now() - startedAt);
|
|
366
|
+
if (remainingMs <= 0) break;
|
|
367
|
+
await deps.sleep(Math.min(pollIntervalMs, remainingMs));
|
|
361
368
|
}
|
|
362
|
-
if (!ready && i < attempts - 1) await deps.sleep(pollIntervalMs);
|
|
363
|
-
}
|
|
364
369
|
|
|
365
|
-
if (!ready) {
|
|
366
370
|
const diagnostics = exitDiagnostics ?? child.diagnostics?.();
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
371
|
+
if (!ready) {
|
|
372
|
+
cleanup();
|
|
373
|
+
if (diagnostics?.exited) {
|
|
374
|
+
lastRefusal = refuse(
|
|
375
|
+
`\`codex app-server\` exited before becoming ready (${formatExitReason(diagnostics)})`,
|
|
376
|
+
diagnostics.stderr,
|
|
377
|
+
);
|
|
378
|
+
} else {
|
|
379
|
+
lastRefusal = refuse(
|
|
380
|
+
`\`codex app-server\` remained running but did not become ready within ${readyTimeoutMs}ms`,
|
|
381
|
+
diagnostics?.stderr ?? '',
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
continue;
|
|
374
385
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
ready: true,
|
|
389
|
+
args: ['--remote', `unix://${socketPath}`],
|
|
390
|
+
env: { [BORG_CODEX_REMOTE_WAKE_ENV]: '1' },
|
|
391
|
+
server: { pid: child.pid, socketPath, cleanup },
|
|
392
|
+
};
|
|
380
393
|
}
|
|
381
394
|
|
|
382
|
-
|
|
383
|
-
return {
|
|
384
|
-
args: ['--remote', `unix://${socketPath}`],
|
|
385
|
-
env: { [BORG_CODEX_REMOTE_WAKE_ENV]: '1' },
|
|
386
|
-
server: { pid: child.pid, socketPath, cleanup },
|
|
387
|
-
};
|
|
395
|
+
return lastRefusal;
|
|
388
396
|
}
|
|
389
397
|
|
|
390
398
|
/**
|