@raysonmeng/agentbridge 0.1.11 → 0.1.13
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/.claude-plugin/marketplace.json +1 -1
- package/README.md +1 -1
- package/dist/cli.js +1583 -577
- package/dist/daemon.js +2039 -624
- package/package.json +3 -1
- package/plugins/agentbridge/.claude-plugin/plugin.json +1 -1
- package/plugins/agentbridge/server/bridge-server.js +918 -275
- package/plugins/agentbridge/server/daemon.js +2039 -624
package/dist/cli.js
CHANGED
|
@@ -17,10 +17,67 @@ var __export = (target, all) => {
|
|
|
17
17
|
};
|
|
18
18
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
19
19
|
|
|
20
|
+
// src/cli-invocation.ts
|
|
21
|
+
import { basename } from "path";
|
|
22
|
+
function cliInvocationName(argv = process.argv) {
|
|
23
|
+
const raw = argv[1];
|
|
24
|
+
if (typeof raw !== "string" || raw.length === 0)
|
|
25
|
+
return DEFAULT_CLI_NAME;
|
|
26
|
+
const name = basename(raw).replace(/\.(ts|js|mjs|cjs)$/, "");
|
|
27
|
+
return isCliName(name) ? name : DEFAULT_CLI_NAME;
|
|
28
|
+
}
|
|
29
|
+
function isCliName(value) {
|
|
30
|
+
return CLI_NAMES.includes(value);
|
|
31
|
+
}
|
|
32
|
+
var CLI_NAMES, DEFAULT_CLI_NAME = "abg";
|
|
33
|
+
var init_cli_invocation = __esm(() => {
|
|
34
|
+
CLI_NAMES = ["abg", "agentbridge"];
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// src/atomic-json.ts
|
|
38
|
+
import * as fs from "fs";
|
|
39
|
+
import { randomUUID } from "crypto";
|
|
40
|
+
import { dirname } from "path";
|
|
41
|
+
function tmpPathFor(targetPath) {
|
|
42
|
+
return `${targetPath}.tmp.${process.pid}.${randomUUID()}`;
|
|
43
|
+
}
|
|
44
|
+
function atomicWriteText(path, content, options = {}) {
|
|
45
|
+
fs.mkdirSync(dirname(path), { recursive: true });
|
|
46
|
+
const tmp = tmpPathFor(path);
|
|
47
|
+
let renamed = false;
|
|
48
|
+
const fd = fs.openSync(tmp, "w", options.mode ?? 438);
|
|
49
|
+
try {
|
|
50
|
+
try {
|
|
51
|
+
fs.writeFileSync(fd, content, "utf-8");
|
|
52
|
+
if (options.fsync)
|
|
53
|
+
fs.fsyncSync(fd);
|
|
54
|
+
} finally {
|
|
55
|
+
fs.closeSync(fd);
|
|
56
|
+
}
|
|
57
|
+
fs.renameSync(tmp, path);
|
|
58
|
+
renamed = true;
|
|
59
|
+
} finally {
|
|
60
|
+
if (!renamed) {
|
|
61
|
+
try {
|
|
62
|
+
fs.unlinkSync(tmp);
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function atomicWriteJson(path, value, options = {}) {
|
|
68
|
+
atomicWriteText(path, JSON.stringify(value, null, 2) + `
|
|
69
|
+
`, options);
|
|
70
|
+
}
|
|
71
|
+
var init_atomic_json = () => {};
|
|
72
|
+
|
|
20
73
|
// src/state-dir.ts
|
|
21
|
-
import { mkdirSync, existsSync } from "fs";
|
|
74
|
+
import { mkdirSync as mkdirSync2, existsSync } from "fs";
|
|
22
75
|
import { join } from "path";
|
|
23
76
|
import { homedir, platform } from "os";
|
|
77
|
+
function resolveXdgStateBase(rawXdg = process.env.XDG_STATE_HOME) {
|
|
78
|
+
const xdgState = rawXdg && rawXdg.length > 0 ? rawXdg : join(homedir(), ".local", "state");
|
|
79
|
+
return join(xdgState, "agentbridge");
|
|
80
|
+
}
|
|
24
81
|
|
|
25
82
|
class StateDirResolver {
|
|
26
83
|
stateDir;
|
|
@@ -28,8 +85,7 @@ class StateDirResolver {
|
|
|
28
85
|
if (platform() === "darwin") {
|
|
29
86
|
return join(homedir(), "Library", "Application Support", "AgentBridge");
|
|
30
87
|
}
|
|
31
|
-
|
|
32
|
-
return join(xdgState, "agentbridge");
|
|
88
|
+
return resolveXdgStateBase(process.env.XDG_STATE_HOME);
|
|
33
89
|
}
|
|
34
90
|
constructor(envOverride) {
|
|
35
91
|
const override = envOverride ?? process.env.AGENTBRIDGE_STATE_DIR;
|
|
@@ -37,7 +93,7 @@ class StateDirResolver {
|
|
|
37
93
|
}
|
|
38
94
|
ensure() {
|
|
39
95
|
if (!existsSync(this.stateDir)) {
|
|
40
|
-
|
|
96
|
+
mkdirSync2(this.stateDir, { recursive: true });
|
|
41
97
|
}
|
|
42
98
|
}
|
|
43
99
|
get dir() {
|
|
@@ -55,8 +111,8 @@ class StateDirResolver {
|
|
|
55
111
|
get statusFile() {
|
|
56
112
|
return join(this.stateDir, "status.json");
|
|
57
113
|
}
|
|
58
|
-
get
|
|
59
|
-
return join(this.stateDir, "
|
|
114
|
+
get daemonRecordFile() {
|
|
115
|
+
return join(this.stateDir, "daemon.json");
|
|
60
116
|
}
|
|
61
117
|
get currentThreadFile() {
|
|
62
118
|
return join(this.stateDir, "current-thread.json");
|
|
@@ -120,7 +176,7 @@ function parsePositiveIntEnv(name, fallback, log = () => {}, env = process.env)
|
|
|
120
176
|
var require_package = __commonJS((exports, module) => {
|
|
121
177
|
module.exports = {
|
|
122
178
|
name: "@raysonmeng/agentbridge",
|
|
123
|
-
version: "0.1.
|
|
179
|
+
version: "0.1.13",
|
|
124
180
|
description: "Bridge between Claude Code and Codex \u2014 bidirectional agent communication via MCP Channel + JSON-RPC",
|
|
125
181
|
type: "module",
|
|
126
182
|
packageManager: "bun@1.3.11",
|
|
@@ -151,6 +207,8 @@ var require_package = __commonJS((exports, module) => {
|
|
|
151
207
|
prepublishOnly: "bun run build:cli && bun run build:plugin && bun run verify:plugin-sync && bun scripts/check-plugin-versions.js",
|
|
152
208
|
"validate:plugin": "claude plugin validate plugins/agentbridge && claude plugin validate .claude-plugin/marketplace.json",
|
|
153
209
|
test: "bun test src",
|
|
210
|
+
"test:unit": "bun test src/unit-test",
|
|
211
|
+
"test:integration": "bun test src/integration-test",
|
|
154
212
|
"e2e:transport": "bun scripts/e2e-codex-transport.mjs",
|
|
155
213
|
"install:global": "node scripts/install-global.mjs local",
|
|
156
214
|
"install:global:local": "node scripts/install-global.mjs local",
|
|
@@ -199,7 +257,7 @@ __export(exports_update_notifier, {
|
|
|
199
257
|
buildUpdateNotice: () => buildUpdateNotice,
|
|
200
258
|
PACKAGE_NAME: () => PACKAGE_NAME
|
|
201
259
|
});
|
|
202
|
-
import { readFileSync
|
|
260
|
+
import { readFileSync } from "fs";
|
|
203
261
|
function getCurrentVersion() {
|
|
204
262
|
try {
|
|
205
263
|
return require_package().version;
|
|
@@ -235,9 +293,7 @@ function readCache(stateDir) {
|
|
|
235
293
|
}
|
|
236
294
|
function writeCache(stateDir, cache) {
|
|
237
295
|
try {
|
|
238
|
-
stateDir.
|
|
239
|
-
writeFileSync(stateDir.updateCheckFile, JSON.stringify(cache, null, 2) + `
|
|
240
|
-
`, "utf-8");
|
|
296
|
+
atomicWriteJson(stateDir.updateCheckFile, cache);
|
|
241
297
|
} catch {}
|
|
242
298
|
}
|
|
243
299
|
function parseLatestFromRegistry(body) {
|
|
@@ -311,6 +367,7 @@ function maybeNotifyUpdate(deps = {}) {
|
|
|
311
367
|
}
|
|
312
368
|
var PACKAGE_NAME = "@raysonmeng/agentbridge", REGISTRY_URL, ABBREVIATED_ACCEPT = "application/vnd.npm.install-v1+json", DEFAULT_CHECK_INTERVAL_MS, FETCH_TIMEOUT_MS = 2500, CHECK_INTERVAL_ENV = "AGENTBRIDGE_UPDATE_CHECK_INTERVAL_MS";
|
|
313
369
|
var init_update_notifier = __esm(() => {
|
|
370
|
+
init_atomic_json();
|
|
314
371
|
init_state_dir();
|
|
315
372
|
init_version_utils();
|
|
316
373
|
REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}`;
|
|
@@ -318,11 +375,53 @@ var init_update_notifier = __esm(() => {
|
|
|
318
375
|
});
|
|
319
376
|
|
|
320
377
|
// src/config-service.ts
|
|
321
|
-
import { readFileSync as readFileSync2,
|
|
378
|
+
import { readFileSync as readFileSync2, mkdirSync as mkdirSync3, existsSync as existsSync2 } from "fs";
|
|
322
379
|
import { join as join2 } from "path";
|
|
323
380
|
function isRecord(value) {
|
|
324
381
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
325
382
|
}
|
|
383
|
+
function isCoercibleNumber(value) {
|
|
384
|
+
if (typeof value === "number")
|
|
385
|
+
return Number.isFinite(value);
|
|
386
|
+
if (typeof value === "string")
|
|
387
|
+
return Number.isFinite(Number(value));
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
function findShapeViolation(raw) {
|
|
391
|
+
if ("idleShutdownSeconds" in raw && !isCoercibleNumber(raw.idleShutdownSeconds)) {
|
|
392
|
+
return "idleShutdownSeconds is present but not a number";
|
|
393
|
+
}
|
|
394
|
+
if ("budget" in raw) {
|
|
395
|
+
const budget = raw.budget;
|
|
396
|
+
if (!isRecord(budget)) {
|
|
397
|
+
return "budget is present but not an object";
|
|
398
|
+
}
|
|
399
|
+
const numericKeys = ["pauseAt", "resumeBelow", "pollSeconds", "syncDriftPct"];
|
|
400
|
+
for (const key of numericKeys) {
|
|
401
|
+
if (key in budget && !isCoercibleNumber(budget[key])) {
|
|
402
|
+
return `budget.${key} is present but not a number`;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if ("parallel" in budget) {
|
|
406
|
+
const parallel = budget.parallel;
|
|
407
|
+
if (!isRecord(parallel)) {
|
|
408
|
+
return "budget.parallel is present but not an object";
|
|
409
|
+
}
|
|
410
|
+
for (const key of ["minRemainingPct", "timeWindowSec"]) {
|
|
411
|
+
if (key in parallel && !isCoercibleNumber(parallel[key])) {
|
|
412
|
+
return `budget.parallel.${key} is present but not a number`;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
function hasCustomDecisionValues(config) {
|
|
420
|
+
const d = DEFAULT_CONFIG;
|
|
421
|
+
const b = config.budget;
|
|
422
|
+
const db = d.budget;
|
|
423
|
+
return config.idleShutdownSeconds !== d.idleShutdownSeconds || config.turnCoordination.attentionWindowSeconds !== d.turnCoordination.attentionWindowSeconds || config.codex.appPort !== d.codex.appPort || config.codex.proxyPort !== d.codex.proxyPort || b.enabled !== db.enabled || b.pollSeconds !== db.pollSeconds || b.pauseAt !== db.pauseAt || b.resumeBelow !== db.resumeBelow || b.syncDriftPct !== db.syncDriftPct || b.parallel.minRemainingPct !== db.parallel.minRemainingPct || b.parallel.timeWindowSec !== db.parallel.timeWindowSec || b.codexTierControl !== db.codexTierControl;
|
|
424
|
+
}
|
|
326
425
|
function normalizeInteger(value, fallback) {
|
|
327
426
|
if (typeof value === "number" && Number.isFinite(value))
|
|
328
427
|
return value;
|
|
@@ -358,35 +457,35 @@ function normalizeCodexOverride(raw) {
|
|
|
358
457
|
override.effort = raw.effort.trim();
|
|
359
458
|
return Object.keys(override).length > 0 ? override : null;
|
|
360
459
|
}
|
|
361
|
-
function normalizeCodexTiers(raw) {
|
|
460
|
+
function normalizeCodexTiers(raw, fallback = DEFAULT_BUDGET_CONFIG.codexTiers) {
|
|
362
461
|
const tiers = isRecord(raw) ? raw : {};
|
|
363
462
|
return {
|
|
364
463
|
full: normalizeCodexOverride(tiers.full),
|
|
365
|
-
balanced: normalizeCodexOverride(tiers.balanced) ??
|
|
366
|
-
eco: normalizeCodexOverride(tiers.eco) ??
|
|
464
|
+
balanced: normalizeCodexOverride(tiers.balanced) ?? fallback.balanced,
|
|
465
|
+
eco: normalizeCodexOverride(tiers.eco) ?? fallback.eco
|
|
367
466
|
};
|
|
368
467
|
}
|
|
369
|
-
function normalizeBudgetConfig(raw) {
|
|
468
|
+
function normalizeBudgetConfig(raw, fallback = DEFAULT_BUDGET_CONFIG) {
|
|
370
469
|
const budget = isRecord(raw) ? raw : {};
|
|
371
470
|
const parallel = isRecord(budget.parallel) ? budget.parallel : {};
|
|
372
|
-
const codexTiers = normalizeCodexTiers(budget.codexTiers);
|
|
373
|
-
let pauseAt = normalizeBoundedInteger(budget.pauseAt,
|
|
374
|
-
let resumeBelow = normalizeBoundedInteger(budget.resumeBelow,
|
|
471
|
+
const codexTiers = normalizeCodexTiers(budget.codexTiers, fallback.codexTiers);
|
|
472
|
+
let pauseAt = normalizeBoundedInteger(budget.pauseAt, fallback.pauseAt, 1, 100);
|
|
473
|
+
let resumeBelow = normalizeBoundedInteger(budget.resumeBelow, fallback.resumeBelow, 0, 99);
|
|
375
474
|
if (pauseAt <= resumeBelow) {
|
|
376
475
|
pauseAt = DEFAULT_BUDGET_CONFIG.pauseAt;
|
|
377
476
|
resumeBelow = DEFAULT_BUDGET_CONFIG.resumeBelow;
|
|
378
477
|
}
|
|
379
478
|
return {
|
|
380
|
-
enabled: normalizeBoolean(budget.enabled,
|
|
381
|
-
pollSeconds: normalizeBoundedInteger(budget.pollSeconds,
|
|
479
|
+
enabled: normalizeBoolean(budget.enabled, fallback.enabled),
|
|
480
|
+
pollSeconds: normalizeBoundedInteger(budget.pollSeconds, fallback.pollSeconds, 5, 3600),
|
|
382
481
|
pauseAt,
|
|
383
482
|
resumeBelow,
|
|
384
|
-
syncDriftPct: normalizeBoundedInteger(budget.syncDriftPct,
|
|
483
|
+
syncDriftPct: normalizeBoundedInteger(budget.syncDriftPct, fallback.syncDriftPct, 1, 100),
|
|
385
484
|
parallel: {
|
|
386
|
-
minRemainingPct: normalizeBoundedInteger(parallel.minRemainingPct,
|
|
387
|
-
timeWindowSec: normalizeBoundedInteger(parallel.timeWindowSec,
|
|
485
|
+
minRemainingPct: normalizeBoundedInteger(parallel.minRemainingPct, fallback.parallel.minRemainingPct, 1, 100),
|
|
486
|
+
timeWindowSec: normalizeBoundedInteger(parallel.timeWindowSec, fallback.parallel.timeWindowSec, 60, 604800)
|
|
388
487
|
},
|
|
389
|
-
codexTierControl: normalizeBoolean(budget.codexTierControl,
|
|
488
|
+
codexTierControl: normalizeBoolean(budget.codexTierControl, fallback.codexTierControl) && codexTiers.full !== null,
|
|
390
489
|
codexTiers
|
|
391
490
|
};
|
|
392
491
|
}
|
|
@@ -400,13 +499,13 @@ function normalizeConfig(raw) {
|
|
|
400
499
|
return {
|
|
401
500
|
version: typeof config.version === "string" ? config.version : DEFAULT_CONFIG.version,
|
|
402
501
|
codex: {
|
|
403
|
-
appPort:
|
|
404
|
-
proxyPort:
|
|
502
|
+
appPort: normalizeBoundedInteger(codex.appPort ?? daemon.port, DEFAULT_CONFIG.codex.appPort, 1, 65535),
|
|
503
|
+
proxyPort: normalizeBoundedInteger(codex.proxyPort ?? daemon.proxyPort, DEFAULT_CONFIG.codex.proxyPort, 1, 65535)
|
|
405
504
|
},
|
|
406
505
|
turnCoordination: {
|
|
407
|
-
attentionWindowSeconds:
|
|
506
|
+
attentionWindowSeconds: normalizeBoundedInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds, 0, Number.MAX_SAFE_INTEGER)
|
|
408
507
|
},
|
|
409
|
-
idleShutdownSeconds:
|
|
508
|
+
idleShutdownSeconds: normalizeBoundedInteger(config.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds, 1, Number.MAX_SAFE_INTEGER),
|
|
410
509
|
budget: normalizeBudgetConfig(config.budget)
|
|
411
510
|
};
|
|
412
511
|
}
|
|
@@ -423,20 +522,62 @@ class ConfigService {
|
|
|
423
522
|
return existsSync2(this.configPath);
|
|
424
523
|
}
|
|
425
524
|
load() {
|
|
525
|
+
let raw;
|
|
426
526
|
try {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
527
|
+
raw = readFileSync2(this.configPath, "utf-8");
|
|
528
|
+
} catch (err) {
|
|
529
|
+
if (err?.code === "ENOENT") {
|
|
530
|
+
return { state: "absent" };
|
|
531
|
+
}
|
|
532
|
+
return { state: "corrupt", reason: `config.json is unreadable: ${err.message}` };
|
|
533
|
+
}
|
|
534
|
+
let parsed;
|
|
535
|
+
try {
|
|
536
|
+
parsed = JSON.parse(raw);
|
|
537
|
+
} catch (err) {
|
|
538
|
+
return {
|
|
539
|
+
state: "corrupt",
|
|
540
|
+
reason: `config.json is not valid JSON: ${err.message}`
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
if (!isRecord(parsed)) {
|
|
544
|
+
return { state: "corrupt", reason: "config.json is not a JSON object" };
|
|
545
|
+
}
|
|
546
|
+
const violation = findShapeViolation(parsed);
|
|
547
|
+
if (violation) {
|
|
548
|
+
return { state: "corrupt", reason: `config.json is shape-invalid: ${violation}` };
|
|
549
|
+
}
|
|
550
|
+
const config = normalizeConfig(parsed);
|
|
551
|
+
if (!config) {
|
|
552
|
+
return { state: "corrupt", reason: "config.json could not be normalized" };
|
|
431
553
|
}
|
|
554
|
+
return { state: "parsed", config };
|
|
432
555
|
}
|
|
433
|
-
loadOrDefault() {
|
|
434
|
-
|
|
556
|
+
loadOrDefault(log = NOOP_LOGGER) {
|
|
557
|
+
const result = this.load();
|
|
558
|
+
if (result.state === "parsed")
|
|
559
|
+
return result.config;
|
|
560
|
+
if (result.state === "corrupt") {
|
|
561
|
+
log(`config.json at ${this.configPath} is unusable (${result.reason}); ` + "falling back to defaults \u2014 your custom budget thresholds / idle-shutdown settings are NOT in effect. " + "Fix the file and restart to re-apply them.");
|
|
562
|
+
}
|
|
563
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
564
|
+
}
|
|
565
|
+
describeConfig() {
|
|
566
|
+
const result = this.load();
|
|
567
|
+
if (result.state === "absent") {
|
|
568
|
+
return { state: "absent", path: this.configPath, customValues: false };
|
|
569
|
+
}
|
|
570
|
+
if (result.state === "corrupt") {
|
|
571
|
+
return { state: "corrupt", path: this.configPath, reason: result.reason, customValues: false };
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
state: "parsed",
|
|
575
|
+
path: this.configPath,
|
|
576
|
+
customValues: hasCustomDecisionValues(result.config)
|
|
577
|
+
};
|
|
435
578
|
}
|
|
436
579
|
save(config) {
|
|
437
|
-
this.
|
|
438
|
-
writeFileSync2(this.configPath, JSON.stringify(config, null, 2) + `
|
|
439
|
-
`, "utf-8");
|
|
580
|
+
atomicWriteJson(this.configPath, config);
|
|
440
581
|
}
|
|
441
582
|
initDefaults() {
|
|
442
583
|
this.ensureConfigDir();
|
|
@@ -452,15 +593,16 @@ class ConfigService {
|
|
|
452
593
|
}
|
|
453
594
|
ensureConfigDir() {
|
|
454
595
|
if (!existsSync2(this.configDir)) {
|
|
455
|
-
|
|
596
|
+
mkdirSync3(this.configDir, { recursive: true });
|
|
456
597
|
}
|
|
457
598
|
}
|
|
458
599
|
}
|
|
459
|
-
var DEFAULT_BUDGET_CONFIG, DEFAULT_CONFIG, CONFIG_DIR = ".agentbridge", CONFIG_FILE = "config.json";
|
|
600
|
+
var DEFAULT_BUDGET_CONFIG, DEFAULT_CONFIG, CONFIG_DIR = ".agentbridge", CONFIG_FILE = "config.json", NOOP_LOGGER = () => {};
|
|
460
601
|
var init_config_service = __esm(() => {
|
|
602
|
+
init_atomic_json();
|
|
461
603
|
DEFAULT_BUDGET_CONFIG = {
|
|
462
604
|
enabled: true,
|
|
463
|
-
pollSeconds:
|
|
605
|
+
pollSeconds: 300,
|
|
464
606
|
pauseAt: 90,
|
|
465
607
|
resumeBelow: 30,
|
|
466
608
|
syncDriftPct: 10,
|
|
@@ -490,7 +632,7 @@ var init_config_service = __esm(() => {
|
|
|
490
632
|
});
|
|
491
633
|
|
|
492
634
|
// src/cli/pkg-root.ts
|
|
493
|
-
import { dirname, join as join3 } from "path";
|
|
635
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
494
636
|
import { existsSync as existsSync3 } from "fs";
|
|
495
637
|
import { execFileSync } from "child_process";
|
|
496
638
|
function findPackageRoot() {
|
|
@@ -499,7 +641,7 @@ function findPackageRoot() {
|
|
|
499
641
|
if (existsSync3(join3(dir, "package.json"))) {
|
|
500
642
|
return dir;
|
|
501
643
|
}
|
|
502
|
-
const parent =
|
|
644
|
+
const parent = dirname2(dir);
|
|
503
645
|
if (parent === dir) {
|
|
504
646
|
throw new Error("Could not find package.json in any parent directory");
|
|
505
647
|
}
|
|
@@ -513,6 +655,30 @@ function registerMarketplace(marketplaceRoot) {
|
|
|
513
655
|
}
|
|
514
656
|
var init_pkg_root = () => {};
|
|
515
657
|
|
|
658
|
+
// src/cli/plugin-cache.ts
|
|
659
|
+
import { existsSync as existsSync4 } from "fs";
|
|
660
|
+
import { homedir as homedir2 } from "os";
|
|
661
|
+
import { join as join4, resolve } from "path";
|
|
662
|
+
function pluginCacheRoot(home = homedir2()) {
|
|
663
|
+
return join4(home, ".claude", "plugins", "cache", MARKETPLACE_NAME, PLUGIN_NAME);
|
|
664
|
+
}
|
|
665
|
+
function isInsideRepoCheckout(projectRoot) {
|
|
666
|
+
const buildScript = resolve(projectRoot, "scripts", "build-bundles.mjs");
|
|
667
|
+
return existsSync4(buildScript);
|
|
668
|
+
}
|
|
669
|
+
function shouldWarnMissingPluginCache(cacheExists) {
|
|
670
|
+
return !cacheExists;
|
|
671
|
+
}
|
|
672
|
+
var MARKETPLACE_STEPS;
|
|
673
|
+
var init_plugin_cache = __esm(() => {
|
|
674
|
+
init_cli();
|
|
675
|
+
MARKETPLACE_STEPS = [
|
|
676
|
+
`/plugin marketplace add raysonmeng/agent-bridge`,
|
|
677
|
+
`/plugin install ${PLUGIN_NAME}@${MARKETPLACE_NAME}`,
|
|
678
|
+
`/reload-plugins`
|
|
679
|
+
];
|
|
680
|
+
});
|
|
681
|
+
|
|
516
682
|
// src/marker-section.ts
|
|
517
683
|
function upsertMarkedSection(content, sectionId, section) {
|
|
518
684
|
const startMarker = MARKER_START(sectionId);
|
|
@@ -649,18 +815,25 @@ var exports_init = {};
|
|
|
649
815
|
__export(exports_init, {
|
|
650
816
|
writeCollaborationSections: () => writeCollaborationSections,
|
|
651
817
|
runInit: () => runInit,
|
|
818
|
+
pluginInstallFallbackGuidance: () => pluginInstallFallbackGuidance,
|
|
819
|
+
formatDepChecks: () => formatDepChecks,
|
|
652
820
|
compareVersions: () => compareVersions
|
|
653
821
|
});
|
|
654
822
|
import { execSync, execFileSync as execFileSync2 } from "child_process";
|
|
655
|
-
import { readFileSync as readFileSync3, writeFileSync as
|
|
656
|
-
import { join as
|
|
823
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
824
|
+
import { join as join5 } from "path";
|
|
657
825
|
async function runInit() {
|
|
658
826
|
console.log(`AgentBridge Init
|
|
659
827
|
`);
|
|
828
|
+
const cli = cliInvocationName();
|
|
660
829
|
console.log("Checking dependencies...");
|
|
661
|
-
checkBun();
|
|
662
|
-
|
|
663
|
-
|
|
830
|
+
const depChecks = [checkBun(), checkClaude(), checkCodex()];
|
|
831
|
+
for (const line of formatDepChecks(depChecks, cli)) {
|
|
832
|
+
console.log(line);
|
|
833
|
+
}
|
|
834
|
+
if (depChecks.some((check) => check.status === "fail")) {
|
|
835
|
+
process.exit(1);
|
|
836
|
+
}
|
|
664
837
|
console.log("");
|
|
665
838
|
console.log("Generating project config...");
|
|
666
839
|
const configService = new ConfigService;
|
|
@@ -681,72 +854,123 @@ async function runInit() {
|
|
|
681
854
|
}
|
|
682
855
|
console.log("");
|
|
683
856
|
console.log("Installing AgentBridge plugin...");
|
|
857
|
+
let pluginInstalled = false;
|
|
684
858
|
try {
|
|
685
|
-
|
|
859
|
+
const packageRoot = findPackageRoot();
|
|
860
|
+
registerMarketplace(packageRoot);
|
|
686
861
|
execFileSync2("claude", ["plugin", "install", `${PLUGIN_NAME}@${MARKETPLACE_NAME}`], {
|
|
687
862
|
stdio: "inherit"
|
|
688
863
|
});
|
|
689
864
|
console.log(" Plugin installed successfully.");
|
|
865
|
+
pluginInstalled = true;
|
|
690
866
|
} catch {
|
|
691
867
|
console.log(" Plugin install skipped (marketplace registration or install failed).");
|
|
692
|
-
|
|
693
|
-
|
|
868
|
+
for (const line of pluginInstallFallbackGuidance(detectRepoCheckout(), cli)) {
|
|
869
|
+
console.log(line);
|
|
870
|
+
}
|
|
694
871
|
}
|
|
695
872
|
console.log("");
|
|
696
|
-
|
|
873
|
+
if (pluginInstalled) {
|
|
874
|
+
console.log(`Setup complete!
|
|
875
|
+
`);
|
|
876
|
+
} else {
|
|
877
|
+
console.log(`Setup incomplete \u2014 plugin not installed.
|
|
697
878
|
`);
|
|
879
|
+
process.exitCode = 1;
|
|
880
|
+
}
|
|
698
881
|
console.log("Next steps:");
|
|
699
882
|
console.log(" 1. If Claude Code is already running, execute /reload-plugins in your session");
|
|
700
|
-
console.log(
|
|
701
|
-
console.log(
|
|
883
|
+
console.log(` 2. Start Claude Code: ${cli} claude`);
|
|
884
|
+
console.log(` 3. Start Codex TUI: ${cli} codex`);
|
|
885
|
+
}
|
|
886
|
+
function detectRepoCheckout() {
|
|
887
|
+
try {
|
|
888
|
+
return isInsideRepoCheckout(findPackageRoot());
|
|
889
|
+
} catch {
|
|
890
|
+
return false;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
function pluginInstallFallbackGuidance(insideRepo, cli = cliInvocationName()) {
|
|
894
|
+
if (insideRepo) {
|
|
895
|
+
return [
|
|
896
|
+
" You can install it later with:",
|
|
897
|
+
` ${cli} dev # registers marketplace and installs plugin`
|
|
898
|
+
];
|
|
899
|
+
}
|
|
900
|
+
return [
|
|
901
|
+
" Install the plugin from Claude Code with these steps:",
|
|
902
|
+
...MARKETPLACE_STEPS.map((step) => ` ${step}`)
|
|
903
|
+
];
|
|
904
|
+
}
|
|
905
|
+
function formatDepChecks(checks, cli) {
|
|
906
|
+
const lines = [];
|
|
907
|
+
for (const check of checks) {
|
|
908
|
+
lines.push(` ${check.status.toUpperCase().padEnd(4)} ${check.name}: ${check.detail}`);
|
|
909
|
+
if ((check.status === "warn" || check.status === "fail") && check.hint) {
|
|
910
|
+
lines.push(` \u21B3 ${check.hint}`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
lines.push(` \u9A8C\u8BC1\u5B89\u88C5: ${cli} doctor`);
|
|
914
|
+
return lines;
|
|
702
915
|
}
|
|
703
916
|
function checkBun() {
|
|
704
917
|
try {
|
|
705
918
|
const version = execSync("bun --version", { encoding: "utf-8" }).trim();
|
|
706
|
-
|
|
919
|
+
return { name: "bun", status: "ok", detail: version };
|
|
707
920
|
} catch {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
921
|
+
return {
|
|
922
|
+
name: "bun",
|
|
923
|
+
status: "fail",
|
|
924
|
+
detail: "not found in PATH",
|
|
925
|
+
hint: "Install Bun: https://bun.sh"
|
|
926
|
+
};
|
|
711
927
|
}
|
|
712
928
|
}
|
|
713
929
|
function checkClaude() {
|
|
930
|
+
let versionOutput;
|
|
714
931
|
try {
|
|
715
|
-
|
|
716
|
-
const match = versionOutput.match(/(\d+\.\d+\.\d+)/);
|
|
717
|
-
if (match) {
|
|
718
|
-
const version = match[1];
|
|
719
|
-
console.log(` claude: ${version}`);
|
|
720
|
-
if (compareVersions(version, MIN_CLAUDE_VERSION) < 0) {
|
|
721
|
-
console.error(` ERROR: Claude Code version ${version} is too old.`);
|
|
722
|
-
console.error(` Channels require >= ${MIN_CLAUDE_VERSION}.`);
|
|
723
|
-
console.error(" Update: npm update -g @anthropic-ai/claude-code");
|
|
724
|
-
process.exit(1);
|
|
725
|
-
}
|
|
726
|
-
} else {
|
|
727
|
-
console.log(` claude: ${versionOutput} (version check skipped)`);
|
|
728
|
-
}
|
|
932
|
+
versionOutput = execSync("claude --version", { encoding: "utf-8" }).trim();
|
|
729
933
|
} catch {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
934
|
+
return {
|
|
935
|
+
name: "claude",
|
|
936
|
+
status: "fail",
|
|
937
|
+
detail: "not found in PATH",
|
|
938
|
+
hint: "Install Claude Code: npm install -g @anthropic-ai/claude-code"
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
const match = versionOutput.match(/(\d+\.\d+\.\d+)/);
|
|
942
|
+
if (!match) {
|
|
943
|
+
return { name: "claude", status: "ok", detail: `${versionOutput} (version check skipped)` };
|
|
944
|
+
}
|
|
945
|
+
const version = match[1];
|
|
946
|
+
if (compareVersions(version, MIN_CLAUDE_VERSION) < 0) {
|
|
947
|
+
return {
|
|
948
|
+
name: "claude",
|
|
949
|
+
status: "fail",
|
|
950
|
+
detail: `${version} is too old (channels require >= ${MIN_CLAUDE_VERSION})`,
|
|
951
|
+
hint: "Update: npm update -g @anthropic-ai/claude-code"
|
|
952
|
+
};
|
|
733
953
|
}
|
|
954
|
+
return { name: "claude", status: "ok", detail: version };
|
|
734
955
|
}
|
|
735
956
|
function checkCodex() {
|
|
736
957
|
try {
|
|
737
958
|
const version = execSync("codex --version", { encoding: "utf-8" }).trim();
|
|
738
|
-
|
|
959
|
+
return { name: "codex", status: "ok", detail: version };
|
|
739
960
|
} catch {
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
961
|
+
return {
|
|
962
|
+
name: "codex",
|
|
963
|
+
status: "warn",
|
|
964
|
+
detail: "not found in PATH (the Codex side will be unavailable until installed)",
|
|
965
|
+
hint: "Install Codex when you want to pair: https://github.com/openai/codex"
|
|
966
|
+
};
|
|
743
967
|
}
|
|
744
968
|
}
|
|
745
969
|
function writeCollaborationSections(projectRoot) {
|
|
746
970
|
const results = [];
|
|
747
971
|
const files = [
|
|
748
|
-
{ name: "CLAUDE.md", path:
|
|
749
|
-
{ name: "AGENTS.md", path:
|
|
972
|
+
{ name: "CLAUDE.md", path: join5(projectRoot, "CLAUDE.md"), section: CLAUDE_MD_SECTION },
|
|
973
|
+
{ name: "AGENTS.md", path: join5(projectRoot, "AGENTS.md"), section: AGENTS_MD_SECTION }
|
|
750
974
|
];
|
|
751
975
|
for (const { name, path, section } of files) {
|
|
752
976
|
let existing = "";
|
|
@@ -765,7 +989,7 @@ function writeCollaborationSections(projectRoot) {
|
|
|
765
989
|
results.push(`${name}: unchanged (section already up to date)`);
|
|
766
990
|
continue;
|
|
767
991
|
}
|
|
768
|
-
|
|
992
|
+
writeFileSync2(path, updated, "utf-8");
|
|
769
993
|
if (existing === "") {
|
|
770
994
|
results.push(`${name}: created with collaboration section`);
|
|
771
995
|
} else if (existing.includes(`<!-- ${MARKER_ID}:start -->`)) {
|
|
@@ -778,9 +1002,11 @@ function writeCollaborationSections(projectRoot) {
|
|
|
778
1002
|
}
|
|
779
1003
|
var MIN_CLAUDE_VERSION = "2.1.80";
|
|
780
1004
|
var init_init = __esm(() => {
|
|
1005
|
+
init_cli_invocation();
|
|
781
1006
|
init_config_service();
|
|
782
1007
|
init_cli();
|
|
783
1008
|
init_pkg_root();
|
|
1009
|
+
init_plugin_cache();
|
|
784
1010
|
init_version_utils();
|
|
785
1011
|
});
|
|
786
1012
|
|
|
@@ -790,19 +1016,17 @@ __export(exports_dev, {
|
|
|
790
1016
|
runDev: () => runDev
|
|
791
1017
|
});
|
|
792
1018
|
import { execFileSync as execFileSync3, spawnSync } from "child_process";
|
|
793
|
-
import { resolve } from "path";
|
|
794
|
-
import { existsSync as
|
|
795
|
-
import { homedir as homedir2 } from "os";
|
|
1019
|
+
import { resolve as resolve2 } from "path";
|
|
1020
|
+
import { existsSync as existsSync5, cpSync, rmSync } from "fs";
|
|
796
1021
|
async function runDev(args = []) {
|
|
797
1022
|
console.log(`AgentBridge Dev Setup
|
|
798
1023
|
`);
|
|
799
1024
|
const skipBuild = args.includes("--skip-build");
|
|
800
1025
|
const projectRoot = findPackageRoot();
|
|
801
|
-
const marketplacePath =
|
|
802
|
-
const pluginDir =
|
|
803
|
-
const pluginManifest =
|
|
804
|
-
|
|
805
|
-
if (!existsSync4(buildScript)) {
|
|
1026
|
+
const marketplacePath = resolve2(projectRoot, ".claude-plugin", "marketplace.json");
|
|
1027
|
+
const pluginDir = resolve2(projectRoot, "plugins", "agentbridge");
|
|
1028
|
+
const pluginManifest = resolve2(pluginDir, ".claude-plugin", "plugin.json");
|
|
1029
|
+
if (!isInsideRepoCheckout(projectRoot)) {
|
|
806
1030
|
console.error(" ERROR: 'agentbridge dev' must run inside an AgentBridge repository checkout \u2014");
|
|
807
1031
|
console.error(" the published package does not ship the build scripts.");
|
|
808
1032
|
console.error("");
|
|
@@ -839,12 +1063,12 @@ async function runDev(args = []) {
|
|
|
839
1063
|
console.log(` \u2713 Plugin built successfully
|
|
840
1064
|
`);
|
|
841
1065
|
}
|
|
842
|
-
if (!
|
|
1066
|
+
if (!existsSync5(pluginManifest)) {
|
|
843
1067
|
console.error(` ERROR: Plugin manifest not found at ${pluginManifest}`);
|
|
844
1068
|
console.error(" Run 'bun run build:plugin' first, or check your working tree.");
|
|
845
1069
|
process.exit(1);
|
|
846
1070
|
}
|
|
847
|
-
if (!
|
|
1071
|
+
if (!existsSync5(marketplacePath)) {
|
|
848
1072
|
console.error(` ERROR: Marketplace manifest not found at ${marketplacePath}`);
|
|
849
1073
|
process.exit(1);
|
|
850
1074
|
}
|
|
@@ -873,12 +1097,12 @@ Installing plugin...`);
|
|
|
873
1097
|
}
|
|
874
1098
|
console.log(`
|
|
875
1099
|
Syncing local plugin to cache...`);
|
|
876
|
-
const cacheDir =
|
|
877
|
-
if (
|
|
1100
|
+
const cacheDir = pluginCacheRoot();
|
|
1101
|
+
if (existsSync5(cacheDir)) {
|
|
878
1102
|
const versionDirs = Bun.spawnSync(["ls", cacheDir]).stdout.toString().trim().split(`
|
|
879
1103
|
`).filter(Boolean);
|
|
880
1104
|
for (const ver of versionDirs) {
|
|
881
|
-
const targetDir =
|
|
1105
|
+
const targetDir = resolve2(cacheDir, ver);
|
|
882
1106
|
rmSync(targetDir, { recursive: true, force: true });
|
|
883
1107
|
cpSync(pluginDir, targetDir, { recursive: true });
|
|
884
1108
|
console.log(` Synced to ${targetDir}`);
|
|
@@ -898,22 +1122,101 @@ Syncing local plugin to cache...`);
|
|
|
898
1122
|
var init_dev = __esm(() => {
|
|
899
1123
|
init_cli();
|
|
900
1124
|
init_pkg_root();
|
|
1125
|
+
init_plugin_cache();
|
|
901
1126
|
});
|
|
902
1127
|
|
|
903
1128
|
// src/control-protocol.ts
|
|
904
|
-
var CLOSE_CODE_REPLACED = 4001, CLOSE_CODE_EVICTED_STALE = 4002, CLOSE_CODE_PROBE_IN_PROGRESS = 4003, CLOSE_CODE_PAIR_MISMATCH = 4004;
|
|
1129
|
+
var CLOSE_CODE_REPLACED = 4001, CLOSE_CODE_EVICTED_STALE = 4002, CLOSE_CODE_PROBE_IN_PROGRESS = 4003, CLOSE_CODE_PAIR_MISMATCH = 4004, CLOSE_CODE_TOKEN_MISMATCH = 4005, CLOSE_CODE_CONTRACT_MISMATCH = 4006;
|
|
1130
|
+
|
|
1131
|
+
// src/interrupt-timing.ts
|
|
1132
|
+
var CLIENT_REPLY_TIMEOUT_MS = 15000, INTERRUPT_CLIENT_MARGIN_MS = 2000, MAX_INTERRUPT_TIMEOUT_MS;
|
|
1133
|
+
var init_interrupt_timing = __esm(() => {
|
|
1134
|
+
MAX_INTERRUPT_TIMEOUT_MS = CLIENT_REPLY_TIMEOUT_MS - INTERRUPT_CLIENT_MARGIN_MS;
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
// src/pending-request-registry.ts
|
|
1138
|
+
class PendingRequestRegistry {
|
|
1139
|
+
entries = new Map;
|
|
1140
|
+
setTimer;
|
|
1141
|
+
clearTimer;
|
|
1142
|
+
constructor(deps = {}) {
|
|
1143
|
+
this.setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
1144
|
+
this.clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
|
|
1145
|
+
}
|
|
1146
|
+
get size() {
|
|
1147
|
+
return this.entries.size;
|
|
1148
|
+
}
|
|
1149
|
+
has(id) {
|
|
1150
|
+
return this.entries.has(id);
|
|
1151
|
+
}
|
|
1152
|
+
register(id, options) {
|
|
1153
|
+
const existing = this.entries.get(id);
|
|
1154
|
+
if (existing) {
|
|
1155
|
+
this.clearTimer(existing.timer);
|
|
1156
|
+
this.entries.delete(id);
|
|
1157
|
+
}
|
|
1158
|
+
return new Promise((resolve3, reject) => {
|
|
1159
|
+
const timer = this.setTimer(() => {
|
|
1160
|
+
if (!this.entries.has(id))
|
|
1161
|
+
return;
|
|
1162
|
+
this.entries.delete(id);
|
|
1163
|
+
options.onTimeout({ resolve: resolve3, reject });
|
|
1164
|
+
}, options.timeoutMs);
|
|
1165
|
+
if (options.unref) {
|
|
1166
|
+
timer.unref?.();
|
|
1167
|
+
}
|
|
1168
|
+
this.entries.set(id, { resolve: resolve3, reject, timer });
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
settle(id, value) {
|
|
1172
|
+
const entry = this.entries.get(id);
|
|
1173
|
+
if (!entry)
|
|
1174
|
+
return false;
|
|
1175
|
+
this.clearTimer(entry.timer);
|
|
1176
|
+
this.entries.delete(id);
|
|
1177
|
+
entry.resolve(value);
|
|
1178
|
+
return true;
|
|
1179
|
+
}
|
|
1180
|
+
reject(id, error) {
|
|
1181
|
+
const entry = this.entries.get(id);
|
|
1182
|
+
if (!entry)
|
|
1183
|
+
return false;
|
|
1184
|
+
this.clearTimer(entry.timer);
|
|
1185
|
+
this.entries.delete(id);
|
|
1186
|
+
entry.reject(error);
|
|
1187
|
+
return true;
|
|
1188
|
+
}
|
|
1189
|
+
settleAll(value) {
|
|
1190
|
+
const make = typeof value === "function" ? value : () => value;
|
|
1191
|
+
for (const [id, entry] of this.entries) {
|
|
1192
|
+
this.clearTimer(entry.timer);
|
|
1193
|
+
this.entries.delete(id);
|
|
1194
|
+
entry.resolve(make(id));
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
rejectAll(error) {
|
|
1198
|
+
const make = typeof error === "function" ? error : () => error;
|
|
1199
|
+
for (const [id, entry] of this.entries) {
|
|
1200
|
+
this.clearTimer(entry.timer);
|
|
1201
|
+
this.entries.delete(id);
|
|
1202
|
+
entry.reject(make(id));
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
905
1206
|
|
|
906
1207
|
// src/daemon-client.ts
|
|
907
1208
|
import { EventEmitter } from "events";
|
|
908
1209
|
var nextSocketId = 0, DaemonClient;
|
|
909
1210
|
var init_daemon_client = __esm(() => {
|
|
1211
|
+
init_interrupt_timing();
|
|
910
1212
|
DaemonClient = class DaemonClient extends EventEmitter {
|
|
911
1213
|
url;
|
|
912
1214
|
options;
|
|
913
1215
|
ws = null;
|
|
914
1216
|
wsId = 0;
|
|
915
1217
|
nextRequestId = 1;
|
|
916
|
-
pendingReplies = new
|
|
1218
|
+
pendingReplies = new PendingRequestRegistry;
|
|
1219
|
+
pendingEventWaiters = new PendingRequestRegistry;
|
|
917
1220
|
constructor(url, options = {}) {
|
|
918
1221
|
super();
|
|
919
1222
|
this.url = url;
|
|
@@ -933,7 +1236,7 @@ var init_daemon_client = __esm(() => {
|
|
|
933
1236
|
this.ws = null;
|
|
934
1237
|
}
|
|
935
1238
|
const socketId = ++nextSocketId;
|
|
936
|
-
await new Promise((
|
|
1239
|
+
await new Promise((resolve3, reject) => {
|
|
937
1240
|
const ws = new WebSocket(this.url);
|
|
938
1241
|
let settled = false;
|
|
939
1242
|
ws.onopen = () => {
|
|
@@ -942,7 +1245,7 @@ var init_daemon_client = __esm(() => {
|
|
|
942
1245
|
this.wsId = socketId;
|
|
943
1246
|
this.attachSocketHandlers(ws, socketId);
|
|
944
1247
|
this.log(`ws#${socketId} opened and attached`);
|
|
945
|
-
|
|
1248
|
+
resolve3();
|
|
946
1249
|
};
|
|
947
1250
|
ws.onerror = () => {
|
|
948
1251
|
if (settled)
|
|
@@ -959,82 +1262,73 @@ var init_daemon_client = __esm(() => {
|
|
|
959
1262
|
});
|
|
960
1263
|
}
|
|
961
1264
|
attachClaude() {
|
|
1265
|
+
const identity = this.resolveIdentity();
|
|
962
1266
|
this.send({
|
|
963
1267
|
type: "claude_connect",
|
|
964
|
-
...
|
|
1268
|
+
...identity ? { identity } : {}
|
|
965
1269
|
});
|
|
966
1270
|
}
|
|
1271
|
+
resolveIdentity() {
|
|
1272
|
+
const opt = this.options.identity;
|
|
1273
|
+
return typeof opt === "function" ? opt() : opt;
|
|
1274
|
+
}
|
|
967
1275
|
async attachClaudeAndWaitForStatus(timeoutMs = 1000) {
|
|
968
1276
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
969
1277
|
return null;
|
|
970
1278
|
}
|
|
971
|
-
return
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
if (timer) {
|
|
979
|
-
clearTimeout(timer);
|
|
980
|
-
timer = null;
|
|
981
|
-
}
|
|
982
|
-
this.off("status", onStatus);
|
|
983
|
-
this.off("rejected", onRejected);
|
|
984
|
-
this.off("disconnect", onDisconnect);
|
|
985
|
-
};
|
|
986
|
-
const finish = (value) => {
|
|
987
|
-
cleanup();
|
|
988
|
-
resolve2(value);
|
|
989
|
-
};
|
|
990
|
-
const onStatus = (status) => finish(status);
|
|
991
|
-
const onRejected = () => finish(null);
|
|
992
|
-
const onDisconnect = () => finish(null);
|
|
993
|
-
this.on("status", onStatus);
|
|
994
|
-
this.on("rejected", onRejected);
|
|
995
|
-
this.on("disconnect", onDisconnect);
|
|
996
|
-
timer = setTimeout(() => {
|
|
997
|
-
finish(null);
|
|
998
|
-
}, timeoutMs);
|
|
999
|
-
try {
|
|
1000
|
-
this.attachClaude();
|
|
1001
|
-
} catch {
|
|
1002
|
-
finish(null);
|
|
1003
|
-
}
|
|
1279
|
+
return this.awaitTypedResponse({
|
|
1280
|
+
key: "status",
|
|
1281
|
+
successEvent: "status",
|
|
1282
|
+
successValue: (status) => status,
|
|
1283
|
+
failValue: null,
|
|
1284
|
+
timeoutMs,
|
|
1285
|
+
send: () => this.attachClaude()
|
|
1004
1286
|
});
|
|
1005
1287
|
}
|
|
1006
1288
|
async probeIncumbent(timeoutMs = 3000) {
|
|
1007
1289
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1008
1290
|
return { connected: false, alive: false };
|
|
1009
1291
|
}
|
|
1010
|
-
return
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
if (timer)
|
|
1018
|
-
clearTimeout(timer);
|
|
1019
|
-
this.off("incumbentStatus", onStatus);
|
|
1020
|
-
this.off("disconnect", onDisconnect);
|
|
1021
|
-
this.off("rejected", onRejected);
|
|
1022
|
-
resolve2(value);
|
|
1023
|
-
};
|
|
1024
|
-
const onStatus = (s) => finish(s);
|
|
1025
|
-
const onDisconnect = () => finish({ connected: false, alive: false });
|
|
1026
|
-
const onRejected = () => finish({ connected: false, alive: false });
|
|
1027
|
-
this.on("incumbentStatus", onStatus);
|
|
1028
|
-
this.on("disconnect", onDisconnect);
|
|
1029
|
-
this.on("rejected", onRejected);
|
|
1030
|
-
timer = setTimeout(() => finish({ connected: false, alive: false }), timeoutMs);
|
|
1031
|
-
try {
|
|
1032
|
-
this.send({ type: "probe_incumbent" });
|
|
1033
|
-
} catch {
|
|
1034
|
-
finish({ connected: false, alive: false });
|
|
1035
|
-
}
|
|
1292
|
+
return this.awaitTypedResponse({
|
|
1293
|
+
key: "incumbent_status",
|
|
1294
|
+
successEvent: "incumbentStatus",
|
|
1295
|
+
successValue: (s) => s,
|
|
1296
|
+
failValue: { connected: false, alive: false },
|
|
1297
|
+
timeoutMs,
|
|
1298
|
+
send: () => this.send({ type: "probe_incumbent" })
|
|
1036
1299
|
});
|
|
1037
1300
|
}
|
|
1301
|
+
awaitTypedResponse(opts) {
|
|
1302
|
+
const { key, successEvent, successValue, failValue, timeoutMs, send } = opts;
|
|
1303
|
+
const onSuccess = (payload) => {
|
|
1304
|
+
this.pendingEventWaiters.settle(key, successValue(payload));
|
|
1305
|
+
};
|
|
1306
|
+
const onRejected = () => {
|
|
1307
|
+
this.pendingEventWaiters.settle(key, failValue);
|
|
1308
|
+
};
|
|
1309
|
+
const onDisconnect = () => {
|
|
1310
|
+
this.pendingEventWaiters.settle(key, failValue);
|
|
1311
|
+
};
|
|
1312
|
+
const pending = this.pendingEventWaiters.register(key, {
|
|
1313
|
+
timeoutMs,
|
|
1314
|
+
onTimeout: ({ resolve: resolve3 }) => resolve3(failValue)
|
|
1315
|
+
});
|
|
1316
|
+
const cleanup = () => {
|
|
1317
|
+
this.off(successEvent, onSuccess);
|
|
1318
|
+
this.off("rejected", onRejected);
|
|
1319
|
+
this.off("disconnect", onDisconnect);
|
|
1320
|
+
};
|
|
1321
|
+
pending.finally(cleanup);
|
|
1322
|
+
this.on(successEvent, onSuccess);
|
|
1323
|
+
this.on("rejected", onRejected);
|
|
1324
|
+
this.on("disconnect", onDisconnect);
|
|
1325
|
+
try {
|
|
1326
|
+
send();
|
|
1327
|
+
} catch {
|
|
1328
|
+
this.pendingEventWaiters.settle(key, failValue);
|
|
1329
|
+
}
|
|
1330
|
+
return pending;
|
|
1331
|
+
}
|
|
1038
1332
|
async disconnect() {
|
|
1039
1333
|
if (!this.ws)
|
|
1040
1334
|
return;
|
|
@@ -1047,25 +1341,24 @@ var init_daemon_client = __esm(() => {
|
|
|
1047
1341
|
this.ws = null;
|
|
1048
1342
|
this.rejectPendingReplies("Daemon connection closed");
|
|
1049
1343
|
}
|
|
1050
|
-
async sendReply(message, requireReply, onBusy) {
|
|
1344
|
+
async sendReply(message, requireReply, onBusy, idempotencyKey) {
|
|
1051
1345
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1052
1346
|
return { success: false, error: "AgentBridge daemon is not connected." };
|
|
1053
1347
|
}
|
|
1054
1348
|
const requestId = `reply_${Date.now()}_${this.nextRequestId++}`;
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
resolve2({ success: false, error: "Timed out waiting for AgentBridge daemon reply." });
|
|
1059
|
-
}, 15000);
|
|
1060
|
-
this.pendingReplies.set(requestId, { resolve: resolve2, timer });
|
|
1061
|
-
this.send({
|
|
1062
|
-
type: "claude_to_codex",
|
|
1063
|
-
requestId,
|
|
1064
|
-
message,
|
|
1065
|
-
...requireReply ? { requireReply: true } : {},
|
|
1066
|
-
...onBusy && onBusy !== "reject" ? { onBusy } : {}
|
|
1067
|
-
});
|
|
1349
|
+
const pending = this.pendingReplies.register(requestId, {
|
|
1350
|
+
timeoutMs: CLIENT_REPLY_TIMEOUT_MS,
|
|
1351
|
+
onTimeout: ({ resolve: resolve3 }) => resolve3({ success: false, error: "Timed out waiting for AgentBridge daemon reply." })
|
|
1068
1352
|
});
|
|
1353
|
+
this.send({
|
|
1354
|
+
type: "claude_to_codex",
|
|
1355
|
+
requestId,
|
|
1356
|
+
message,
|
|
1357
|
+
...requireReply ? { requireReply: true } : {},
|
|
1358
|
+
...onBusy && onBusy !== "reject" ? { onBusy } : {},
|
|
1359
|
+
...idempotencyKey ? { idempotencyKey } : {}
|
|
1360
|
+
});
|
|
1361
|
+
return pending;
|
|
1069
1362
|
}
|
|
1070
1363
|
attachSocketHandlers(ws, socketId) {
|
|
1071
1364
|
ws.onmessage = (event) => {
|
|
@@ -1081,14 +1374,23 @@ var init_daemon_client = __esm(() => {
|
|
|
1081
1374
|
this.emit("codexMessage", message.message);
|
|
1082
1375
|
return;
|
|
1083
1376
|
case "claude_to_codex_result": {
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1377
|
+
this.pendingReplies.settle(message.requestId, {
|
|
1378
|
+
success: message.success,
|
|
1379
|
+
error: message.error,
|
|
1380
|
+
...message.code !== undefined ? { code: message.code } : {},
|
|
1381
|
+
...message.phase !== undefined ? { phase: message.phase } : {},
|
|
1382
|
+
...message.retryAfterMs !== undefined ? { retryAfterMs: message.retryAfterMs } : {}
|
|
1383
|
+
});
|
|
1090
1384
|
return;
|
|
1091
1385
|
}
|
|
1386
|
+
case "turn_started":
|
|
1387
|
+
this.emit("turnStarted", {
|
|
1388
|
+
requestId: message.requestId,
|
|
1389
|
+
...message.idempotencyKey !== undefined ? { idempotencyKey: message.idempotencyKey } : {},
|
|
1390
|
+
threadId: message.threadId,
|
|
1391
|
+
turnId: message.turnId
|
|
1392
|
+
});
|
|
1393
|
+
return;
|
|
1092
1394
|
case "status":
|
|
1093
1395
|
this.emit("status", message.status);
|
|
1094
1396
|
return;
|
|
@@ -1103,7 +1405,7 @@ var init_daemon_client = __esm(() => {
|
|
|
1103
1405
|
if (isCurrent) {
|
|
1104
1406
|
this.ws = null;
|
|
1105
1407
|
this.rejectPendingReplies("AgentBridge daemon disconnected.");
|
|
1106
|
-
if (event.code === CLOSE_CODE_REPLACED || event.code === CLOSE_CODE_EVICTED_STALE || event.code === CLOSE_CODE_PROBE_IN_PROGRESS || event.code === CLOSE_CODE_PAIR_MISMATCH) {
|
|
1408
|
+
if (event.code === CLOSE_CODE_REPLACED || event.code === CLOSE_CODE_EVICTED_STALE || event.code === CLOSE_CODE_PROBE_IN_PROGRESS || event.code === CLOSE_CODE_PAIR_MISMATCH || event.code === CLOSE_CODE_TOKEN_MISMATCH || event.code === CLOSE_CODE_CONTRACT_MISMATCH) {
|
|
1107
1409
|
this.emit("rejected", event.code);
|
|
1108
1410
|
} else {
|
|
1109
1411
|
this.emit("disconnect");
|
|
@@ -1113,11 +1415,7 @@ var init_daemon_client = __esm(() => {
|
|
|
1113
1415
|
ws.onerror = () => {};
|
|
1114
1416
|
}
|
|
1115
1417
|
rejectPendingReplies(error) {
|
|
1116
|
-
|
|
1117
|
-
clearTimeout(pending.timer);
|
|
1118
|
-
pending.resolve({ success: false, error });
|
|
1119
|
-
this.pendingReplies.delete(requestId);
|
|
1120
|
-
}
|
|
1418
|
+
this.pendingReplies.settleAll(() => ({ success: false, error }));
|
|
1121
1419
|
}
|
|
1122
1420
|
send(message) {
|
|
1123
1421
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
@@ -1136,6 +1434,10 @@ var init_daemon_client = __esm(() => {
|
|
|
1136
1434
|
var CONTRACT_VERSION = 1;
|
|
1137
1435
|
|
|
1138
1436
|
// src/build-info.ts
|
|
1437
|
+
function hasValidCodeHash(build) {
|
|
1438
|
+
const hash = build?.codeHash;
|
|
1439
|
+
return typeof hash === "string" && hash.length > 0 && hash !== CODE_HASH_SENTINEL;
|
|
1440
|
+
}
|
|
1139
1441
|
function defineString(value, fallback) {
|
|
1140
1442
|
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
1141
1443
|
}
|
|
@@ -1150,7 +1452,14 @@ function defineNumber(value, fallback) {
|
|
|
1150
1452
|
function sameRuntimeContract(a, b) {
|
|
1151
1453
|
if (!a || !b)
|
|
1152
1454
|
return false;
|
|
1153
|
-
|
|
1455
|
+
if (a.version !== b.version || a.contractVersion !== b.contractVersion)
|
|
1456
|
+
return false;
|
|
1457
|
+
if (hasValidCodeHash(a) && hasValidCodeHash(b))
|
|
1458
|
+
return a.codeHash === b.codeHash;
|
|
1459
|
+
return a.commit === b.commit;
|
|
1460
|
+
}
|
|
1461
|
+
function runtimeContractComparisonBasis(a, b) {
|
|
1462
|
+
return hasValidCodeHash(a) && hasValidCodeHash(b) ? "codeHash" : "commit";
|
|
1154
1463
|
}
|
|
1155
1464
|
function compatibleContractVersion(a, b) {
|
|
1156
1465
|
if (!a || !b)
|
|
@@ -1160,21 +1469,23 @@ function compatibleContractVersion(a, b) {
|
|
|
1160
1469
|
function formatBuildInfo(build) {
|
|
1161
1470
|
if (!build)
|
|
1162
1471
|
return "<unknown>";
|
|
1163
|
-
|
|
1472
|
+
const codeHash = hasValidCodeHash(build) ? `/code-${build.codeHash}` : "";
|
|
1473
|
+
return `${build.version}/${build.commit}/${build.bundle}/contract-v${build.contractVersion}${codeHash}`;
|
|
1164
1474
|
}
|
|
1165
|
-
var BUILD_INFO;
|
|
1475
|
+
var CODE_HASH_SENTINEL = "source", BUILD_INFO;
|
|
1166
1476
|
var init_build_info = __esm(() => {
|
|
1167
1477
|
BUILD_INFO = Object.freeze({
|
|
1168
|
-
version: defineString("0.1.
|
|
1169
|
-
commit: defineString("
|
|
1478
|
+
version: defineString("0.1.13", "0.0.0-source"),
|
|
1479
|
+
commit: defineString("7a71869", "source"),
|
|
1170
1480
|
bundle: defineBundle("dist"),
|
|
1171
|
-
contractVersion: defineNumber(1, CONTRACT_VERSION)
|
|
1481
|
+
contractVersion: defineNumber(1, CONTRACT_VERSION),
|
|
1482
|
+
codeHash: defineString("e1fd67d07c62", "source")
|
|
1172
1483
|
});
|
|
1173
1484
|
});
|
|
1174
1485
|
|
|
1175
1486
|
// src/process-lifecycle.ts
|
|
1176
1487
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
1177
|
-
import { basename } from "path";
|
|
1488
|
+
import { basename as basename2 } from "path";
|
|
1178
1489
|
function parsePsProcessList(output) {
|
|
1179
1490
|
const entries = [];
|
|
1180
1491
|
for (const line of output.split(/\r?\n/)) {
|
|
@@ -1190,11 +1501,11 @@ function parsePsProcessList(output) {
|
|
|
1190
1501
|
}
|
|
1191
1502
|
function invokesCodexBinary(command) {
|
|
1192
1503
|
const tokens = command.trim().split(/\s+/);
|
|
1193
|
-
const exe = tokens[0] ?
|
|
1504
|
+
const exe = tokens[0] ? basename2(tokens[0]) : "";
|
|
1194
1505
|
if (exe === "codex")
|
|
1195
1506
|
return true;
|
|
1196
1507
|
if ((exe === "node" || exe === "bun") && tokens[1]) {
|
|
1197
|
-
return
|
|
1508
|
+
return basename2(tokens[1]) === "codex";
|
|
1198
1509
|
}
|
|
1199
1510
|
return false;
|
|
1200
1511
|
}
|
|
@@ -1330,19 +1641,205 @@ var init_process_lifecycle = __esm(() => {
|
|
|
1330
1641
|
isProcessAlive = pidLooksAlive;
|
|
1331
1642
|
});
|
|
1332
1643
|
|
|
1644
|
+
// src/daemon-record.ts
|
|
1645
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1646
|
+
function writeDaemonRecord(path, record) {
|
|
1647
|
+
atomicWriteJson(path, record);
|
|
1648
|
+
}
|
|
1649
|
+
function sanitizePorts(value) {
|
|
1650
|
+
if (typeof value !== "object" || value === null)
|
|
1651
|
+
return;
|
|
1652
|
+
const raw = value;
|
|
1653
|
+
const ports = {};
|
|
1654
|
+
if (typeof raw.appPort === "number")
|
|
1655
|
+
ports.appPort = raw.appPort;
|
|
1656
|
+
if (typeof raw.proxyPort === "number")
|
|
1657
|
+
ports.proxyPort = raw.proxyPort;
|
|
1658
|
+
if (typeof raw.controlPort === "number")
|
|
1659
|
+
ports.controlPort = raw.controlPort;
|
|
1660
|
+
return Object.keys(ports).length > 0 ? ports : undefined;
|
|
1661
|
+
}
|
|
1662
|
+
function readDaemonRecord(path, read = defaultRead) {
|
|
1663
|
+
let parsed;
|
|
1664
|
+
try {
|
|
1665
|
+
parsed = JSON.parse(read(path));
|
|
1666
|
+
} catch {
|
|
1667
|
+
return null;
|
|
1668
|
+
}
|
|
1669
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
1670
|
+
return null;
|
|
1671
|
+
const obj = parsed;
|
|
1672
|
+
if (typeof obj.pid !== "number" || !Number.isFinite(obj.pid))
|
|
1673
|
+
return null;
|
|
1674
|
+
const phase = obj.phase === "ready" ? "ready" : "booting";
|
|
1675
|
+
const record = { pid: obj.pid, phase };
|
|
1676
|
+
if (typeof obj.startedAt === "number")
|
|
1677
|
+
record.startedAt = obj.startedAt;
|
|
1678
|
+
if (typeof obj.nonce === "string")
|
|
1679
|
+
record.nonce = obj.nonce;
|
|
1680
|
+
if (obj.pairId === null || typeof obj.pairId === "string")
|
|
1681
|
+
record.pairId = obj.pairId;
|
|
1682
|
+
if (obj.cwd === null || typeof obj.cwd === "string")
|
|
1683
|
+
record.cwd = obj.cwd;
|
|
1684
|
+
if (obj.stateDir === null || typeof obj.stateDir === "string")
|
|
1685
|
+
record.stateDir = obj.stateDir;
|
|
1686
|
+
if (typeof obj.proxyUrl === "string")
|
|
1687
|
+
record.proxyUrl = obj.proxyUrl;
|
|
1688
|
+
if (typeof obj.appServerUrl === "string")
|
|
1689
|
+
record.appServerUrl = obj.appServerUrl;
|
|
1690
|
+
const ports = sanitizePorts(obj.ports);
|
|
1691
|
+
if (ports !== undefined)
|
|
1692
|
+
record.ports = ports;
|
|
1693
|
+
if (typeof obj.build === "object" && obj.build !== null) {
|
|
1694
|
+
record.build = obj.build;
|
|
1695
|
+
}
|
|
1696
|
+
if (typeof obj.turnPhase === "string")
|
|
1697
|
+
record.turnPhase = obj.turnPhase;
|
|
1698
|
+
if (typeof obj.turnInProgress === "boolean")
|
|
1699
|
+
record.turnInProgress = obj.turnInProgress;
|
|
1700
|
+
if (typeof obj.attentionWindowActive === "boolean") {
|
|
1701
|
+
record.attentionWindowActive = obj.attentionWindowActive;
|
|
1702
|
+
}
|
|
1703
|
+
return record;
|
|
1704
|
+
}
|
|
1705
|
+
function synthesizeLegacyRecord(pidFilePath, statusFilePath, read = defaultRead) {
|
|
1706
|
+
let pidFromPidFile = null;
|
|
1707
|
+
try {
|
|
1708
|
+
const raw = read(pidFilePath).trim();
|
|
1709
|
+
const n = Number.parseInt(raw, 10);
|
|
1710
|
+
if (Number.isFinite(n))
|
|
1711
|
+
pidFromPidFile = n;
|
|
1712
|
+
} catch {}
|
|
1713
|
+
let status = null;
|
|
1714
|
+
try {
|
|
1715
|
+
const parsed = JSON.parse(read(statusFilePath));
|
|
1716
|
+
if (typeof parsed === "object" && parsed !== null)
|
|
1717
|
+
status = parsed;
|
|
1718
|
+
} catch {}
|
|
1719
|
+
const pidFromStatus = status && typeof status.pid === "number" && Number.isFinite(status.pid) ? status.pid : null;
|
|
1720
|
+
const pid = pidFromPidFile ?? pidFromStatus;
|
|
1721
|
+
if (pid === null)
|
|
1722
|
+
return null;
|
|
1723
|
+
const record = {
|
|
1724
|
+
pid,
|
|
1725
|
+
phase: status ? "ready" : "booting"
|
|
1726
|
+
};
|
|
1727
|
+
if (status) {
|
|
1728
|
+
if (typeof status.proxyUrl === "string")
|
|
1729
|
+
record.proxyUrl = status.proxyUrl;
|
|
1730
|
+
if (typeof status.appServerUrl === "string")
|
|
1731
|
+
record.appServerUrl = status.appServerUrl;
|
|
1732
|
+
const controlPort = typeof status.controlPort === "number" ? status.controlPort : undefined;
|
|
1733
|
+
const proxyPort = portFromUrl(status.proxyUrl);
|
|
1734
|
+
const appPort = portFromUrl(status.appServerUrl);
|
|
1735
|
+
if (controlPort !== undefined || proxyPort !== undefined || appPort !== undefined) {
|
|
1736
|
+
record.ports = {};
|
|
1737
|
+
if (appPort !== undefined)
|
|
1738
|
+
record.ports.appPort = appPort;
|
|
1739
|
+
if (proxyPort !== undefined)
|
|
1740
|
+
record.ports.proxyPort = proxyPort;
|
|
1741
|
+
if (controlPort !== undefined)
|
|
1742
|
+
record.ports.controlPort = controlPort;
|
|
1743
|
+
}
|
|
1744
|
+
if (status.pairId === null || typeof status.pairId === "string")
|
|
1745
|
+
record.pairId = status.pairId;
|
|
1746
|
+
if (status.cwd === null || typeof status.cwd === "string")
|
|
1747
|
+
record.cwd = status.cwd;
|
|
1748
|
+
if (status.stateDir === null || typeof status.stateDir === "string")
|
|
1749
|
+
record.stateDir = status.stateDir;
|
|
1750
|
+
if (typeof status.build === "object" && status.build !== null) {
|
|
1751
|
+
record.build = status.build;
|
|
1752
|
+
}
|
|
1753
|
+
if (typeof status.turnPhase === "string")
|
|
1754
|
+
record.turnPhase = status.turnPhase;
|
|
1755
|
+
if (typeof status.turnInProgress === "boolean")
|
|
1756
|
+
record.turnInProgress = status.turnInProgress;
|
|
1757
|
+
if (typeof status.attentionWindowActive === "boolean") {
|
|
1758
|
+
record.attentionWindowActive = status.attentionWindowActive;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
return record;
|
|
1762
|
+
}
|
|
1763
|
+
function readUnifiedDaemonRecord(paths, read = defaultRead) {
|
|
1764
|
+
return readDaemonRecord(paths.daemonRecordFile, read) ?? synthesizeLegacyRecord(paths.pidFile, paths.statusFile, read);
|
|
1765
|
+
}
|
|
1766
|
+
function portFromUrl(url) {
|
|
1767
|
+
if (typeof url !== "string")
|
|
1768
|
+
return;
|
|
1769
|
+
const match = url.match(/:(\d+)(?:[/?]|$)/);
|
|
1770
|
+
return match ? Number.parseInt(match[1], 10) : undefined;
|
|
1771
|
+
}
|
|
1772
|
+
var defaultRead = (path) => readFileSync4(path, "utf-8");
|
|
1773
|
+
var init_daemon_record = __esm(() => {
|
|
1774
|
+
init_atomic_json();
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1333
1777
|
// src/daemon-lifecycle.ts
|
|
1334
1778
|
import { spawn } from "child_process";
|
|
1335
|
-
import { existsSync as
|
|
1779
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, statSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3, openSync as openSync2, closeSync as closeSync2, constants } from "fs";
|
|
1336
1780
|
import { fileURLToPath } from "url";
|
|
1781
|
+
function isReuseVerdict(verdict) {
|
|
1782
|
+
return verdict === "reuse" || verdict === "reuse-despite-drift";
|
|
1783
|
+
}
|
|
1784
|
+
function classifyDaemon(expectedPairId, status, buildInfo) {
|
|
1785
|
+
if (!status) {
|
|
1786
|
+
return { verdict: "unreachable", reason: "daemon status is unavailable or unparseable" };
|
|
1787
|
+
}
|
|
1788
|
+
const reportedPairId = status.pairId;
|
|
1789
|
+
if (!expectedPairId && reportedPairId != null) {
|
|
1790
|
+
return {
|
|
1791
|
+
verdict: "manual-conflict",
|
|
1792
|
+
reason: `manual mode must not adopt registered pair ${reportedPairId}`
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
if (expectedPairId) {
|
|
1796
|
+
if (reportedPairId == null) {
|
|
1797
|
+
return {
|
|
1798
|
+
verdict: "replace-foreign",
|
|
1799
|
+
reason: `pair ${expectedPairId} found daemon without pair identity`
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
if (reportedPairId !== expectedPairId) {
|
|
1803
|
+
return {
|
|
1804
|
+
verdict: "replace-foreign",
|
|
1805
|
+
reason: `pair ${expectedPairId} found daemon for pair ${reportedPairId}`
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
if (!sameRuntimeContract(status.build, buildInfo)) {
|
|
1810
|
+
if (compatibleContractVersion(status.build, buildInfo) && status.tuiConnected === true) {
|
|
1811
|
+
return {
|
|
1812
|
+
verdict: "reuse-despite-drift",
|
|
1813
|
+
reason: "runtime build drift has a compatible contract and a live Codex TUI is attached"
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
const basis = runtimeContractComparisonBasis(status.build, buildInfo) === "codeHash" ? "compared by codeHash" : "compared by commit stamp; legacy build without codeHash";
|
|
1817
|
+
return {
|
|
1818
|
+
verdict: "replace-drifted",
|
|
1819
|
+
reason: `runtime build ${formatBuildInfo(status.build)} does not match launcher ` + `${formatBuildInfo(buildInfo)} (${basis})`
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
return { verdict: "reuse", reason: "daemon pair and runtime contract match" };
|
|
1823
|
+
}
|
|
1824
|
+
function resolveTiming(timing) {
|
|
1825
|
+
return {
|
|
1826
|
+
reuseReadyRetries: timing?.reuseReadyRetries ?? REUSE_READY_RETRIES,
|
|
1827
|
+
reuseReadyDelayMs: timing?.reuseReadyDelayMs ?? REUSE_READY_DELAY_MS,
|
|
1828
|
+
waitReadyRetries: timing?.waitReadyRetries ?? WAIT_READY_RETRIES,
|
|
1829
|
+
waitReadyDelayMs: timing?.waitReadyDelayMs ?? WAIT_READY_DELAY_MS
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1337
1832
|
|
|
1338
1833
|
class DaemonLifecycle {
|
|
1339
1834
|
stateDir;
|
|
1340
1835
|
controlPort;
|
|
1341
1836
|
log;
|
|
1837
|
+
timing;
|
|
1342
1838
|
constructor(opts) {
|
|
1343
1839
|
this.stateDir = opts.stateDir;
|
|
1344
1840
|
this.controlPort = opts.controlPort;
|
|
1345
1841
|
this.log = opts.log;
|
|
1842
|
+
this.timing = resolveTiming(opts.timing);
|
|
1346
1843
|
}
|
|
1347
1844
|
get healthUrl() {
|
|
1348
1845
|
return `http://127.0.0.1:${this.controlPort}/healthz`;
|
|
@@ -1366,55 +1863,40 @@ class DaemonLifecycle {
|
|
|
1366
1863
|
return null;
|
|
1367
1864
|
}
|
|
1368
1865
|
}
|
|
1369
|
-
|
|
1370
|
-
const
|
|
1371
|
-
if (
|
|
1372
|
-
return
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
const reported = status.pairId;
|
|
1376
|
-
if (reported == null)
|
|
1377
|
-
return true;
|
|
1378
|
-
return reported !== expected;
|
|
1379
|
-
}
|
|
1380
|
-
isRegisteredPairDaemonInManualMode(status) {
|
|
1381
|
-
return !this.expectedPairId && status?.pairId != null;
|
|
1382
|
-
}
|
|
1383
|
-
isBuildDrifted(status) {
|
|
1384
|
-
if (process.env.AGENTBRIDGE_ALLOW_BUILD_DRIFT === "1")
|
|
1385
|
-
return false;
|
|
1386
|
-
const runtime = status?.build;
|
|
1387
|
-
if (!runtime)
|
|
1388
|
-
return true;
|
|
1389
|
-
return !sameRuntimeContract(runtime, BUILD_INFO);
|
|
1866
|
+
classifyDaemon(status) {
|
|
1867
|
+
const classification = classifyDaemon(this.expectedPairId, status, BUILD_INFO);
|
|
1868
|
+
if (process.env.AGENTBRIDGE_ALLOW_BUILD_DRIFT === "1" && (classification.verdict === "replace-drifted" || classification.verdict === "unreachable")) {
|
|
1869
|
+
return { verdict: "reuse", reason: "build drift replacement disabled by AGENTBRIDGE_ALLOW_BUILD_DRIFT" };
|
|
1870
|
+
}
|
|
1871
|
+
return classification;
|
|
1390
1872
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
return false;
|
|
1394
|
-
return status?.tuiConnected === true;
|
|
1873
|
+
manualConflictError(status) {
|
|
1874
|
+
return new Error(`Control port ${this.controlPort} is owned by registered pair ${status?.pairId}. ` + `This session has no pair identity (manual mode) and will not reuse or replace it \u2014 ` + `start with \`agentbridge claude\` from that pair's directory, or set AGENTBRIDGE_CONTROL_PORT to a free port.`);
|
|
1395
1875
|
}
|
|
1396
1876
|
async ensureRunning() {
|
|
1397
1877
|
if (await this.isHealthy()) {
|
|
1398
1878
|
const status = await this.fetchStatus();
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
this.log(`Daemon on control port ${this.controlPort} is running build ${formatBuildInfo(status?.build)} ` + `(launcher ${formatBuildInfo(BUILD_INFO)}) but a live Codex TUI is attached \u2014 reusing instead of ` + `replacing; the new build is picked up at the next restart (abg kill, then relaunch)`);
|
|
1410
|
-
} else {
|
|
1879
|
+
const classification = this.classifyDaemon(status);
|
|
1880
|
+
switch (classification.verdict) {
|
|
1881
|
+
case "manual-conflict":
|
|
1882
|
+
throw this.manualConflictError(status);
|
|
1883
|
+
case "replace-foreign":
|
|
1884
|
+
this.log(`Control port ${this.controlPort} held by a daemon for pair ${status?.pairId ?? "<none>"}, ` + `but this pair is ${this.expectedPairId} \u2014 replacing foreign daemon`);
|
|
1885
|
+
await this.replaceUnhealthyDaemon(status?.pid);
|
|
1886
|
+
return;
|
|
1887
|
+
case "replace-drifted":
|
|
1888
|
+
case "unreachable":
|
|
1411
1889
|
this.log(`Daemon on control port ${this.controlPort} is running build ${formatBuildInfo(status?.build)} ` + `but launcher is ${formatBuildInfo(BUILD_INFO)} \u2014 replacing drifted daemon`);
|
|
1412
1890
|
await this.replaceUnhealthyDaemon(status?.pid);
|
|
1413
1891
|
return;
|
|
1414
|
-
|
|
1892
|
+
case "reuse-despite-drift":
|
|
1893
|
+
this.log(`Daemon on control port ${this.controlPort} is running build ${formatBuildInfo(status?.build)} ` + `(launcher ${formatBuildInfo(BUILD_INFO)}) but a live Codex TUI is attached \u2014 reusing instead of ` + `replacing; the new build is picked up at the next restart (abg kill, then relaunch)`);
|
|
1894
|
+
break;
|
|
1895
|
+
case "reuse":
|
|
1896
|
+
break;
|
|
1415
1897
|
}
|
|
1416
1898
|
try {
|
|
1417
|
-
await this.waitForReady(
|
|
1899
|
+
await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
|
|
1418
1900
|
return;
|
|
1419
1901
|
} catch {
|
|
1420
1902
|
this.log(`Daemon on control port ${this.controlPort} is healthy but not ready within reuse window \u2014 replacing`);
|
|
@@ -1427,7 +1909,7 @@ class DaemonLifecycle {
|
|
|
1427
1909
|
if (isProcessAlive(existingPid)) {
|
|
1428
1910
|
if (isAgentBridgeDaemon(existingPid)) {
|
|
1429
1911
|
try {
|
|
1430
|
-
await this.waitForReady(
|
|
1912
|
+
await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
|
|
1431
1913
|
return;
|
|
1432
1914
|
} catch {
|
|
1433
1915
|
this.log(`Existing daemon process ${existingPid} never became ready \u2014 replacing`);
|
|
@@ -1441,18 +1923,21 @@ class DaemonLifecycle {
|
|
|
1441
1923
|
}
|
|
1442
1924
|
await this.withStartupLockStrict(async (locked) => {
|
|
1443
1925
|
if (!locked) {
|
|
1444
|
-
this.
|
|
1445
|
-
await this.waitForReadyAndOurs();
|
|
1926
|
+
await this.waitForContendedStartupLock();
|
|
1446
1927
|
return;
|
|
1447
1928
|
}
|
|
1448
1929
|
if (await this.isHealthy()) {
|
|
1449
1930
|
const status = await this.fetchStatus();
|
|
1450
|
-
|
|
1451
|
-
|
|
1931
|
+
const classification = this.classifyDaemon(status);
|
|
1932
|
+
if (classification.verdict === "manual-conflict") {
|
|
1933
|
+
throw this.manualConflictError(status);
|
|
1934
|
+
}
|
|
1935
|
+
if (!isReuseVerdict(classification.verdict)) {
|
|
1936
|
+
this.log(`Daemon on control port ${this.controlPort} is not reusable under startup lock ` + `(pair=${status?.pairId ?? "<none>"}, build=${formatBuildInfo(status?.build)}, ` + `reason=${classification.reason}) \u2014 replacing`);
|
|
1452
1937
|
await this.kill(3000, status?.pid);
|
|
1453
1938
|
} else {
|
|
1454
1939
|
try {
|
|
1455
|
-
await this.waitForReady(
|
|
1940
|
+
await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
|
|
1456
1941
|
return;
|
|
1457
1942
|
} catch {
|
|
1458
1943
|
this.log(`Daemon on control port ${this.controlPort} is healthy but not ready under startup lock \u2014 replacing`);
|
|
@@ -1461,7 +1946,7 @@ class DaemonLifecycle {
|
|
|
1461
1946
|
}
|
|
1462
1947
|
}
|
|
1463
1948
|
this.launch();
|
|
1464
|
-
await this.waitForReady();
|
|
1949
|
+
await this.waitForReady(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
|
|
1465
1950
|
});
|
|
1466
1951
|
}
|
|
1467
1952
|
async isHealthy() {
|
|
@@ -1476,7 +1961,7 @@ class DaemonLifecycle {
|
|
|
1476
1961
|
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
1477
1962
|
if (await this.isHealthy())
|
|
1478
1963
|
return;
|
|
1479
|
-
await new Promise((
|
|
1964
|
+
await new Promise((resolve3) => setTimeout(resolve3, delayMs));
|
|
1480
1965
|
}
|
|
1481
1966
|
throw new Error(`Timed out waiting for AgentBridge daemon health on ${this.healthUrl}`);
|
|
1482
1967
|
}
|
|
@@ -1488,42 +1973,59 @@ class DaemonLifecycle {
|
|
|
1488
1973
|
return false;
|
|
1489
1974
|
}
|
|
1490
1975
|
}
|
|
1491
|
-
async waitForReady(maxRetries =
|
|
1976
|
+
async waitForReady(maxRetries = WAIT_READY_RETRIES, delayMs = WAIT_READY_DELAY_MS) {
|
|
1492
1977
|
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
1493
1978
|
if (await this.isReady())
|
|
1494
1979
|
return;
|
|
1495
|
-
await new Promise((
|
|
1980
|
+
await new Promise((resolve3) => setTimeout(resolve3, delayMs));
|
|
1496
1981
|
}
|
|
1497
1982
|
throw new Error(`Timed out waiting for AgentBridge daemon readiness on ${this.readyUrl}`);
|
|
1498
1983
|
}
|
|
1499
|
-
async waitForReadyAndOurs(maxRetries =
|
|
1984
|
+
async waitForReadyAndOurs(maxRetries = WAIT_READY_RETRIES, delayMs = WAIT_READY_DELAY_MS) {
|
|
1500
1985
|
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
1501
1986
|
if (await this.isReady()) {
|
|
1502
1987
|
const status = await this.fetchStatus();
|
|
1503
|
-
|
|
1988
|
+
const classification = this.classifyDaemon(status);
|
|
1989
|
+
if (classification.verdict === "manual-conflict") {
|
|
1990
|
+
throw this.manualConflictError(status);
|
|
1991
|
+
}
|
|
1992
|
+
if (isReuseVerdict(classification.verdict)) {
|
|
1504
1993
|
return;
|
|
1505
1994
|
}
|
|
1506
1995
|
}
|
|
1507
|
-
await new Promise((
|
|
1996
|
+
await new Promise((resolve3) => setTimeout(resolve3, delayMs));
|
|
1508
1997
|
}
|
|
1509
1998
|
throw new Error(`Timed out waiting for AgentBridge daemon readiness+identity on ${this.readyUrl} (control port ${this.controlPort})`);
|
|
1510
1999
|
}
|
|
2000
|
+
readDaemonRecord() {
|
|
2001
|
+
return readUnifiedDaemonRecord({
|
|
2002
|
+
daemonRecordFile: this.stateDir.daemonRecordFile,
|
|
2003
|
+
pidFile: this.stateDir.pidFile,
|
|
2004
|
+
statusFile: this.stateDir.statusFile
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
writeDaemonRecord(record) {
|
|
2008
|
+
writeDaemonRecord(this.stateDir.daemonRecordFile, record);
|
|
2009
|
+
}
|
|
2010
|
+
removeDaemonRecord() {
|
|
2011
|
+
try {
|
|
2012
|
+
unlinkSync2(this.stateDir.daemonRecordFile);
|
|
2013
|
+
} catch {}
|
|
2014
|
+
}
|
|
1511
2015
|
readStatus() {
|
|
1512
2016
|
try {
|
|
1513
|
-
const raw =
|
|
2017
|
+
const raw = readFileSync5(this.stateDir.statusFile, "utf-8");
|
|
1514
2018
|
return JSON.parse(raw);
|
|
1515
2019
|
} catch {
|
|
1516
2020
|
return null;
|
|
1517
2021
|
}
|
|
1518
2022
|
}
|
|
1519
2023
|
writeStatus(status) {
|
|
1520
|
-
this.stateDir.
|
|
1521
|
-
writeFileSync4(this.stateDir.statusFile, JSON.stringify(status, null, 2) + `
|
|
1522
|
-
`, "utf-8");
|
|
2024
|
+
atomicWriteJson(this.stateDir.statusFile, status);
|
|
1523
2025
|
}
|
|
1524
2026
|
readPid() {
|
|
1525
2027
|
try {
|
|
1526
|
-
const raw =
|
|
2028
|
+
const raw = readFileSync5(this.stateDir.pidFile, "utf-8").trim();
|
|
1527
2029
|
if (!raw)
|
|
1528
2030
|
return null;
|
|
1529
2031
|
const pid = Number.parseInt(raw, 10);
|
|
@@ -1533,32 +2035,31 @@ class DaemonLifecycle {
|
|
|
1533
2035
|
}
|
|
1534
2036
|
}
|
|
1535
2037
|
writePid(pid) {
|
|
1536
|
-
this.stateDir.
|
|
1537
|
-
|
|
1538
|
-
`, "utf-8");
|
|
2038
|
+
atomicWriteText(this.stateDir.pidFile, `${pid ?? process.pid}
|
|
2039
|
+
`);
|
|
1539
2040
|
}
|
|
1540
2041
|
removePidFile() {
|
|
1541
2042
|
try {
|
|
1542
|
-
|
|
2043
|
+
unlinkSync2(this.stateDir.pidFile);
|
|
1543
2044
|
} catch {}
|
|
1544
2045
|
}
|
|
1545
2046
|
removeStatusFile() {
|
|
1546
2047
|
try {
|
|
1547
|
-
|
|
2048
|
+
unlinkSync2(this.stateDir.statusFile);
|
|
1548
2049
|
} catch {}
|
|
1549
2050
|
}
|
|
1550
2051
|
markKilled() {
|
|
1551
2052
|
this.stateDir.ensure();
|
|
1552
|
-
|
|
2053
|
+
writeFileSync3(this.stateDir.killedFile, `${Date.now()}
|
|
1553
2054
|
`, "utf-8");
|
|
1554
2055
|
}
|
|
1555
2056
|
clearKilled() {
|
|
1556
2057
|
try {
|
|
1557
|
-
|
|
2058
|
+
unlinkSync2(this.stateDir.killedFile);
|
|
1558
2059
|
} catch {}
|
|
1559
2060
|
}
|
|
1560
2061
|
wasKilled() {
|
|
1561
|
-
return
|
|
2062
|
+
return existsSync6(this.stateDir.killedFile);
|
|
1562
2063
|
}
|
|
1563
2064
|
launch() {
|
|
1564
2065
|
this.stateDir.ensure();
|
|
@@ -1576,21 +2077,26 @@ class DaemonLifecycle {
|
|
|
1576
2077
|
daemonProc.unref();
|
|
1577
2078
|
}
|
|
1578
2079
|
removeStalePidFile() {
|
|
1579
|
-
this.log("Removing stale
|
|
2080
|
+
this.log("Removing stale daemon identity files");
|
|
1580
2081
|
this.removePidFile();
|
|
2082
|
+
this.removeStatusFile();
|
|
2083
|
+
this.removeDaemonRecord();
|
|
1581
2084
|
}
|
|
1582
2085
|
async replaceUnhealthyDaemon(statusPid) {
|
|
1583
2086
|
await this.withStartupLockStrict(async (locked) => {
|
|
1584
2087
|
if (!locked) {
|
|
1585
|
-
this.
|
|
1586
|
-
await this.waitForReadyAndOurs();
|
|
2088
|
+
await this.waitForContendedStartupLock();
|
|
1587
2089
|
return;
|
|
1588
2090
|
}
|
|
1589
2091
|
if (await this.isHealthy()) {
|
|
1590
2092
|
const status = await this.fetchStatus();
|
|
1591
|
-
|
|
2093
|
+
const classification = this.classifyDaemon(status);
|
|
2094
|
+
if (classification.verdict === "manual-conflict") {
|
|
2095
|
+
throw this.manualConflictError(status);
|
|
2096
|
+
}
|
|
2097
|
+
if (isReuseVerdict(classification.verdict)) {
|
|
1592
2098
|
try {
|
|
1593
|
-
await this.waitForReady(
|
|
2099
|
+
await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
|
|
1594
2100
|
return;
|
|
1595
2101
|
} catch {}
|
|
1596
2102
|
}
|
|
@@ -1598,9 +2104,13 @@ class DaemonLifecycle {
|
|
|
1598
2104
|
this.log(`Killing unhealthy daemon on control port ${this.controlPort} and relaunching`);
|
|
1599
2105
|
await this.kill(3000, statusPid);
|
|
1600
2106
|
this.launch();
|
|
1601
|
-
await this.waitForReady();
|
|
2107
|
+
await this.waitForReady(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
|
|
1602
2108
|
});
|
|
1603
2109
|
}
|
|
2110
|
+
async waitForContendedStartupLock() {
|
|
2111
|
+
this.log("Another process holds the startup lock, waiting for readiness+identity...");
|
|
2112
|
+
await this.waitForReadyAndOurs(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
|
|
2113
|
+
}
|
|
1604
2114
|
async withStartupLockStrict(fn) {
|
|
1605
2115
|
const locked = this.acquireLockStrict();
|
|
1606
2116
|
try {
|
|
@@ -1614,15 +2124,15 @@ class DaemonLifecycle {
|
|
|
1614
2124
|
this.stateDir.ensure();
|
|
1615
2125
|
let fd = null;
|
|
1616
2126
|
try {
|
|
1617
|
-
fd =
|
|
1618
|
-
|
|
2127
|
+
fd = openSync2(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
|
|
2128
|
+
writeFileSync3(fd, `${process.pid}
|
|
1619
2129
|
`);
|
|
1620
|
-
|
|
2130
|
+
closeSync2(fd);
|
|
1621
2131
|
return true;
|
|
1622
2132
|
} catch (err) {
|
|
1623
2133
|
if (fd !== null && err.code !== "EEXIST") {
|
|
1624
2134
|
try {
|
|
1625
|
-
|
|
2135
|
+
closeSync2(fd);
|
|
1626
2136
|
} catch {}
|
|
1627
2137
|
this.releaseLock();
|
|
1628
2138
|
}
|
|
@@ -1630,7 +2140,7 @@ class DaemonLifecycle {
|
|
|
1630
2140
|
if (reclaimed)
|
|
1631
2141
|
return false;
|
|
1632
2142
|
try {
|
|
1633
|
-
const holderPid = Number.parseInt(
|
|
2143
|
+
const holderPid = Number.parseInt(readFileSync5(this.stateDir.lockFile, "utf-8").trim(), 10);
|
|
1634
2144
|
if (Number.isFinite(holderPid) && !isProcessAlive(holderPid)) {
|
|
1635
2145
|
this.log(`Stale startup lock from dead process ${holderPid}, reclaiming`);
|
|
1636
2146
|
this.releaseLock();
|
|
@@ -1659,7 +2169,7 @@ class DaemonLifecycle {
|
|
|
1659
2169
|
}
|
|
1660
2170
|
releaseLock() {
|
|
1661
2171
|
try {
|
|
1662
|
-
|
|
2172
|
+
unlinkSync2(this.stateDir.lockFile);
|
|
1663
2173
|
} catch {}
|
|
1664
2174
|
}
|
|
1665
2175
|
async kill(gracefulTimeoutMs = 3000, pidOverride) {
|
|
@@ -1693,7 +2203,7 @@ class DaemonLifecycle {
|
|
|
1693
2203
|
this.cleanup();
|
|
1694
2204
|
return true;
|
|
1695
2205
|
}
|
|
1696
|
-
await new Promise((
|
|
2206
|
+
await new Promise((resolve3) => setTimeout(resolve3, 200));
|
|
1697
2207
|
}
|
|
1698
2208
|
this.log(`Daemon pid ${pid} did not stop gracefully, sending SIGKILL`);
|
|
1699
2209
|
try {
|
|
@@ -1705,6 +2215,7 @@ class DaemonLifecycle {
|
|
|
1705
2215
|
cleanup() {
|
|
1706
2216
|
this.removePidFile();
|
|
1707
2217
|
this.removeStatusFile();
|
|
2218
|
+
this.removeDaemonRecord();
|
|
1708
2219
|
}
|
|
1709
2220
|
}
|
|
1710
2221
|
async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
|
|
@@ -1716,10 +2227,12 @@ async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
|
|
|
1716
2227
|
clearTimeout(timer);
|
|
1717
2228
|
}
|
|
1718
2229
|
}
|
|
1719
|
-
var DEFAULT_DAEMON_ENTRY, DAEMON_ENTRY, DAEMON_PATH, REUSE_READY_RETRIES, REUSE_READY_DELAY_MS = 250, HEALTH_FETCH_TIMEOUT_MS = 500, LOCK_IDENTITY_GRACE_MS;
|
|
2230
|
+
var DEFAULT_DAEMON_ENTRY, DAEMON_ENTRY, DAEMON_PATH, REUSE_READY_RETRIES, REUSE_READY_DELAY_MS = 250, WAIT_READY_RETRIES = 40, WAIT_READY_DELAY_MS = 250, HEALTH_FETCH_TIMEOUT_MS = 500, LOCK_IDENTITY_GRACE_MS;
|
|
1720
2231
|
var init_daemon_lifecycle = __esm(() => {
|
|
2232
|
+
init_atomic_json();
|
|
1721
2233
|
init_build_info();
|
|
1722
2234
|
init_process_lifecycle();
|
|
2235
|
+
init_daemon_record();
|
|
1723
2236
|
DEFAULT_DAEMON_ENTRY = import.meta.url.endsWith(".ts") ? "./daemon.ts" : "./daemon.js";
|
|
1724
2237
|
DAEMON_ENTRY = process.env.AGENTBRIDGE_DAEMON_ENTRY || DEFAULT_DAEMON_ENTRY;
|
|
1725
2238
|
DAEMON_PATH = fileURLToPath(new URL(DAEMON_ENTRY, import.meta.url));
|
|
@@ -1730,26 +2243,22 @@ var init_daemon_lifecycle = __esm(() => {
|
|
|
1730
2243
|
// src/pair-registry.ts
|
|
1731
2244
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
1732
2245
|
import {
|
|
1733
|
-
|
|
1734
|
-
existsSync as existsSync6,
|
|
1735
|
-
fsyncSync,
|
|
2246
|
+
existsSync as existsSync7,
|
|
1736
2247
|
linkSync,
|
|
1737
2248
|
lstatSync,
|
|
1738
|
-
mkdirSync as
|
|
1739
|
-
openSync as openSync2,
|
|
2249
|
+
mkdirSync as mkdirSync4,
|
|
1740
2250
|
readdirSync,
|
|
1741
|
-
readFileSync as
|
|
2251
|
+
readFileSync as readFileSync6,
|
|
1742
2252
|
realpathSync,
|
|
1743
|
-
renameSync,
|
|
1744
2253
|
rmSync as rmSync2,
|
|
1745
2254
|
statSync as statSync2,
|
|
1746
|
-
unlinkSync as
|
|
1747
|
-
writeFileSync as
|
|
2255
|
+
unlinkSync as unlinkSync3,
|
|
2256
|
+
writeFileSync as writeFileSync4
|
|
1748
2257
|
} from "fs";
|
|
1749
2258
|
import { createServer } from "net";
|
|
1750
|
-
import { createHash, randomUUID } from "crypto";
|
|
2259
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
1751
2260
|
import { hostname, userInfo } from "os";
|
|
1752
|
-
import { basename as
|
|
2261
|
+
import { basename as basename3, join as join6, resolve as resolve3, sep } from "path";
|
|
1753
2262
|
function portsForSlot(slot) {
|
|
1754
2263
|
if (!Number.isInteger(slot) || slot < 0) {
|
|
1755
2264
|
throw new PairError("PAIR_ID_INVALID", `Invalid slot: ${slot}`);
|
|
@@ -1787,18 +2296,18 @@ function pickLowestFreeSlot(entries) {
|
|
|
1787
2296
|
return slot;
|
|
1788
2297
|
}
|
|
1789
2298
|
function pairsDir(base) {
|
|
1790
|
-
return
|
|
2299
|
+
return join6(base, "pairs");
|
|
1791
2300
|
}
|
|
1792
2301
|
function registryPath(base) {
|
|
1793
|
-
return
|
|
2302
|
+
return join6(pairsDir(base), REGISTRY_FILE_NAME);
|
|
1794
2303
|
}
|
|
1795
2304
|
function readRegistry(base) {
|
|
1796
2305
|
const path = registryPath(base);
|
|
1797
|
-
if (!
|
|
2306
|
+
if (!existsSync7(path))
|
|
1798
2307
|
return { version: 1, pairs: [] };
|
|
1799
2308
|
let parsed;
|
|
1800
2309
|
try {
|
|
1801
|
-
parsed = JSON.parse(
|
|
2310
|
+
parsed = JSON.parse(readFileSync6(path, "utf-8"));
|
|
1802
2311
|
} catch (err) {
|
|
1803
2312
|
throw new PairError("PAIR_REGISTRY_CORRUPT", `Registry JSON is not parseable at ${path}: ${err.message}`, {
|
|
1804
2313
|
path
|
|
@@ -1829,26 +2338,14 @@ function readRegistry(base) {
|
|
|
1829
2338
|
return parsed;
|
|
1830
2339
|
}
|
|
1831
2340
|
function writeRegistry(base, reg) {
|
|
1832
|
-
|
|
1833
|
-
const target = registryPath(base);
|
|
1834
|
-
const tmp = `${target}.tmp.${process.pid}`;
|
|
1835
|
-
const data = JSON.stringify(reg, null, 2) + `
|
|
1836
|
-
`;
|
|
1837
|
-
const fd = openSync2(tmp, "w");
|
|
1838
|
-
try {
|
|
1839
|
-
writeFileSync5(fd, data);
|
|
1840
|
-
fsyncSync(fd);
|
|
1841
|
-
} finally {
|
|
1842
|
-
closeSync2(fd);
|
|
1843
|
-
}
|
|
1844
|
-
renameSync(tmp, target);
|
|
2341
|
+
atomicWriteJson(registryPath(base), reg, { fsync: true });
|
|
1845
2342
|
}
|
|
1846
2343
|
function lockFilePath(base) {
|
|
1847
|
-
return
|
|
2344
|
+
return join6(pairsDir(base), LOCK_FILE_NAME);
|
|
1848
2345
|
}
|
|
1849
2346
|
function readLockOwner(lockFile) {
|
|
1850
2347
|
try {
|
|
1851
|
-
const parsed = JSON.parse(
|
|
2348
|
+
const parsed = JSON.parse(readFileSync6(lockFile, "utf-8"));
|
|
1852
2349
|
if (typeof parsed.pid === "number" && typeof parsed.nonce === "string")
|
|
1853
2350
|
return parsed;
|
|
1854
2351
|
return null;
|
|
@@ -1878,7 +2375,7 @@ function safeUid() {
|
|
|
1878
2375
|
}
|
|
1879
2376
|
}
|
|
1880
2377
|
function sleep(ms) {
|
|
1881
|
-
return new Promise((
|
|
2378
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1882
2379
|
}
|
|
1883
2380
|
function lockIsStale(lockFile) {
|
|
1884
2381
|
const owner = readLockOwner(lockFile);
|
|
@@ -1888,7 +2385,7 @@ function lockIsStale(lockFile) {
|
|
|
1888
2385
|
}
|
|
1889
2386
|
function attemptReclaim(lockFile) {
|
|
1890
2387
|
const reclaimLock = `${lockFile}.reclaim`;
|
|
1891
|
-
const myNonce =
|
|
2388
|
+
const myNonce = randomUUID2();
|
|
1892
2389
|
const ownerJson = JSON.stringify({
|
|
1893
2390
|
pid: process.pid,
|
|
1894
2391
|
createdAt: Date.now(),
|
|
@@ -1896,10 +2393,10 @@ function attemptReclaim(lockFile) {
|
|
|
1896
2393
|
hostname: safeHostname(),
|
|
1897
2394
|
uid: safeUid()
|
|
1898
2395
|
});
|
|
1899
|
-
const tmp = `${reclaimLock}.acq.${process.pid}.${
|
|
2396
|
+
const tmp = `${reclaimLock}.acq.${process.pid}.${randomUUID2()}`;
|
|
1900
2397
|
let held = false;
|
|
1901
2398
|
try {
|
|
1902
|
-
|
|
2399
|
+
writeFileSync4(tmp, ownerJson);
|
|
1903
2400
|
try {
|
|
1904
2401
|
linkSync(tmp, reclaimLock);
|
|
1905
2402
|
held = true;
|
|
@@ -1907,7 +2404,7 @@ function attemptReclaim(lockFile) {
|
|
|
1907
2404
|
if (err?.code === "EEXIST") {
|
|
1908
2405
|
if (lockIsStale(reclaimLock)) {
|
|
1909
2406
|
try {
|
|
1910
|
-
|
|
2407
|
+
unlinkSync3(reclaimLock);
|
|
1911
2408
|
} catch {}
|
|
1912
2409
|
}
|
|
1913
2410
|
return;
|
|
@@ -1916,7 +2413,7 @@ function attemptReclaim(lockFile) {
|
|
|
1916
2413
|
}
|
|
1917
2414
|
} finally {
|
|
1918
2415
|
try {
|
|
1919
|
-
|
|
2416
|
+
unlinkSync3(tmp);
|
|
1920
2417
|
} catch {}
|
|
1921
2418
|
}
|
|
1922
2419
|
if (!held)
|
|
@@ -1926,22 +2423,22 @@ function attemptReclaim(lockFile) {
|
|
|
1926
2423
|
return;
|
|
1927
2424
|
if (lockIsStale(lockFile)) {
|
|
1928
2425
|
try {
|
|
1929
|
-
|
|
2426
|
+
unlinkSync3(lockFile);
|
|
1930
2427
|
} catch {}
|
|
1931
2428
|
}
|
|
1932
2429
|
} finally {
|
|
1933
2430
|
if (readLockOwner(reclaimLock)?.nonce === myNonce) {
|
|
1934
2431
|
try {
|
|
1935
|
-
|
|
2432
|
+
unlinkSync3(reclaimLock);
|
|
1936
2433
|
} catch {}
|
|
1937
2434
|
}
|
|
1938
2435
|
}
|
|
1939
2436
|
}
|
|
1940
2437
|
async function withRegistryLock(base, fn) {
|
|
1941
|
-
|
|
2438
|
+
mkdirSync4(pairsDir(base), { recursive: true });
|
|
1942
2439
|
const lockFile = lockFilePath(base);
|
|
1943
2440
|
const deadline = Date.now() + LOCK_DEADLINE_MS;
|
|
1944
|
-
const myNonce =
|
|
2441
|
+
const myNonce = randomUUID2();
|
|
1945
2442
|
const ownerJson = JSON.stringify({
|
|
1946
2443
|
pid: process.pid,
|
|
1947
2444
|
createdAt: Date.now(),
|
|
@@ -1950,10 +2447,10 @@ async function withRegistryLock(base, fn) {
|
|
|
1950
2447
|
uid: safeUid()
|
|
1951
2448
|
});
|
|
1952
2449
|
for (;; ) {
|
|
1953
|
-
const tmp = `${lockFile}.acq.${process.pid}.${
|
|
2450
|
+
const tmp = `${lockFile}.acq.${process.pid}.${randomUUID2()}`;
|
|
1954
2451
|
let acquired = false;
|
|
1955
2452
|
try {
|
|
1956
|
-
|
|
2453
|
+
writeFileSync4(tmp, ownerJson);
|
|
1957
2454
|
try {
|
|
1958
2455
|
linkSync(tmp, lockFile);
|
|
1959
2456
|
acquired = true;
|
|
@@ -1963,7 +2460,7 @@ async function withRegistryLock(base, fn) {
|
|
|
1963
2460
|
}
|
|
1964
2461
|
} finally {
|
|
1965
2462
|
try {
|
|
1966
|
-
|
|
2463
|
+
unlinkSync3(tmp);
|
|
1967
2464
|
} catch {}
|
|
1968
2465
|
}
|
|
1969
2466
|
if (acquired) {
|
|
@@ -1973,7 +2470,7 @@ async function withRegistryLock(base, fn) {
|
|
|
1973
2470
|
const current = readLockOwner(lockFile);
|
|
1974
2471
|
if (!current || current.nonce === myNonce) {
|
|
1975
2472
|
try {
|
|
1976
|
-
|
|
2473
|
+
unlinkSync3(lockFile);
|
|
1977
2474
|
} catch {}
|
|
1978
2475
|
}
|
|
1979
2476
|
}
|
|
@@ -1990,12 +2487,12 @@ async function withRegistryLock(base, fn) {
|
|
|
1990
2487
|
}
|
|
1991
2488
|
}
|
|
1992
2489
|
function detectLegacyRootDaemon(base) {
|
|
1993
|
-
const rootPidFile =
|
|
1994
|
-
if (!
|
|
2490
|
+
const rootPidFile = join6(base, "daemon.pid");
|
|
2491
|
+
if (!existsSync7(rootPidFile))
|
|
1995
2492
|
return null;
|
|
1996
2493
|
let pid;
|
|
1997
2494
|
try {
|
|
1998
|
-
const raw =
|
|
2495
|
+
const raw = readFileSync6(rootPidFile, "utf-8").trim();
|
|
1999
2496
|
pid = Number.parseInt(raw, 10);
|
|
2000
2497
|
} catch {
|
|
2001
2498
|
return null;
|
|
@@ -2005,11 +2502,11 @@ function detectLegacyRootDaemon(base) {
|
|
|
2005
2502
|
return { pid, controlPort: LEGACY_ROOT_CONTROL_PORT };
|
|
2006
2503
|
}
|
|
2007
2504
|
function probePortFree(port) {
|
|
2008
|
-
return new Promise((
|
|
2505
|
+
return new Promise((resolve4) => {
|
|
2009
2506
|
const server = createServer();
|
|
2010
|
-
server.once("error", () =>
|
|
2507
|
+
server.once("error", () => resolve4(false));
|
|
2011
2508
|
server.once("listening", () => {
|
|
2012
|
-
server.close(() =>
|
|
2509
|
+
server.close(() => resolve4(true));
|
|
2013
2510
|
});
|
|
2014
2511
|
server.listen(port, "127.0.0.1");
|
|
2015
2512
|
});
|
|
@@ -2086,7 +2583,7 @@ async function resolvePair(base, opts) {
|
|
|
2086
2583
|
pairId: entry.pairId,
|
|
2087
2584
|
slot,
|
|
2088
2585
|
ports,
|
|
2089
|
-
stateDir:
|
|
2586
|
+
stateDir: join6(pairsDir(base), entry.pairId),
|
|
2090
2587
|
name: entry.name ?? name,
|
|
2091
2588
|
entry,
|
|
2092
2589
|
warning
|
|
@@ -2094,7 +2591,7 @@ async function resolvePair(base, opts) {
|
|
|
2094
2591
|
}
|
|
2095
2592
|
async function removeAllocatedPairIfUnchanged(base, pairId, slot) {
|
|
2096
2593
|
await withRegistryLock(base, () => {
|
|
2097
|
-
if (
|
|
2594
|
+
if (existsSync7(pairDirPath(base, pairId)) || pairDirDaemonAlive(base, pairId))
|
|
2098
2595
|
return;
|
|
2099
2596
|
const reg = readRegistry(base);
|
|
2100
2597
|
const nextPairs = reg.pairs.filter((pair) => !(pair.pairId === pairId && pair.slot === slot));
|
|
@@ -2103,21 +2600,24 @@ async function removeAllocatedPairIfUnchanged(base, pairId, slot) {
|
|
|
2103
2600
|
writeRegistry(base, { version: 1, pairs: nextPairs });
|
|
2104
2601
|
});
|
|
2105
2602
|
}
|
|
2603
|
+
function pairsRootDir(base) {
|
|
2604
|
+
return pairsDir(base);
|
|
2605
|
+
}
|
|
2106
2606
|
function pairDirPath(base, pairId) {
|
|
2107
2607
|
const id = validatePairId(pairId);
|
|
2108
|
-
return
|
|
2608
|
+
return join6(pairsDir(base), id);
|
|
2109
2609
|
}
|
|
2110
2610
|
function removePairDir(base, pairId) {
|
|
2111
2611
|
const id = validatePairId(pairId);
|
|
2112
2612
|
const root = pairsDir(base);
|
|
2113
|
-
const dir =
|
|
2114
|
-
const canonicalRoot =
|
|
2115
|
-
const canonicalDir =
|
|
2613
|
+
const dir = join6(root, id);
|
|
2614
|
+
const canonicalRoot = resolve3(root);
|
|
2615
|
+
const canonicalDir = resolve3(dir);
|
|
2116
2616
|
if (canonicalDir === canonicalRoot || !canonicalDir.startsWith(canonicalRoot + sep)) {
|
|
2117
2617
|
throw new PairError("PAIR_ID_INVALID", `Refusing to remove a pair dir outside ${canonicalRoot}: ${canonicalDir}`, { pairId });
|
|
2118
2618
|
}
|
|
2119
2619
|
assertPairsRootNotSymlinked(root);
|
|
2120
|
-
if (!
|
|
2620
|
+
if (!existsSync7(canonicalDir))
|
|
2121
2621
|
return false;
|
|
2122
2622
|
rmSync2(canonicalDir, { recursive: true, force: true });
|
|
2123
2623
|
return true;
|
|
@@ -2135,22 +2635,27 @@ function assertPairsRootNotSymlinked(root) {
|
|
|
2135
2635
|
}
|
|
2136
2636
|
function listPairDirs(base) {
|
|
2137
2637
|
const root = pairsDir(base);
|
|
2138
|
-
if (!
|
|
2638
|
+
if (!existsSync7(root))
|
|
2139
2639
|
return [];
|
|
2140
2640
|
if (lstatSync(root).isSymbolicLink())
|
|
2141
2641
|
return [];
|
|
2142
2642
|
return readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
2143
2643
|
}
|
|
2144
2644
|
function pairDirDaemonAlive(base, pairId) {
|
|
2145
|
-
const dir =
|
|
2645
|
+
const dir = join6(pairsDir(base), pairId);
|
|
2146
2646
|
const pids = [];
|
|
2147
2647
|
try {
|
|
2148
|
-
const
|
|
2648
|
+
const record = JSON.parse(readFileSync6(join6(dir, "daemon.json"), "utf-8"));
|
|
2649
|
+
if (typeof record?.pid === "number" && Number.isFinite(record.pid))
|
|
2650
|
+
pids.push(record.pid);
|
|
2651
|
+
} catch {}
|
|
2652
|
+
try {
|
|
2653
|
+
const pid = Number.parseInt(readFileSync6(join6(dir, "daemon.pid"), "utf-8").trim(), 10);
|
|
2149
2654
|
if (Number.isFinite(pid))
|
|
2150
2655
|
pids.push(pid);
|
|
2151
2656
|
} catch {}
|
|
2152
2657
|
try {
|
|
2153
|
-
const status = JSON.parse(
|
|
2658
|
+
const status = JSON.parse(readFileSync6(join6(dir, "status.json"), "utf-8"));
|
|
2154
2659
|
if (typeof status?.pid === "number")
|
|
2155
2660
|
pids.push(status.pid);
|
|
2156
2661
|
} catch {}
|
|
@@ -2184,10 +2689,54 @@ async function removeUnregisteredPairDir(base, pairId) {
|
|
|
2184
2689
|
return { removed: removePairDir(base, pairId) };
|
|
2185
2690
|
});
|
|
2186
2691
|
}
|
|
2187
|
-
|
|
2692
|
+
async function removeOrphanPairDirIgnoringRegistry(base, pairId) {
|
|
2693
|
+
return withRegistryLock(base, () => {
|
|
2694
|
+
if (pairDirDaemonAlive(base, pairId)) {
|
|
2695
|
+
return { removed: false, reason: "live" };
|
|
2696
|
+
}
|
|
2697
|
+
return { removed: removePairDir(base, pairId) };
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
function isEntryReclaimable(signals) {
|
|
2701
|
+
return signals.cwdGone && signals.dead && signals.old;
|
|
2702
|
+
}
|
|
2703
|
+
function cwdMissing(cwd) {
|
|
2704
|
+
try {
|
|
2705
|
+
statSync2(cwd);
|
|
2706
|
+
return false;
|
|
2707
|
+
} catch (err) {
|
|
2708
|
+
return err?.code === "ENOENT";
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
function parseCreatedAtMs(createdAt) {
|
|
2712
|
+
if (typeof createdAt !== "string")
|
|
2713
|
+
return null;
|
|
2714
|
+
const ms = Date.parse(createdAt);
|
|
2715
|
+
return Number.isFinite(ms) ? ms : null;
|
|
2716
|
+
}
|
|
2717
|
+
function classifyReclaimableEntries(base, now = Date.now()) {
|
|
2718
|
+
const reg = readRegistry(base);
|
|
2719
|
+
const out = [];
|
|
2720
|
+
for (const entry of reg.pairs) {
|
|
2721
|
+
const createdMs = parseCreatedAtMs(entry.createdAt);
|
|
2722
|
+
const ageMs = createdMs === null ? null : Math.max(0, now - createdMs);
|
|
2723
|
+
const signals = {
|
|
2724
|
+
cwdGone: cwdMissing(entry.cwd),
|
|
2725
|
+
dead: !pairDirDaemonAlive(base, entry.pairId),
|
|
2726
|
+
old: ageMs !== null && ageMs >= RECLAIMABLE_MIN_AGE_MS,
|
|
2727
|
+
ageMs
|
|
2728
|
+
};
|
|
2729
|
+
if (isEntryReclaimable(signals))
|
|
2730
|
+
out.push({ entry, signals });
|
|
2731
|
+
}
|
|
2732
|
+
return out;
|
|
2733
|
+
}
|
|
2734
|
+
var PAIR_BASE_PORT = 4500, PAIR_SLOT_STRIDE = 10, PAIR_ID_REGEX, DEFAULT_PAIR_NAME = "main", RECLAIMABLE_MIN_AGE_MS, LOCK_FILE_NAME = ".registry.lock", REGISTRY_FILE_NAME = "registry.json", LOCK_DEADLINE_MS = 1e4, ORPHAN_GRACE_MS = 3000, LEGACY_ROOT_CONTROL_PORT = 4502, WINDOWS_RESERVED_RE, PairError, MAX_PAIR_SLOT;
|
|
2188
2735
|
var init_pair_registry = __esm(() => {
|
|
2736
|
+
init_atomic_json();
|
|
2189
2737
|
init_process_lifecycle();
|
|
2190
2738
|
PAIR_ID_REGEX = /^[A-Za-z0-9._-]{1,64}$/;
|
|
2739
|
+
RECLAIMABLE_MIN_AGE_MS = 24 * 60 * 60 * 1000;
|
|
2191
2740
|
WINDOWS_RESERVED_RE = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
|
2192
2741
|
PairError = class PairError extends Error {
|
|
2193
2742
|
code;
|
|
@@ -2283,7 +2832,7 @@ var init_env_guard = __esm(() => {
|
|
|
2283
2832
|
|
|
2284
2833
|
// src/pair-resolver.ts
|
|
2285
2834
|
import { realpathSync as realpathSync2 } from "fs";
|
|
2286
|
-
import { join as
|
|
2835
|
+
import { join as join7, resolve as resolve4 } from "path";
|
|
2287
2836
|
function computeBaseDir() {
|
|
2288
2837
|
return process.env.AGENTBRIDGE_BASE_DIR || process.env.AGENTBRIDGE_STATE_DIR || StateDirResolver.platformBaseDir();
|
|
2289
2838
|
}
|
|
@@ -2410,7 +2959,7 @@ function resolvePairReadOnly(pairFlag) {
|
|
|
2410
2959
|
pairId: entry.pairId,
|
|
2411
2960
|
slot: entry.slot,
|
|
2412
2961
|
ports: portsForEntry(entry),
|
|
2413
|
-
stateDir: new StateDirResolver(
|
|
2962
|
+
stateDir: new StateDirResolver(join7(base, "pairs", entry.pairId)),
|
|
2414
2963
|
name: entry.name ?? name,
|
|
2415
2964
|
manual: false
|
|
2416
2965
|
}
|
|
@@ -2423,7 +2972,7 @@ function resolvePairReadOnly(pairFlag) {
|
|
|
2423
2972
|
pairId,
|
|
2424
2973
|
slot: null,
|
|
2425
2974
|
ports: { appPort: 0, proxyPort: 0, controlPort: 0 },
|
|
2426
|
-
stateDir: new StateDirResolver(
|
|
2975
|
+
stateDir: new StateDirResolver(join7(base, "pairs", pairId)),
|
|
2427
2976
|
name,
|
|
2428
2977
|
manual: false
|
|
2429
2978
|
}
|
|
@@ -2453,7 +3002,7 @@ function portsForEntry(entry) {
|
|
|
2453
3002
|
return portsForSlot(entry.slot);
|
|
2454
3003
|
}
|
|
2455
3004
|
function canonicalizeCwd(cwd) {
|
|
2456
|
-
const absolute =
|
|
3005
|
+
const absolute = resolve4(cwd);
|
|
2457
3006
|
try {
|
|
2458
3007
|
return realpathSync2.native(absolute);
|
|
2459
3008
|
} catch {
|
|
@@ -2470,8 +3019,8 @@ var init_pair_resolver = __esm(() => {
|
|
|
2470
3019
|
});
|
|
2471
3020
|
|
|
2472
3021
|
// src/trace-log.ts
|
|
2473
|
-
import { appendFileSync, mkdirSync as
|
|
2474
|
-
import { join as
|
|
3022
|
+
import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync4 } from "fs";
|
|
3023
|
+
import { join as join8 } from "path";
|
|
2475
3024
|
function pickRelevantEnv(env) {
|
|
2476
3025
|
const picked = {};
|
|
2477
3026
|
for (const [key, value] of Object.entries(env)) {
|
|
@@ -2506,7 +3055,7 @@ function redactArgv(argv) {
|
|
|
2506
3055
|
}
|
|
2507
3056
|
function traceLogPath(cwd, timestamp) {
|
|
2508
3057
|
const day = timestamp.slice(0, 10);
|
|
2509
|
-
return
|
|
3058
|
+
return join8(cwd, ".agentbridge", "logs", `trace-${day}.jsonl`);
|
|
2510
3059
|
}
|
|
2511
3060
|
function appendTraceEvent(input) {
|
|
2512
3061
|
const timestamp = input.timestamp ?? new Date().toISOString();
|
|
@@ -2520,11 +3069,39 @@ function appendTraceEvent(input) {
|
|
|
2520
3069
|
...input.env ? { env: pickRelevantEnv(input.env) } : {},
|
|
2521
3070
|
...input.data ? { data: redactData(input.data) } : {}
|
|
2522
3071
|
};
|
|
2523
|
-
|
|
3072
|
+
const logsDir = join8(input.cwd, ".agentbridge", "logs");
|
|
3073
|
+
const isNewDayFile = !existsSync8(path);
|
|
3074
|
+
mkdirSync5(logsDir, { recursive: true });
|
|
3075
|
+
if (isNewDayFile) {
|
|
3076
|
+
pruneOldTraceLogs(logsDir, path, Date.parse(timestamp));
|
|
3077
|
+
}
|
|
2524
3078
|
appendFileSync(path, JSON.stringify(event) + `
|
|
2525
3079
|
`, "utf-8");
|
|
2526
3080
|
return path;
|
|
2527
3081
|
}
|
|
3082
|
+
function pruneOldTraceLogs(logsDir, keepPath, nowMs) {
|
|
3083
|
+
if (!Number.isFinite(nowMs))
|
|
3084
|
+
return;
|
|
3085
|
+
const cutoff = nowMs - TRACE_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
|
3086
|
+
let entries;
|
|
3087
|
+
try {
|
|
3088
|
+
entries = readdirSync2(logsDir);
|
|
3089
|
+
} catch {
|
|
3090
|
+
return;
|
|
3091
|
+
}
|
|
3092
|
+
for (const name of entries) {
|
|
3093
|
+
if (!TRACE_FILE_RE.test(name))
|
|
3094
|
+
continue;
|
|
3095
|
+
const filePath = join8(logsDir, name);
|
|
3096
|
+
if (filePath === keepPath)
|
|
3097
|
+
continue;
|
|
3098
|
+
try {
|
|
3099
|
+
if (statSync3(filePath).mtimeMs < cutoff) {
|
|
3100
|
+
unlinkSync4(filePath);
|
|
3101
|
+
}
|
|
3102
|
+
} catch {}
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
2528
3105
|
function isEnvSnapshot(key, value) {
|
|
2529
3106
|
return /env$/i.test(key) && !!value && typeof value === "object" && !Array.isArray(value);
|
|
2530
3107
|
}
|
|
@@ -2550,8 +3127,9 @@ function redactData(value, key = "") {
|
|
|
2550
3127
|
}
|
|
2551
3128
|
return value;
|
|
2552
3129
|
}
|
|
2553
|
-
var SECRET_KEY_RE, SECRET_ARG_RE, RELEVANT_ENV_RE;
|
|
3130
|
+
var TRACE_RETENTION_DAYS = 7, TRACE_FILE_RE, SECRET_KEY_RE, SECRET_ARG_RE, RELEVANT_ENV_RE;
|
|
2554
3131
|
var init_trace_log = __esm(() => {
|
|
3132
|
+
TRACE_FILE_RE = /^trace-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
2555
3133
|
SECRET_KEY_RE = /(token|secret|password|passwd|api[_-]?key|auth|cookie|session)/i;
|
|
2556
3134
|
SECRET_ARG_RE = /^--?(?:token|secret|password|passwd|apikey|api-key|api_key|auth|cookie|session)(?:=.*)?$/i;
|
|
2557
3135
|
RELEVANT_ENV_RE = /^(AGENTBRIDGE_|CODEX_)/;
|
|
@@ -2600,10 +3178,14 @@ var init_max_permissions = __esm(() => {
|
|
|
2600
3178
|
// src/cli/claude.ts
|
|
2601
3179
|
var exports_claude = {};
|
|
2602
3180
|
__export(exports_claude, {
|
|
3181
|
+
warnIfPluginCacheMissing: () => warnIfPluginCacheMissing,
|
|
2603
3182
|
runClaude: () => runClaude,
|
|
3183
|
+
mapChildExitCode: () => mapChildExitCode,
|
|
2604
3184
|
checkOwnedFlagConflicts: () => checkOwnedFlagConflicts
|
|
2605
3185
|
});
|
|
2606
3186
|
import { spawn as spawn2 } from "child_process";
|
|
3187
|
+
import { existsSync as existsSync9 } from "fs";
|
|
3188
|
+
import { constants as osConstants } from "os";
|
|
2607
3189
|
async function runClaude(args) {
|
|
2608
3190
|
const originalEnv = { ...process.env };
|
|
2609
3191
|
const envGuardResult = guardAgentBridgeEnv({
|
|
@@ -2641,6 +3223,7 @@ async function runClaude(args) {
|
|
|
2641
3223
|
}
|
|
2642
3224
|
await assertPairNotLive(lifecycle, pair);
|
|
2643
3225
|
lifecycle.clearKilled();
|
|
3226
|
+
warnIfPluginCacheMissing();
|
|
2644
3227
|
const channelEntry = `plugin:${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
2645
3228
|
if (permissionPlan.inject) {
|
|
2646
3229
|
console.error(`[agentbridge] running with ${CLAUDE_MAX_PERMISSION_FLAG} (default; opt out with --safe or AGENTBRIDGE_SAFE=1)`);
|
|
@@ -2655,8 +3238,8 @@ async function runClaude(args) {
|
|
|
2655
3238
|
stdio: "inherit",
|
|
2656
3239
|
env: process.env
|
|
2657
3240
|
});
|
|
2658
|
-
child.on("exit", (code) => {
|
|
2659
|
-
process.exit(code
|
|
3241
|
+
child.on("exit", (code, signal) => {
|
|
3242
|
+
process.exit(mapChildExitCode(code, signal));
|
|
2660
3243
|
});
|
|
2661
3244
|
child.on("error", (err) => {
|
|
2662
3245
|
if (err.code === "ENOENT") {
|
|
@@ -2668,6 +3251,24 @@ async function runClaude(args) {
|
|
|
2668
3251
|
process.exit(1);
|
|
2669
3252
|
});
|
|
2670
3253
|
}
|
|
3254
|
+
function mapChildExitCode(code, signal) {
|
|
3255
|
+
if (signal) {
|
|
3256
|
+
return 128 + (osConstants.signals[signal] ?? 0);
|
|
3257
|
+
}
|
|
3258
|
+
return code ?? 0;
|
|
3259
|
+
}
|
|
3260
|
+
function warnIfPluginCacheMissing(cacheRoot = pluginCacheRoot(), log = (msg) => console.error(msg)) {
|
|
3261
|
+
let cacheExists;
|
|
3262
|
+
try {
|
|
3263
|
+
cacheExists = existsSync9(cacheRoot);
|
|
3264
|
+
} catch {
|
|
3265
|
+
return false;
|
|
3266
|
+
}
|
|
3267
|
+
if (!shouldWarnMissingPluginCache(cacheExists))
|
|
3268
|
+
return false;
|
|
3269
|
+
log("[agentbridge] \u26A0\uFE0F Plugin not installed (no plugin cache found). Run `abg init`, " + `or in Claude Code: ${MARKETPLACE_STEPS.join(" \u2192 ")}. Launching anyway\u2026`);
|
|
3270
|
+
return true;
|
|
3271
|
+
}
|
|
2671
3272
|
function traceCliStart(event, args, originalEnv, envGuardAction, pair) {
|
|
2672
3273
|
try {
|
|
2673
3274
|
appendTraceEvent({
|
|
@@ -2744,6 +3345,7 @@ function checkOwnedFlagConflicts(args, commandName, ownedFlags) {
|
|
|
2744
3345
|
var OWNED_FLAGS;
|
|
2745
3346
|
var init_claude = __esm(() => {
|
|
2746
3347
|
init_cli();
|
|
3348
|
+
init_plugin_cache();
|
|
2747
3349
|
init_daemon_client();
|
|
2748
3350
|
init_daemon_lifecycle();
|
|
2749
3351
|
init_build_info();
|
|
@@ -2755,15 +3357,15 @@ var init_claude = __esm(() => {
|
|
|
2755
3357
|
});
|
|
2756
3358
|
|
|
2757
3359
|
// src/agents-contract.ts
|
|
2758
|
-
import { existsSync as
|
|
2759
|
-
import { join as
|
|
3360
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
|
|
3361
|
+
import { join as join9 } from "path";
|
|
2760
3362
|
function checkAgentsMdContract(cwd) {
|
|
2761
|
-
const path =
|
|
2762
|
-
const exists =
|
|
3363
|
+
const path = join9(cwd, "AGENTS.md");
|
|
3364
|
+
const exists = existsSync10(path);
|
|
2763
3365
|
let content = "";
|
|
2764
3366
|
if (exists) {
|
|
2765
3367
|
try {
|
|
2766
|
-
content =
|
|
3368
|
+
content = readFileSync7(path, "utf-8");
|
|
2767
3369
|
} catch {
|
|
2768
3370
|
return {
|
|
2769
3371
|
fresh: false,
|
|
@@ -2790,8 +3392,8 @@ function isFreshAgentsMdContract(content) {
|
|
|
2790
3392
|
var init_agents_contract = () => {};
|
|
2791
3393
|
|
|
2792
3394
|
// src/wrapper-exit-observability.ts
|
|
2793
|
-
import { readFileSync as
|
|
2794
|
-
import { join as
|
|
3395
|
+
import { readFileSync as readFileSync8, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
3396
|
+
import { join as join10 } from "path";
|
|
2795
3397
|
function discoverNativeChildPid(launcherPid, run) {
|
|
2796
3398
|
try {
|
|
2797
3399
|
const out = run("pgrep", ["-P", String(launcherPid)]);
|
|
@@ -2801,17 +3403,15 @@ function discoverNativeChildPid(launcherPid, run) {
|
|
|
2801
3403
|
return null;
|
|
2802
3404
|
}
|
|
2803
3405
|
}
|
|
2804
|
-
function
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
if (typeof status.turnInProgress !== "boolean")
|
|
2808
|
-
return null;
|
|
2809
|
-
if (typeof status.pid === "number" && !isPidAlive(status.pid))
|
|
2810
|
-
return null;
|
|
2811
|
-
return status.turnInProgress;
|
|
2812
|
-
} catch {
|
|
3406
|
+
function readUnifiedTurnInProgress(paths, read = (p) => readFileSync8(p, "utf-8"), isPidAlive = defaultIsPidAlive) {
|
|
3407
|
+
const record = readUnifiedDaemonRecord(paths, read);
|
|
3408
|
+
if (!record)
|
|
2813
3409
|
return null;
|
|
2814
|
-
|
|
3410
|
+
if (typeof record.turnInProgress !== "boolean")
|
|
3411
|
+
return null;
|
|
3412
|
+
if (typeof record.pid === "number" && !isPidAlive(record.pid))
|
|
3413
|
+
return null;
|
|
3414
|
+
return record.turnInProgress;
|
|
2815
3415
|
}
|
|
2816
3416
|
function refineCleanExitClassification(turnInProgress) {
|
|
2817
3417
|
if (turnInProgress === true)
|
|
@@ -2820,14 +3420,14 @@ function refineCleanExitClassification(turnInProgress) {
|
|
|
2820
3420
|
return "exit_0_idle";
|
|
2821
3421
|
return "exit_0_turn_unknown";
|
|
2822
3422
|
}
|
|
2823
|
-
function findCodexSqliteLog(codexHome,
|
|
3423
|
+
function findCodexSqliteLog(codexHome, fs2 = { readdir: readdirSync3, stat: statSync4 }) {
|
|
2824
3424
|
try {
|
|
2825
|
-
const entries =
|
|
3425
|
+
const entries = fs2.readdir(codexHome).filter((name) => /^logs.*\.sqlite$/.test(String(name)));
|
|
2826
3426
|
let best = null;
|
|
2827
3427
|
for (const name of entries) {
|
|
2828
|
-
const path =
|
|
3428
|
+
const path = join10(codexHome, String(name));
|
|
2829
3429
|
try {
|
|
2830
|
-
const mtime =
|
|
3430
|
+
const mtime = fs2.stat(path).mtimeMs;
|
|
2831
3431
|
if (!best || mtime > best.mtime)
|
|
2832
3432
|
best = { path, mtime };
|
|
2833
3433
|
} catch {}
|
|
@@ -2862,14 +3462,15 @@ function captureTuiLogTail(options) {
|
|
|
2862
3462
|
var defaultIsPidAlive;
|
|
2863
3463
|
var init_wrapper_exit_observability = __esm(() => {
|
|
2864
3464
|
init_process_lifecycle();
|
|
3465
|
+
init_daemon_record();
|
|
2865
3466
|
defaultIsPidAlive = pidLooksAlive;
|
|
2866
3467
|
});
|
|
2867
3468
|
|
|
2868
3469
|
// src/pair-command.ts
|
|
2869
|
-
function pairScopedCommand(cmd) {
|
|
3470
|
+
function pairScopedCommand(cmd, name = cliInvocationName()) {
|
|
2870
3471
|
const pairId = process.env.AGENTBRIDGE_PAIR_ID;
|
|
2871
3472
|
if (!pairId)
|
|
2872
|
-
return
|
|
3473
|
+
return `${name} ${cmd}`;
|
|
2873
3474
|
let selector = process.env.AGENTBRIDGE_PAIR_NAME;
|
|
2874
3475
|
if (!selector) {
|
|
2875
3476
|
try {
|
|
@@ -2878,22 +3479,23 @@ function pairScopedCommand(cmd) {
|
|
|
2878
3479
|
selector = pairId;
|
|
2879
3480
|
}
|
|
2880
3481
|
}
|
|
2881
|
-
return
|
|
3482
|
+
return `${name} --pair ${selector} ${cmd}`;
|
|
2882
3483
|
}
|
|
2883
3484
|
var init_pair_command = __esm(() => {
|
|
3485
|
+
init_cli_invocation();
|
|
2884
3486
|
init_pair_resolver();
|
|
2885
3487
|
});
|
|
2886
3488
|
|
|
2887
3489
|
// src/rotating-log.ts
|
|
2888
|
-
import { appendFileSync as appendFileSync2, existsSync as
|
|
2889
|
-
import { dirname as
|
|
2890
|
-
function appendRotatingLog(path, content, options = {}) {
|
|
3490
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync11, renameSync as renameSync2, statSync as statSync5, unlinkSync as unlinkSync5 } from "fs";
|
|
3491
|
+
import { dirname as dirname3 } from "path";
|
|
3492
|
+
function appendRotatingLog(path, content, options = {}, fsOps = REAL_FS_OPS) {
|
|
2891
3493
|
const maxBytes = options.maxBytes ?? positiveIntFromEnv("AGENTBRIDGE_LOG_MAX_BYTES", DEFAULT_MAX_BYTES);
|
|
2892
3494
|
const keep = options.keep ?? positiveIntFromEnv("AGENTBRIDGE_LOG_ROTATE_KEEP", DEFAULT_KEEP);
|
|
2893
|
-
if (!
|
|
3495
|
+
if (!fsOps.existsSync(dirname3(path)))
|
|
2894
3496
|
return;
|
|
2895
|
-
rotateIfNeeded(path, Buffer.byteLength(content), maxBytes, keep);
|
|
2896
|
-
|
|
3497
|
+
rotateIfNeeded(path, Buffer.byteLength(content), maxBytes, keep, fsOps);
|
|
3498
|
+
fsOps.appendFileSync(path, content, "utf-8");
|
|
2897
3499
|
}
|
|
2898
3500
|
function positiveIntFromEnv(name, fallback) {
|
|
2899
3501
|
const value = process.env[name];
|
|
@@ -2902,30 +3504,53 @@ function positiveIntFromEnv(name, fallback) {
|
|
|
2902
3504
|
const parsed = Number(value);
|
|
2903
3505
|
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
2904
3506
|
}
|
|
2905
|
-
function
|
|
3507
|
+
function isEnoent(error) {
|
|
3508
|
+
return !!error && error.code === "ENOENT";
|
|
3509
|
+
}
|
|
3510
|
+
function renameIfPresent(from, to, fsOps) {
|
|
3511
|
+
try {
|
|
3512
|
+
fsOps.renameSync(from, to);
|
|
3513
|
+
} catch (error) {
|
|
3514
|
+
if (!isEnoent(error))
|
|
3515
|
+
throw error;
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
function unlinkIfPresent(path, fsOps) {
|
|
3519
|
+
try {
|
|
3520
|
+
fsOps.unlinkSync(path);
|
|
3521
|
+
} catch (error) {
|
|
3522
|
+
if (!isEnoent(error))
|
|
3523
|
+
throw error;
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
function rotateIfNeeded(path, incomingBytes, maxBytes, keep, fsOps) {
|
|
2906
3527
|
if (!Number.isFinite(maxBytes) || maxBytes <= 0 || keep <= 0)
|
|
2907
3528
|
return;
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
3529
|
+
let size;
|
|
3530
|
+
try {
|
|
3531
|
+
size = fsOps.statSync(path).size;
|
|
3532
|
+
} catch (error) {
|
|
3533
|
+
if (isEnoent(error))
|
|
3534
|
+
return;
|
|
3535
|
+
throw error;
|
|
3536
|
+
}
|
|
2911
3537
|
if (size + incomingBytes <= maxBytes)
|
|
2912
3538
|
return;
|
|
2913
3539
|
for (let index = keep;index >= 1; index--) {
|
|
2914
3540
|
const current = `${path}.${index}`;
|
|
2915
3541
|
const next = `${path}.${index + 1}`;
|
|
2916
|
-
if (!existsSync8(current))
|
|
2917
|
-
continue;
|
|
2918
3542
|
if (index === keep) {
|
|
2919
|
-
|
|
3543
|
+
unlinkIfPresent(current, fsOps);
|
|
2920
3544
|
} else {
|
|
2921
|
-
|
|
3545
|
+
renameIfPresent(current, next, fsOps);
|
|
2922
3546
|
}
|
|
2923
3547
|
}
|
|
2924
|
-
|
|
3548
|
+
renameIfPresent(path, `${path}.1`, fsOps);
|
|
2925
3549
|
}
|
|
2926
|
-
var DEFAULT_MAX_BYTES, DEFAULT_KEEP = 3;
|
|
3550
|
+
var DEFAULT_MAX_BYTES, DEFAULT_KEEP = 3, REAL_FS_OPS;
|
|
2927
3551
|
var init_rotating_log = __esm(() => {
|
|
2928
3552
|
DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
3553
|
+
REAL_FS_OPS = { statSync: statSync5, renameSync: renameSync2, unlinkSync: unlinkSync5, appendFileSync: appendFileSync2, existsSync: existsSync11 };
|
|
2929
3554
|
});
|
|
2930
3555
|
|
|
2931
3556
|
// src/stderr-ring-buffer.ts
|
|
@@ -2978,31 +3603,21 @@ var init_stderr_ring_buffer = __esm(() => {
|
|
|
2978
3603
|
|
|
2979
3604
|
// src/thread-state.ts
|
|
2980
3605
|
import {
|
|
2981
|
-
existsSync as
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
readFileSync as readFileSync8,
|
|
2985
|
-
renameSync as renameSync3,
|
|
2986
|
-
writeFileSync as writeFileSync6
|
|
3606
|
+
existsSync as existsSync12,
|
|
3607
|
+
readdirSync as readdirSync4,
|
|
3608
|
+
readFileSync as readFileSync9
|
|
2987
3609
|
} from "fs";
|
|
2988
3610
|
import { homedir as homedir3 } from "os";
|
|
2989
|
-
import { basename as
|
|
3611
|
+
import { basename as basename4, join as join11 } from "path";
|
|
2990
3612
|
function nowIso() {
|
|
2991
3613
|
return new Date().toISOString();
|
|
2992
3614
|
}
|
|
2993
3615
|
function codexHome(env = process.env) {
|
|
2994
|
-
return env.CODEX_HOME && env.CODEX_HOME.length > 0 ? env.CODEX_HOME :
|
|
2995
|
-
}
|
|
2996
|
-
function atomicWriteJson(path, value) {
|
|
2997
|
-
mkdirSync5(dirname3(path), { recursive: true });
|
|
2998
|
-
const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
|
|
2999
|
-
writeFileSync6(tmp, JSON.stringify(value, null, 2) + `
|
|
3000
|
-
`, "utf-8");
|
|
3001
|
-
renameSync3(tmp, path);
|
|
3616
|
+
return env.CODEX_HOME && env.CODEX_HOME.length > 0 ? env.CODEX_HOME : join11(homedir3(), ".codex");
|
|
3002
3617
|
}
|
|
3003
3618
|
function readRawCurrentThread(stateDir) {
|
|
3004
3619
|
try {
|
|
3005
|
-
const parsed = JSON.parse(
|
|
3620
|
+
const parsed = JSON.parse(readFileSync9(stateDir.currentThreadFile, "utf-8"));
|
|
3006
3621
|
if (parsed?.version === 1 && typeof parsed.threadId === "string" && parsed.threadId.length > 0 && (parsed.status === "pending" || parsed.status === "current") && typeof parsed.cwd === "string") {
|
|
3007
3622
|
return parsed;
|
|
3008
3623
|
}
|
|
@@ -3010,8 +3625,8 @@ function readRawCurrentThread(stateDir) {
|
|
|
3010
3625
|
return null;
|
|
3011
3626
|
}
|
|
3012
3627
|
function findCodexRolloutFile(threadId, env = process.env, maxEntries = 20000) {
|
|
3013
|
-
const sessionsDir =
|
|
3014
|
-
if (!threadId || !
|
|
3628
|
+
const sessionsDir = join11(codexHome(env), "sessions");
|
|
3629
|
+
if (!threadId || !existsSync12(sessionsDir))
|
|
3015
3630
|
return null;
|
|
3016
3631
|
const exactName = `rollout-${threadId}.jsonl`;
|
|
3017
3632
|
const stack = [sessionsDir];
|
|
@@ -3020,20 +3635,20 @@ function findCodexRolloutFile(threadId, env = process.env, maxEntries = 20000) {
|
|
|
3020
3635
|
const dir = stack.pop();
|
|
3021
3636
|
let entries;
|
|
3022
3637
|
try {
|
|
3023
|
-
entries =
|
|
3638
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
3024
3639
|
} catch {
|
|
3025
3640
|
continue;
|
|
3026
3641
|
}
|
|
3027
3642
|
for (const entry of entries) {
|
|
3028
3643
|
visited++;
|
|
3029
|
-
const path =
|
|
3644
|
+
const path = join11(dir, entry.name);
|
|
3030
3645
|
if (entry.isDirectory()) {
|
|
3031
3646
|
stack.push(path);
|
|
3032
3647
|
continue;
|
|
3033
3648
|
}
|
|
3034
3649
|
if (!entry.isFile())
|
|
3035
3650
|
continue;
|
|
3036
|
-
const name =
|
|
3651
|
+
const name = basename4(entry.name);
|
|
3037
3652
|
if (name === exactName || name.startsWith("rollout-") && name.endsWith(".jsonl") && name.includes(threadId)) {
|
|
3038
3653
|
return path;
|
|
3039
3654
|
}
|
|
@@ -3051,7 +3666,7 @@ function readUsableCurrentThread(identity, env = process.env) {
|
|
|
3051
3666
|
return null;
|
|
3052
3667
|
if (state.cwd !== identity.cwd)
|
|
3053
3668
|
return null;
|
|
3054
|
-
if (state.rolloutPath &&
|
|
3669
|
+
if (state.rolloutPath && existsSync12(state.rolloutPath))
|
|
3055
3670
|
return state;
|
|
3056
3671
|
const rolloutPath = findCodexRolloutFile(state.threadId, env);
|
|
3057
3672
|
if (!rolloutPath)
|
|
@@ -3065,7 +3680,9 @@ function readUsableCurrentThread(identity, env = process.env) {
|
|
|
3065
3680
|
atomicWriteJson(identity.stateDir.currentThreadFile, repaired);
|
|
3066
3681
|
return repaired;
|
|
3067
3682
|
}
|
|
3068
|
-
var init_thread_state = () => {
|
|
3683
|
+
var init_thread_state = __esm(() => {
|
|
3684
|
+
init_atomic_json();
|
|
3685
|
+
});
|
|
3069
3686
|
|
|
3070
3687
|
// src/cli/codex.ts
|
|
3071
3688
|
var exports_codex = {};
|
|
@@ -3080,18 +3697,18 @@ import {
|
|
|
3080
3697
|
openSync as openSync3,
|
|
3081
3698
|
writeSync,
|
|
3082
3699
|
closeSync as closeSync3,
|
|
3083
|
-
writeFileSync as
|
|
3084
|
-
readFileSync as
|
|
3085
|
-
unlinkSync as
|
|
3086
|
-
existsSync as
|
|
3700
|
+
writeFileSync as writeFileSync5,
|
|
3701
|
+
readFileSync as readFileSync10,
|
|
3702
|
+
unlinkSync as unlinkSync6,
|
|
3703
|
+
existsSync as existsSync13,
|
|
3087
3704
|
mkdirSync as mkdirSync6
|
|
3088
3705
|
} from "fs";
|
|
3089
3706
|
import { homedir as homedir4 } from "os";
|
|
3090
|
-
import { dirname as dirname4, join as
|
|
3707
|
+
import { dirname as dirname4, join as join12 } from "path";
|
|
3091
3708
|
function appendWrapperLog(path, entry) {
|
|
3092
3709
|
try {
|
|
3093
3710
|
const dir = dirname4(path);
|
|
3094
|
-
if (!
|
|
3711
|
+
if (!existsSync13(dir)) {
|
|
3095
3712
|
mkdirSync6(dir, { recursive: true });
|
|
3096
3713
|
}
|
|
3097
3714
|
appendRotatingLog(path, `[${new Date().toISOString()}] ${entry}
|
|
@@ -3254,11 +3871,11 @@ async function runCodex(args) {
|
|
|
3254
3871
|
process.exit(1);
|
|
3255
3872
|
}
|
|
3256
3873
|
let proxyUrl;
|
|
3257
|
-
const
|
|
3258
|
-
if (
|
|
3259
|
-
proxyUrl =
|
|
3874
|
+
const record = lifecycle.readDaemonRecord();
|
|
3875
|
+
if (typeof record?.proxyUrl === "string" && record.proxyUrl.length > 0) {
|
|
3876
|
+
proxyUrl = record.proxyUrl;
|
|
3260
3877
|
} else {
|
|
3261
|
-
const fallbackProxyPort = process.env.CODEX_PROXY_PORT ?? String(new ConfigService().loadOrDefault().codex.proxyPort);
|
|
3878
|
+
const fallbackProxyPort = process.env.CODEX_PROXY_PORT ?? String(new ConfigService().loadOrDefault((msg) => console.error(`[agentbridge] ${msg}`)).codex.proxyPort);
|
|
3262
3879
|
proxyUrl = `ws://127.0.0.1:${fallbackProxyPort}`;
|
|
3263
3880
|
console.error(`[agentbridge] No daemon status found, using fallback proxy port: ${proxyUrl}`);
|
|
3264
3881
|
}
|
|
@@ -3338,7 +3955,7 @@ async function runCodex(args) {
|
|
|
3338
3955
|
env: buildChildEnv()
|
|
3339
3956
|
});
|
|
3340
3957
|
if (typeof child.pid === "number") {
|
|
3341
|
-
|
|
3958
|
+
writeFileSync5(stateDir.tuiPidFile, `${child.pid}
|
|
3342
3959
|
`, "utf-8");
|
|
3343
3960
|
appendWrapperLog(wrapperLogPath, `child pid=${child.pid}`);
|
|
3344
3961
|
}
|
|
@@ -3378,7 +3995,7 @@ async function runCodex(args) {
|
|
|
3378
3995
|
return;
|
|
3379
3996
|
cleanedTuiPid = true;
|
|
3380
3997
|
try {
|
|
3381
|
-
|
|
3998
|
+
unlinkSync6(stateDir.tuiPidFile);
|
|
3382
3999
|
} catch {}
|
|
3383
4000
|
}
|
|
3384
4001
|
function requestChildTermination(reason) {
|
|
@@ -3449,10 +4066,14 @@ async function runCodex(args) {
|
|
|
3449
4066
|
else if (typeof code === "number" && code !== 0)
|
|
3450
4067
|
classification = `nonzero_exit:${code}`;
|
|
3451
4068
|
else if (code === 0 && tail.trim().length === 0) {
|
|
3452
|
-
classification = refineCleanExitClassification(
|
|
4069
|
+
classification = refineCleanExitClassification(readUnifiedTurnInProgress({
|
|
4070
|
+
daemonRecordFile: stateDir.daemonRecordFile,
|
|
4071
|
+
pidFile: stateDir.pidFile,
|
|
4072
|
+
statusFile: stateDir.statusFile
|
|
4073
|
+
}));
|
|
3453
4074
|
}
|
|
3454
4075
|
const tuiLogTail = captureTuiLogTail({
|
|
3455
|
-
codexHome:
|
|
4076
|
+
codexHome: join12(homedir4(), ".codex"),
|
|
3456
4077
|
nativePid: nativeChildPid,
|
|
3457
4078
|
run: (cmd, args2) => execFileSync6(cmd, args2, { encoding: "utf-8", timeout: 2000 })
|
|
3458
4079
|
});
|
|
@@ -3508,12 +4129,12 @@ function guardNoLiveManagedTui(stateDir, proxyUrl) {
|
|
|
3508
4129
|
if (pid) {
|
|
3509
4130
|
if (!isProcessAlive(pid)) {
|
|
3510
4131
|
try {
|
|
3511
|
-
|
|
4132
|
+
unlinkSync6(stateDir.tuiPidFile);
|
|
3512
4133
|
} catch {}
|
|
3513
4134
|
} else if (!isManagedCodexTuiProcess(pid, proxyUrl)) {
|
|
3514
4135
|
appendWrapperLog(stateDir.codexWrapperLogFile, `stale tui pid file pointed at unmanaged live pid=${pid}; removing`);
|
|
3515
4136
|
try {
|
|
3516
|
-
|
|
4137
|
+
unlinkSync6(stateDir.tuiPidFile);
|
|
3517
4138
|
} catch {}
|
|
3518
4139
|
} else {
|
|
3519
4140
|
console.error(`[agentbridge] This pair already has a managed Codex TUI running (pid ${pid}).`);
|
|
@@ -3530,7 +4151,7 @@ function guardNoLiveManagedTui(stateDir, proxyUrl) {
|
|
|
3530
4151
|
}
|
|
3531
4152
|
function readTuiPid(stateDir) {
|
|
3532
4153
|
try {
|
|
3533
|
-
const raw =
|
|
4154
|
+
const raw = readFileSync10(stateDir.tuiPidFile, "utf-8").trim();
|
|
3534
4155
|
if (!raw)
|
|
3535
4156
|
return null;
|
|
3536
4157
|
const pid = Number.parseInt(raw, 10);
|
|
@@ -3544,7 +4165,12 @@ function isManagedCodexTuiProcess(pid, proxyUrl) {
|
|
|
3544
4165
|
return cmd !== null && commandMatchesManagedCodexTui(cmd, proxyUrl);
|
|
3545
4166
|
}
|
|
3546
4167
|
function proxyHealthUrl(proxyUrl) {
|
|
3547
|
-
|
|
4168
|
+
let url;
|
|
4169
|
+
try {
|
|
4170
|
+
url = new URL(proxyUrl);
|
|
4171
|
+
} catch {
|
|
4172
|
+
throw new Error(`Malformed Codex proxy URL: ${JSON.stringify(proxyUrl)}`);
|
|
4173
|
+
}
|
|
3548
4174
|
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
3549
4175
|
url.pathname = "/healthz";
|
|
3550
4176
|
url.search = "";
|
|
@@ -3560,7 +4186,7 @@ async function waitForProxyReady(proxyUrl, maxRetries = 20, delayMs = 100) {
|
|
|
3560
4186
|
return;
|
|
3561
4187
|
}
|
|
3562
4188
|
} catch {}
|
|
3563
|
-
await new Promise((
|
|
4189
|
+
await new Promise((resolve5) => setTimeout(resolve5, delayMs));
|
|
3564
4190
|
}
|
|
3565
4191
|
throw new Error(`Timed out waiting for Codex proxy readiness on ${healthUrl}`);
|
|
3566
4192
|
}
|
|
@@ -3609,17 +4235,17 @@ var init_codex = __esm(() => {
|
|
|
3609
4235
|
});
|
|
3610
4236
|
|
|
3611
4237
|
// src/claude-session.ts
|
|
3612
|
-
import { readdirSync as
|
|
4238
|
+
import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
|
|
3613
4239
|
import { homedir as homedir5 } from "os";
|
|
3614
|
-
import { join as
|
|
4240
|
+
import { join as join13 } from "path";
|
|
3615
4241
|
function encodeClaudeProjectDir(cwd) {
|
|
3616
4242
|
return cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
3617
4243
|
}
|
|
3618
|
-
function findLatestClaudeSession(cwd, claudeHome = process.env.CLAUDE_CONFIG_DIR ||
|
|
3619
|
-
const dir =
|
|
4244
|
+
function findLatestClaudeSession(cwd, claudeHome = process.env.CLAUDE_CONFIG_DIR || join13(homedir5(), ".claude")) {
|
|
4245
|
+
const dir = join13(claudeHome, "projects", encodeClaudeProjectDir(cwd));
|
|
3620
4246
|
let entries;
|
|
3621
4247
|
try {
|
|
3622
|
-
entries =
|
|
4248
|
+
entries = readdirSync5(dir);
|
|
3623
4249
|
} catch {
|
|
3624
4250
|
return null;
|
|
3625
4251
|
}
|
|
@@ -3630,10 +4256,10 @@ function findLatestClaudeSession(cwd, claudeHome = process.env.CLAUDE_CONFIG_DIR
|
|
|
3630
4256
|
const sessionId = name.slice(0, -".jsonl".length);
|
|
3631
4257
|
if (!SESSION_ID_PATTERN.test(sessionId))
|
|
3632
4258
|
continue;
|
|
3633
|
-
const file =
|
|
4259
|
+
const file = join13(dir, name);
|
|
3634
4260
|
let mtimeMs;
|
|
3635
4261
|
try {
|
|
3636
|
-
const st =
|
|
4262
|
+
const st = statSync6(file);
|
|
3637
4263
|
if (!st.isFile())
|
|
3638
4264
|
continue;
|
|
3639
4265
|
mtimeMs = st.mtimeMs;
|
|
@@ -3739,8 +4365,8 @@ __export(exports_kill, {
|
|
|
3739
4365
|
runKill: () => runKill,
|
|
3740
4366
|
formatKillReport: () => formatKillReport
|
|
3741
4367
|
});
|
|
3742
|
-
import { readFileSync as
|
|
3743
|
-
import { join as
|
|
4368
|
+
import { readFileSync as readFileSync11, unlinkSync as unlinkSync7 } from "fs";
|
|
4369
|
+
import { join as join14 } from "path";
|
|
3744
4370
|
async function runKill(args = []) {
|
|
3745
4371
|
const argError = validateKillArgs(args);
|
|
3746
4372
|
if (argError === "help") {
|
|
@@ -3760,8 +4386,9 @@ async function runKill(args = []) {
|
|
|
3760
4386
|
const base = computeBaseDir();
|
|
3761
4387
|
console.log(`AgentBridge Kill \u2014 stopping AgentBridge pair processes
|
|
3762
4388
|
`);
|
|
4389
|
+
const cli = cliInvocationName();
|
|
3763
4390
|
const results = [];
|
|
3764
|
-
let restartCommand =
|
|
4391
|
+
let restartCommand = `${cli} claude`;
|
|
3765
4392
|
if (parsed.pairFlag !== undefined) {
|
|
3766
4393
|
let pair;
|
|
3767
4394
|
try {
|
|
@@ -3775,7 +4402,7 @@ async function runKill(args = []) {
|
|
|
3775
4402
|
printKnownPairs(base);
|
|
3776
4403
|
return;
|
|
3777
4404
|
}
|
|
3778
|
-
restartCommand =
|
|
4405
|
+
restartCommand = `${cli} --pair ${pair.name ?? parsed.pairFlag} claude`;
|
|
3779
4406
|
results.push(await stopPairEntry(base, pair));
|
|
3780
4407
|
} else if (parsed.all) {
|
|
3781
4408
|
let registered = [];
|
|
@@ -3792,7 +4419,7 @@ async function runKill(args = []) {
|
|
|
3792
4419
|
for (const dirName of listPairDirsSafe(base)) {
|
|
3793
4420
|
if (registeredIds.has(dirName))
|
|
3794
4421
|
continue;
|
|
3795
|
-
const stateDir = new StateDirResolver(
|
|
4422
|
+
const stateDir = new StateDirResolver(join14(base, "pairs", dirName));
|
|
3796
4423
|
results.push(await stopStateDir(`${dirName} (unregistered)`, stateDir, portsFromStateDir(stateDir)));
|
|
3797
4424
|
}
|
|
3798
4425
|
const legacy = detectLegacyRootDaemon(base);
|
|
@@ -3809,7 +4436,7 @@ async function runKill(args = []) {
|
|
|
3809
4436
|
cwdPairs = listPairsForCwd(base, process.cwd());
|
|
3810
4437
|
} catch (error) {
|
|
3811
4438
|
const message = error instanceof Error ? error.message : String(error);
|
|
3812
|
-
console.log(`\u26A0\uFE0F pair registry \u4E0D\u53EF\u8BFB\uFF08${message}\uFF09\u2014\u2014\u65E0\u6CD5\u6309\u76EE\u5F55\u5B9A\u4F4D pair\u3002` +
|
|
4439
|
+
console.log(`\u26A0\uFE0F pair registry \u4E0D\u53EF\u8BFB\uFF08${message}\uFF09\u2014\u2014\u65E0\u6CD5\u6309\u76EE\u5F55\u5B9A\u4F4D pair\u3002` + `\u8FD0\u884C \`${cli} kill --all\` \u53EF\u964D\u7EA7\u4E3A\u5168\u76D8\u72B6\u6001\u76EE\u5F55\u626B\u63CF\uFF0C\u505C\u6B62\u6240\u6709\u80FD\u627E\u5230\u7684 pair\u3002`);
|
|
3813
4440
|
process.exitCode = 2;
|
|
3814
4441
|
}
|
|
3815
4442
|
for (const pair of cwdPairs) {
|
|
@@ -3825,7 +4452,7 @@ async function runKill(args = []) {
|
|
|
3825
4452
|
}
|
|
3826
4453
|
if (results.length === 0) {
|
|
3827
4454
|
console.log(`No AgentBridge pairs registered for current directory: ${process.cwd()}`);
|
|
3828
|
-
console.log(
|
|
4455
|
+
console.log(`Use \`${cli} kill all\` or \`${cli} kill --all\` to stop pairs from every directory.`);
|
|
3829
4456
|
return;
|
|
3830
4457
|
}
|
|
3831
4458
|
}
|
|
@@ -3877,7 +4504,7 @@ No arguments stop this directory's registered pairs and any legacy-root daemon.
|
|
|
3877
4504
|
}
|
|
3878
4505
|
async function stopPairEntry(base, pair) {
|
|
3879
4506
|
const ports = portsForEntry(pair);
|
|
3880
|
-
const stateDir = new StateDirResolver(
|
|
4507
|
+
const stateDir = new StateDirResolver(join14(base, "pairs", pair.pairId));
|
|
3881
4508
|
return stopStateDir(pair.pairId, stateDir, ports);
|
|
3882
4509
|
}
|
|
3883
4510
|
function listPairDirsSafe(base) {
|
|
@@ -3888,22 +4515,18 @@ function listPairDirsSafe(base) {
|
|
|
3888
4515
|
}
|
|
3889
4516
|
}
|
|
3890
4517
|
function portsFromStateDir(stateDir) {
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
};
|
|
3898
|
-
} catch {
|
|
4518
|
+
const record = readUnifiedDaemonRecord({
|
|
4519
|
+
daemonRecordFile: stateDir.daemonRecordFile,
|
|
4520
|
+
pidFile: stateDir.pidFile,
|
|
4521
|
+
statusFile: stateDir.statusFile
|
|
4522
|
+
});
|
|
4523
|
+
if (!record)
|
|
3899
4524
|
return { appPort: 0, proxyPort: 0, controlPort: 0 };
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
const match = url.match(/:(\d+)(?:[/?]|$)/);
|
|
3906
|
-
return match ? Number.parseInt(match[1], 10) : null;
|
|
4525
|
+
return {
|
|
4526
|
+
appPort: record.ports?.appPort ?? portFromUrl(record.appServerUrl) ?? 0,
|
|
4527
|
+
proxyPort: record.ports?.proxyPort ?? portFromUrl(record.proxyUrl) ?? 0,
|
|
4528
|
+
controlPort: record.ports?.controlPort ?? 0
|
|
4529
|
+
};
|
|
3907
4530
|
}
|
|
3908
4531
|
async function stopStateDir(label, stateDir, ports) {
|
|
3909
4532
|
const portsLabel = `${ports.appPort}/${ports.proxyPort}/${ports.controlPort}`;
|
|
@@ -3916,8 +4539,8 @@ async function stopStateDir(label, stateDir, ports) {
|
|
|
3916
4539
|
log
|
|
3917
4540
|
});
|
|
3918
4541
|
lifecycle.markKilled();
|
|
3919
|
-
const
|
|
3920
|
-
const proxyUrl = typeof
|
|
4542
|
+
const record = lifecycle.readDaemonRecord();
|
|
4543
|
+
const proxyUrl = typeof record?.proxyUrl === "string" && record.proxyUrl.length > 0 ? record.proxyUrl : `ws://127.0.0.1:${ports.proxyPort}`;
|
|
3921
4544
|
const tuiKilled = await killManagedCodexTui(stateDir, proxyUrl, log);
|
|
3922
4545
|
const daemonKilled = await lifecycle.kill();
|
|
3923
4546
|
return { label, portsLabel, daemonKilled, tuiKilled, details };
|
|
@@ -3982,9 +4605,10 @@ function formatKillReport(results, frontends, restartCommand) {
|
|
|
3982
4605
|
}
|
|
3983
4606
|
lines.push("");
|
|
3984
4607
|
if (stopped.length > 0) {
|
|
4608
|
+
const cliName = restartCommand.split(" ")[0] ?? "abg";
|
|
3985
4609
|
lines.push("AgentBridge stopped.");
|
|
3986
4610
|
lines.push(`Please restart Claude Code (\`${restartCommand}\`), switch to a new conversation, or run \`/resume\` to fully disconnect.`);
|
|
3987
|
-
lines.push("\u2139\uFE0F \u5DF2\u5199\u5165 killed \u54E8\u5175\uFF1A\u88AB\u505C\u6B62\u7684 pair \u4E0D\u4F1A\u88AB\u81EA\u52A8\u590D\u6D3B\uFF1B" + `\u4E0B\u6B21 \`${restartCommand}\` /
|
|
4611
|
+
lines.push("\u2139\uFE0F \u5DF2\u5199\u5165 killed \u54E8\u5175\uFF1A\u88AB\u505C\u6B62\u7684 pair \u4E0D\u4F1A\u88AB\u81EA\u52A8\u590D\u6D3B\uFF1B" + `\u4E0B\u6B21 \`${restartCommand}\` / \`${cliName} codex\` \u4F1A\u6E05\u9664\u54E8\u5175\u5E76\u7528\u5F53\u524D\u5B89\u88C5\u7248\u672C\u542F\u52A8\u5168\u65B0 daemon\u3002`);
|
|
3988
4612
|
} else {
|
|
3989
4613
|
lines.push("No running AgentBridge daemon or managed Codex TUI found.");
|
|
3990
4614
|
lines.push("\u2139\uFE0F \u76EE\u6807 pair \u90FD\u6CA1\u6709\u5728\u8FD0\u884C\u7684\u8FDB\u7A0B\u2014\u2014\u5982\u679C\u4F60\u4ECD\u770B\u5230 AgentBridge \u6D3B\u52A8\uFF0C\u89C1\u4E0B\u65B9\u524D\u7AEF\u63D0\u793A\u3002");
|
|
@@ -4035,7 +4659,7 @@ async function killManagedCodexTui(stateDir, proxyUrl, log, gracefulTimeoutMs =
|
|
|
4035
4659
|
}
|
|
4036
4660
|
function readTuiPid2(stateDir) {
|
|
4037
4661
|
try {
|
|
4038
|
-
const raw =
|
|
4662
|
+
const raw = readFileSync11(stateDir.tuiPidFile, "utf-8").trim();
|
|
4039
4663
|
if (!raw)
|
|
4040
4664
|
return null;
|
|
4041
4665
|
const pid = Number.parseInt(raw, 10);
|
|
@@ -4046,7 +4670,7 @@ function readTuiPid2(stateDir) {
|
|
|
4046
4670
|
}
|
|
4047
4671
|
function removeTuiPidFile(stateDir) {
|
|
4048
4672
|
try {
|
|
4049
|
-
|
|
4673
|
+
unlinkSync7(stateDir.tuiPidFile);
|
|
4050
4674
|
} catch {}
|
|
4051
4675
|
}
|
|
4052
4676
|
function isManagedCodexTuiProcess2(pid, proxyUrl) {
|
|
@@ -4054,7 +4678,9 @@ function isManagedCodexTuiProcess2(pid, proxyUrl) {
|
|
|
4054
4678
|
return cmd !== null && commandMatchesManagedCodexTui(cmd, proxyUrl);
|
|
4055
4679
|
}
|
|
4056
4680
|
var init_kill = __esm(() => {
|
|
4681
|
+
init_cli_invocation();
|
|
4057
4682
|
init_daemon_lifecycle();
|
|
4683
|
+
init_daemon_record();
|
|
4058
4684
|
init_pair_registry();
|
|
4059
4685
|
init_pair_resolver();
|
|
4060
4686
|
init_process_lifecycle();
|
|
@@ -4066,7 +4692,14 @@ var exports_pairs = {};
|
|
|
4066
4692
|
__export(exports_pairs, {
|
|
4067
4693
|
runPairs: () => runPairs
|
|
4068
4694
|
});
|
|
4069
|
-
import { join as
|
|
4695
|
+
import { join as join15 } from "path";
|
|
4696
|
+
function isRegistryCorruptError(error) {
|
|
4697
|
+
return error instanceof PairError && error.code === "PAIR_REGISTRY_CORRUPT";
|
|
4698
|
+
}
|
|
4699
|
+
function registryPathForNotice(base, error) {
|
|
4700
|
+
const fromDetails = error.details?.path;
|
|
4701
|
+
return typeof fromDetails === "string" && fromDetails.length > 0 ? fromDetails : join15(pairsRootDir(base), "registry.json");
|
|
4702
|
+
}
|
|
4070
4703
|
async function runPairs(args = []) {
|
|
4071
4704
|
const [command, ...rest] = args;
|
|
4072
4705
|
if (command === "rm") {
|
|
@@ -4079,7 +4712,7 @@ async function runPairs(args = []) {
|
|
|
4079
4712
|
}
|
|
4080
4713
|
if (command && command !== "list" && command !== "--json" && command !== "--threads") {
|
|
4081
4714
|
console.error(`Unknown pairs command: ${command}`);
|
|
4082
|
-
console.error("Usage: abg pairs [--json] [--threads] | abg pairs rm <name|id> | abg pairs prune [--
|
|
4715
|
+
console.error("Usage: abg pairs [--json] [--threads] | abg pairs rm <name|id> | abg pairs prune [--apply]");
|
|
4083
4716
|
process.exit(1);
|
|
4084
4717
|
}
|
|
4085
4718
|
const json = command === "--json" || rest.includes("--json");
|
|
@@ -4134,16 +4767,40 @@ async function runRemove(args) {
|
|
|
4134
4767
|
}
|
|
4135
4768
|
}
|
|
4136
4769
|
async function runPrune(args) {
|
|
4137
|
-
const
|
|
4770
|
+
const apply = args.includes("--apply");
|
|
4138
4771
|
for (const arg of args) {
|
|
4139
|
-
if (arg !== "--dry-run") {
|
|
4772
|
+
if (arg !== "--apply" && arg !== "--dry-run") {
|
|
4140
4773
|
console.error(`Unknown prune argument: ${arg}`);
|
|
4141
|
-
console.error("Usage: abg pairs prune [--
|
|
4774
|
+
console.error("Usage: abg pairs prune [--apply]");
|
|
4142
4775
|
process.exit(1);
|
|
4143
4776
|
}
|
|
4144
4777
|
}
|
|
4778
|
+
if (apply && args.includes("--dry-run")) {
|
|
4779
|
+
console.error("Error: --apply and --dry-run are mutually exclusive.");
|
|
4780
|
+
console.error("Usage: abg pairs prune [--apply]");
|
|
4781
|
+
process.exit(1);
|
|
4782
|
+
}
|
|
4145
4783
|
const base = computeBaseDir();
|
|
4146
|
-
|
|
4784
|
+
let reclaimable;
|
|
4785
|
+
let registryReadable = true;
|
|
4786
|
+
try {
|
|
4787
|
+
reclaimable = classifyReclaimableEntries(base);
|
|
4788
|
+
} catch (error) {
|
|
4789
|
+
if (!isRegistryCorruptError(error))
|
|
4790
|
+
throw error;
|
|
4791
|
+
registryReadable = false;
|
|
4792
|
+
reclaimable = [];
|
|
4793
|
+
console.error(`\u26A0\uFE0F pair registry \u4E0D\u53EF\u8BFB\uFF08${error.message}\uFF09\u2014\u2014` + `\u4F4D\u4E8E ${registryPathForNotice(base, error)}\u3002` + `\u8DF3\u8FC7 registry \u6761\u76EE\u56DE\u6536\uFF0C\u964D\u7EA7\u4E3A\u78C1\u76D8\u626B\u63CF\u6E05\u7406\u5B64\u513F\u76EE\u5F55\uFF08\u65E0\u9700\u53EF\u89E3\u6790\u7684 registry\uFF09\u3002` + `\u4FEE\u590D\u6216\u5220\u9664\u8BE5\u6587\u4EF6\u540E\u53EF\u6062\u590D\u5B8C\u6574 prune\u3002`);
|
|
4794
|
+
process.exitCode = 2;
|
|
4795
|
+
}
|
|
4796
|
+
const reclaimableIds = new Set(reclaimable.map((c) => c.entry.pairId.toLowerCase()));
|
|
4797
|
+
const dirResult = pruneOrphanDirs(base, apply, reclaimableIds, registryReadable);
|
|
4798
|
+
const entryResult = await pruneReclaimableEntries(reclaimable, base, apply);
|
|
4799
|
+
const resolvedDirResult = await dirResult;
|
|
4800
|
+
printPruneSummary(resolvedDirResult, entryResult, apply);
|
|
4801
|
+
}
|
|
4802
|
+
async function pruneOrphanDirs(base, apply, reclaimableIds, registryReadable) {
|
|
4803
|
+
const registered = registryReadable ? new Set(listPairs(base).map((pair) => pair.pairId.toLowerCase())) : new Set;
|
|
4147
4804
|
const removed = [];
|
|
4148
4805
|
const kept = [];
|
|
4149
4806
|
for (const name of listPairDirs(base)) {
|
|
@@ -4158,6 +4815,9 @@ async function runPrune(args) {
|
|
|
4158
4815
|
kept.push({ name, reason: "directory name is not a canonical pair id" });
|
|
4159
4816
|
continue;
|
|
4160
4817
|
}
|
|
4818
|
+
if (reclaimableIds.has(id.toLowerCase())) {
|
|
4819
|
+
continue;
|
|
4820
|
+
}
|
|
4161
4821
|
if (registered.has(id.toLowerCase())) {
|
|
4162
4822
|
kept.push({ name, reason: "registered \u2014 use `abg pairs rm`" });
|
|
4163
4823
|
continue;
|
|
@@ -4166,12 +4826,12 @@ async function runPrune(args) {
|
|
|
4166
4826
|
kept.push({ name, reason: "daemon still alive" });
|
|
4167
4827
|
continue;
|
|
4168
4828
|
}
|
|
4169
|
-
if (
|
|
4829
|
+
if (!apply) {
|
|
4170
4830
|
removed.push(name);
|
|
4171
4831
|
continue;
|
|
4172
4832
|
}
|
|
4173
4833
|
try {
|
|
4174
|
-
const outcome = await removeUnregisteredPairDir(base, id);
|
|
4834
|
+
const outcome = registryReadable ? await removeUnregisteredPairDir(base, id) : await removeOrphanPairDirIgnoringRegistry(base, id);
|
|
4175
4835
|
if (outcome.removed) {
|
|
4176
4836
|
removed.push(name);
|
|
4177
4837
|
} else if (outcome.reason === "registered") {
|
|
@@ -4185,33 +4845,90 @@ async function runPrune(args) {
|
|
|
4185
4845
|
kept.push({ name, reason: `error: ${err instanceof Error ? err.message : String(err)}` });
|
|
4186
4846
|
}
|
|
4187
4847
|
}
|
|
4188
|
-
|
|
4848
|
+
return { removed, kept };
|
|
4849
|
+
}
|
|
4850
|
+
async function pruneReclaimableEntries(candidates, base, apply) {
|
|
4851
|
+
const reclaimed = [];
|
|
4852
|
+
const kept = [];
|
|
4853
|
+
for (const candidate of candidates) {
|
|
4854
|
+
const reason = describeReclaimReason(candidate);
|
|
4855
|
+
if (!apply) {
|
|
4856
|
+
reclaimed.push({ pairId: candidate.entry.pairId, slot: candidate.entry.slot, reason });
|
|
4857
|
+
continue;
|
|
4858
|
+
}
|
|
4859
|
+
try {
|
|
4860
|
+
const res = await removePairEntryAndDir(base, candidate.entry.pairId);
|
|
4861
|
+
if (res.keptLive) {
|
|
4862
|
+
kept.push({ pairId: candidate.entry.pairId, reason: "became live during prune" });
|
|
4863
|
+
} else {
|
|
4864
|
+
reclaimed.push({ pairId: candidate.entry.pairId, slot: candidate.entry.slot, reason });
|
|
4865
|
+
}
|
|
4866
|
+
} catch (err) {
|
|
4867
|
+
kept.push({
|
|
4868
|
+
pairId: candidate.entry.pairId,
|
|
4869
|
+
reason: `error: ${err instanceof Error ? err.message : String(err)}`
|
|
4870
|
+
});
|
|
4871
|
+
}
|
|
4872
|
+
}
|
|
4873
|
+
return { reclaimed, kept };
|
|
4189
4874
|
}
|
|
4190
|
-
function
|
|
4191
|
-
|
|
4192
|
-
|
|
4875
|
+
function describeReclaimReason(candidate) {
|
|
4876
|
+
const { signals } = candidate;
|
|
4877
|
+
const age = signals.ageMs === null ? "age?" : `age ${formatAgeDays(signals.ageMs)}`;
|
|
4878
|
+
return `cwd-gone, dead, ${age}`;
|
|
4879
|
+
}
|
|
4880
|
+
function formatAgeDays(ageMs) {
|
|
4881
|
+
const days = ageMs / (24 * 60 * 60 * 1000);
|
|
4882
|
+
return days >= 10 ? `${Math.round(days)}d` : `${days.toFixed(1)}d`;
|
|
4883
|
+
}
|
|
4884
|
+
function printPruneSummary(dirResult, entryResult, apply) {
|
|
4885
|
+
const { removed: dirsRemoved, kept: dirsKept } = dirResult;
|
|
4886
|
+
const { reclaimed: entriesReclaimed, kept: entriesKept } = entryResult;
|
|
4887
|
+
const nothingFound = dirsRemoved.length === 0 && dirsKept.length === 0 && entriesReclaimed.length === 0 && entriesKept.length === 0;
|
|
4888
|
+
if (nothingFound) {
|
|
4889
|
+
console.log("Nothing to prune: no orphan pair directories or reclaimable entries found.");
|
|
4193
4890
|
return;
|
|
4194
4891
|
}
|
|
4195
|
-
if (
|
|
4196
|
-
console.log(
|
|
4197
|
-
for (const name of
|
|
4892
|
+
if (dirsRemoved.length > 0) {
|
|
4893
|
+
console.log(apply ? "Removed orphan pair directories:" : "Would remove orphan pair directories:");
|
|
4894
|
+
for (const name of dirsRemoved)
|
|
4198
4895
|
console.log(` ${name}`);
|
|
4199
|
-
} else {
|
|
4200
|
-
console.log(dryRun ? "No orphan pair directories to remove." : "No orphan pair directories removed.");
|
|
4201
4896
|
}
|
|
4202
|
-
if (
|
|
4897
|
+
if (entriesReclaimed.length > 0) {
|
|
4898
|
+
console.log(apply ? "Reclaimed registry entries:" : "Would reclaim registry entries:");
|
|
4899
|
+
for (const { pairId, slot, reason } of entriesReclaimed) {
|
|
4900
|
+
console.log(` ${pairId} (slot ${slot}) \u2014 ${reason}`);
|
|
4901
|
+
}
|
|
4902
|
+
}
|
|
4903
|
+
if (dirsRemoved.length === 0 && entriesReclaimed.length === 0) {
|
|
4904
|
+
console.log(apply ? "Nothing was reclaimed." : "Nothing to reclaim.");
|
|
4905
|
+
}
|
|
4906
|
+
const keptLines = [
|
|
4907
|
+
...dirsKept.map(({ name, reason }) => ` ${name} (${reason})`),
|
|
4908
|
+
...entriesKept.map(({ pairId, reason }) => ` ${pairId} (${reason})`)
|
|
4909
|
+
];
|
|
4910
|
+
if (keptLines.length > 0) {
|
|
4203
4911
|
console.log("Kept:");
|
|
4204
|
-
for (const
|
|
4205
|
-
console.log(
|
|
4912
|
+
for (const line of keptLines)
|
|
4913
|
+
console.log(line);
|
|
4206
4914
|
}
|
|
4207
|
-
if (
|
|
4915
|
+
if (!apply) {
|
|
4208
4916
|
console.log(`
|
|
4209
|
-
(dry run \u2014 nothing was deleted. Re-run
|
|
4917
|
+
(dry run \u2014 nothing was deleted. Re-run with --apply to reclaim.)`);
|
|
4210
4918
|
}
|
|
4211
4919
|
}
|
|
4212
4920
|
async function collectRows() {
|
|
4213
4921
|
const base = computeBaseDir();
|
|
4214
|
-
|
|
4922
|
+
let rows;
|
|
4923
|
+
try {
|
|
4924
|
+
rows = await Promise.all(listPairs(base).map((pair) => rowForPair(base, pair)));
|
|
4925
|
+
} catch (error) {
|
|
4926
|
+
if (!isRegistryCorruptError(error))
|
|
4927
|
+
throw error;
|
|
4928
|
+
console.error(`\u26A0\uFE0F pair registry \u4E0D\u53EF\u8BFB\uFF08${error.message}\uFF09\u2014\u2014` + `\u4F4D\u4E8E ${registryPathForNotice(base, error)}\u3002` + `\u964D\u7EA7\u4E3A\u78C1\u76D8\u626B\u63CF\u5217\u51FA ${pairsRootDir(base)} \u4E0B\u7684 pair \u76EE\u5F55\uFF08slot/name/cwd \u7B49\u9700 registry \u7684\u5B57\u6BB5\u663E\u793A\u4E3A -\uFF09\u3002` + `\u4FEE\u590D\u6216\u5220\u9664\u8BE5\u6587\u4EF6\u540E\u53EF\u6062\u590D\u5B8C\u6574\u5217\u8868\uFF1B\u7528 \`abg pairs prune\` \u6E05\u7406\u5B64\u513F\u76EE\u5F55\u3002`);
|
|
4929
|
+
process.exitCode = 2;
|
|
4930
|
+
rows = await collectDiskScanRows(base);
|
|
4931
|
+
}
|
|
4215
4932
|
const legacy = detectLegacyRootDaemon(base);
|
|
4216
4933
|
if (legacy) {
|
|
4217
4934
|
rows.push({
|
|
@@ -4232,15 +4949,15 @@ async function collectRows() {
|
|
|
4232
4949
|
}
|
|
4233
4950
|
async function rowForPair(base, pair) {
|
|
4234
4951
|
const ports = portsForEntry(pair);
|
|
4235
|
-
const stateDir = new StateDirResolver(
|
|
4952
|
+
const stateDir = new StateDirResolver(join15(base, "pairs", pair.pairId));
|
|
4236
4953
|
const lifecycle = new DaemonLifecycle({
|
|
4237
4954
|
stateDir,
|
|
4238
4955
|
controlPort: ports.controlPort,
|
|
4239
4956
|
log: () => {}
|
|
4240
4957
|
});
|
|
4241
|
-
const [running,
|
|
4958
|
+
const [running, record] = await Promise.all([
|
|
4242
4959
|
lifecycle.isHealthy(),
|
|
4243
|
-
Promise.resolve(lifecycle.
|
|
4960
|
+
Promise.resolve(lifecycle.readDaemonRecord())
|
|
4244
4961
|
]);
|
|
4245
4962
|
const thread = readRawCurrentThread(stateDir);
|
|
4246
4963
|
return {
|
|
@@ -4251,7 +4968,42 @@ async function rowForPair(base, pair) {
|
|
|
4251
4968
|
source: pair.source,
|
|
4252
4969
|
cwd: pair.cwd,
|
|
4253
4970
|
running,
|
|
4254
|
-
pid: typeof
|
|
4971
|
+
pid: typeof record?.pid === "number" ? record.pid : null,
|
|
4972
|
+
threadId: thread?.threadId ?? null,
|
|
4973
|
+
threadStatus: thread?.status ?? null,
|
|
4974
|
+
threadUpdatedAt: thread?.updatedAt ?? null
|
|
4975
|
+
};
|
|
4976
|
+
}
|
|
4977
|
+
async function collectDiskScanRows(base) {
|
|
4978
|
+
const names = listPairDirsSafe2(base);
|
|
4979
|
+
return Promise.all(names.map((name) => rowForDiskScanDir(base, name)));
|
|
4980
|
+
}
|
|
4981
|
+
function listPairDirsSafe2(base) {
|
|
4982
|
+
try {
|
|
4983
|
+
return listPairDirs(base);
|
|
4984
|
+
} catch {
|
|
4985
|
+
return [];
|
|
4986
|
+
}
|
|
4987
|
+
}
|
|
4988
|
+
async function rowForDiskScanDir(base, dirName) {
|
|
4989
|
+
const stateDir = new StateDirResolver(join15(base, "pairs", dirName));
|
|
4990
|
+
const record = new DaemonLifecycle({ stateDir, controlPort: 0, log: () => {} }).readDaemonRecord();
|
|
4991
|
+
const ports = {
|
|
4992
|
+
appPort: record?.ports?.appPort ?? 0,
|
|
4993
|
+
proxyPort: record?.ports?.proxyPort ?? 0,
|
|
4994
|
+
controlPort: record?.ports?.controlPort ?? 0
|
|
4995
|
+
};
|
|
4996
|
+
const running = ports.controlPort > 0 ? await new DaemonLifecycle({ stateDir, controlPort: ports.controlPort, log: () => {} }).isHealthy() : false;
|
|
4997
|
+
const thread = readRawCurrentThread(stateDir);
|
|
4998
|
+
return {
|
|
4999
|
+
pairId: dirName,
|
|
5000
|
+
name: "-",
|
|
5001
|
+
slot: null,
|
|
5002
|
+
ports,
|
|
5003
|
+
source: "cwd",
|
|
5004
|
+
cwd: "-",
|
|
5005
|
+
running,
|
|
5006
|
+
pid: typeof record?.pid === "number" ? record.pid : null,
|
|
4255
5007
|
threadId: thread?.threadId ?? null,
|
|
4256
5008
|
threadStatus: thread?.status ?? null,
|
|
4257
5009
|
threadUpdatedAt: thread?.updatedAt ?? null
|
|
@@ -4339,20 +5091,20 @@ var DAEMON_STATUS_FETCH_TIMEOUT_MS = 1000;
|
|
|
4339
5091
|
import { Database } from "bun:sqlite";
|
|
4340
5092
|
import {
|
|
4341
5093
|
copyFileSync,
|
|
4342
|
-
existsSync as
|
|
5094
|
+
existsSync as existsSync14,
|
|
4343
5095
|
mkdirSync as mkdirSync7,
|
|
4344
|
-
readFileSync as
|
|
5096
|
+
readFileSync as readFileSync12
|
|
4345
5097
|
} from "fs";
|
|
4346
|
-
import { dirname as dirname5, join as
|
|
5098
|
+
import { dirname as dirname5, join as join16 } from "path";
|
|
4347
5099
|
function isKickoffText(text) {
|
|
4348
5100
|
if (!text)
|
|
4349
5101
|
return false;
|
|
4350
5102
|
return KICKOFF_FINGERPRINTS.some((fingerprint) => text.includes(fingerprint));
|
|
4351
5103
|
}
|
|
4352
5104
|
function extractFirstRealUserMessage(rolloutPath) {
|
|
4353
|
-
if (!
|
|
5105
|
+
if (!existsSync14(rolloutPath))
|
|
4354
5106
|
return null;
|
|
4355
|
-
const raw =
|
|
5107
|
+
const raw = readFileSync12(rolloutPath, "utf-8");
|
|
4356
5108
|
for (const line of raw.split(`
|
|
4357
5109
|
`)) {
|
|
4358
5110
|
if (!line.trim())
|
|
@@ -4374,8 +5126,8 @@ function extractFirstRealUserMessage(rolloutPath) {
|
|
|
4374
5126
|
}
|
|
4375
5127
|
function scanResumePollution(options = {}) {
|
|
4376
5128
|
const codexHome2 = options.codexHome ?? codexHome();
|
|
4377
|
-
const dbPath = options.dbPath ??
|
|
4378
|
-
if (!
|
|
5129
|
+
const dbPath = options.dbPath ?? join16(codexHome2, "state_5.sqlite");
|
|
5130
|
+
if (!existsSync14(dbPath)) {
|
|
4379
5131
|
return { codexHome: codexHome2, dbPath, scanned: 0, candidates: [], applied: 0, renamed: 0, deleted: 0 };
|
|
4380
5132
|
}
|
|
4381
5133
|
const db = options.apply ? new Database(dbPath) : new Database(dbPath, { readonly: true });
|
|
@@ -4468,12 +5220,12 @@ function scanResumePollution(options = {}) {
|
|
|
4468
5220
|
}
|
|
4469
5221
|
function backupCodexStateFiles(dbPath, now = new Date().toISOString()) {
|
|
4470
5222
|
const safeStamp = now.replace(/[:.]/g, "-");
|
|
4471
|
-
const base =
|
|
5223
|
+
const base = join16(dirname5(dbPath), "agentbridge-backups", `resume-pollution-${safeStamp}`);
|
|
4472
5224
|
mkdirSync7(base, { recursive: true });
|
|
4473
5225
|
for (const path of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
4474
|
-
if (!
|
|
5226
|
+
if (!existsSync14(path))
|
|
4475
5227
|
continue;
|
|
4476
|
-
const target =
|
|
5228
|
+
const target = join16(base, path.split("/").pop());
|
|
4477
5229
|
mkdirSync7(dirname5(target), { recursive: true });
|
|
4478
5230
|
copyFileSync(path, target);
|
|
4479
5231
|
}
|
|
@@ -4519,11 +5271,12 @@ var init_resume_pollution = __esm(() => {
|
|
|
4519
5271
|
var exports_doctor = {};
|
|
4520
5272
|
__export(exports_doctor, {
|
|
4521
5273
|
runDoctor: () => runDoctor,
|
|
4522
|
-
formatDoctorReport: () => formatDoctorReport
|
|
5274
|
+
formatDoctorReport: () => formatDoctorReport,
|
|
5275
|
+
evaluateArtifactAlignment: () => evaluateArtifactAlignment,
|
|
5276
|
+
describeBuildDrift: () => describeBuildDrift
|
|
4523
5277
|
});
|
|
4524
|
-
import { existsSync as
|
|
4525
|
-
import {
|
|
4526
|
-
import { join as join16 } from "path";
|
|
5278
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13, readdirSync as readdirSync6, realpathSync as realpathSync3, statSync as statSync7 } from "fs";
|
|
5279
|
+
import { join as join17 } from "path";
|
|
4527
5280
|
async function runDoctor(args = []) {
|
|
4528
5281
|
if (args[0] === "resume-pollution") {
|
|
4529
5282
|
runResumePollution(args.slice(1));
|
|
@@ -4599,6 +5352,7 @@ function runResumePollution(args) {
|
|
|
4599
5352
|
}
|
|
4600
5353
|
async function buildDoctorReport(pair, registered) {
|
|
4601
5354
|
const cwd = process.cwd();
|
|
5355
|
+
const cli = cliInvocationName();
|
|
4602
5356
|
const env = inspectAgentBridgeEnv({ cwd, env: process.env });
|
|
4603
5357
|
const [health, ready] = registered ? await Promise.all([
|
|
4604
5358
|
fetchDaemonStatus(pair.ports.controlPort, "/healthz"),
|
|
@@ -4618,19 +5372,20 @@ async function buildDoctorReport(pair, registered) {
|
|
|
4618
5372
|
name: "pair registration",
|
|
4619
5373
|
status: registered ? "ok" : "warn",
|
|
4620
5374
|
detail: registered ? pair.manual ? "manual mode (explicit env)" : `registered as ${pair.pairId}` : `not registered yet \u2014 would be ${pair.pairId} (created on first launch)`,
|
|
4621
|
-
hint: registered ? undefined :
|
|
5375
|
+
hint: registered ? undefined : `\u8BE5\u76EE\u5F55\u8FD8\u6CA1\u6709\u6CE8\u518C\u8FC7 pair\uFF1A\u8FD0\u884C \`${cli} claude\` \u5373\u4F1A\u521B\u5EFA\u3002\u4EE5\u4E0B\u68C0\u67E5\u6309\u672A\u542F\u52A8\u72B6\u6001\u89E3\u8BFB\u3002`
|
|
4622
5376
|
});
|
|
4623
5377
|
checks.push({
|
|
4624
5378
|
name: "env",
|
|
4625
5379
|
status: env.ok ? "ok" : "fail",
|
|
4626
5380
|
detail: env.ok ? "AgentBridge env matches cwd" : env.reasons.join("; "),
|
|
4627
|
-
hint: env.ok ? undefined :
|
|
5381
|
+
hint: env.ok ? undefined : `\u73AF\u5883\u53D8\u91CF\u4E0E\u5F53\u524D\u76EE\u5F55\u4E0D\u5339\u914D\uFF1A\u8BF7\u5728\u6B63\u786E\u7684\u9879\u76EE\u76EE\u5F55\u91CC\u91CD\u65B0\u8FD0\u884C \`${cli} claude\`\uFF0C\u4E0D\u8981\u590D\u7528\u5176\u4ED6\u76EE\u5F55\u7684\u4F1A\u8BDD\u73AF\u5883\u3002`
|
|
4628
5382
|
});
|
|
5383
|
+
checks.push(configParseabilityCheck(cwd, cli));
|
|
4629
5384
|
checks.push({
|
|
4630
5385
|
name: "daemon health",
|
|
4631
5386
|
status: health ? "ok" : "warn",
|
|
4632
5387
|
detail: health ? `healthz reachable pid=${health.pid}` : registered ? `no daemon reachable on :${pair.ports.controlPort}` : "n/a \u2014 pair not registered",
|
|
4633
|
-
hint: health ? undefined :
|
|
5388
|
+
hint: health ? undefined : `daemon \u672A\u8FD0\u884C\u3002\u8FD0\u884C \`${cli} claude\`\uFF08\u6216 \`${cli} codex\`\uFF09\u4F1A\u81EA\u52A8\u542F\u52A8\u5B83\u3002`
|
|
4634
5389
|
});
|
|
4635
5390
|
checks.push({
|
|
4636
5391
|
name: "daemon readiness",
|
|
@@ -4638,18 +5393,26 @@ async function buildDoctorReport(pair, registered) {
|
|
|
4638
5393
|
detail: ready ? `ready thread=${ready.threadId ?? "none"}` : health ? "readyz is not OK" : "n/a \u2014 daemon not running",
|
|
4639
5394
|
hint: !ready && health ? "daemon \u5728\u8FD0\u884C\u4F46 codex app-server \u5C1A\u672A\u5C31\u7EEA\uFF1B\u7A0D\u5019\u7247\u523B\u91CD\u8BD5\uFF0C\u6301\u7EED\u4E0D\u5C31\u7EEA\u8BF7\u67E5\u770B\u4E0B\u65B9 daemon log\u3002" : undefined
|
|
4640
5395
|
});
|
|
5396
|
+
const appServerInfo = health?.appServerInfo ?? null;
|
|
5397
|
+
checks.push({
|
|
5398
|
+
name: "codex app-server",
|
|
5399
|
+
status: health ? "ok" : "skip",
|
|
5400
|
+
detail: !health ? "n/a \u2014 daemon not running" : appServerInfo ? `version=${appServerInfo.version ?? "unknown"}` + (appServerInfo.platformOs ? ` platform=${appServerInfo.platformOs}` : "") : "not captured yet \u2014 connect Codex (initialize handshake) to populate",
|
|
5401
|
+
hint: health && appServerInfo && appServerInfo.version === null ? "app-server \u672A\u8FD4\u56DE\u53EF\u89E3\u6790\u7684\u7248\u672C\u53F7\uFF08userAgent \u5F02\u5E38\uFF09\u3002\u82E5\u521A\u5347\u7EA7\u8FC7 Codex\uFF0C\u8BF7\u6838\u5BF9 codex-adapter \u7684 version-coupling checklist\u3002" : undefined
|
|
5402
|
+
});
|
|
5403
|
+
const drift = buildDrift === true ? describeBuildDrift(health?.build, BUILD_INFO, cli) : null;
|
|
4641
5404
|
checks.push({
|
|
4642
5405
|
name: "build drift",
|
|
4643
5406
|
status: buildDrift === false ? "ok" : buildDrift === true ? "fail" : "skip",
|
|
4644
|
-
detail: buildDrift === false ? `runtime matches launcher ${formatBuildInfo(BUILD_INFO)}` :
|
|
4645
|
-
hint:
|
|
5407
|
+
detail: buildDrift === false ? `runtime matches launcher ${formatBuildInfo(BUILD_INFO)}` : drift ? drift.detail : launcherStamped ? "n/a \u2014 daemon not running" : "n/a \u2014 launcher running from source (unstamped)",
|
|
5408
|
+
hint: drift?.hint
|
|
4646
5409
|
});
|
|
4647
5410
|
checks.push(artifactAlignmentCheck());
|
|
4648
5411
|
checks.push({
|
|
4649
5412
|
name: "current thread",
|
|
4650
5413
|
status: usableThread ? "ok" : rawThread ? "warn" : registered ? "warn" : "skip",
|
|
4651
5414
|
detail: usableThread ? `current=${usableThread.threadId}` : rawThread ? rawThread.status === "current" ? `stored ${rawThread.threadId} has no rollout file yet` : `stored ${rawThread.threadId} is still ${rawThread.status} (no first response yet)` : registered ? "no current-thread.json for this pair" : "n/a \u2014 pair not registered",
|
|
4652
|
-
hint: usableThread ? undefined : rawThread ? "\u901A\u5E38\u65E0\u5BB3\uFF1A\u7EBF\u7A0B\u8FD8\u6CA1\u6709\u4EA7\u751F\u9996\u6761\u56DE\u5E94\u3001\u6216 rollout \u6587\u4EF6\u5C1A\u672A\u843D\u76D8\u3002" +
|
|
5415
|
+
hint: usableThread ? undefined : rawThread ? "\u901A\u5E38\u65E0\u5BB3\uFF1A\u7EBF\u7A0B\u8FD8\u6CA1\u6709\u4EA7\u751F\u9996\u6761\u56DE\u5E94\u3001\u6216 rollout \u6587\u4EF6\u5C1A\u672A\u843D\u76D8\u3002" + `\u4EC5\u5F53 \`${cli} codex\`\uFF08resume\uFF09\u5931\u8D25\u65F6\u624D\u9700\u8981\u5904\u7406\uFF1A\u7528 \`${cli} codex --new\` \u5F00\u65B0\u7EBF\u7A0B\u3002` : registered ? "\u5C1A\u65E0\u7EBF\u7A0B\u8BB0\u5F55\uFF1A\u8FDE\u63A5 Codex \u540E\u5EFA\u7ACB\u9996\u4E2A\u7EBF\u7A0B\u65F6\u4F1A\u81EA\u52A8\u5199\u5165\uFF0C\u65E0\u9700\u5904\u7406\u3002" : undefined
|
|
4653
5416
|
});
|
|
4654
5417
|
const pairProxyUrl = `ws://127.0.0.1:${pair.ports.proxyPort}`;
|
|
4655
5418
|
const managedTuis = listManagedCodexTuiProcesses();
|
|
@@ -4666,19 +5429,19 @@ async function buildDoctorReport(pair, registered) {
|
|
|
4666
5429
|
name: "codex tui (this pair)",
|
|
4667
5430
|
status: attachedHere.length > 0 ? "ok" : "warn",
|
|
4668
5431
|
detail: attachedHere.length > 0 ? `${attachedHere.length} attached to ${pairProxyUrl} (pid ${attachedHere.map((t) => t.pid).join(", ")})` : `no managed Codex TUI attached to this pair's proxy ${pairProxyUrl}`,
|
|
4669
|
-
hint: attachedHere.length > 0 ? undefined :
|
|
5432
|
+
hint: attachedHere.length > 0 ? undefined : `\u53E6\u5F00\u4E00\u4E2A\u7EC8\u7AEF\u3001\u5728\u540C\u4E00\u76EE\u5F55\u8FD0\u884C \`${cli} codex\` \u8FDE\u63A5\u672C pair\u3002`
|
|
4670
5433
|
});
|
|
4671
5434
|
checks.push({
|
|
4672
5435
|
name: "codex tui (other pairs)",
|
|
4673
5436
|
status: attachedElsewhere.length > 0 ? "warn" : "ok",
|
|
4674
5437
|
detail: attachedElsewhere.length > 0 ? `${attachedElsewhere.length} managed Codex TUI(s) attached to a DIFFERENT pair/proxy \u2014 likely started from another cwd, will not bridge here: ` + attachedElsewhere.map((t) => `pid ${t.pid}\u2192${t.remoteUrl ?? "?"}`).join(", ") : "no managed Codex TUI attached to another pair",
|
|
4675
|
-
hint: attachedElsewhere.length > 0 ?
|
|
5438
|
+
hint: attachedElsewhere.length > 0 ? `\u8FD9\u4E9B TUI \u5C5E\u4E8E\u5176\u4ED6\u76EE\u5F55\u7684 pair\uFF0C\u4E0D\u5F71\u54CD\u672C pair\uFF1B\u5B83\u4EEC\u4E0D\u4F1A\u6865\u63A5\u5230\u8FD9\u91CC\u3002\u5982\u4E0D\u518D\u9700\u8981\uFF0C\u53BB\u5BF9\u5E94\u76EE\u5F55\u8FD0\u884C \`${cli} kill\`\u3002` : undefined
|
|
4676
5439
|
});
|
|
4677
5440
|
for (const [name, path] of [
|
|
4678
5441
|
["daemon log", pair.stateDir.logFile],
|
|
4679
5442
|
["codex wrapper log", pair.stateDir.codexWrapperLogFile]
|
|
4680
5443
|
]) {
|
|
4681
|
-
checks.push(logCheck(name, path));
|
|
5444
|
+
checks.push(logCheck(name, path, cli));
|
|
4682
5445
|
}
|
|
4683
5446
|
return {
|
|
4684
5447
|
cwd,
|
|
@@ -4699,62 +5462,126 @@ async function buildDoctorReport(pair, registered) {
|
|
|
4699
5462
|
checks
|
|
4700
5463
|
};
|
|
4701
5464
|
}
|
|
5465
|
+
function describeBuildDrift(runtime, launcher, cli = "abg") {
|
|
5466
|
+
const basis = runtimeContractComparisonBasis(runtime, launcher);
|
|
5467
|
+
const baseDetail = `runtime ${formatBuildInfo(runtime)} differs from launcher ${formatBuildInfo(launcher)}`;
|
|
5468
|
+
const baseHint = "daemon \u8FD0\u884C\u7684\u662F\u65E7\u6784\u5EFA\uFF08\u901A\u5E38\u7531\u65E7\u7248 CLI \u6216\u672A\u91CD\u5F00\u7684 Claude Code \u7A97\u53E3\u542F\u52A8\uFF09\u3002" + `\u6CA1\u6709\u8FDB\u884C\u4E2D\u7684 Codex \u4F1A\u8BDD\u65F6\uFF0C\u8FD0\u884C \`${cli} kill\` \u540E\u91CD\u65B0 \`${cli} claude\` \u5373\u53EF\u5BF9\u9F50\uFF1B` + "\u6709\u6D3B\u8DC3\u4F1A\u8BDD\u5219\u7B49\u6536\u5C3E\u540E\u518D\u91CD\u542F\u2014\u2014\u7248\u672C\u5DEE\u5F02\u4E0D\u4F1A\u5F3A\u6740\u6D3B\u8DC3\u4F1A\u8BDD\uFF0C\u53EF\u4EE5\u7EE7\u7EED\u7528\u3002";
|
|
5469
|
+
if (basis === "codeHash") {
|
|
5470
|
+
return { detail: `${baseDetail} [compared by codeHash \u2014 real code difference]`, hint: baseHint };
|
|
5471
|
+
}
|
|
5472
|
+
return {
|
|
5473
|
+
detail: `${baseDetail} [compared by commit stamp \u2014 legacy build without codeHash]`,
|
|
5474
|
+
hint: baseHint + "\uFF08\u6CE8\u610F\uFF1A\u672C\u5224\u5B9A\u57FA\u4E8E commit stamp \u53E3\u5F84\u2014\u2014\u6709\u4E00\u4FA7\u662F\u7F3A codeHash \u7684\u65E7\u6784\u5EFA\uFF1Bsquash \u5408\u5E76\u4F1A\u8BA9 stamp \u6EDE\u540E\u4E00\u683C\uFF0C" + "\u6E90\u7801\u4E00\u81F4\u65F6\u4E5F\u53EF\u80FD\u8BEF\u62A5\u3002\u5347\u7EA7\u4E24\u7AEF\u5230\u5E26 codeHash \u7684\u6784\u5EFA\u540E\u5C06\u6309\u4EE3\u7801\u5185\u5BB9\u5224\u5B9A\u3002\uFF09"
|
|
5475
|
+
};
|
|
5476
|
+
}
|
|
5477
|
+
function isUsableCodeHash(hash) {
|
|
5478
|
+
return typeof hash === "string" && hash.length > 0 && hash !== "source";
|
|
5479
|
+
}
|
|
5480
|
+
function evaluateArtifactAlignment(stamps) {
|
|
5481
|
+
if (stamps.length < 2) {
|
|
5482
|
+
return {
|
|
5483
|
+
name: "artifact alignment",
|
|
5484
|
+
status: "skip",
|
|
5485
|
+
detail: "n/a \u2014 fewer than two stamped artifacts found"
|
|
5486
|
+
};
|
|
5487
|
+
}
|
|
5488
|
+
if (stamps.every((stamp) => isUsableCodeHash(stamp.codeHash))) {
|
|
5489
|
+
const rendered2 = stamps.map((stamp) => `${stamp.label}=${stamp.codeHash}`).join(", ");
|
|
5490
|
+
if (new Set(stamps.map((stamp) => stamp.codeHash)).size === 1) {
|
|
5491
|
+
return { name: "artifact alignment", status: "ok", detail: `codeHash basis: ${rendered2}` };
|
|
5492
|
+
}
|
|
5493
|
+
return {
|
|
5494
|
+
name: "artifact alignment",
|
|
5495
|
+
status: "fail",
|
|
5496
|
+
detail: `deployed artifacts contain DIFFERENT code (codeHash basis): ${rendered2}`,
|
|
5497
|
+
hint: "\u90E8\u7F72\u7269\u4EE3\u7801\u5206\u88C2\u4F1A\u5BFC\u81F4\u4E92\u76F8\u66FF\u6362 daemon\uFF08\u6740\u6389\u6D3B\u4F1A\u8BDD\uFF09\u3002\u5728\u4ED3\u5E93\u76EE\u5F55\u8FD0\u884C `bun run install:global` " + "\u4E00\u6B21\u6027\u5BF9\u9F50\u5168\u5C40 CLI \u4E0E\u63D2\u4EF6\u7F13\u5B58\uFF0C\u7136\u540E\u5173\u95ED\u5E76\u91CD\u5F00\u4ECD\u5728\u4F7F\u7528\u65E7\u63D2\u4EF6\u7684 Claude Code \u7A97\u53E3\u3002"
|
|
5498
|
+
};
|
|
5499
|
+
}
|
|
5500
|
+
const rendered = stamps.map((stamp) => `${stamp.label}=${stamp.commit}`).join(", ");
|
|
5501
|
+
if (new Set(stamps.map((stamp) => stamp.commit)).size === 1) {
|
|
5502
|
+
return {
|
|
5503
|
+
name: "artifact alignment",
|
|
5504
|
+
status: "ok",
|
|
5505
|
+
detail: `legacy commit-stamp basis: ${rendered}`
|
|
5506
|
+
};
|
|
5507
|
+
}
|
|
5508
|
+
return {
|
|
5509
|
+
name: "artifact alignment",
|
|
5510
|
+
status: "fail",
|
|
5511
|
+
detail: `deployed artifacts are at DIFFERENT builds (legacy commit-stamp basis): ${rendered}`,
|
|
5512
|
+
hint: "\uFF08stamp \u53E3\u5F84\uFF1A\u5B58\u5728\u7F3A codeHash \u7684\u65E7\u90E8\u7F72\u7269\uFF0C\u4E14 squash \u5408\u5E76\u4F1A\u8BA9 stamp \u6EDE\u540E\u4E00\u683C\uFF0C\u6E90\u7801\u4E00\u81F4\u65F6\u4E5F\u53EF\u80FD\u8BEF\u62A5\u3002\uFF09" + "\u90E8\u7F72\u7269\u7248\u672C\u5206\u88C2\u4F1A\u5BFC\u81F4\u4E92\u76F8\u66FF\u6362 daemon\uFF08\u6740\u6389\u6D3B\u4F1A\u8BDD\uFF09\u3002\u5728\u4ED3\u5E93\u76EE\u5F55\u8FD0\u884C `bun run install:global` " + "\u4E00\u6B21\u6027\u5BF9\u9F50\u5168\u5C40 CLI \u4E0E\u63D2\u4EF6\u7F13\u5B58\u5E76\u5347\u7EA7\u5230\u5E26 codeHash \u7684\u6784\u5EFA\uFF0C\u7136\u540E\u5173\u95ED\u5E76\u91CD\u5F00\u4ECD\u5728\u4F7F\u7528\u65E7\u63D2\u4EF6\u7684 Claude Code \u7A97\u53E3\uFF1B" + "\u5BF9\u9F50\u540E\u6B64\u68C0\u67E5\u5C06\u6309\u4EE3\u7801\u5185\u5BB9\uFF08codeHash\uFF09\u5224\u5B9A\uFF0Cstamp \u6EDE\u540E\u4E0D\u518D\u8BEF\u62A5\u3002"
|
|
5513
|
+
};
|
|
5514
|
+
}
|
|
4702
5515
|
function artifactAlignmentCheck() {
|
|
4703
5516
|
const stamps = [];
|
|
4704
5517
|
if (BUILD_INFO.commit !== "source") {
|
|
4705
|
-
stamps.push({
|
|
5518
|
+
stamps.push({
|
|
5519
|
+
label: `launcher(${BUILD_INFO.bundle})`,
|
|
5520
|
+
commit: BUILD_INFO.commit,
|
|
5521
|
+
codeHash: hasValidCodeHash(BUILD_INFO) ? BUILD_INFO.codeHash ?? null : null
|
|
5522
|
+
});
|
|
4706
5523
|
}
|
|
4707
5524
|
const bin = Bun.which("agentbridge") ?? Bun.which("abg");
|
|
4708
5525
|
if (bin) {
|
|
4709
5526
|
try {
|
|
4710
|
-
const
|
|
4711
|
-
if (
|
|
4712
|
-
stamps.push({ label: "global-cli",
|
|
5527
|
+
const stamp = extractBundleStamp(realpathSync3(bin));
|
|
5528
|
+
if (stamp)
|
|
5529
|
+
stamps.push({ label: "global-cli", ...stamp });
|
|
4713
5530
|
} catch {}
|
|
4714
5531
|
}
|
|
4715
|
-
const cacheRoot =
|
|
5532
|
+
const cacheRoot = pluginCacheRoot();
|
|
4716
5533
|
try {
|
|
4717
|
-
for (const version of
|
|
4718
|
-
const
|
|
4719
|
-
if (
|
|
4720
|
-
stamps.push({ label: `plugin-cache@${version}`,
|
|
5534
|
+
for (const version of readdirSync6(cacheRoot)) {
|
|
5535
|
+
const stamp = extractBundleStamp(join17(cacheRoot, version, "server", "daemon.js"));
|
|
5536
|
+
if (stamp)
|
|
5537
|
+
stamps.push({ label: `plugin-cache@${version}`, ...stamp });
|
|
4721
5538
|
}
|
|
4722
5539
|
} catch {}
|
|
4723
|
-
const repoBundle =
|
|
4724
|
-
if (
|
|
4725
|
-
const
|
|
4726
|
-
if (
|
|
4727
|
-
stamps.push({ label: "repo-bundle",
|
|
5540
|
+
const repoBundle = join17(process.cwd(), "plugins", "agentbridge", "server", "daemon.js");
|
|
5541
|
+
if (existsSync15(repoBundle)) {
|
|
5542
|
+
const stamp = extractBundleStamp(repoBundle);
|
|
5543
|
+
if (stamp)
|
|
5544
|
+
stamps.push({ label: "repo-bundle", ...stamp });
|
|
4728
5545
|
}
|
|
4729
|
-
|
|
5546
|
+
return evaluateArtifactAlignment(stamps);
|
|
5547
|
+
}
|
|
5548
|
+
function extractBundleStamp(path) {
|
|
5549
|
+
try {
|
|
5550
|
+
const text = readFileSync13(path, "utf-8");
|
|
5551
|
+
const commit = text.match(/commit:\s*defineString\("([^"]+)",\s*"source"\)/)?.[1] ?? null;
|
|
5552
|
+
if (!commit)
|
|
5553
|
+
return null;
|
|
5554
|
+
const codeHash = text.match(/codeHash:\s*defineString\("([^"]+)",\s*"source"\)/)?.[1] ?? null;
|
|
5555
|
+
return { commit, codeHash };
|
|
5556
|
+
} catch {
|
|
5557
|
+
return null;
|
|
5558
|
+
}
|
|
5559
|
+
}
|
|
5560
|
+
function configParseabilityCheck(cwd, cli) {
|
|
5561
|
+
const desc = new ConfigService(cwd).describeConfig();
|
|
5562
|
+
if (desc.state === "absent") {
|
|
4730
5563
|
return {
|
|
4731
|
-
name: "
|
|
4732
|
-
status: "
|
|
4733
|
-
detail:
|
|
5564
|
+
name: "config.json",
|
|
5565
|
+
status: "ok",
|
|
5566
|
+
detail: `no project config at ${desc.path} \u2014 built-in defaults in effect`
|
|
4734
5567
|
};
|
|
4735
5568
|
}
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
5569
|
+
if (desc.state === "corrupt") {
|
|
5570
|
+
return {
|
|
5571
|
+
name: "config.json",
|
|
5572
|
+
status: "warn",
|
|
5573
|
+
detail: `unparseable at ${desc.path} (${desc.reason}) \u2014 custom thresholds NOT in effect, using defaults`,
|
|
5574
|
+
hint: "config.json \u635F\u574F\u6216\u5B57\u6BB5\u7C7B\u578B\u9519\u8BEF\uFF1Abridge \u5DF2\u56DE\u9000\u5230\u9ED8\u8BA4\u9608\u503C\uFF0C\u4F60\u7684\u81EA\u5B9A\u4E49 budget/idle \u8BBE\u7F6E\u672A\u751F\u6548\u3002" + `\u4FEE\u6B63\u8BE5\u6587\u4EF6\u7684 JSON \u8BED\u6CD5/\u5B57\u6BB5\u7C7B\u578B\u540E\u91CD\u542F \`${cli} claude\` \u5373\u53EF\u91CD\u65B0\u751F\u6548\u3002`
|
|
5575
|
+
};
|
|
4740
5576
|
}
|
|
4741
5577
|
return {
|
|
4742
|
-
name: "
|
|
4743
|
-
status: "
|
|
4744
|
-
detail:
|
|
4745
|
-
hint: "\u90E8\u7F72\u7269\u7248\u672C\u5206\u88C2\u4F1A\u5BFC\u81F4\u4E92\u76F8\u66FF\u6362 daemon\uFF08\u6740\u6389\u6D3B\u4F1A\u8BDD\uFF09\u3002\u5728\u4ED3\u5E93\u76EE\u5F55\u8FD0\u884C `bun run install:global` " + "\u4E00\u6B21\u6027\u5BF9\u9F50\u5168\u5C40 CLI \u4E0E\u63D2\u4EF6\u7F13\u5B58\uFF0C\u7136\u540E\u5173\u95ED\u5E76\u91CD\u5F00\u4ECD\u5728\u4F7F\u7528\u65E7\u63D2\u4EF6\u7684 Claude Code \u7A97\u53E3\u3002"
|
|
5578
|
+
name: "config.json",
|
|
5579
|
+
status: "ok",
|
|
5580
|
+
detail: desc.customValues ? `parsed at ${desc.path} \u2014 custom values in effect` : `parsed at ${desc.path} \u2014 all values match defaults`
|
|
4746
5581
|
};
|
|
4747
5582
|
}
|
|
4748
|
-
function
|
|
4749
|
-
|
|
4750
|
-
const match = readFileSync12(path, "utf-8").match(/commit:\s*defineString\("([^"]+)",\s*"source"\)/);
|
|
4751
|
-
return match ? match[1] : null;
|
|
4752
|
-
} catch {
|
|
4753
|
-
return null;
|
|
4754
|
-
}
|
|
4755
|
-
}
|
|
4756
|
-
function logCheck(name, path) {
|
|
4757
|
-
if (!existsSync12(path)) {
|
|
5583
|
+
function logCheck(name, path, cli) {
|
|
5584
|
+
if (!existsSync15(path)) {
|
|
4758
5585
|
return {
|
|
4759
5586
|
name,
|
|
4760
5587
|
status: "warn",
|
|
@@ -4762,13 +5589,13 @@ function logCheck(name, path) {
|
|
|
4762
5589
|
hint: "\u65E5\u5FD7\u4F1A\u5728\u76F8\u5E94\u8FDB\u7A0B\u9996\u6B21\u542F\u52A8\u65F6\u521B\u5EFA\uFF1B\u8FDB\u7A0B\u4ECE\u672A\u542F\u52A8\u8FC7\u65F6\u8FD9\u662F\u6B63\u5E38\u7684\u3002"
|
|
4763
5590
|
};
|
|
4764
5591
|
}
|
|
4765
|
-
const stat =
|
|
5592
|
+
const stat = statSync7(path);
|
|
4766
5593
|
if (stat.size > LARGE_LOG_WARN_BYTES) {
|
|
4767
5594
|
return {
|
|
4768
5595
|
name,
|
|
4769
5596
|
status: "warn",
|
|
4770
5597
|
detail: `${path} (${stat.size} bytes, oversized; stop the pair, rebuild/reinstall, then rotate or remove this log)`,
|
|
4771
|
-
hint:
|
|
5598
|
+
hint: `\u65E5\u5FD7\u8FC7\u5927\uFF1A\`${cli} kill\` \u505C\u6B62 pair \u540E\u5220\u9664\u8BE5\u6587\u4EF6\u518D\u91CD\u542F\u5373\u53EF\u3002`
|
|
4772
5599
|
};
|
|
4773
5600
|
}
|
|
4774
5601
|
return { name, status: "ok", detail: `${path} (${stat.size} bytes)` };
|
|
@@ -4804,7 +5631,10 @@ function printDoctorReport(report) {
|
|
|
4804
5631
|
}
|
|
4805
5632
|
var LARGE_LOG_WARN_BYTES;
|
|
4806
5633
|
var init_doctor = __esm(() => {
|
|
5634
|
+
init_plugin_cache();
|
|
4807
5635
|
init_build_info();
|
|
5636
|
+
init_cli_invocation();
|
|
5637
|
+
init_config_service();
|
|
4808
5638
|
init_env_guard();
|
|
4809
5639
|
init_pair_resolver();
|
|
4810
5640
|
init_thread_state();
|
|
@@ -4836,6 +5666,9 @@ function formatAgent(name, usage, snapshotAt) {
|
|
|
4836
5666
|
if (usage.rateLimitedUntil > 0) {
|
|
4837
5667
|
parts.push(`\u9650\u6D41\u81F3 ${formatEpoch(usage.rateLimitedUntil)}`);
|
|
4838
5668
|
}
|
|
5669
|
+
if (usage.parsedVia === "positional") {
|
|
5670
|
+
parts.push("\u26A0\uFE0F \u7A97\u53E3\u8BC6\u522B\u4F7F\u7528\u4F4D\u7F6E\u515C\u5E95");
|
|
5671
|
+
}
|
|
4839
5672
|
const ageSec = usage.fetchedAt > 0 ? snapshotAt - usage.fetchedAt : 0;
|
|
4840
5673
|
if (ageSec > 300) {
|
|
4841
5674
|
parts.push(`\u26A0\uFE0F \u6570\u636E\u91C7\u96C6\u4E8E ${Math.round(ageSec / 60)} \u5206\u949F\u524D`);
|
|
@@ -4902,6 +5735,7 @@ __export(exports_budget, {
|
|
|
4902
5735
|
});
|
|
4903
5736
|
async function runBudget(args) {
|
|
4904
5737
|
const json = args.includes("--json");
|
|
5738
|
+
const cli = cliInvocationName();
|
|
4905
5739
|
const { pairFlag } = parsePairFlag(args.filter((arg) => arg !== "--json"));
|
|
4906
5740
|
let resolution;
|
|
4907
5741
|
try {
|
|
@@ -4921,7 +5755,7 @@ async function runBudget(args) {
|
|
|
4921
5755
|
if (json) {
|
|
4922
5756
|
console.log(JSON.stringify({ ok: false, error: "pair_not_registered" }));
|
|
4923
5757
|
} else {
|
|
4924
|
-
console.error(
|
|
5758
|
+
console.error(`\u8BE5\u76EE\u5F55\u5C1A\u65E0 pair\uFF0C\u5148\u8FD0\u884C ${cli} claude`);
|
|
4925
5759
|
}
|
|
4926
5760
|
process.exit(1);
|
|
4927
5761
|
return;
|
|
@@ -4931,7 +5765,7 @@ async function runBudget(args) {
|
|
|
4931
5765
|
if (json) {
|
|
4932
5766
|
console.log(JSON.stringify({ ok: false, pairId: pair.pairId, error: "daemon_unreachable" }));
|
|
4933
5767
|
} else {
|
|
4934
|
-
console.error(`AgentBridge daemon \u672A\u8FD0\u884C\uFF08pair ${pair.pairId}\uFF0C\u63A7\u5236\u7AEF\u53E3 ${pair.ports.controlPort}\uFF09\u3002` +
|
|
5768
|
+
console.error(`AgentBridge daemon \u672A\u8FD0\u884C\uFF08pair ${pair.pairId}\uFF0C\u63A7\u5236\u7AEF\u53E3 ${pair.ports.controlPort}\uFF09\u3002` + `\u5148\u8FD0\u884C \`${cli} claude\` \u542F\u52A8\u4F1A\u8BDD\u3002`);
|
|
4935
5769
|
}
|
|
4936
5770
|
process.exit(1);
|
|
4937
5771
|
}
|
|
@@ -4943,10 +5777,165 @@ async function runBudget(args) {
|
|
|
4943
5777
|
console.log(status.budget ? renderBudgetSnapshot(status.budget) : BUDGET_UNAVAILABLE_TEXT);
|
|
4944
5778
|
}
|
|
4945
5779
|
var init_budget = __esm(() => {
|
|
5780
|
+
init_cli_invocation();
|
|
4946
5781
|
init_pair_resolver();
|
|
4947
5782
|
init_render();
|
|
4948
5783
|
});
|
|
4949
5784
|
|
|
5785
|
+
// src/cli/logs.ts
|
|
5786
|
+
var exports_logs = {};
|
|
5787
|
+
__export(exports_logs, {
|
|
5788
|
+
tailLines: () => tailLines,
|
|
5789
|
+
runLogs: () => runLogs,
|
|
5790
|
+
parseLogsArgs: () => parseLogsArgs,
|
|
5791
|
+
followLog: () => followLog
|
|
5792
|
+
});
|
|
5793
|
+
import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
|
|
5794
|
+
import { spawn as spawn4 } from "child_process";
|
|
5795
|
+
function parseLogsArgs(args) {
|
|
5796
|
+
let codex = false;
|
|
5797
|
+
let follow = false;
|
|
5798
|
+
let lines = DEFAULT_LINES;
|
|
5799
|
+
for (let i = 0;i < args.length; i++) {
|
|
5800
|
+
const a = args[i];
|
|
5801
|
+
if (a === "--codex") {
|
|
5802
|
+
codex = true;
|
|
5803
|
+
continue;
|
|
5804
|
+
}
|
|
5805
|
+
if (a === "-f" || a === "--follow") {
|
|
5806
|
+
follow = true;
|
|
5807
|
+
continue;
|
|
5808
|
+
}
|
|
5809
|
+
if (a === "-n" || a === "--lines") {
|
|
5810
|
+
const next = args[i + 1];
|
|
5811
|
+
if (next === undefined) {
|
|
5812
|
+
throw new Error(`${a} requires a positive integer (e.g. ${a} 200)`);
|
|
5813
|
+
}
|
|
5814
|
+
lines = parsePositiveInt(next, a);
|
|
5815
|
+
i++;
|
|
5816
|
+
continue;
|
|
5817
|
+
}
|
|
5818
|
+
if (a.startsWith("-n")) {
|
|
5819
|
+
lines = parsePositiveInt(a.slice(2), "-n");
|
|
5820
|
+
continue;
|
|
5821
|
+
}
|
|
5822
|
+
if (a.startsWith("--lines=")) {
|
|
5823
|
+
lines = parsePositiveInt(a.slice("--lines=".length), "--lines");
|
|
5824
|
+
continue;
|
|
5825
|
+
}
|
|
5826
|
+
throw new Error(`Unknown logs flag: ${a}`);
|
|
5827
|
+
}
|
|
5828
|
+
return { codex, follow, lines };
|
|
5829
|
+
}
|
|
5830
|
+
function parsePositiveInt(raw, flag) {
|
|
5831
|
+
if (!/^\d+$/.test(raw)) {
|
|
5832
|
+
throw new Error(`${flag} must be a positive integer, got "${raw}"`);
|
|
5833
|
+
}
|
|
5834
|
+
const n = Number.parseInt(raw, 10);
|
|
5835
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
5836
|
+
throw new Error(`${flag} must be a positive integer, got "${raw}"`);
|
|
5837
|
+
}
|
|
5838
|
+
return n;
|
|
5839
|
+
}
|
|
5840
|
+
function tailLines(text, count) {
|
|
5841
|
+
const body = text.endsWith(`
|
|
5842
|
+
`) ? text.slice(0, -1) : text;
|
|
5843
|
+
if (body.length === 0)
|
|
5844
|
+
return [];
|
|
5845
|
+
const all = body.split(`
|
|
5846
|
+
`);
|
|
5847
|
+
return all.length <= count ? all : all.slice(all.length - count);
|
|
5848
|
+
}
|
|
5849
|
+
async function runLogs(args) {
|
|
5850
|
+
const { pairFlag } = parsePairFlag(args);
|
|
5851
|
+
const rest = stripPairTokens(args);
|
|
5852
|
+
let options;
|
|
5853
|
+
try {
|
|
5854
|
+
options = parseLogsArgs(rest);
|
|
5855
|
+
} catch (err) {
|
|
5856
|
+
console.error(`[agentbridge] ${err instanceof Error ? err.message : String(err)}`);
|
|
5857
|
+
process.exit(1);
|
|
5858
|
+
return;
|
|
5859
|
+
}
|
|
5860
|
+
let resolution;
|
|
5861
|
+
try {
|
|
5862
|
+
resolution = resolvePairReadOnly(pairFlag);
|
|
5863
|
+
} catch (err) {
|
|
5864
|
+
console.error(`[agentbridge] ${err instanceof Error ? err.message : String(err)}`);
|
|
5865
|
+
process.exit(1);
|
|
5866
|
+
return;
|
|
5867
|
+
}
|
|
5868
|
+
const { pair } = resolution;
|
|
5869
|
+
const logPath = options.codex ? pair.stateDir.codexWrapperLogFile : pair.stateDir.logFile;
|
|
5870
|
+
const logLabel = options.codex ? "codex wrapper log" : "daemon log";
|
|
5871
|
+
if (!existsSync16(logPath)) {
|
|
5872
|
+
const which = options.codex ? "codex wrapper log" : "daemon log";
|
|
5873
|
+
console.error(`no ${which} for pair ${pair.name} yet \u2014 start it with \`abg claude\` (${logPath})`);
|
|
5874
|
+
process.exit(1);
|
|
5875
|
+
return;
|
|
5876
|
+
}
|
|
5877
|
+
if (options.follow) {
|
|
5878
|
+
await followLog(logPath, options.lines);
|
|
5879
|
+
return;
|
|
5880
|
+
}
|
|
5881
|
+
printTail(logPath, options.lines, logLabel, pair.name);
|
|
5882
|
+
}
|
|
5883
|
+
function printTail(logPath, count, label, pairName) {
|
|
5884
|
+
let text;
|
|
5885
|
+
try {
|
|
5886
|
+
text = readFileSync14(logPath, "utf8");
|
|
5887
|
+
} catch (err) {
|
|
5888
|
+
console.error(`[agentbridge] failed to read ${label} for pair ${pairName}: ` + `${err instanceof Error ? err.message : String(err)} (${logPath})`);
|
|
5889
|
+
process.exit(1);
|
|
5890
|
+
return;
|
|
5891
|
+
}
|
|
5892
|
+
const lines = tailLines(text, count);
|
|
5893
|
+
for (const line of lines)
|
|
5894
|
+
console.log(line);
|
|
5895
|
+
}
|
|
5896
|
+
function followLog(logPath, count) {
|
|
5897
|
+
return new Promise((resolvePromise) => {
|
|
5898
|
+
const child = spawn4("tail", ["-f", "-n", String(count), logPath], {
|
|
5899
|
+
stdio: "inherit"
|
|
5900
|
+
});
|
|
5901
|
+
child.on("error", (err) => {
|
|
5902
|
+
console.error(`[agentbridge] failed to follow log: ${err.message}`);
|
|
5903
|
+
process.exit(1);
|
|
5904
|
+
});
|
|
5905
|
+
child.on("exit", (code, signal) => {
|
|
5906
|
+
if (signal === "SIGINT" || signal === "SIGTERM") {
|
|
5907
|
+
resolvePromise();
|
|
5908
|
+
return;
|
|
5909
|
+
}
|
|
5910
|
+
if (code != null && code !== 0) {
|
|
5911
|
+
process.exit(code);
|
|
5912
|
+
return;
|
|
5913
|
+
}
|
|
5914
|
+
resolvePromise();
|
|
5915
|
+
});
|
|
5916
|
+
});
|
|
5917
|
+
}
|
|
5918
|
+
function stripPairTokens(args) {
|
|
5919
|
+
const out = [];
|
|
5920
|
+
for (let i = 0;i < args.length; i++) {
|
|
5921
|
+
const a = args[i];
|
|
5922
|
+
if (a === "--pair") {
|
|
5923
|
+
const next = args[i + 1];
|
|
5924
|
+
if (next !== undefined && !next.startsWith("-"))
|
|
5925
|
+
i++;
|
|
5926
|
+
continue;
|
|
5927
|
+
}
|
|
5928
|
+
if (a.startsWith("--pair="))
|
|
5929
|
+
continue;
|
|
5930
|
+
out.push(a);
|
|
5931
|
+
}
|
|
5932
|
+
return out;
|
|
5933
|
+
}
|
|
5934
|
+
var DEFAULT_LINES = 100;
|
|
5935
|
+
var init_logs = __esm(() => {
|
|
5936
|
+
init_pair_resolver();
|
|
5937
|
+
});
|
|
5938
|
+
|
|
4950
5939
|
// src/cli.ts
|
|
4951
5940
|
function parseTopLevel(args) {
|
|
4952
5941
|
const pairTokens = [];
|
|
@@ -5019,6 +6008,10 @@ async function main(command, restArgs) {
|
|
|
5019
6008
|
const { runBudget: runBudget2 } = await Promise.resolve().then(() => (init_budget(), exports_budget));
|
|
5020
6009
|
await runBudget2(restArgs);
|
|
5021
6010
|
break;
|
|
6011
|
+
case "logs":
|
|
6012
|
+
const { runLogs: runLogs2 } = await Promise.resolve().then(() => (init_logs(), exports_logs));
|
|
6013
|
+
await runLogs2(restArgs);
|
|
6014
|
+
break;
|
|
5022
6015
|
case "--help":
|
|
5023
6016
|
case "-h":
|
|
5024
6017
|
case undefined:
|
|
@@ -5035,6 +6028,7 @@ async function main(command, restArgs) {
|
|
|
5035
6028
|
}
|
|
5036
6029
|
}
|
|
5037
6030
|
function printHelp() {
|
|
6031
|
+
const cli = cliInvocationName();
|
|
5038
6032
|
console.log(`
|
|
5039
6033
|
AgentBridge \u2014 Multi-agent collaboration bridge
|
|
5040
6034
|
|
|
@@ -5052,11 +6046,15 @@ Commands:
|
|
|
5052
6046
|
No target: print resume commands for this directory's last
|
|
5053
6047
|
Claude session + this pair's current Codex thread.
|
|
5054
6048
|
With target: resume that side directly.
|
|
5055
|
-
pairs [rm <name|id> | prune [--
|
|
5056
|
-
List pairs; remove one (rm), or
|
|
6049
|
+
pairs [rm <name|id> | prune [--apply]]
|
|
6050
|
+
List pairs; remove one (rm), or reclaim orphan dirs + stranded
|
|
6051
|
+
entries (prune previews by default; --apply to delete)
|
|
5057
6052
|
doctor [--json] Diagnose env, daemon, build drift, logs, and current thread
|
|
5058
6053
|
doctor resume-pollution [--apply] Find/fix old AgentBridge kickoff metadata
|
|
5059
6054
|
budget [--json] Show both agents' subscription quota snapshot (5h/weekly, drift, pause state)
|
|
6055
|
+
logs [--codex] [-f] [-n N]
|
|
6056
|
+
Tail this pair's daemon log (or the codex wrapper log with
|
|
6057
|
+
--codex). -n N: last N lines (default 100). -f: follow/stream.
|
|
5060
6058
|
kill [all | --all | --pair <name|id>]
|
|
5061
6059
|
Stop this directory's pairs (default), every pair (all/--all), or one (--pair)
|
|
5062
6060
|
|
|
@@ -5082,26 +6080,30 @@ Multi-pair:
|
|
|
5082
6080
|
contesting it \u2014 pick another --pair name (or kill the live one first).
|
|
5083
6081
|
|
|
5084
6082
|
Examples:
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
6083
|
+
${cli} init # First-time setup
|
|
6084
|
+
${cli} claude # Start the "main" pair for this directory
|
|
6085
|
+
${cli} codex # Connect Codex to this directory's "main" pair
|
|
6086
|
+
${cli} resume # Print resume commands for both sides
|
|
6087
|
+
${cli} resume claude # Resume the last Claude Code session here
|
|
6088
|
+
${cli} resume codex # Resume this pair's current Codex thread
|
|
6089
|
+
${cli} claude --safe # One launch without the max-permission default
|
|
6090
|
+
${cli} --pair work claude # Start a named pair "work" (this directory)
|
|
6091
|
+
${cli} --pair work codex # Connect Codex to the "work" pair
|
|
6092
|
+
${cli} --pair review claude # A second, parallel pair
|
|
6093
|
+
${cli} pairs # List all pairs and their ports/status
|
|
6094
|
+
${cli} pairs --threads # Include current thread mapping
|
|
6095
|
+
${cli} doctor --json # Emit a structured diagnostics report
|
|
6096
|
+
${cli} logs # Tail the last 100 lines of this pair's daemon log
|
|
6097
|
+
${cli} logs -f -n 200 # Follow the log, starting from the last 200 lines
|
|
6098
|
+
${cli} logs --codex # Tail the codex wrapper log instead
|
|
6099
|
+
${cli} --pair work logs # Tail the "work" pair's daemon log
|
|
6100
|
+
${cli} pairs rm work # Stop this directory's "work" pair and free its slot
|
|
6101
|
+
${cli} pairs rm work-1a2b3c4d # ...or by its full id (from that pair's directory)
|
|
6102
|
+
${cli} pairs prune # Preview reclaimable: orphan dirs + stranded entries (cwd-gone, dead, >1d)
|
|
6103
|
+
${cli} pairs prune --apply # ...actually delete the previewed dirs + entries
|
|
6104
|
+
${cli} --pair work kill # Stop only this directory's "work" pair
|
|
6105
|
+
${cli} kill # Stop this directory's pairs (+ any legacy-root daemon)
|
|
6106
|
+
${cli} kill all # Stop every pair in every directory (+ legacy-root)
|
|
5105
6107
|
`.trim());
|
|
5106
6108
|
}
|
|
5107
6109
|
function printVersion() {
|
|
@@ -5114,9 +6116,10 @@ function printVersion() {
|
|
|
5114
6116
|
}
|
|
5115
6117
|
var MARKETPLACE_NAME = "agentbridge", PLUGIN_NAME = "agentbridge", REFRESH_COMMANDS, NOTIFY_COMMANDS, PAIR_AWARE_COMMANDS;
|
|
5116
6118
|
var init_cli = __esm(() => {
|
|
6119
|
+
init_cli_invocation();
|
|
5117
6120
|
REFRESH_COMMANDS = new Set(["claude", "codex", "resume"]);
|
|
5118
6121
|
NOTIFY_COMMANDS = new Set(["claude", "codex", "init", "dev", "resume"]);
|
|
5119
|
-
PAIR_AWARE_COMMANDS = new Set(["claude", "codex", "kill", "doctor", "budget", "resume"]);
|
|
6122
|
+
PAIR_AWARE_COMMANDS = new Set(["claude", "codex", "kill", "doctor", "budget", "resume", "logs"]);
|
|
5120
6123
|
if (import.meta.main) {
|
|
5121
6124
|
const { command, restArgs } = parseTopLevel(process.argv.slice(2));
|
|
5122
6125
|
main(command, restArgs).catch((err) => {
|
|
@@ -5129,6 +6132,9 @@ init_cli();
|
|
|
5129
6132
|
|
|
5130
6133
|
export {
|
|
5131
6134
|
parseTopLevel,
|
|
6135
|
+
REFRESH_COMMANDS,
|
|
5132
6136
|
PLUGIN_NAME,
|
|
6137
|
+
PAIR_AWARE_COMMANDS,
|
|
6138
|
+
NOTIFY_COMMANDS,
|
|
5133
6139
|
MARKETPLACE_NAME
|
|
5134
6140
|
};
|