@raysonmeng/agentbridge 0.1.12 → 0.1.14
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 +1235 -503
- package/dist/daemon.js +1262 -432
- package/package.json +3 -1
- package/plugins/agentbridge/.claude-plugin/plugin.json +1 -1
- package/plugins/agentbridge/server/bridge-server.js +726 -183
- package/plugins/agentbridge/server/daemon.js +1262 -432
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.14",
|
|
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,7 +375,7 @@ 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);
|
|
@@ -381,6 +438,9 @@ function normalizeBoundedInteger(value, fallback, min, max) {
|
|
|
381
438
|
return fallback;
|
|
382
439
|
return parsed;
|
|
383
440
|
}
|
|
441
|
+
function normalizeStrategy(value, fallback) {
|
|
442
|
+
return value === "conserve" || value === "maximize" ? value : fallback;
|
|
443
|
+
}
|
|
384
444
|
function normalizeBoolean(value, fallback) {
|
|
385
445
|
if (typeof value === "boolean")
|
|
386
446
|
return value;
|
|
@@ -429,8 +489,26 @@ function normalizeBudgetConfig(raw, fallback = DEFAULT_BUDGET_CONFIG) {
|
|
|
429
489
|
timeWindowSec: normalizeBoundedInteger(parallel.timeWindowSec, fallback.parallel.timeWindowSec, 60, 604800)
|
|
430
490
|
},
|
|
431
491
|
codexTierControl: normalizeBoolean(budget.codexTierControl, fallback.codexTierControl) && codexTiers.full !== null,
|
|
432
|
-
codexTiers
|
|
492
|
+
codexTiers,
|
|
493
|
+
strategy: normalizeStrategy(budget.strategy, fallback.strategy)
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
function applyBudgetEnvOverrides(budget, env = process.env) {
|
|
497
|
+
const overlay = {
|
|
498
|
+
enabled: env.AGENTBRIDGE_BUDGET_ENABLED ?? budget.enabled,
|
|
499
|
+
pollSeconds: env.AGENTBRIDGE_BUDGET_POLL_SECONDS ?? budget.pollSeconds,
|
|
500
|
+
pauseAt: env.AGENTBRIDGE_BUDGET_PAUSE_AT ?? budget.pauseAt,
|
|
501
|
+
resumeBelow: env.AGENTBRIDGE_BUDGET_RESUME_BELOW ?? budget.resumeBelow,
|
|
502
|
+
syncDriftPct: env.AGENTBRIDGE_BUDGET_SYNC_DRIFT_PCT ?? budget.syncDriftPct,
|
|
503
|
+
parallel: {
|
|
504
|
+
minRemainingPct: env.AGENTBRIDGE_BUDGET_PARALLEL_MIN_REMAINING_PCT ?? budget.parallel.minRemainingPct,
|
|
505
|
+
timeWindowSec: env.AGENTBRIDGE_BUDGET_PARALLEL_TIME_WINDOW_SEC ?? budget.parallel.timeWindowSec
|
|
506
|
+
},
|
|
507
|
+
codexTierControl: env.AGENTBRIDGE_BUDGET_CODEX_TIER_CONTROL ?? budget.codexTierControl,
|
|
508
|
+
codexTiers: budget.codexTiers,
|
|
509
|
+
strategy: env.AGENTBRIDGE_BUDGET_STRATEGY ?? budget.strategy
|
|
433
510
|
};
|
|
511
|
+
return normalizeBudgetConfig(overlay, budget);
|
|
434
512
|
}
|
|
435
513
|
function normalizeConfig(raw) {
|
|
436
514
|
if (!isRecord(raw))
|
|
@@ -442,13 +520,13 @@ function normalizeConfig(raw) {
|
|
|
442
520
|
return {
|
|
443
521
|
version: typeof config.version === "string" ? config.version : DEFAULT_CONFIG.version,
|
|
444
522
|
codex: {
|
|
445
|
-
appPort:
|
|
446
|
-
proxyPort:
|
|
523
|
+
appPort: normalizeBoundedInteger(codex.appPort ?? daemon.port, DEFAULT_CONFIG.codex.appPort, 1, 65535),
|
|
524
|
+
proxyPort: normalizeBoundedInteger(codex.proxyPort ?? daemon.proxyPort, DEFAULT_CONFIG.codex.proxyPort, 1, 65535)
|
|
447
525
|
},
|
|
448
526
|
turnCoordination: {
|
|
449
|
-
attentionWindowSeconds:
|
|
527
|
+
attentionWindowSeconds: normalizeBoundedInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds, 0, Number.MAX_SAFE_INTEGER)
|
|
450
528
|
},
|
|
451
|
-
idleShutdownSeconds:
|
|
529
|
+
idleShutdownSeconds: normalizeBoundedInteger(config.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds, 1, Number.MAX_SAFE_INTEGER),
|
|
452
530
|
budget: normalizeBudgetConfig(config.budget)
|
|
453
531
|
};
|
|
454
532
|
}
|
|
@@ -520,9 +598,7 @@ class ConfigService {
|
|
|
520
598
|
};
|
|
521
599
|
}
|
|
522
600
|
save(config) {
|
|
523
|
-
this.
|
|
524
|
-
writeFileSync2(this.configPath, JSON.stringify(config, null, 2) + `
|
|
525
|
-
`, "utf-8");
|
|
601
|
+
atomicWriteJson(this.configPath, config);
|
|
526
602
|
}
|
|
527
603
|
initDefaults() {
|
|
528
604
|
this.ensureConfigDir();
|
|
@@ -538,12 +614,13 @@ class ConfigService {
|
|
|
538
614
|
}
|
|
539
615
|
ensureConfigDir() {
|
|
540
616
|
if (!existsSync2(this.configDir)) {
|
|
541
|
-
|
|
617
|
+
mkdirSync3(this.configDir, { recursive: true });
|
|
542
618
|
}
|
|
543
619
|
}
|
|
544
620
|
}
|
|
545
621
|
var DEFAULT_BUDGET_CONFIG, DEFAULT_CONFIG, CONFIG_DIR = ".agentbridge", CONFIG_FILE = "config.json", NOOP_LOGGER = () => {};
|
|
546
622
|
var init_config_service = __esm(() => {
|
|
623
|
+
init_atomic_json();
|
|
547
624
|
DEFAULT_BUDGET_CONFIG = {
|
|
548
625
|
enabled: true,
|
|
549
626
|
pollSeconds: 300,
|
|
@@ -559,7 +636,8 @@ var init_config_service = __esm(() => {
|
|
|
559
636
|
full: null,
|
|
560
637
|
balanced: { effort: "medium" },
|
|
561
638
|
eco: { effort: "low" }
|
|
562
|
-
}
|
|
639
|
+
},
|
|
640
|
+
strategy: "conserve"
|
|
563
641
|
};
|
|
564
642
|
DEFAULT_CONFIG = {
|
|
565
643
|
version: "1.0",
|
|
@@ -576,7 +654,7 @@ var init_config_service = __esm(() => {
|
|
|
576
654
|
});
|
|
577
655
|
|
|
578
656
|
// src/cli/pkg-root.ts
|
|
579
|
-
import { dirname, join as join3 } from "path";
|
|
657
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
580
658
|
import { existsSync as existsSync3 } from "fs";
|
|
581
659
|
import { execFileSync } from "child_process";
|
|
582
660
|
function findPackageRoot() {
|
|
@@ -585,7 +663,7 @@ function findPackageRoot() {
|
|
|
585
663
|
if (existsSync3(join3(dir, "package.json"))) {
|
|
586
664
|
return dir;
|
|
587
665
|
}
|
|
588
|
-
const parent =
|
|
666
|
+
const parent = dirname2(dir);
|
|
589
667
|
if (parent === dir) {
|
|
590
668
|
throw new Error("Could not find package.json in any parent directory");
|
|
591
669
|
}
|
|
@@ -760,18 +838,24 @@ __export(exports_init, {
|
|
|
760
838
|
writeCollaborationSections: () => writeCollaborationSections,
|
|
761
839
|
runInit: () => runInit,
|
|
762
840
|
pluginInstallFallbackGuidance: () => pluginInstallFallbackGuidance,
|
|
841
|
+
formatDepChecks: () => formatDepChecks,
|
|
763
842
|
compareVersions: () => compareVersions
|
|
764
843
|
});
|
|
765
844
|
import { execSync, execFileSync as execFileSync2 } from "child_process";
|
|
766
|
-
import { readFileSync as readFileSync3, writeFileSync as
|
|
845
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
767
846
|
import { join as join5 } from "path";
|
|
768
847
|
async function runInit() {
|
|
769
848
|
console.log(`AgentBridge Init
|
|
770
849
|
`);
|
|
850
|
+
const cli = cliInvocationName();
|
|
771
851
|
console.log("Checking dependencies...");
|
|
772
|
-
checkBun();
|
|
773
|
-
|
|
774
|
-
|
|
852
|
+
const depChecks = [checkBun(), checkClaude(), checkCodex()];
|
|
853
|
+
for (const line of formatDepChecks(depChecks, cli)) {
|
|
854
|
+
console.log(line);
|
|
855
|
+
}
|
|
856
|
+
if (depChecks.some((check) => check.status === "fail")) {
|
|
857
|
+
process.exit(1);
|
|
858
|
+
}
|
|
775
859
|
console.log("");
|
|
776
860
|
console.log("Generating project config...");
|
|
777
861
|
const configService = new ConfigService;
|
|
@@ -803,7 +887,7 @@ async function runInit() {
|
|
|
803
887
|
pluginInstalled = true;
|
|
804
888
|
} catch {
|
|
805
889
|
console.log(" Plugin install skipped (marketplace registration or install failed).");
|
|
806
|
-
for (const line of pluginInstallFallbackGuidance(detectRepoCheckout())) {
|
|
890
|
+
for (const line of pluginInstallFallbackGuidance(detectRepoCheckout(), cli)) {
|
|
807
891
|
console.log(line);
|
|
808
892
|
}
|
|
809
893
|
}
|
|
@@ -818,8 +902,8 @@ async function runInit() {
|
|
|
818
902
|
}
|
|
819
903
|
console.log("Next steps:");
|
|
820
904
|
console.log(" 1. If Claude Code is already running, execute /reload-plugins in your session");
|
|
821
|
-
console.log(
|
|
822
|
-
console.log(
|
|
905
|
+
console.log(` 2. Start Claude Code: ${cli} claude`);
|
|
906
|
+
console.log(` 3. Start Codex TUI: ${cli} codex`);
|
|
823
907
|
}
|
|
824
908
|
function detectRepoCheckout() {
|
|
825
909
|
try {
|
|
@@ -828,11 +912,11 @@ function detectRepoCheckout() {
|
|
|
828
912
|
return false;
|
|
829
913
|
}
|
|
830
914
|
}
|
|
831
|
-
function pluginInstallFallbackGuidance(insideRepo) {
|
|
915
|
+
function pluginInstallFallbackGuidance(insideRepo, cli = cliInvocationName()) {
|
|
832
916
|
if (insideRepo) {
|
|
833
917
|
return [
|
|
834
918
|
" You can install it later with:",
|
|
835
|
-
|
|
919
|
+
` ${cli} dev # registers marketplace and installs plugin`
|
|
836
920
|
];
|
|
837
921
|
}
|
|
838
922
|
return [
|
|
@@ -840,46 +924,68 @@ function pluginInstallFallbackGuidance(insideRepo) {
|
|
|
840
924
|
...MARKETPLACE_STEPS.map((step) => ` ${step}`)
|
|
841
925
|
];
|
|
842
926
|
}
|
|
927
|
+
function formatDepChecks(checks, cli) {
|
|
928
|
+
const lines = [];
|
|
929
|
+
for (const check of checks) {
|
|
930
|
+
lines.push(` ${check.status.toUpperCase().padEnd(4)} ${check.name}: ${check.detail}`);
|
|
931
|
+
if ((check.status === "warn" || check.status === "fail") && check.hint) {
|
|
932
|
+
lines.push(` \u21B3 ${check.hint}`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
lines.push(` \u9A8C\u8BC1\u5B89\u88C5: ${cli} doctor`);
|
|
936
|
+
return lines;
|
|
937
|
+
}
|
|
843
938
|
function checkBun() {
|
|
844
939
|
try {
|
|
845
940
|
const version = execSync("bun --version", { encoding: "utf-8" }).trim();
|
|
846
|
-
|
|
941
|
+
return { name: "bun", status: "ok", detail: version };
|
|
847
942
|
} catch {
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
943
|
+
return {
|
|
944
|
+
name: "bun",
|
|
945
|
+
status: "fail",
|
|
946
|
+
detail: "not found in PATH",
|
|
947
|
+
hint: "Install Bun: https://bun.sh"
|
|
948
|
+
};
|
|
851
949
|
}
|
|
852
950
|
}
|
|
853
951
|
function checkClaude() {
|
|
952
|
+
let versionOutput;
|
|
854
953
|
try {
|
|
855
|
-
|
|
856
|
-
const match = versionOutput.match(/(\d+\.\d+\.\d+)/);
|
|
857
|
-
if (match) {
|
|
858
|
-
const version = match[1];
|
|
859
|
-
console.log(` claude: ${version}`);
|
|
860
|
-
if (compareVersions(version, MIN_CLAUDE_VERSION) < 0) {
|
|
861
|
-
console.error(` ERROR: Claude Code version ${version} is too old.`);
|
|
862
|
-
console.error(` Channels require >= ${MIN_CLAUDE_VERSION}.`);
|
|
863
|
-
console.error(" Update: npm update -g @anthropic-ai/claude-code");
|
|
864
|
-
process.exit(1);
|
|
865
|
-
}
|
|
866
|
-
} else {
|
|
867
|
-
console.log(` claude: ${versionOutput} (version check skipped)`);
|
|
868
|
-
}
|
|
954
|
+
versionOutput = execSync("claude --version", { encoding: "utf-8" }).trim();
|
|
869
955
|
} catch {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
956
|
+
return {
|
|
957
|
+
name: "claude",
|
|
958
|
+
status: "fail",
|
|
959
|
+
detail: "not found in PATH",
|
|
960
|
+
hint: "Install Claude Code: npm install -g @anthropic-ai/claude-code"
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
const match = versionOutput.match(/(\d+\.\d+\.\d+)/);
|
|
964
|
+
if (!match) {
|
|
965
|
+
return { name: "claude", status: "ok", detail: `${versionOutput} (version check skipped)` };
|
|
966
|
+
}
|
|
967
|
+
const version = match[1];
|
|
968
|
+
if (compareVersions(version, MIN_CLAUDE_VERSION) < 0) {
|
|
969
|
+
return {
|
|
970
|
+
name: "claude",
|
|
971
|
+
status: "fail",
|
|
972
|
+
detail: `${version} is too old (channels require >= ${MIN_CLAUDE_VERSION})`,
|
|
973
|
+
hint: "Update: npm update -g @anthropic-ai/claude-code"
|
|
974
|
+
};
|
|
873
975
|
}
|
|
976
|
+
return { name: "claude", status: "ok", detail: version };
|
|
874
977
|
}
|
|
875
978
|
function checkCodex() {
|
|
876
979
|
try {
|
|
877
980
|
const version = execSync("codex --version", { encoding: "utf-8" }).trim();
|
|
878
|
-
|
|
981
|
+
return { name: "codex", status: "ok", detail: version };
|
|
879
982
|
} catch {
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
983
|
+
return {
|
|
984
|
+
name: "codex",
|
|
985
|
+
status: "warn",
|
|
986
|
+
detail: "not found in PATH (the Codex side will be unavailable until installed)",
|
|
987
|
+
hint: "Install Codex when you want to pair: https://github.com/openai/codex"
|
|
988
|
+
};
|
|
883
989
|
}
|
|
884
990
|
}
|
|
885
991
|
function writeCollaborationSections(projectRoot) {
|
|
@@ -905,7 +1011,7 @@ function writeCollaborationSections(projectRoot) {
|
|
|
905
1011
|
results.push(`${name}: unchanged (section already up to date)`);
|
|
906
1012
|
continue;
|
|
907
1013
|
}
|
|
908
|
-
|
|
1014
|
+
writeFileSync2(path, updated, "utf-8");
|
|
909
1015
|
if (existing === "") {
|
|
910
1016
|
results.push(`${name}: created with collaboration section`);
|
|
911
1017
|
} else if (existing.includes(`<!-- ${MARKER_ID}:start -->`)) {
|
|
@@ -918,6 +1024,7 @@ function writeCollaborationSections(projectRoot) {
|
|
|
918
1024
|
}
|
|
919
1025
|
var MIN_CLAUDE_VERSION = "2.1.80";
|
|
920
1026
|
var init_init = __esm(() => {
|
|
1027
|
+
init_cli_invocation();
|
|
921
1028
|
init_config_service();
|
|
922
1029
|
init_cli();
|
|
923
1030
|
init_pkg_root();
|
|
@@ -1041,7 +1148,7 @@ var init_dev = __esm(() => {
|
|
|
1041
1148
|
});
|
|
1042
1149
|
|
|
1043
1150
|
// src/control-protocol.ts
|
|
1044
|
-
var CLOSE_CODE_REPLACED = 4001, CLOSE_CODE_EVICTED_STALE = 4002, CLOSE_CODE_PROBE_IN_PROGRESS = 4003, CLOSE_CODE_PAIR_MISMATCH = 4004;
|
|
1151
|
+
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;
|
|
1045
1152
|
|
|
1046
1153
|
// src/interrupt-timing.ts
|
|
1047
1154
|
var CLIENT_REPLY_TIMEOUT_MS = 15000, INTERRUPT_CLIENT_MARGIN_MS = 2000, MAX_INTERRUPT_TIMEOUT_MS;
|
|
@@ -1049,6 +1156,76 @@ var init_interrupt_timing = __esm(() => {
|
|
|
1049
1156
|
MAX_INTERRUPT_TIMEOUT_MS = CLIENT_REPLY_TIMEOUT_MS - INTERRUPT_CLIENT_MARGIN_MS;
|
|
1050
1157
|
});
|
|
1051
1158
|
|
|
1159
|
+
// src/pending-request-registry.ts
|
|
1160
|
+
class PendingRequestRegistry {
|
|
1161
|
+
entries = new Map;
|
|
1162
|
+
setTimer;
|
|
1163
|
+
clearTimer;
|
|
1164
|
+
constructor(deps = {}) {
|
|
1165
|
+
this.setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
1166
|
+
this.clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
|
|
1167
|
+
}
|
|
1168
|
+
get size() {
|
|
1169
|
+
return this.entries.size;
|
|
1170
|
+
}
|
|
1171
|
+
has(id) {
|
|
1172
|
+
return this.entries.has(id);
|
|
1173
|
+
}
|
|
1174
|
+
register(id, options) {
|
|
1175
|
+
const existing = this.entries.get(id);
|
|
1176
|
+
if (existing) {
|
|
1177
|
+
this.clearTimer(existing.timer);
|
|
1178
|
+
this.entries.delete(id);
|
|
1179
|
+
}
|
|
1180
|
+
return new Promise((resolve3, reject) => {
|
|
1181
|
+
const timer = this.setTimer(() => {
|
|
1182
|
+
if (!this.entries.has(id))
|
|
1183
|
+
return;
|
|
1184
|
+
this.entries.delete(id);
|
|
1185
|
+
options.onTimeout({ resolve: resolve3, reject });
|
|
1186
|
+
}, options.timeoutMs);
|
|
1187
|
+
if (options.unref) {
|
|
1188
|
+
timer.unref?.();
|
|
1189
|
+
}
|
|
1190
|
+
this.entries.set(id, { resolve: resolve3, reject, timer });
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
settle(id, value) {
|
|
1194
|
+
const entry = this.entries.get(id);
|
|
1195
|
+
if (!entry)
|
|
1196
|
+
return false;
|
|
1197
|
+
this.clearTimer(entry.timer);
|
|
1198
|
+
this.entries.delete(id);
|
|
1199
|
+
entry.resolve(value);
|
|
1200
|
+
return true;
|
|
1201
|
+
}
|
|
1202
|
+
reject(id, error) {
|
|
1203
|
+
const entry = this.entries.get(id);
|
|
1204
|
+
if (!entry)
|
|
1205
|
+
return false;
|
|
1206
|
+
this.clearTimer(entry.timer);
|
|
1207
|
+
this.entries.delete(id);
|
|
1208
|
+
entry.reject(error);
|
|
1209
|
+
return true;
|
|
1210
|
+
}
|
|
1211
|
+
settleAll(value) {
|
|
1212
|
+
const make = typeof value === "function" ? value : () => value;
|
|
1213
|
+
for (const [id, entry] of this.entries) {
|
|
1214
|
+
this.clearTimer(entry.timer);
|
|
1215
|
+
this.entries.delete(id);
|
|
1216
|
+
entry.resolve(make(id));
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
rejectAll(error) {
|
|
1220
|
+
const make = typeof error === "function" ? error : () => error;
|
|
1221
|
+
for (const [id, entry] of this.entries) {
|
|
1222
|
+
this.clearTimer(entry.timer);
|
|
1223
|
+
this.entries.delete(id);
|
|
1224
|
+
entry.reject(make(id));
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1052
1229
|
// src/daemon-client.ts
|
|
1053
1230
|
import { EventEmitter } from "events";
|
|
1054
1231
|
var nextSocketId = 0, DaemonClient;
|
|
@@ -1060,7 +1237,8 @@ var init_daemon_client = __esm(() => {
|
|
|
1060
1237
|
ws = null;
|
|
1061
1238
|
wsId = 0;
|
|
1062
1239
|
nextRequestId = 1;
|
|
1063
|
-
pendingReplies = new
|
|
1240
|
+
pendingReplies = new PendingRequestRegistry;
|
|
1241
|
+
pendingEventWaiters = new PendingRequestRegistry;
|
|
1064
1242
|
constructor(url, options = {}) {
|
|
1065
1243
|
super();
|
|
1066
1244
|
this.url = url;
|
|
@@ -1106,82 +1284,73 @@ var init_daemon_client = __esm(() => {
|
|
|
1106
1284
|
});
|
|
1107
1285
|
}
|
|
1108
1286
|
attachClaude() {
|
|
1287
|
+
const identity = this.resolveIdentity();
|
|
1109
1288
|
this.send({
|
|
1110
1289
|
type: "claude_connect",
|
|
1111
|
-
...
|
|
1290
|
+
...identity ? { identity } : {}
|
|
1112
1291
|
});
|
|
1113
1292
|
}
|
|
1293
|
+
resolveIdentity() {
|
|
1294
|
+
const opt = this.options.identity;
|
|
1295
|
+
return typeof opt === "function" ? opt() : opt;
|
|
1296
|
+
}
|
|
1114
1297
|
async attachClaudeAndWaitForStatus(timeoutMs = 1000) {
|
|
1115
1298
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1116
1299
|
return null;
|
|
1117
1300
|
}
|
|
1118
|
-
return
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
if (timer) {
|
|
1126
|
-
clearTimeout(timer);
|
|
1127
|
-
timer = null;
|
|
1128
|
-
}
|
|
1129
|
-
this.off("status", onStatus);
|
|
1130
|
-
this.off("rejected", onRejected);
|
|
1131
|
-
this.off("disconnect", onDisconnect);
|
|
1132
|
-
};
|
|
1133
|
-
const finish = (value) => {
|
|
1134
|
-
cleanup();
|
|
1135
|
-
resolve3(value);
|
|
1136
|
-
};
|
|
1137
|
-
const onStatus = (status) => finish(status);
|
|
1138
|
-
const onRejected = () => finish(null);
|
|
1139
|
-
const onDisconnect = () => finish(null);
|
|
1140
|
-
this.on("status", onStatus);
|
|
1141
|
-
this.on("rejected", onRejected);
|
|
1142
|
-
this.on("disconnect", onDisconnect);
|
|
1143
|
-
timer = setTimeout(() => {
|
|
1144
|
-
finish(null);
|
|
1145
|
-
}, timeoutMs);
|
|
1146
|
-
try {
|
|
1147
|
-
this.attachClaude();
|
|
1148
|
-
} catch {
|
|
1149
|
-
finish(null);
|
|
1150
|
-
}
|
|
1301
|
+
return this.awaitTypedResponse({
|
|
1302
|
+
key: "status",
|
|
1303
|
+
successEvent: "status",
|
|
1304
|
+
successValue: (status) => status,
|
|
1305
|
+
failValue: null,
|
|
1306
|
+
timeoutMs,
|
|
1307
|
+
send: () => this.attachClaude()
|
|
1151
1308
|
});
|
|
1152
1309
|
}
|
|
1153
1310
|
async probeIncumbent(timeoutMs = 3000) {
|
|
1154
1311
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1155
1312
|
return { connected: false, alive: false };
|
|
1156
1313
|
}
|
|
1157
|
-
return
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
if (timer)
|
|
1165
|
-
clearTimeout(timer);
|
|
1166
|
-
this.off("incumbentStatus", onStatus);
|
|
1167
|
-
this.off("disconnect", onDisconnect);
|
|
1168
|
-
this.off("rejected", onRejected);
|
|
1169
|
-
resolve3(value);
|
|
1170
|
-
};
|
|
1171
|
-
const onStatus = (s) => finish(s);
|
|
1172
|
-
const onDisconnect = () => finish({ connected: false, alive: false });
|
|
1173
|
-
const onRejected = () => finish({ connected: false, alive: false });
|
|
1174
|
-
this.on("incumbentStatus", onStatus);
|
|
1175
|
-
this.on("disconnect", onDisconnect);
|
|
1176
|
-
this.on("rejected", onRejected);
|
|
1177
|
-
timer = setTimeout(() => finish({ connected: false, alive: false }), timeoutMs);
|
|
1178
|
-
try {
|
|
1179
|
-
this.send({ type: "probe_incumbent" });
|
|
1180
|
-
} catch {
|
|
1181
|
-
finish({ connected: false, alive: false });
|
|
1182
|
-
}
|
|
1314
|
+
return this.awaitTypedResponse({
|
|
1315
|
+
key: "incumbent_status",
|
|
1316
|
+
successEvent: "incumbentStatus",
|
|
1317
|
+
successValue: (s) => s,
|
|
1318
|
+
failValue: { connected: false, alive: false },
|
|
1319
|
+
timeoutMs,
|
|
1320
|
+
send: () => this.send({ type: "probe_incumbent" })
|
|
1183
1321
|
});
|
|
1184
1322
|
}
|
|
1323
|
+
awaitTypedResponse(opts) {
|
|
1324
|
+
const { key, successEvent, successValue, failValue, timeoutMs, send } = opts;
|
|
1325
|
+
const onSuccess = (payload) => {
|
|
1326
|
+
this.pendingEventWaiters.settle(key, successValue(payload));
|
|
1327
|
+
};
|
|
1328
|
+
const onRejected = () => {
|
|
1329
|
+
this.pendingEventWaiters.settle(key, failValue);
|
|
1330
|
+
};
|
|
1331
|
+
const onDisconnect = () => {
|
|
1332
|
+
this.pendingEventWaiters.settle(key, failValue);
|
|
1333
|
+
};
|
|
1334
|
+
const pending = this.pendingEventWaiters.register(key, {
|
|
1335
|
+
timeoutMs,
|
|
1336
|
+
onTimeout: ({ resolve: resolve3 }) => resolve3(failValue)
|
|
1337
|
+
});
|
|
1338
|
+
const cleanup = () => {
|
|
1339
|
+
this.off(successEvent, onSuccess);
|
|
1340
|
+
this.off("rejected", onRejected);
|
|
1341
|
+
this.off("disconnect", onDisconnect);
|
|
1342
|
+
};
|
|
1343
|
+
pending.finally(cleanup);
|
|
1344
|
+
this.on(successEvent, onSuccess);
|
|
1345
|
+
this.on("rejected", onRejected);
|
|
1346
|
+
this.on("disconnect", onDisconnect);
|
|
1347
|
+
try {
|
|
1348
|
+
send();
|
|
1349
|
+
} catch {
|
|
1350
|
+
this.pendingEventWaiters.settle(key, failValue);
|
|
1351
|
+
}
|
|
1352
|
+
return pending;
|
|
1353
|
+
}
|
|
1185
1354
|
async disconnect() {
|
|
1186
1355
|
if (!this.ws)
|
|
1187
1356
|
return;
|
|
@@ -1199,21 +1368,19 @@ var init_daemon_client = __esm(() => {
|
|
|
1199
1368
|
return { success: false, error: "AgentBridge daemon is not connected." };
|
|
1200
1369
|
}
|
|
1201
1370
|
const requestId = `reply_${Date.now()}_${this.nextRequestId++}`;
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
...onBusy && onBusy !== "reject" ? { onBusy } : {},
|
|
1214
|
-
...idempotencyKey ? { idempotencyKey } : {}
|
|
1215
|
-
});
|
|
1371
|
+
const pending = this.pendingReplies.register(requestId, {
|
|
1372
|
+
timeoutMs: CLIENT_REPLY_TIMEOUT_MS,
|
|
1373
|
+
onTimeout: ({ resolve: resolve3 }) => resolve3({ success: false, error: "Timed out waiting for AgentBridge daemon reply." })
|
|
1374
|
+
});
|
|
1375
|
+
this.send({
|
|
1376
|
+
type: "claude_to_codex",
|
|
1377
|
+
requestId,
|
|
1378
|
+
message,
|
|
1379
|
+
...requireReply ? { requireReply: true } : {},
|
|
1380
|
+
...onBusy && onBusy !== "reject" ? { onBusy } : {},
|
|
1381
|
+
...idempotencyKey ? { idempotencyKey } : {}
|
|
1216
1382
|
});
|
|
1383
|
+
return pending;
|
|
1217
1384
|
}
|
|
1218
1385
|
attachSocketHandlers(ws, socketId) {
|
|
1219
1386
|
ws.onmessage = (event) => {
|
|
@@ -1229,12 +1396,7 @@ var init_daemon_client = __esm(() => {
|
|
|
1229
1396
|
this.emit("codexMessage", message.message);
|
|
1230
1397
|
return;
|
|
1231
1398
|
case "claude_to_codex_result": {
|
|
1232
|
-
|
|
1233
|
-
if (!pending)
|
|
1234
|
-
return;
|
|
1235
|
-
clearTimeout(pending.timer);
|
|
1236
|
-
this.pendingReplies.delete(message.requestId);
|
|
1237
|
-
pending.resolve({
|
|
1399
|
+
this.pendingReplies.settle(message.requestId, {
|
|
1238
1400
|
success: message.success,
|
|
1239
1401
|
error: message.error,
|
|
1240
1402
|
...message.code !== undefined ? { code: message.code } : {},
|
|
@@ -1265,7 +1427,7 @@ var init_daemon_client = __esm(() => {
|
|
|
1265
1427
|
if (isCurrent) {
|
|
1266
1428
|
this.ws = null;
|
|
1267
1429
|
this.rejectPendingReplies("AgentBridge daemon disconnected.");
|
|
1268
|
-
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) {
|
|
1430
|
+
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) {
|
|
1269
1431
|
this.emit("rejected", event.code);
|
|
1270
1432
|
} else {
|
|
1271
1433
|
this.emit("disconnect");
|
|
@@ -1275,11 +1437,7 @@ var init_daemon_client = __esm(() => {
|
|
|
1275
1437
|
ws.onerror = () => {};
|
|
1276
1438
|
}
|
|
1277
1439
|
rejectPendingReplies(error) {
|
|
1278
|
-
|
|
1279
|
-
clearTimeout(pending.timer);
|
|
1280
|
-
pending.resolve({ success: false, error });
|
|
1281
|
-
this.pendingReplies.delete(requestId);
|
|
1282
|
-
}
|
|
1440
|
+
this.pendingReplies.settleAll(() => ({ success: false, error }));
|
|
1283
1441
|
}
|
|
1284
1442
|
send(message) {
|
|
1285
1443
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
@@ -1298,6 +1456,10 @@ var init_daemon_client = __esm(() => {
|
|
|
1298
1456
|
var CONTRACT_VERSION = 1;
|
|
1299
1457
|
|
|
1300
1458
|
// src/build-info.ts
|
|
1459
|
+
function hasValidCodeHash(build) {
|
|
1460
|
+
const hash = build?.codeHash;
|
|
1461
|
+
return typeof hash === "string" && hash.length > 0 && hash !== CODE_HASH_SENTINEL;
|
|
1462
|
+
}
|
|
1301
1463
|
function defineString(value, fallback) {
|
|
1302
1464
|
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
1303
1465
|
}
|
|
@@ -1312,7 +1474,14 @@ function defineNumber(value, fallback) {
|
|
|
1312
1474
|
function sameRuntimeContract(a, b) {
|
|
1313
1475
|
if (!a || !b)
|
|
1314
1476
|
return false;
|
|
1315
|
-
|
|
1477
|
+
if (a.version !== b.version || a.contractVersion !== b.contractVersion)
|
|
1478
|
+
return false;
|
|
1479
|
+
if (hasValidCodeHash(a) && hasValidCodeHash(b))
|
|
1480
|
+
return a.codeHash === b.codeHash;
|
|
1481
|
+
return a.commit === b.commit;
|
|
1482
|
+
}
|
|
1483
|
+
function runtimeContractComparisonBasis(a, b) {
|
|
1484
|
+
return hasValidCodeHash(a) && hasValidCodeHash(b) ? "codeHash" : "commit";
|
|
1316
1485
|
}
|
|
1317
1486
|
function compatibleContractVersion(a, b) {
|
|
1318
1487
|
if (!a || !b)
|
|
@@ -1322,21 +1491,23 @@ function compatibleContractVersion(a, b) {
|
|
|
1322
1491
|
function formatBuildInfo(build) {
|
|
1323
1492
|
if (!build)
|
|
1324
1493
|
return "<unknown>";
|
|
1325
|
-
|
|
1494
|
+
const codeHash = hasValidCodeHash(build) ? `/code-${build.codeHash}` : "";
|
|
1495
|
+
return `${build.version}/${build.commit}/${build.bundle}/contract-v${build.contractVersion}${codeHash}`;
|
|
1326
1496
|
}
|
|
1327
|
-
var BUILD_INFO;
|
|
1497
|
+
var CODE_HASH_SENTINEL = "source", BUILD_INFO;
|
|
1328
1498
|
var init_build_info = __esm(() => {
|
|
1329
1499
|
BUILD_INFO = Object.freeze({
|
|
1330
|
-
version: defineString("0.1.
|
|
1331
|
-
commit: defineString("
|
|
1500
|
+
version: defineString("0.1.14", "0.0.0-source"),
|
|
1501
|
+
commit: defineString("f5a9562", "source"),
|
|
1332
1502
|
bundle: defineBundle("dist"),
|
|
1333
|
-
contractVersion: defineNumber(1, CONTRACT_VERSION)
|
|
1503
|
+
contractVersion: defineNumber(1, CONTRACT_VERSION),
|
|
1504
|
+
codeHash: defineString("e05d18c3cc72", "source")
|
|
1334
1505
|
});
|
|
1335
1506
|
});
|
|
1336
1507
|
|
|
1337
1508
|
// src/process-lifecycle.ts
|
|
1338
1509
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
1339
|
-
import { basename } from "path";
|
|
1510
|
+
import { basename as basename2 } from "path";
|
|
1340
1511
|
function parsePsProcessList(output) {
|
|
1341
1512
|
const entries = [];
|
|
1342
1513
|
for (const line of output.split(/\r?\n/)) {
|
|
@@ -1352,11 +1523,11 @@ function parsePsProcessList(output) {
|
|
|
1352
1523
|
}
|
|
1353
1524
|
function invokesCodexBinary(command) {
|
|
1354
1525
|
const tokens = command.trim().split(/\s+/);
|
|
1355
|
-
const exe = tokens[0] ?
|
|
1526
|
+
const exe = tokens[0] ? basename2(tokens[0]) : "";
|
|
1356
1527
|
if (exe === "codex")
|
|
1357
1528
|
return true;
|
|
1358
1529
|
if ((exe === "node" || exe === "bun") && tokens[1]) {
|
|
1359
|
-
return
|
|
1530
|
+
return basename2(tokens[1]) === "codex";
|
|
1360
1531
|
}
|
|
1361
1532
|
return false;
|
|
1362
1533
|
}
|
|
@@ -1492,9 +1663,142 @@ var init_process_lifecycle = __esm(() => {
|
|
|
1492
1663
|
isProcessAlive = pidLooksAlive;
|
|
1493
1664
|
});
|
|
1494
1665
|
|
|
1666
|
+
// src/daemon-record.ts
|
|
1667
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1668
|
+
function writeDaemonRecord(path, record) {
|
|
1669
|
+
atomicWriteJson(path, record);
|
|
1670
|
+
}
|
|
1671
|
+
function sanitizePorts(value) {
|
|
1672
|
+
if (typeof value !== "object" || value === null)
|
|
1673
|
+
return;
|
|
1674
|
+
const raw = value;
|
|
1675
|
+
const ports = {};
|
|
1676
|
+
if (typeof raw.appPort === "number")
|
|
1677
|
+
ports.appPort = raw.appPort;
|
|
1678
|
+
if (typeof raw.proxyPort === "number")
|
|
1679
|
+
ports.proxyPort = raw.proxyPort;
|
|
1680
|
+
if (typeof raw.controlPort === "number")
|
|
1681
|
+
ports.controlPort = raw.controlPort;
|
|
1682
|
+
return Object.keys(ports).length > 0 ? ports : undefined;
|
|
1683
|
+
}
|
|
1684
|
+
function readDaemonRecord(path, read = defaultRead) {
|
|
1685
|
+
let parsed;
|
|
1686
|
+
try {
|
|
1687
|
+
parsed = JSON.parse(read(path));
|
|
1688
|
+
} catch {
|
|
1689
|
+
return null;
|
|
1690
|
+
}
|
|
1691
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
1692
|
+
return null;
|
|
1693
|
+
const obj = parsed;
|
|
1694
|
+
if (typeof obj.pid !== "number" || !Number.isFinite(obj.pid))
|
|
1695
|
+
return null;
|
|
1696
|
+
const phase = obj.phase === "ready" ? "ready" : "booting";
|
|
1697
|
+
const record = { pid: obj.pid, phase };
|
|
1698
|
+
if (typeof obj.startedAt === "number")
|
|
1699
|
+
record.startedAt = obj.startedAt;
|
|
1700
|
+
if (typeof obj.nonce === "string")
|
|
1701
|
+
record.nonce = obj.nonce;
|
|
1702
|
+
if (obj.pairId === null || typeof obj.pairId === "string")
|
|
1703
|
+
record.pairId = obj.pairId;
|
|
1704
|
+
if (obj.cwd === null || typeof obj.cwd === "string")
|
|
1705
|
+
record.cwd = obj.cwd;
|
|
1706
|
+
if (obj.stateDir === null || typeof obj.stateDir === "string")
|
|
1707
|
+
record.stateDir = obj.stateDir;
|
|
1708
|
+
if (typeof obj.proxyUrl === "string")
|
|
1709
|
+
record.proxyUrl = obj.proxyUrl;
|
|
1710
|
+
if (typeof obj.appServerUrl === "string")
|
|
1711
|
+
record.appServerUrl = obj.appServerUrl;
|
|
1712
|
+
const ports = sanitizePorts(obj.ports);
|
|
1713
|
+
if (ports !== undefined)
|
|
1714
|
+
record.ports = ports;
|
|
1715
|
+
if (typeof obj.build === "object" && obj.build !== null) {
|
|
1716
|
+
record.build = obj.build;
|
|
1717
|
+
}
|
|
1718
|
+
if (typeof obj.turnPhase === "string")
|
|
1719
|
+
record.turnPhase = obj.turnPhase;
|
|
1720
|
+
if (typeof obj.turnInProgress === "boolean")
|
|
1721
|
+
record.turnInProgress = obj.turnInProgress;
|
|
1722
|
+
if (typeof obj.attentionWindowActive === "boolean") {
|
|
1723
|
+
record.attentionWindowActive = obj.attentionWindowActive;
|
|
1724
|
+
}
|
|
1725
|
+
return record;
|
|
1726
|
+
}
|
|
1727
|
+
function synthesizeLegacyRecord(pidFilePath, statusFilePath, read = defaultRead) {
|
|
1728
|
+
let pidFromPidFile = null;
|
|
1729
|
+
try {
|
|
1730
|
+
const raw = read(pidFilePath).trim();
|
|
1731
|
+
const n = Number.parseInt(raw, 10);
|
|
1732
|
+
if (Number.isFinite(n))
|
|
1733
|
+
pidFromPidFile = n;
|
|
1734
|
+
} catch {}
|
|
1735
|
+
let status = null;
|
|
1736
|
+
try {
|
|
1737
|
+
const parsed = JSON.parse(read(statusFilePath));
|
|
1738
|
+
if (typeof parsed === "object" && parsed !== null)
|
|
1739
|
+
status = parsed;
|
|
1740
|
+
} catch {}
|
|
1741
|
+
const pidFromStatus = status && typeof status.pid === "number" && Number.isFinite(status.pid) ? status.pid : null;
|
|
1742
|
+
const pid = pidFromPidFile ?? pidFromStatus;
|
|
1743
|
+
if (pid === null)
|
|
1744
|
+
return null;
|
|
1745
|
+
const record = {
|
|
1746
|
+
pid,
|
|
1747
|
+
phase: status ? "ready" : "booting"
|
|
1748
|
+
};
|
|
1749
|
+
if (status) {
|
|
1750
|
+
if (typeof status.proxyUrl === "string")
|
|
1751
|
+
record.proxyUrl = status.proxyUrl;
|
|
1752
|
+
if (typeof status.appServerUrl === "string")
|
|
1753
|
+
record.appServerUrl = status.appServerUrl;
|
|
1754
|
+
const controlPort = typeof status.controlPort === "number" ? status.controlPort : undefined;
|
|
1755
|
+
const proxyPort = portFromUrl(status.proxyUrl);
|
|
1756
|
+
const appPort = portFromUrl(status.appServerUrl);
|
|
1757
|
+
if (controlPort !== undefined || proxyPort !== undefined || appPort !== undefined) {
|
|
1758
|
+
record.ports = {};
|
|
1759
|
+
if (appPort !== undefined)
|
|
1760
|
+
record.ports.appPort = appPort;
|
|
1761
|
+
if (proxyPort !== undefined)
|
|
1762
|
+
record.ports.proxyPort = proxyPort;
|
|
1763
|
+
if (controlPort !== undefined)
|
|
1764
|
+
record.ports.controlPort = controlPort;
|
|
1765
|
+
}
|
|
1766
|
+
if (status.pairId === null || typeof status.pairId === "string")
|
|
1767
|
+
record.pairId = status.pairId;
|
|
1768
|
+
if (status.cwd === null || typeof status.cwd === "string")
|
|
1769
|
+
record.cwd = status.cwd;
|
|
1770
|
+
if (status.stateDir === null || typeof status.stateDir === "string")
|
|
1771
|
+
record.stateDir = status.stateDir;
|
|
1772
|
+
if (typeof status.build === "object" && status.build !== null) {
|
|
1773
|
+
record.build = status.build;
|
|
1774
|
+
}
|
|
1775
|
+
if (typeof status.turnPhase === "string")
|
|
1776
|
+
record.turnPhase = status.turnPhase;
|
|
1777
|
+
if (typeof status.turnInProgress === "boolean")
|
|
1778
|
+
record.turnInProgress = status.turnInProgress;
|
|
1779
|
+
if (typeof status.attentionWindowActive === "boolean") {
|
|
1780
|
+
record.attentionWindowActive = status.attentionWindowActive;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
return record;
|
|
1784
|
+
}
|
|
1785
|
+
function readUnifiedDaemonRecord(paths, read = defaultRead) {
|
|
1786
|
+
return readDaemonRecord(paths.daemonRecordFile, read) ?? synthesizeLegacyRecord(paths.pidFile, paths.statusFile, read);
|
|
1787
|
+
}
|
|
1788
|
+
function portFromUrl(url) {
|
|
1789
|
+
if (typeof url !== "string")
|
|
1790
|
+
return;
|
|
1791
|
+
const match = url.match(/:(\d+)(?:[/?]|$)/);
|
|
1792
|
+
return match ? Number.parseInt(match[1], 10) : undefined;
|
|
1793
|
+
}
|
|
1794
|
+
var defaultRead = (path) => readFileSync4(path, "utf-8");
|
|
1795
|
+
var init_daemon_record = __esm(() => {
|
|
1796
|
+
init_atomic_json();
|
|
1797
|
+
});
|
|
1798
|
+
|
|
1495
1799
|
// src/daemon-lifecycle.ts
|
|
1496
1800
|
import { spawn } from "child_process";
|
|
1497
|
-
import { existsSync as existsSync6, readFileSync as
|
|
1801
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, statSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3, openSync as openSync2, closeSync as closeSync2, constants } from "fs";
|
|
1498
1802
|
import { fileURLToPath } from "url";
|
|
1499
1803
|
function isReuseVerdict(verdict) {
|
|
1500
1804
|
return verdict === "reuse" || verdict === "reuse-despite-drift";
|
|
@@ -1531,22 +1835,33 @@ function classifyDaemon(expectedPairId, status, buildInfo) {
|
|
|
1531
1835
|
reason: "runtime build drift has a compatible contract and a live Codex TUI is attached"
|
|
1532
1836
|
};
|
|
1533
1837
|
}
|
|
1838
|
+
const basis = runtimeContractComparisonBasis(status.build, buildInfo) === "codeHash" ? "compared by codeHash" : "compared by commit stamp; legacy build without codeHash";
|
|
1534
1839
|
return {
|
|
1535
1840
|
verdict: "replace-drifted",
|
|
1536
|
-
reason: `runtime build ${formatBuildInfo(status.build)} does not match launcher
|
|
1841
|
+
reason: `runtime build ${formatBuildInfo(status.build)} does not match launcher ` + `${formatBuildInfo(buildInfo)} (${basis})`
|
|
1537
1842
|
};
|
|
1538
1843
|
}
|
|
1539
1844
|
return { verdict: "reuse", reason: "daemon pair and runtime contract match" };
|
|
1540
1845
|
}
|
|
1846
|
+
function resolveTiming(timing) {
|
|
1847
|
+
return {
|
|
1848
|
+
reuseReadyRetries: timing?.reuseReadyRetries ?? REUSE_READY_RETRIES,
|
|
1849
|
+
reuseReadyDelayMs: timing?.reuseReadyDelayMs ?? REUSE_READY_DELAY_MS,
|
|
1850
|
+
waitReadyRetries: timing?.waitReadyRetries ?? WAIT_READY_RETRIES,
|
|
1851
|
+
waitReadyDelayMs: timing?.waitReadyDelayMs ?? WAIT_READY_DELAY_MS
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1541
1854
|
|
|
1542
1855
|
class DaemonLifecycle {
|
|
1543
1856
|
stateDir;
|
|
1544
1857
|
controlPort;
|
|
1545
1858
|
log;
|
|
1859
|
+
timing;
|
|
1546
1860
|
constructor(opts) {
|
|
1547
1861
|
this.stateDir = opts.stateDir;
|
|
1548
1862
|
this.controlPort = opts.controlPort;
|
|
1549
1863
|
this.log = opts.log;
|
|
1864
|
+
this.timing = resolveTiming(opts.timing);
|
|
1550
1865
|
}
|
|
1551
1866
|
get healthUrl() {
|
|
1552
1867
|
return `http://127.0.0.1:${this.controlPort}/healthz`;
|
|
@@ -1603,7 +1918,7 @@ class DaemonLifecycle {
|
|
|
1603
1918
|
break;
|
|
1604
1919
|
}
|
|
1605
1920
|
try {
|
|
1606
|
-
await this.waitForReady(
|
|
1921
|
+
await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
|
|
1607
1922
|
return;
|
|
1608
1923
|
} catch {
|
|
1609
1924
|
this.log(`Daemon on control port ${this.controlPort} is healthy but not ready within reuse window \u2014 replacing`);
|
|
@@ -1616,7 +1931,7 @@ class DaemonLifecycle {
|
|
|
1616
1931
|
if (isProcessAlive(existingPid)) {
|
|
1617
1932
|
if (isAgentBridgeDaemon(existingPid)) {
|
|
1618
1933
|
try {
|
|
1619
|
-
await this.waitForReady(
|
|
1934
|
+
await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
|
|
1620
1935
|
return;
|
|
1621
1936
|
} catch {
|
|
1622
1937
|
this.log(`Existing daemon process ${existingPid} never became ready \u2014 replacing`);
|
|
@@ -1644,7 +1959,7 @@ class DaemonLifecycle {
|
|
|
1644
1959
|
await this.kill(3000, status?.pid);
|
|
1645
1960
|
} else {
|
|
1646
1961
|
try {
|
|
1647
|
-
await this.waitForReady(
|
|
1962
|
+
await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
|
|
1648
1963
|
return;
|
|
1649
1964
|
} catch {
|
|
1650
1965
|
this.log(`Daemon on control port ${this.controlPort} is healthy but not ready under startup lock \u2014 replacing`);
|
|
@@ -1653,7 +1968,7 @@ class DaemonLifecycle {
|
|
|
1653
1968
|
}
|
|
1654
1969
|
}
|
|
1655
1970
|
this.launch();
|
|
1656
|
-
await this.waitForReady();
|
|
1971
|
+
await this.waitForReady(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
|
|
1657
1972
|
});
|
|
1658
1973
|
}
|
|
1659
1974
|
async isHealthy() {
|
|
@@ -1680,7 +1995,7 @@ class DaemonLifecycle {
|
|
|
1680
1995
|
return false;
|
|
1681
1996
|
}
|
|
1682
1997
|
}
|
|
1683
|
-
async waitForReady(maxRetries =
|
|
1998
|
+
async waitForReady(maxRetries = WAIT_READY_RETRIES, delayMs = WAIT_READY_DELAY_MS) {
|
|
1684
1999
|
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
1685
2000
|
if (await this.isReady())
|
|
1686
2001
|
return;
|
|
@@ -1688,7 +2003,7 @@ class DaemonLifecycle {
|
|
|
1688
2003
|
}
|
|
1689
2004
|
throw new Error(`Timed out waiting for AgentBridge daemon readiness on ${this.readyUrl}`);
|
|
1690
2005
|
}
|
|
1691
|
-
async waitForReadyAndOurs(maxRetries =
|
|
2006
|
+
async waitForReadyAndOurs(maxRetries = WAIT_READY_RETRIES, delayMs = WAIT_READY_DELAY_MS) {
|
|
1692
2007
|
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
1693
2008
|
if (await this.isReady()) {
|
|
1694
2009
|
const status = await this.fetchStatus();
|
|
@@ -1704,22 +2019,35 @@ class DaemonLifecycle {
|
|
|
1704
2019
|
}
|
|
1705
2020
|
throw new Error(`Timed out waiting for AgentBridge daemon readiness+identity on ${this.readyUrl} (control port ${this.controlPort})`);
|
|
1706
2021
|
}
|
|
2022
|
+
readDaemonRecord() {
|
|
2023
|
+
return readUnifiedDaemonRecord({
|
|
2024
|
+
daemonRecordFile: this.stateDir.daemonRecordFile,
|
|
2025
|
+
pidFile: this.stateDir.pidFile,
|
|
2026
|
+
statusFile: this.stateDir.statusFile
|
|
2027
|
+
});
|
|
2028
|
+
}
|
|
2029
|
+
writeDaemonRecord(record) {
|
|
2030
|
+
writeDaemonRecord(this.stateDir.daemonRecordFile, record);
|
|
2031
|
+
}
|
|
2032
|
+
removeDaemonRecord() {
|
|
2033
|
+
try {
|
|
2034
|
+
unlinkSync2(this.stateDir.daemonRecordFile);
|
|
2035
|
+
} catch {}
|
|
2036
|
+
}
|
|
1707
2037
|
readStatus() {
|
|
1708
2038
|
try {
|
|
1709
|
-
const raw =
|
|
2039
|
+
const raw = readFileSync5(this.stateDir.statusFile, "utf-8");
|
|
1710
2040
|
return JSON.parse(raw);
|
|
1711
2041
|
} catch {
|
|
1712
2042
|
return null;
|
|
1713
2043
|
}
|
|
1714
2044
|
}
|
|
1715
2045
|
writeStatus(status) {
|
|
1716
|
-
this.stateDir.
|
|
1717
|
-
writeFileSync4(this.stateDir.statusFile, JSON.stringify(status, null, 2) + `
|
|
1718
|
-
`, "utf-8");
|
|
2046
|
+
atomicWriteJson(this.stateDir.statusFile, status);
|
|
1719
2047
|
}
|
|
1720
2048
|
readPid() {
|
|
1721
2049
|
try {
|
|
1722
|
-
const raw =
|
|
2050
|
+
const raw = readFileSync5(this.stateDir.pidFile, "utf-8").trim();
|
|
1723
2051
|
if (!raw)
|
|
1724
2052
|
return null;
|
|
1725
2053
|
const pid = Number.parseInt(raw, 10);
|
|
@@ -1729,28 +2057,27 @@ class DaemonLifecycle {
|
|
|
1729
2057
|
}
|
|
1730
2058
|
}
|
|
1731
2059
|
writePid(pid) {
|
|
1732
|
-
this.stateDir.
|
|
1733
|
-
|
|
1734
|
-
`, "utf-8");
|
|
2060
|
+
atomicWriteText(this.stateDir.pidFile, `${pid ?? process.pid}
|
|
2061
|
+
`);
|
|
1735
2062
|
}
|
|
1736
2063
|
removePidFile() {
|
|
1737
2064
|
try {
|
|
1738
|
-
|
|
2065
|
+
unlinkSync2(this.stateDir.pidFile);
|
|
1739
2066
|
} catch {}
|
|
1740
2067
|
}
|
|
1741
2068
|
removeStatusFile() {
|
|
1742
2069
|
try {
|
|
1743
|
-
|
|
2070
|
+
unlinkSync2(this.stateDir.statusFile);
|
|
1744
2071
|
} catch {}
|
|
1745
2072
|
}
|
|
1746
2073
|
markKilled() {
|
|
1747
2074
|
this.stateDir.ensure();
|
|
1748
|
-
|
|
2075
|
+
writeFileSync3(this.stateDir.killedFile, `${Date.now()}
|
|
1749
2076
|
`, "utf-8");
|
|
1750
2077
|
}
|
|
1751
2078
|
clearKilled() {
|
|
1752
2079
|
try {
|
|
1753
|
-
|
|
2080
|
+
unlinkSync2(this.stateDir.killedFile);
|
|
1754
2081
|
} catch {}
|
|
1755
2082
|
}
|
|
1756
2083
|
wasKilled() {
|
|
@@ -1772,8 +2099,10 @@ class DaemonLifecycle {
|
|
|
1772
2099
|
daemonProc.unref();
|
|
1773
2100
|
}
|
|
1774
2101
|
removeStalePidFile() {
|
|
1775
|
-
this.log("Removing stale
|
|
2102
|
+
this.log("Removing stale daemon identity files");
|
|
1776
2103
|
this.removePidFile();
|
|
2104
|
+
this.removeStatusFile();
|
|
2105
|
+
this.removeDaemonRecord();
|
|
1777
2106
|
}
|
|
1778
2107
|
async replaceUnhealthyDaemon(statusPid) {
|
|
1779
2108
|
await this.withStartupLockStrict(async (locked) => {
|
|
@@ -1789,7 +2118,7 @@ class DaemonLifecycle {
|
|
|
1789
2118
|
}
|
|
1790
2119
|
if (isReuseVerdict(classification.verdict)) {
|
|
1791
2120
|
try {
|
|
1792
|
-
await this.waitForReady(
|
|
2121
|
+
await this.waitForReady(this.timing.reuseReadyRetries, this.timing.reuseReadyDelayMs);
|
|
1793
2122
|
return;
|
|
1794
2123
|
} catch {}
|
|
1795
2124
|
}
|
|
@@ -1797,12 +2126,12 @@ class DaemonLifecycle {
|
|
|
1797
2126
|
this.log(`Killing unhealthy daemon on control port ${this.controlPort} and relaunching`);
|
|
1798
2127
|
await this.kill(3000, statusPid);
|
|
1799
2128
|
this.launch();
|
|
1800
|
-
await this.waitForReady();
|
|
2129
|
+
await this.waitForReady(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
|
|
1801
2130
|
});
|
|
1802
2131
|
}
|
|
1803
2132
|
async waitForContendedStartupLock() {
|
|
1804
2133
|
this.log("Another process holds the startup lock, waiting for readiness+identity...");
|
|
1805
|
-
await this.waitForReadyAndOurs();
|
|
2134
|
+
await this.waitForReadyAndOurs(this.timing.waitReadyRetries, this.timing.waitReadyDelayMs);
|
|
1806
2135
|
}
|
|
1807
2136
|
async withStartupLockStrict(fn) {
|
|
1808
2137
|
const locked = this.acquireLockStrict();
|
|
@@ -1817,15 +2146,15 @@ class DaemonLifecycle {
|
|
|
1817
2146
|
this.stateDir.ensure();
|
|
1818
2147
|
let fd = null;
|
|
1819
2148
|
try {
|
|
1820
|
-
fd =
|
|
1821
|
-
|
|
2149
|
+
fd = openSync2(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
|
|
2150
|
+
writeFileSync3(fd, `${process.pid}
|
|
1822
2151
|
`);
|
|
1823
|
-
|
|
2152
|
+
closeSync2(fd);
|
|
1824
2153
|
return true;
|
|
1825
2154
|
} catch (err) {
|
|
1826
2155
|
if (fd !== null && err.code !== "EEXIST") {
|
|
1827
2156
|
try {
|
|
1828
|
-
|
|
2157
|
+
closeSync2(fd);
|
|
1829
2158
|
} catch {}
|
|
1830
2159
|
this.releaseLock();
|
|
1831
2160
|
}
|
|
@@ -1833,7 +2162,7 @@ class DaemonLifecycle {
|
|
|
1833
2162
|
if (reclaimed)
|
|
1834
2163
|
return false;
|
|
1835
2164
|
try {
|
|
1836
|
-
const holderPid = Number.parseInt(
|
|
2165
|
+
const holderPid = Number.parseInt(readFileSync5(this.stateDir.lockFile, "utf-8").trim(), 10);
|
|
1837
2166
|
if (Number.isFinite(holderPid) && !isProcessAlive(holderPid)) {
|
|
1838
2167
|
this.log(`Stale startup lock from dead process ${holderPid}, reclaiming`);
|
|
1839
2168
|
this.releaseLock();
|
|
@@ -1862,7 +2191,7 @@ class DaemonLifecycle {
|
|
|
1862
2191
|
}
|
|
1863
2192
|
releaseLock() {
|
|
1864
2193
|
try {
|
|
1865
|
-
|
|
2194
|
+
unlinkSync2(this.stateDir.lockFile);
|
|
1866
2195
|
} catch {}
|
|
1867
2196
|
}
|
|
1868
2197
|
async kill(gracefulTimeoutMs = 3000, pidOverride) {
|
|
@@ -1908,6 +2237,7 @@ class DaemonLifecycle {
|
|
|
1908
2237
|
cleanup() {
|
|
1909
2238
|
this.removePidFile();
|
|
1910
2239
|
this.removeStatusFile();
|
|
2240
|
+
this.removeDaemonRecord();
|
|
1911
2241
|
}
|
|
1912
2242
|
}
|
|
1913
2243
|
async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
|
|
@@ -1919,10 +2249,12 @@ async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
|
|
|
1919
2249
|
clearTimeout(timer);
|
|
1920
2250
|
}
|
|
1921
2251
|
}
|
|
1922
|
-
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;
|
|
2252
|
+
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;
|
|
1923
2253
|
var init_daemon_lifecycle = __esm(() => {
|
|
2254
|
+
init_atomic_json();
|
|
1924
2255
|
init_build_info();
|
|
1925
2256
|
init_process_lifecycle();
|
|
2257
|
+
init_daemon_record();
|
|
1926
2258
|
DEFAULT_DAEMON_ENTRY = import.meta.url.endsWith(".ts") ? "./daemon.ts" : "./daemon.js";
|
|
1927
2259
|
DAEMON_ENTRY = process.env.AGENTBRIDGE_DAEMON_ENTRY || DEFAULT_DAEMON_ENTRY;
|
|
1928
2260
|
DAEMON_PATH = fileURLToPath(new URL(DAEMON_ENTRY, import.meta.url));
|
|
@@ -1933,26 +2265,22 @@ var init_daemon_lifecycle = __esm(() => {
|
|
|
1933
2265
|
// src/pair-registry.ts
|
|
1934
2266
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
1935
2267
|
import {
|
|
1936
|
-
closeSync as closeSync2,
|
|
1937
2268
|
existsSync as existsSync7,
|
|
1938
|
-
fsyncSync,
|
|
1939
2269
|
linkSync,
|
|
1940
2270
|
lstatSync,
|
|
1941
|
-
mkdirSync as
|
|
1942
|
-
openSync as openSync2,
|
|
2271
|
+
mkdirSync as mkdirSync4,
|
|
1943
2272
|
readdirSync,
|
|
1944
|
-
readFileSync as
|
|
2273
|
+
readFileSync as readFileSync6,
|
|
1945
2274
|
realpathSync,
|
|
1946
|
-
renameSync,
|
|
1947
2275
|
rmSync as rmSync2,
|
|
1948
2276
|
statSync as statSync2,
|
|
1949
|
-
unlinkSync as
|
|
1950
|
-
writeFileSync as
|
|
2277
|
+
unlinkSync as unlinkSync3,
|
|
2278
|
+
writeFileSync as writeFileSync4
|
|
1951
2279
|
} from "fs";
|
|
1952
2280
|
import { createServer } from "net";
|
|
1953
|
-
import { createHash, randomUUID } from "crypto";
|
|
2281
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
1954
2282
|
import { hostname, userInfo } from "os";
|
|
1955
|
-
import { basename as
|
|
2283
|
+
import { basename as basename3, join as join6, resolve as resolve3, sep } from "path";
|
|
1956
2284
|
function portsForSlot(slot) {
|
|
1957
2285
|
if (!Number.isInteger(slot) || slot < 0) {
|
|
1958
2286
|
throw new PairError("PAIR_ID_INVALID", `Invalid slot: ${slot}`);
|
|
@@ -2001,7 +2329,7 @@ function readRegistry(base) {
|
|
|
2001
2329
|
return { version: 1, pairs: [] };
|
|
2002
2330
|
let parsed;
|
|
2003
2331
|
try {
|
|
2004
|
-
parsed = JSON.parse(
|
|
2332
|
+
parsed = JSON.parse(readFileSync6(path, "utf-8"));
|
|
2005
2333
|
} catch (err) {
|
|
2006
2334
|
throw new PairError("PAIR_REGISTRY_CORRUPT", `Registry JSON is not parseable at ${path}: ${err.message}`, {
|
|
2007
2335
|
path
|
|
@@ -2032,26 +2360,14 @@ function readRegistry(base) {
|
|
|
2032
2360
|
return parsed;
|
|
2033
2361
|
}
|
|
2034
2362
|
function writeRegistry(base, reg) {
|
|
2035
|
-
|
|
2036
|
-
const target = registryPath(base);
|
|
2037
|
-
const tmp = `${target}.tmp.${process.pid}`;
|
|
2038
|
-
const data = JSON.stringify(reg, null, 2) + `
|
|
2039
|
-
`;
|
|
2040
|
-
const fd = openSync2(tmp, "w");
|
|
2041
|
-
try {
|
|
2042
|
-
writeFileSync5(fd, data);
|
|
2043
|
-
fsyncSync(fd);
|
|
2044
|
-
} finally {
|
|
2045
|
-
closeSync2(fd);
|
|
2046
|
-
}
|
|
2047
|
-
renameSync(tmp, target);
|
|
2363
|
+
atomicWriteJson(registryPath(base), reg, { fsync: true });
|
|
2048
2364
|
}
|
|
2049
2365
|
function lockFilePath(base) {
|
|
2050
2366
|
return join6(pairsDir(base), LOCK_FILE_NAME);
|
|
2051
2367
|
}
|
|
2052
2368
|
function readLockOwner(lockFile) {
|
|
2053
2369
|
try {
|
|
2054
|
-
const parsed = JSON.parse(
|
|
2370
|
+
const parsed = JSON.parse(readFileSync6(lockFile, "utf-8"));
|
|
2055
2371
|
if (typeof parsed.pid === "number" && typeof parsed.nonce === "string")
|
|
2056
2372
|
return parsed;
|
|
2057
2373
|
return null;
|
|
@@ -2091,7 +2407,7 @@ function lockIsStale(lockFile) {
|
|
|
2091
2407
|
}
|
|
2092
2408
|
function attemptReclaim(lockFile) {
|
|
2093
2409
|
const reclaimLock = `${lockFile}.reclaim`;
|
|
2094
|
-
const myNonce =
|
|
2410
|
+
const myNonce = randomUUID2();
|
|
2095
2411
|
const ownerJson = JSON.stringify({
|
|
2096
2412
|
pid: process.pid,
|
|
2097
2413
|
createdAt: Date.now(),
|
|
@@ -2099,10 +2415,10 @@ function attemptReclaim(lockFile) {
|
|
|
2099
2415
|
hostname: safeHostname(),
|
|
2100
2416
|
uid: safeUid()
|
|
2101
2417
|
});
|
|
2102
|
-
const tmp = `${reclaimLock}.acq.${process.pid}.${
|
|
2418
|
+
const tmp = `${reclaimLock}.acq.${process.pid}.${randomUUID2()}`;
|
|
2103
2419
|
let held = false;
|
|
2104
2420
|
try {
|
|
2105
|
-
|
|
2421
|
+
writeFileSync4(tmp, ownerJson);
|
|
2106
2422
|
try {
|
|
2107
2423
|
linkSync(tmp, reclaimLock);
|
|
2108
2424
|
held = true;
|
|
@@ -2110,7 +2426,7 @@ function attemptReclaim(lockFile) {
|
|
|
2110
2426
|
if (err?.code === "EEXIST") {
|
|
2111
2427
|
if (lockIsStale(reclaimLock)) {
|
|
2112
2428
|
try {
|
|
2113
|
-
|
|
2429
|
+
unlinkSync3(reclaimLock);
|
|
2114
2430
|
} catch {}
|
|
2115
2431
|
}
|
|
2116
2432
|
return;
|
|
@@ -2119,7 +2435,7 @@ function attemptReclaim(lockFile) {
|
|
|
2119
2435
|
}
|
|
2120
2436
|
} finally {
|
|
2121
2437
|
try {
|
|
2122
|
-
|
|
2438
|
+
unlinkSync3(tmp);
|
|
2123
2439
|
} catch {}
|
|
2124
2440
|
}
|
|
2125
2441
|
if (!held)
|
|
@@ -2129,22 +2445,22 @@ function attemptReclaim(lockFile) {
|
|
|
2129
2445
|
return;
|
|
2130
2446
|
if (lockIsStale(lockFile)) {
|
|
2131
2447
|
try {
|
|
2132
|
-
|
|
2448
|
+
unlinkSync3(lockFile);
|
|
2133
2449
|
} catch {}
|
|
2134
2450
|
}
|
|
2135
2451
|
} finally {
|
|
2136
2452
|
if (readLockOwner(reclaimLock)?.nonce === myNonce) {
|
|
2137
2453
|
try {
|
|
2138
|
-
|
|
2454
|
+
unlinkSync3(reclaimLock);
|
|
2139
2455
|
} catch {}
|
|
2140
2456
|
}
|
|
2141
2457
|
}
|
|
2142
2458
|
}
|
|
2143
2459
|
async function withRegistryLock(base, fn) {
|
|
2144
|
-
|
|
2460
|
+
mkdirSync4(pairsDir(base), { recursive: true });
|
|
2145
2461
|
const lockFile = lockFilePath(base);
|
|
2146
2462
|
const deadline = Date.now() + LOCK_DEADLINE_MS;
|
|
2147
|
-
const myNonce =
|
|
2463
|
+
const myNonce = randomUUID2();
|
|
2148
2464
|
const ownerJson = JSON.stringify({
|
|
2149
2465
|
pid: process.pid,
|
|
2150
2466
|
createdAt: Date.now(),
|
|
@@ -2153,10 +2469,10 @@ async function withRegistryLock(base, fn) {
|
|
|
2153
2469
|
uid: safeUid()
|
|
2154
2470
|
});
|
|
2155
2471
|
for (;; ) {
|
|
2156
|
-
const tmp = `${lockFile}.acq.${process.pid}.${
|
|
2472
|
+
const tmp = `${lockFile}.acq.${process.pid}.${randomUUID2()}`;
|
|
2157
2473
|
let acquired = false;
|
|
2158
2474
|
try {
|
|
2159
|
-
|
|
2475
|
+
writeFileSync4(tmp, ownerJson);
|
|
2160
2476
|
try {
|
|
2161
2477
|
linkSync(tmp, lockFile);
|
|
2162
2478
|
acquired = true;
|
|
@@ -2166,7 +2482,7 @@ async function withRegistryLock(base, fn) {
|
|
|
2166
2482
|
}
|
|
2167
2483
|
} finally {
|
|
2168
2484
|
try {
|
|
2169
|
-
|
|
2485
|
+
unlinkSync3(tmp);
|
|
2170
2486
|
} catch {}
|
|
2171
2487
|
}
|
|
2172
2488
|
if (acquired) {
|
|
@@ -2176,7 +2492,7 @@ async function withRegistryLock(base, fn) {
|
|
|
2176
2492
|
const current = readLockOwner(lockFile);
|
|
2177
2493
|
if (!current || current.nonce === myNonce) {
|
|
2178
2494
|
try {
|
|
2179
|
-
|
|
2495
|
+
unlinkSync3(lockFile);
|
|
2180
2496
|
} catch {}
|
|
2181
2497
|
}
|
|
2182
2498
|
}
|
|
@@ -2198,7 +2514,7 @@ function detectLegacyRootDaemon(base) {
|
|
|
2198
2514
|
return null;
|
|
2199
2515
|
let pid;
|
|
2200
2516
|
try {
|
|
2201
|
-
const raw =
|
|
2517
|
+
const raw = readFileSync6(rootPidFile, "utf-8").trim();
|
|
2202
2518
|
pid = Number.parseInt(raw, 10);
|
|
2203
2519
|
} catch {
|
|
2204
2520
|
return null;
|
|
@@ -2306,6 +2622,9 @@ async function removeAllocatedPairIfUnchanged(base, pairId, slot) {
|
|
|
2306
2622
|
writeRegistry(base, { version: 1, pairs: nextPairs });
|
|
2307
2623
|
});
|
|
2308
2624
|
}
|
|
2625
|
+
function pairsRootDir(base) {
|
|
2626
|
+
return pairsDir(base);
|
|
2627
|
+
}
|
|
2309
2628
|
function pairDirPath(base, pairId) {
|
|
2310
2629
|
const id = validatePairId(pairId);
|
|
2311
2630
|
return join6(pairsDir(base), id);
|
|
@@ -2348,12 +2667,17 @@ function pairDirDaemonAlive(base, pairId) {
|
|
|
2348
2667
|
const dir = join6(pairsDir(base), pairId);
|
|
2349
2668
|
const pids = [];
|
|
2350
2669
|
try {
|
|
2351
|
-
const
|
|
2670
|
+
const record = JSON.parse(readFileSync6(join6(dir, "daemon.json"), "utf-8"));
|
|
2671
|
+
if (typeof record?.pid === "number" && Number.isFinite(record.pid))
|
|
2672
|
+
pids.push(record.pid);
|
|
2673
|
+
} catch {}
|
|
2674
|
+
try {
|
|
2675
|
+
const pid = Number.parseInt(readFileSync6(join6(dir, "daemon.pid"), "utf-8").trim(), 10);
|
|
2352
2676
|
if (Number.isFinite(pid))
|
|
2353
2677
|
pids.push(pid);
|
|
2354
2678
|
} catch {}
|
|
2355
2679
|
try {
|
|
2356
|
-
const status = JSON.parse(
|
|
2680
|
+
const status = JSON.parse(readFileSync6(join6(dir, "status.json"), "utf-8"));
|
|
2357
2681
|
if (typeof status?.pid === "number")
|
|
2358
2682
|
pids.push(status.pid);
|
|
2359
2683
|
} catch {}
|
|
@@ -2387,10 +2711,54 @@ async function removeUnregisteredPairDir(base, pairId) {
|
|
|
2387
2711
|
return { removed: removePairDir(base, pairId) };
|
|
2388
2712
|
});
|
|
2389
2713
|
}
|
|
2390
|
-
|
|
2714
|
+
async function removeOrphanPairDirIgnoringRegistry(base, pairId) {
|
|
2715
|
+
return withRegistryLock(base, () => {
|
|
2716
|
+
if (pairDirDaemonAlive(base, pairId)) {
|
|
2717
|
+
return { removed: false, reason: "live" };
|
|
2718
|
+
}
|
|
2719
|
+
return { removed: removePairDir(base, pairId) };
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2722
|
+
function isEntryReclaimable(signals) {
|
|
2723
|
+
return signals.cwdGone && signals.dead && signals.old;
|
|
2724
|
+
}
|
|
2725
|
+
function cwdMissing(cwd) {
|
|
2726
|
+
try {
|
|
2727
|
+
statSync2(cwd);
|
|
2728
|
+
return false;
|
|
2729
|
+
} catch (err) {
|
|
2730
|
+
return err?.code === "ENOENT";
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
function parseCreatedAtMs(createdAt) {
|
|
2734
|
+
if (typeof createdAt !== "string")
|
|
2735
|
+
return null;
|
|
2736
|
+
const ms = Date.parse(createdAt);
|
|
2737
|
+
return Number.isFinite(ms) ? ms : null;
|
|
2738
|
+
}
|
|
2739
|
+
function classifyReclaimableEntries(base, now = Date.now()) {
|
|
2740
|
+
const reg = readRegistry(base);
|
|
2741
|
+
const out = [];
|
|
2742
|
+
for (const entry of reg.pairs) {
|
|
2743
|
+
const createdMs = parseCreatedAtMs(entry.createdAt);
|
|
2744
|
+
const ageMs = createdMs === null ? null : Math.max(0, now - createdMs);
|
|
2745
|
+
const signals = {
|
|
2746
|
+
cwdGone: cwdMissing(entry.cwd),
|
|
2747
|
+
dead: !pairDirDaemonAlive(base, entry.pairId),
|
|
2748
|
+
old: ageMs !== null && ageMs >= RECLAIMABLE_MIN_AGE_MS,
|
|
2749
|
+
ageMs
|
|
2750
|
+
};
|
|
2751
|
+
if (isEntryReclaimable(signals))
|
|
2752
|
+
out.push({ entry, signals });
|
|
2753
|
+
}
|
|
2754
|
+
return out;
|
|
2755
|
+
}
|
|
2756
|
+
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;
|
|
2391
2757
|
var init_pair_registry = __esm(() => {
|
|
2758
|
+
init_atomic_json();
|
|
2392
2759
|
init_process_lifecycle();
|
|
2393
2760
|
PAIR_ID_REGEX = /^[A-Za-z0-9._-]{1,64}$/;
|
|
2761
|
+
RECLAIMABLE_MIN_AGE_MS = 24 * 60 * 60 * 1000;
|
|
2394
2762
|
WINDOWS_RESERVED_RE = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
|
2395
2763
|
PairError = class PairError extends Error {
|
|
2396
2764
|
code;
|
|
@@ -2673,7 +3041,7 @@ var init_pair_resolver = __esm(() => {
|
|
|
2673
3041
|
});
|
|
2674
3042
|
|
|
2675
3043
|
// src/trace-log.ts
|
|
2676
|
-
import { appendFileSync, existsSync as existsSync8, mkdirSync as
|
|
3044
|
+
import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync4 } from "fs";
|
|
2677
3045
|
import { join as join8 } from "path";
|
|
2678
3046
|
function pickRelevantEnv(env) {
|
|
2679
3047
|
const picked = {};
|
|
@@ -2725,7 +3093,7 @@ function appendTraceEvent(input) {
|
|
|
2725
3093
|
};
|
|
2726
3094
|
const logsDir = join8(input.cwd, ".agentbridge", "logs");
|
|
2727
3095
|
const isNewDayFile = !existsSync8(path);
|
|
2728
|
-
|
|
3096
|
+
mkdirSync5(logsDir, { recursive: true });
|
|
2729
3097
|
if (isNewDayFile) {
|
|
2730
3098
|
pruneOldTraceLogs(logsDir, path, Date.parse(timestamp));
|
|
2731
3099
|
}
|
|
@@ -2751,7 +3119,7 @@ function pruneOldTraceLogs(logsDir, keepPath, nowMs) {
|
|
|
2751
3119
|
continue;
|
|
2752
3120
|
try {
|
|
2753
3121
|
if (statSync3(filePath).mtimeMs < cutoff) {
|
|
2754
|
-
|
|
3122
|
+
unlinkSync4(filePath);
|
|
2755
3123
|
}
|
|
2756
3124
|
} catch {}
|
|
2757
3125
|
}
|
|
@@ -2834,10 +3202,12 @@ var exports_claude = {};
|
|
|
2834
3202
|
__export(exports_claude, {
|
|
2835
3203
|
warnIfPluginCacheMissing: () => warnIfPluginCacheMissing,
|
|
2836
3204
|
runClaude: () => runClaude,
|
|
3205
|
+
mapChildExitCode: () => mapChildExitCode,
|
|
2837
3206
|
checkOwnedFlagConflicts: () => checkOwnedFlagConflicts
|
|
2838
3207
|
});
|
|
2839
3208
|
import { spawn as spawn2 } from "child_process";
|
|
2840
3209
|
import { existsSync as existsSync9 } from "fs";
|
|
3210
|
+
import { constants as osConstants } from "os";
|
|
2841
3211
|
async function runClaude(args) {
|
|
2842
3212
|
const originalEnv = { ...process.env };
|
|
2843
3213
|
const envGuardResult = guardAgentBridgeEnv({
|
|
@@ -2890,8 +3260,8 @@ async function runClaude(args) {
|
|
|
2890
3260
|
stdio: "inherit",
|
|
2891
3261
|
env: process.env
|
|
2892
3262
|
});
|
|
2893
|
-
child.on("exit", (code) => {
|
|
2894
|
-
process.exit(code
|
|
3263
|
+
child.on("exit", (code, signal) => {
|
|
3264
|
+
process.exit(mapChildExitCode(code, signal));
|
|
2895
3265
|
});
|
|
2896
3266
|
child.on("error", (err) => {
|
|
2897
3267
|
if (err.code === "ENOENT") {
|
|
@@ -2903,6 +3273,12 @@ async function runClaude(args) {
|
|
|
2903
3273
|
process.exit(1);
|
|
2904
3274
|
});
|
|
2905
3275
|
}
|
|
3276
|
+
function mapChildExitCode(code, signal) {
|
|
3277
|
+
if (signal) {
|
|
3278
|
+
return 128 + (osConstants.signals[signal] ?? 0);
|
|
3279
|
+
}
|
|
3280
|
+
return code ?? 0;
|
|
3281
|
+
}
|
|
2906
3282
|
function warnIfPluginCacheMissing(cacheRoot = pluginCacheRoot(), log = (msg) => console.error(msg)) {
|
|
2907
3283
|
let cacheExists;
|
|
2908
3284
|
try {
|
|
@@ -3003,7 +3379,7 @@ var init_claude = __esm(() => {
|
|
|
3003
3379
|
});
|
|
3004
3380
|
|
|
3005
3381
|
// src/agents-contract.ts
|
|
3006
|
-
import { existsSync as existsSync10, readFileSync as
|
|
3382
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
|
|
3007
3383
|
import { join as join9 } from "path";
|
|
3008
3384
|
function checkAgentsMdContract(cwd) {
|
|
3009
3385
|
const path = join9(cwd, "AGENTS.md");
|
|
@@ -3011,7 +3387,7 @@ function checkAgentsMdContract(cwd) {
|
|
|
3011
3387
|
let content = "";
|
|
3012
3388
|
if (exists) {
|
|
3013
3389
|
try {
|
|
3014
|
-
content =
|
|
3390
|
+
content = readFileSync7(path, "utf-8");
|
|
3015
3391
|
} catch {
|
|
3016
3392
|
return {
|
|
3017
3393
|
fresh: false,
|
|
@@ -3038,7 +3414,7 @@ function isFreshAgentsMdContract(content) {
|
|
|
3038
3414
|
var init_agents_contract = () => {};
|
|
3039
3415
|
|
|
3040
3416
|
// src/wrapper-exit-observability.ts
|
|
3041
|
-
import { readFileSync as
|
|
3417
|
+
import { readFileSync as readFileSync8, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
3042
3418
|
import { join as join10 } from "path";
|
|
3043
3419
|
function discoverNativeChildPid(launcherPid, run) {
|
|
3044
3420
|
try {
|
|
@@ -3049,17 +3425,15 @@ function discoverNativeChildPid(launcherPid, run) {
|
|
|
3049
3425
|
return null;
|
|
3050
3426
|
}
|
|
3051
3427
|
}
|
|
3052
|
-
function
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
if (typeof status.turnInProgress !== "boolean")
|
|
3056
|
-
return null;
|
|
3057
|
-
if (typeof status.pid === "number" && !isPidAlive(status.pid))
|
|
3058
|
-
return null;
|
|
3059
|
-
return status.turnInProgress;
|
|
3060
|
-
} catch {
|
|
3428
|
+
function readUnifiedTurnInProgress(paths, read = (p) => readFileSync8(p, "utf-8"), isPidAlive = defaultIsPidAlive) {
|
|
3429
|
+
const record = readUnifiedDaemonRecord(paths, read);
|
|
3430
|
+
if (!record)
|
|
3061
3431
|
return null;
|
|
3062
|
-
|
|
3432
|
+
if (typeof record.turnInProgress !== "boolean")
|
|
3433
|
+
return null;
|
|
3434
|
+
if (typeof record.pid === "number" && !isPidAlive(record.pid))
|
|
3435
|
+
return null;
|
|
3436
|
+
return record.turnInProgress;
|
|
3063
3437
|
}
|
|
3064
3438
|
function refineCleanExitClassification(turnInProgress) {
|
|
3065
3439
|
if (turnInProgress === true)
|
|
@@ -3068,14 +3442,14 @@ function refineCleanExitClassification(turnInProgress) {
|
|
|
3068
3442
|
return "exit_0_idle";
|
|
3069
3443
|
return "exit_0_turn_unknown";
|
|
3070
3444
|
}
|
|
3071
|
-
function findCodexSqliteLog(codexHome,
|
|
3445
|
+
function findCodexSqliteLog(codexHome, fs2 = { readdir: readdirSync3, stat: statSync4 }) {
|
|
3072
3446
|
try {
|
|
3073
|
-
const entries =
|
|
3447
|
+
const entries = fs2.readdir(codexHome).filter((name) => /^logs.*\.sqlite$/.test(String(name)));
|
|
3074
3448
|
let best = null;
|
|
3075
3449
|
for (const name of entries) {
|
|
3076
3450
|
const path = join10(codexHome, String(name));
|
|
3077
3451
|
try {
|
|
3078
|
-
const mtime =
|
|
3452
|
+
const mtime = fs2.stat(path).mtimeMs;
|
|
3079
3453
|
if (!best || mtime > best.mtime)
|
|
3080
3454
|
best = { path, mtime };
|
|
3081
3455
|
} catch {}
|
|
@@ -3110,14 +3484,15 @@ function captureTuiLogTail(options) {
|
|
|
3110
3484
|
var defaultIsPidAlive;
|
|
3111
3485
|
var init_wrapper_exit_observability = __esm(() => {
|
|
3112
3486
|
init_process_lifecycle();
|
|
3487
|
+
init_daemon_record();
|
|
3113
3488
|
defaultIsPidAlive = pidLooksAlive;
|
|
3114
3489
|
});
|
|
3115
3490
|
|
|
3116
3491
|
// src/pair-command.ts
|
|
3117
|
-
function pairScopedCommand(cmd) {
|
|
3492
|
+
function pairScopedCommand(cmd, name = cliInvocationName()) {
|
|
3118
3493
|
const pairId = process.env.AGENTBRIDGE_PAIR_ID;
|
|
3119
3494
|
if (!pairId)
|
|
3120
|
-
return
|
|
3495
|
+
return `${name} ${cmd}`;
|
|
3121
3496
|
let selector = process.env.AGENTBRIDGE_PAIR_NAME;
|
|
3122
3497
|
if (!selector) {
|
|
3123
3498
|
try {
|
|
@@ -3126,19 +3501,20 @@ function pairScopedCommand(cmd) {
|
|
|
3126
3501
|
selector = pairId;
|
|
3127
3502
|
}
|
|
3128
3503
|
}
|
|
3129
|
-
return
|
|
3504
|
+
return `${name} --pair ${selector} ${cmd}`;
|
|
3130
3505
|
}
|
|
3131
3506
|
var init_pair_command = __esm(() => {
|
|
3507
|
+
init_cli_invocation();
|
|
3132
3508
|
init_pair_resolver();
|
|
3133
3509
|
});
|
|
3134
3510
|
|
|
3135
3511
|
// src/rotating-log.ts
|
|
3136
|
-
import { appendFileSync as appendFileSync2, existsSync as existsSync11, renameSync as renameSync2, statSync as statSync5, unlinkSync as
|
|
3137
|
-
import { dirname as
|
|
3512
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync11, renameSync as renameSync2, statSync as statSync5, unlinkSync as unlinkSync5 } from "fs";
|
|
3513
|
+
import { dirname as dirname3 } from "path";
|
|
3138
3514
|
function appendRotatingLog(path, content, options = {}, fsOps = REAL_FS_OPS) {
|
|
3139
3515
|
const maxBytes = options.maxBytes ?? positiveIntFromEnv("AGENTBRIDGE_LOG_MAX_BYTES", DEFAULT_MAX_BYTES);
|
|
3140
3516
|
const keep = options.keep ?? positiveIntFromEnv("AGENTBRIDGE_LOG_ROTATE_KEEP", DEFAULT_KEEP);
|
|
3141
|
-
if (!fsOps.existsSync(
|
|
3517
|
+
if (!fsOps.existsSync(dirname3(path)))
|
|
3142
3518
|
return;
|
|
3143
3519
|
rotateIfNeeded(path, Buffer.byteLength(content), maxBytes, keep, fsOps);
|
|
3144
3520
|
fsOps.appendFileSync(path, content, "utf-8");
|
|
@@ -3196,7 +3572,7 @@ function rotateIfNeeded(path, incomingBytes, maxBytes, keep, fsOps) {
|
|
|
3196
3572
|
var DEFAULT_MAX_BYTES, DEFAULT_KEEP = 3, REAL_FS_OPS;
|
|
3197
3573
|
var init_rotating_log = __esm(() => {
|
|
3198
3574
|
DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
3199
|
-
REAL_FS_OPS = { statSync: statSync5, renameSync: renameSync2, unlinkSync:
|
|
3575
|
+
REAL_FS_OPS = { statSync: statSync5, renameSync: renameSync2, unlinkSync: unlinkSync5, appendFileSync: appendFileSync2, existsSync: existsSync11 };
|
|
3200
3576
|
});
|
|
3201
3577
|
|
|
3202
3578
|
// src/stderr-ring-buffer.ts
|
|
@@ -3250,30 +3626,20 @@ var init_stderr_ring_buffer = __esm(() => {
|
|
|
3250
3626
|
// src/thread-state.ts
|
|
3251
3627
|
import {
|
|
3252
3628
|
existsSync as existsSync12,
|
|
3253
|
-
mkdirSync as mkdirSync5,
|
|
3254
3629
|
readdirSync as readdirSync4,
|
|
3255
|
-
readFileSync as
|
|
3256
|
-
renameSync as renameSync3,
|
|
3257
|
-
writeFileSync as writeFileSync6
|
|
3630
|
+
readFileSync as readFileSync9
|
|
3258
3631
|
} from "fs";
|
|
3259
3632
|
import { homedir as homedir3 } from "os";
|
|
3260
|
-
import { basename as
|
|
3633
|
+
import { basename as basename4, join as join11 } from "path";
|
|
3261
3634
|
function nowIso() {
|
|
3262
3635
|
return new Date().toISOString();
|
|
3263
3636
|
}
|
|
3264
3637
|
function codexHome(env = process.env) {
|
|
3265
3638
|
return env.CODEX_HOME && env.CODEX_HOME.length > 0 ? env.CODEX_HOME : join11(homedir3(), ".codex");
|
|
3266
3639
|
}
|
|
3267
|
-
function atomicWriteJson(path, value) {
|
|
3268
|
-
mkdirSync5(dirname3(path), { recursive: true });
|
|
3269
|
-
const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
|
|
3270
|
-
writeFileSync6(tmp, JSON.stringify(value, null, 2) + `
|
|
3271
|
-
`, "utf-8");
|
|
3272
|
-
renameSync3(tmp, path);
|
|
3273
|
-
}
|
|
3274
3640
|
function readRawCurrentThread(stateDir) {
|
|
3275
3641
|
try {
|
|
3276
|
-
const parsed = JSON.parse(
|
|
3642
|
+
const parsed = JSON.parse(readFileSync9(stateDir.currentThreadFile, "utf-8"));
|
|
3277
3643
|
if (parsed?.version === 1 && typeof parsed.threadId === "string" && parsed.threadId.length > 0 && (parsed.status === "pending" || parsed.status === "current") && typeof parsed.cwd === "string") {
|
|
3278
3644
|
return parsed;
|
|
3279
3645
|
}
|
|
@@ -3304,7 +3670,7 @@ function findCodexRolloutFile(threadId, env = process.env, maxEntries = 20000) {
|
|
|
3304
3670
|
}
|
|
3305
3671
|
if (!entry.isFile())
|
|
3306
3672
|
continue;
|
|
3307
|
-
const name =
|
|
3673
|
+
const name = basename4(entry.name);
|
|
3308
3674
|
if (name === exactName || name.startsWith("rollout-") && name.endsWith(".jsonl") && name.includes(threadId)) {
|
|
3309
3675
|
return path;
|
|
3310
3676
|
}
|
|
@@ -3316,12 +3682,26 @@ function readUsableCurrentThread(identity, env = process.env) {
|
|
|
3316
3682
|
const state = readRawCurrentThread(identity.stateDir);
|
|
3317
3683
|
if (!state)
|
|
3318
3684
|
return null;
|
|
3319
|
-
if (state.status !== "current")
|
|
3320
|
-
return null;
|
|
3321
3685
|
if (state.pairId !== identity.pairId)
|
|
3322
3686
|
return null;
|
|
3323
3687
|
if (state.cwd !== identity.cwd)
|
|
3324
3688
|
return null;
|
|
3689
|
+
if (state.status === "pending") {
|
|
3690
|
+
const rolloutPath2 = findCodexRolloutFile(state.threadId, env);
|
|
3691
|
+
if (!rolloutPath2)
|
|
3692
|
+
return null;
|
|
3693
|
+
const promoted = {
|
|
3694
|
+
...state,
|
|
3695
|
+
status: "current",
|
|
3696
|
+
rolloutPath: rolloutPath2,
|
|
3697
|
+
rolloutVerifiedAt: nowIso(),
|
|
3698
|
+
updatedAt: nowIso()
|
|
3699
|
+
};
|
|
3700
|
+
try {
|
|
3701
|
+
atomicWriteJson(identity.stateDir.currentThreadFile, promoted);
|
|
3702
|
+
} catch {}
|
|
3703
|
+
return promoted;
|
|
3704
|
+
}
|
|
3325
3705
|
if (state.rolloutPath && existsSync12(state.rolloutPath))
|
|
3326
3706
|
return state;
|
|
3327
3707
|
const rolloutPath = findCodexRolloutFile(state.threadId, env);
|
|
@@ -3336,7 +3716,9 @@ function readUsableCurrentThread(identity, env = process.env) {
|
|
|
3336
3716
|
atomicWriteJson(identity.stateDir.currentThreadFile, repaired);
|
|
3337
3717
|
return repaired;
|
|
3338
3718
|
}
|
|
3339
|
-
var init_thread_state = () => {
|
|
3719
|
+
var init_thread_state = __esm(() => {
|
|
3720
|
+
init_atomic_json();
|
|
3721
|
+
});
|
|
3340
3722
|
|
|
3341
3723
|
// src/cli/codex.ts
|
|
3342
3724
|
var exports_codex = {};
|
|
@@ -3351,9 +3733,9 @@ import {
|
|
|
3351
3733
|
openSync as openSync3,
|
|
3352
3734
|
writeSync,
|
|
3353
3735
|
closeSync as closeSync3,
|
|
3354
|
-
writeFileSync as
|
|
3355
|
-
readFileSync as
|
|
3356
|
-
unlinkSync as
|
|
3736
|
+
writeFileSync as writeFileSync5,
|
|
3737
|
+
readFileSync as readFileSync10,
|
|
3738
|
+
unlinkSync as unlinkSync6,
|
|
3357
3739
|
existsSync as existsSync13,
|
|
3358
3740
|
mkdirSync as mkdirSync6
|
|
3359
3741
|
} from "fs";
|
|
@@ -3413,10 +3795,12 @@ function resolveCodexResumeArgs(parsed, pair, env = process.env) {
|
|
|
3413
3795
|
const current = readUsableCurrentThread(identity, env);
|
|
3414
3796
|
if (parsed.resumeCurrent) {
|
|
3415
3797
|
if (!current) {
|
|
3798
|
+
const raw = readRawCurrentThread(identity.stateDir);
|
|
3799
|
+
const pending = raw && raw.status === "pending" && raw.pairId === identity.pairId && raw.cwd === identity.cwd ? raw : null;
|
|
3416
3800
|
return {
|
|
3417
3801
|
rest: parsed.rest,
|
|
3418
3802
|
mode: "resume-current",
|
|
3419
|
-
error: "No verified current Codex thread for this pair. Start a new one with `abg codex --new`, or resume a specific thread with `abg codex resume <threadId>`."
|
|
3803
|
+
error: pending ? `No verified current Codex thread for this pair. Found a pending (unverified) thread ${pending.threadId} \u2014 its Codex rollout file was not found. Try \`abg codex resume ${pending.threadId}\`, or start fresh with \`abg codex --new\`.` : "No verified current Codex thread for this pair. Start a new one with `abg codex --new`, or resume a specific thread with `abg codex resume <threadId>`."
|
|
3420
3804
|
};
|
|
3421
3805
|
}
|
|
3422
3806
|
return {
|
|
@@ -3525,9 +3909,9 @@ async function runCodex(args) {
|
|
|
3525
3909
|
process.exit(1);
|
|
3526
3910
|
}
|
|
3527
3911
|
let proxyUrl;
|
|
3528
|
-
const
|
|
3529
|
-
if (
|
|
3530
|
-
proxyUrl =
|
|
3912
|
+
const record = lifecycle.readDaemonRecord();
|
|
3913
|
+
if (typeof record?.proxyUrl === "string" && record.proxyUrl.length > 0) {
|
|
3914
|
+
proxyUrl = record.proxyUrl;
|
|
3531
3915
|
} else {
|
|
3532
3916
|
const fallbackProxyPort = process.env.CODEX_PROXY_PORT ?? String(new ConfigService().loadOrDefault((msg) => console.error(`[agentbridge] ${msg}`)).codex.proxyPort);
|
|
3533
3917
|
proxyUrl = `ws://127.0.0.1:${fallbackProxyPort}`;
|
|
@@ -3609,7 +3993,7 @@ async function runCodex(args) {
|
|
|
3609
3993
|
env: buildChildEnv()
|
|
3610
3994
|
});
|
|
3611
3995
|
if (typeof child.pid === "number") {
|
|
3612
|
-
|
|
3996
|
+
writeFileSync5(stateDir.tuiPidFile, `${child.pid}
|
|
3613
3997
|
`, "utf-8");
|
|
3614
3998
|
appendWrapperLog(wrapperLogPath, `child pid=${child.pid}`);
|
|
3615
3999
|
}
|
|
@@ -3649,7 +4033,7 @@ async function runCodex(args) {
|
|
|
3649
4033
|
return;
|
|
3650
4034
|
cleanedTuiPid = true;
|
|
3651
4035
|
try {
|
|
3652
|
-
|
|
4036
|
+
unlinkSync6(stateDir.tuiPidFile);
|
|
3653
4037
|
} catch {}
|
|
3654
4038
|
}
|
|
3655
4039
|
function requestChildTermination(reason) {
|
|
@@ -3720,7 +4104,11 @@ async function runCodex(args) {
|
|
|
3720
4104
|
else if (typeof code === "number" && code !== 0)
|
|
3721
4105
|
classification = `nonzero_exit:${code}`;
|
|
3722
4106
|
else if (code === 0 && tail.trim().length === 0) {
|
|
3723
|
-
classification = refineCleanExitClassification(
|
|
4107
|
+
classification = refineCleanExitClassification(readUnifiedTurnInProgress({
|
|
4108
|
+
daemonRecordFile: stateDir.daemonRecordFile,
|
|
4109
|
+
pidFile: stateDir.pidFile,
|
|
4110
|
+
statusFile: stateDir.statusFile
|
|
4111
|
+
}));
|
|
3724
4112
|
}
|
|
3725
4113
|
const tuiLogTail = captureTuiLogTail({
|
|
3726
4114
|
codexHome: join12(homedir4(), ".codex"),
|
|
@@ -3779,12 +4167,12 @@ function guardNoLiveManagedTui(stateDir, proxyUrl) {
|
|
|
3779
4167
|
if (pid) {
|
|
3780
4168
|
if (!isProcessAlive(pid)) {
|
|
3781
4169
|
try {
|
|
3782
|
-
|
|
4170
|
+
unlinkSync6(stateDir.tuiPidFile);
|
|
3783
4171
|
} catch {}
|
|
3784
4172
|
} else if (!isManagedCodexTuiProcess(pid, proxyUrl)) {
|
|
3785
4173
|
appendWrapperLog(stateDir.codexWrapperLogFile, `stale tui pid file pointed at unmanaged live pid=${pid}; removing`);
|
|
3786
4174
|
try {
|
|
3787
|
-
|
|
4175
|
+
unlinkSync6(stateDir.tuiPidFile);
|
|
3788
4176
|
} catch {}
|
|
3789
4177
|
} else {
|
|
3790
4178
|
console.error(`[agentbridge] This pair already has a managed Codex TUI running (pid ${pid}).`);
|
|
@@ -3801,7 +4189,7 @@ function guardNoLiveManagedTui(stateDir, proxyUrl) {
|
|
|
3801
4189
|
}
|
|
3802
4190
|
function readTuiPid(stateDir) {
|
|
3803
4191
|
try {
|
|
3804
|
-
const raw =
|
|
4192
|
+
const raw = readFileSync10(stateDir.tuiPidFile, "utf-8").trim();
|
|
3805
4193
|
if (!raw)
|
|
3806
4194
|
return null;
|
|
3807
4195
|
const pid = Number.parseInt(raw, 10);
|
|
@@ -3815,7 +4203,12 @@ function isManagedCodexTuiProcess(pid, proxyUrl) {
|
|
|
3815
4203
|
return cmd !== null && commandMatchesManagedCodexTui(cmd, proxyUrl);
|
|
3816
4204
|
}
|
|
3817
4205
|
function proxyHealthUrl(proxyUrl) {
|
|
3818
|
-
|
|
4206
|
+
let url;
|
|
4207
|
+
try {
|
|
4208
|
+
url = new URL(proxyUrl);
|
|
4209
|
+
} catch {
|
|
4210
|
+
throw new Error(`Malformed Codex proxy URL: ${JSON.stringify(proxyUrl)}`);
|
|
4211
|
+
}
|
|
3819
4212
|
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
3820
4213
|
url.pathname = "/healthz";
|
|
3821
4214
|
url.search = "";
|
|
@@ -4010,7 +4403,7 @@ __export(exports_kill, {
|
|
|
4010
4403
|
runKill: () => runKill,
|
|
4011
4404
|
formatKillReport: () => formatKillReport
|
|
4012
4405
|
});
|
|
4013
|
-
import { readFileSync as
|
|
4406
|
+
import { readFileSync as readFileSync11, unlinkSync as unlinkSync7 } from "fs";
|
|
4014
4407
|
import { join as join14 } from "path";
|
|
4015
4408
|
async function runKill(args = []) {
|
|
4016
4409
|
const argError = validateKillArgs(args);
|
|
@@ -4031,8 +4424,9 @@ async function runKill(args = []) {
|
|
|
4031
4424
|
const base = computeBaseDir();
|
|
4032
4425
|
console.log(`AgentBridge Kill \u2014 stopping AgentBridge pair processes
|
|
4033
4426
|
`);
|
|
4427
|
+
const cli = cliInvocationName();
|
|
4034
4428
|
const results = [];
|
|
4035
|
-
let restartCommand =
|
|
4429
|
+
let restartCommand = `${cli} claude`;
|
|
4036
4430
|
if (parsed.pairFlag !== undefined) {
|
|
4037
4431
|
let pair;
|
|
4038
4432
|
try {
|
|
@@ -4046,7 +4440,7 @@ async function runKill(args = []) {
|
|
|
4046
4440
|
printKnownPairs(base);
|
|
4047
4441
|
return;
|
|
4048
4442
|
}
|
|
4049
|
-
restartCommand =
|
|
4443
|
+
restartCommand = `${cli} --pair ${pair.name ?? parsed.pairFlag} claude`;
|
|
4050
4444
|
results.push(await stopPairEntry(base, pair));
|
|
4051
4445
|
} else if (parsed.all) {
|
|
4052
4446
|
let registered = [];
|
|
@@ -4080,7 +4474,7 @@ async function runKill(args = []) {
|
|
|
4080
4474
|
cwdPairs = listPairsForCwd(base, process.cwd());
|
|
4081
4475
|
} catch (error) {
|
|
4082
4476
|
const message = error instanceof Error ? error.message : String(error);
|
|
4083
|
-
console.log(`\u26A0\uFE0F pair registry \u4E0D\u53EF\u8BFB\uFF08${message}\uFF09\u2014\u2014\u65E0\u6CD5\u6309\u76EE\u5F55\u5B9A\u4F4D pair\u3002` +
|
|
4477
|
+
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`);
|
|
4084
4478
|
process.exitCode = 2;
|
|
4085
4479
|
}
|
|
4086
4480
|
for (const pair of cwdPairs) {
|
|
@@ -4096,7 +4490,7 @@ async function runKill(args = []) {
|
|
|
4096
4490
|
}
|
|
4097
4491
|
if (results.length === 0) {
|
|
4098
4492
|
console.log(`No AgentBridge pairs registered for current directory: ${process.cwd()}`);
|
|
4099
|
-
console.log(
|
|
4493
|
+
console.log(`Use \`${cli} kill all\` or \`${cli} kill --all\` to stop pairs from every directory.`);
|
|
4100
4494
|
return;
|
|
4101
4495
|
}
|
|
4102
4496
|
}
|
|
@@ -4159,22 +4553,18 @@ function listPairDirsSafe(base) {
|
|
|
4159
4553
|
}
|
|
4160
4554
|
}
|
|
4161
4555
|
function portsFromStateDir(stateDir) {
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
};
|
|
4169
|
-
} catch {
|
|
4556
|
+
const record = readUnifiedDaemonRecord({
|
|
4557
|
+
daemonRecordFile: stateDir.daemonRecordFile,
|
|
4558
|
+
pidFile: stateDir.pidFile,
|
|
4559
|
+
statusFile: stateDir.statusFile
|
|
4560
|
+
});
|
|
4561
|
+
if (!record)
|
|
4170
4562
|
return { appPort: 0, proxyPort: 0, controlPort: 0 };
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
const match = url.match(/:(\d+)(?:[/?]|$)/);
|
|
4177
|
-
return match ? Number.parseInt(match[1], 10) : null;
|
|
4563
|
+
return {
|
|
4564
|
+
appPort: record.ports?.appPort ?? portFromUrl(record.appServerUrl) ?? 0,
|
|
4565
|
+
proxyPort: record.ports?.proxyPort ?? portFromUrl(record.proxyUrl) ?? 0,
|
|
4566
|
+
controlPort: record.ports?.controlPort ?? 0
|
|
4567
|
+
};
|
|
4178
4568
|
}
|
|
4179
4569
|
async function stopStateDir(label, stateDir, ports) {
|
|
4180
4570
|
const portsLabel = `${ports.appPort}/${ports.proxyPort}/${ports.controlPort}`;
|
|
@@ -4187,8 +4577,8 @@ async function stopStateDir(label, stateDir, ports) {
|
|
|
4187
4577
|
log
|
|
4188
4578
|
});
|
|
4189
4579
|
lifecycle.markKilled();
|
|
4190
|
-
const
|
|
4191
|
-
const proxyUrl = typeof
|
|
4580
|
+
const record = lifecycle.readDaemonRecord();
|
|
4581
|
+
const proxyUrl = typeof record?.proxyUrl === "string" && record.proxyUrl.length > 0 ? record.proxyUrl : `ws://127.0.0.1:${ports.proxyPort}`;
|
|
4192
4582
|
const tuiKilled = await killManagedCodexTui(stateDir, proxyUrl, log);
|
|
4193
4583
|
const daemonKilled = await lifecycle.kill();
|
|
4194
4584
|
return { label, portsLabel, daemonKilled, tuiKilled, details };
|
|
@@ -4253,9 +4643,10 @@ function formatKillReport(results, frontends, restartCommand) {
|
|
|
4253
4643
|
}
|
|
4254
4644
|
lines.push("");
|
|
4255
4645
|
if (stopped.length > 0) {
|
|
4646
|
+
const cliName = restartCommand.split(" ")[0] ?? "abg";
|
|
4256
4647
|
lines.push("AgentBridge stopped.");
|
|
4257
4648
|
lines.push(`Please restart Claude Code (\`${restartCommand}\`), switch to a new conversation, or run \`/resume\` to fully disconnect.`);
|
|
4258
|
-
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}\` /
|
|
4649
|
+
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`);
|
|
4259
4650
|
} else {
|
|
4260
4651
|
lines.push("No running AgentBridge daemon or managed Codex TUI found.");
|
|
4261
4652
|
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");
|
|
@@ -4306,7 +4697,7 @@ async function killManagedCodexTui(stateDir, proxyUrl, log, gracefulTimeoutMs =
|
|
|
4306
4697
|
}
|
|
4307
4698
|
function readTuiPid2(stateDir) {
|
|
4308
4699
|
try {
|
|
4309
|
-
const raw =
|
|
4700
|
+
const raw = readFileSync11(stateDir.tuiPidFile, "utf-8").trim();
|
|
4310
4701
|
if (!raw)
|
|
4311
4702
|
return null;
|
|
4312
4703
|
const pid = Number.parseInt(raw, 10);
|
|
@@ -4317,7 +4708,7 @@ function readTuiPid2(stateDir) {
|
|
|
4317
4708
|
}
|
|
4318
4709
|
function removeTuiPidFile(stateDir) {
|
|
4319
4710
|
try {
|
|
4320
|
-
|
|
4711
|
+
unlinkSync7(stateDir.tuiPidFile);
|
|
4321
4712
|
} catch {}
|
|
4322
4713
|
}
|
|
4323
4714
|
function isManagedCodexTuiProcess2(pid, proxyUrl) {
|
|
@@ -4325,7 +4716,9 @@ function isManagedCodexTuiProcess2(pid, proxyUrl) {
|
|
|
4325
4716
|
return cmd !== null && commandMatchesManagedCodexTui(cmd, proxyUrl);
|
|
4326
4717
|
}
|
|
4327
4718
|
var init_kill = __esm(() => {
|
|
4719
|
+
init_cli_invocation();
|
|
4328
4720
|
init_daemon_lifecycle();
|
|
4721
|
+
init_daemon_record();
|
|
4329
4722
|
init_pair_registry();
|
|
4330
4723
|
init_pair_resolver();
|
|
4331
4724
|
init_process_lifecycle();
|
|
@@ -4338,6 +4731,13 @@ __export(exports_pairs, {
|
|
|
4338
4731
|
runPairs: () => runPairs
|
|
4339
4732
|
});
|
|
4340
4733
|
import { join as join15 } from "path";
|
|
4734
|
+
function isRegistryCorruptError(error) {
|
|
4735
|
+
return error instanceof PairError && error.code === "PAIR_REGISTRY_CORRUPT";
|
|
4736
|
+
}
|
|
4737
|
+
function registryPathForNotice(base, error) {
|
|
4738
|
+
const fromDetails = error.details?.path;
|
|
4739
|
+
return typeof fromDetails === "string" && fromDetails.length > 0 ? fromDetails : join15(pairsRootDir(base), "registry.json");
|
|
4740
|
+
}
|
|
4341
4741
|
async function runPairs(args = []) {
|
|
4342
4742
|
const [command, ...rest] = args;
|
|
4343
4743
|
if (command === "rm") {
|
|
@@ -4350,7 +4750,7 @@ async function runPairs(args = []) {
|
|
|
4350
4750
|
}
|
|
4351
4751
|
if (command && command !== "list" && command !== "--json" && command !== "--threads") {
|
|
4352
4752
|
console.error(`Unknown pairs command: ${command}`);
|
|
4353
|
-
console.error("Usage: abg pairs [--json] [--threads] | abg pairs rm <name|id> | abg pairs prune [--
|
|
4753
|
+
console.error("Usage: abg pairs [--json] [--threads] | abg pairs rm <name|id> | abg pairs prune [--apply]");
|
|
4354
4754
|
process.exit(1);
|
|
4355
4755
|
}
|
|
4356
4756
|
const json = command === "--json" || rest.includes("--json");
|
|
@@ -4405,16 +4805,40 @@ async function runRemove(args) {
|
|
|
4405
4805
|
}
|
|
4406
4806
|
}
|
|
4407
4807
|
async function runPrune(args) {
|
|
4408
|
-
const
|
|
4808
|
+
const apply = args.includes("--apply");
|
|
4409
4809
|
for (const arg of args) {
|
|
4410
|
-
if (arg !== "--dry-run") {
|
|
4810
|
+
if (arg !== "--apply" && arg !== "--dry-run") {
|
|
4411
4811
|
console.error(`Unknown prune argument: ${arg}`);
|
|
4412
|
-
console.error("Usage: abg pairs prune [--
|
|
4812
|
+
console.error("Usage: abg pairs prune [--apply]");
|
|
4413
4813
|
process.exit(1);
|
|
4414
4814
|
}
|
|
4415
4815
|
}
|
|
4816
|
+
if (apply && args.includes("--dry-run")) {
|
|
4817
|
+
console.error("Error: --apply and --dry-run are mutually exclusive.");
|
|
4818
|
+
console.error("Usage: abg pairs prune [--apply]");
|
|
4819
|
+
process.exit(1);
|
|
4820
|
+
}
|
|
4416
4821
|
const base = computeBaseDir();
|
|
4417
|
-
|
|
4822
|
+
let reclaimable;
|
|
4823
|
+
let registryReadable = true;
|
|
4824
|
+
try {
|
|
4825
|
+
reclaimable = classifyReclaimableEntries(base);
|
|
4826
|
+
} catch (error) {
|
|
4827
|
+
if (!isRegistryCorruptError(error))
|
|
4828
|
+
throw error;
|
|
4829
|
+
registryReadable = false;
|
|
4830
|
+
reclaimable = [];
|
|
4831
|
+
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`);
|
|
4832
|
+
process.exitCode = 2;
|
|
4833
|
+
}
|
|
4834
|
+
const reclaimableIds = new Set(reclaimable.map((c) => c.entry.pairId.toLowerCase()));
|
|
4835
|
+
const dirResult = pruneOrphanDirs(base, apply, reclaimableIds, registryReadable);
|
|
4836
|
+
const entryResult = await pruneReclaimableEntries(reclaimable, base, apply);
|
|
4837
|
+
const resolvedDirResult = await dirResult;
|
|
4838
|
+
printPruneSummary(resolvedDirResult, entryResult, apply);
|
|
4839
|
+
}
|
|
4840
|
+
async function pruneOrphanDirs(base, apply, reclaimableIds, registryReadable) {
|
|
4841
|
+
const registered = registryReadable ? new Set(listPairs(base).map((pair) => pair.pairId.toLowerCase())) : new Set;
|
|
4418
4842
|
const removed = [];
|
|
4419
4843
|
const kept = [];
|
|
4420
4844
|
for (const name of listPairDirs(base)) {
|
|
@@ -4429,6 +4853,9 @@ async function runPrune(args) {
|
|
|
4429
4853
|
kept.push({ name, reason: "directory name is not a canonical pair id" });
|
|
4430
4854
|
continue;
|
|
4431
4855
|
}
|
|
4856
|
+
if (reclaimableIds.has(id.toLowerCase())) {
|
|
4857
|
+
continue;
|
|
4858
|
+
}
|
|
4432
4859
|
if (registered.has(id.toLowerCase())) {
|
|
4433
4860
|
kept.push({ name, reason: "registered \u2014 use `abg pairs rm`" });
|
|
4434
4861
|
continue;
|
|
@@ -4437,12 +4864,12 @@ async function runPrune(args) {
|
|
|
4437
4864
|
kept.push({ name, reason: "daemon still alive" });
|
|
4438
4865
|
continue;
|
|
4439
4866
|
}
|
|
4440
|
-
if (
|
|
4867
|
+
if (!apply) {
|
|
4441
4868
|
removed.push(name);
|
|
4442
4869
|
continue;
|
|
4443
4870
|
}
|
|
4444
4871
|
try {
|
|
4445
|
-
const outcome = await removeUnregisteredPairDir(base, id);
|
|
4872
|
+
const outcome = registryReadable ? await removeUnregisteredPairDir(base, id) : await removeOrphanPairDirIgnoringRegistry(base, id);
|
|
4446
4873
|
if (outcome.removed) {
|
|
4447
4874
|
removed.push(name);
|
|
4448
4875
|
} else if (outcome.reason === "registered") {
|
|
@@ -4456,33 +4883,90 @@ async function runPrune(args) {
|
|
|
4456
4883
|
kept.push({ name, reason: `error: ${err instanceof Error ? err.message : String(err)}` });
|
|
4457
4884
|
}
|
|
4458
4885
|
}
|
|
4459
|
-
|
|
4886
|
+
return { removed, kept };
|
|
4460
4887
|
}
|
|
4461
|
-
function
|
|
4462
|
-
|
|
4463
|
-
|
|
4888
|
+
async function pruneReclaimableEntries(candidates, base, apply) {
|
|
4889
|
+
const reclaimed = [];
|
|
4890
|
+
const kept = [];
|
|
4891
|
+
for (const candidate of candidates) {
|
|
4892
|
+
const reason = describeReclaimReason(candidate);
|
|
4893
|
+
if (!apply) {
|
|
4894
|
+
reclaimed.push({ pairId: candidate.entry.pairId, slot: candidate.entry.slot, reason });
|
|
4895
|
+
continue;
|
|
4896
|
+
}
|
|
4897
|
+
try {
|
|
4898
|
+
const res = await removePairEntryAndDir(base, candidate.entry.pairId);
|
|
4899
|
+
if (res.keptLive) {
|
|
4900
|
+
kept.push({ pairId: candidate.entry.pairId, reason: "became live during prune" });
|
|
4901
|
+
} else {
|
|
4902
|
+
reclaimed.push({ pairId: candidate.entry.pairId, slot: candidate.entry.slot, reason });
|
|
4903
|
+
}
|
|
4904
|
+
} catch (err) {
|
|
4905
|
+
kept.push({
|
|
4906
|
+
pairId: candidate.entry.pairId,
|
|
4907
|
+
reason: `error: ${err instanceof Error ? err.message : String(err)}`
|
|
4908
|
+
});
|
|
4909
|
+
}
|
|
4910
|
+
}
|
|
4911
|
+
return { reclaimed, kept };
|
|
4912
|
+
}
|
|
4913
|
+
function describeReclaimReason(candidate) {
|
|
4914
|
+
const { signals } = candidate;
|
|
4915
|
+
const age = signals.ageMs === null ? "age?" : `age ${formatAgeDays(signals.ageMs)}`;
|
|
4916
|
+
return `cwd-gone, dead, ${age}`;
|
|
4917
|
+
}
|
|
4918
|
+
function formatAgeDays(ageMs) {
|
|
4919
|
+
const days = ageMs / (24 * 60 * 60 * 1000);
|
|
4920
|
+
return days >= 10 ? `${Math.round(days)}d` : `${days.toFixed(1)}d`;
|
|
4921
|
+
}
|
|
4922
|
+
function printPruneSummary(dirResult, entryResult, apply) {
|
|
4923
|
+
const { removed: dirsRemoved, kept: dirsKept } = dirResult;
|
|
4924
|
+
const { reclaimed: entriesReclaimed, kept: entriesKept } = entryResult;
|
|
4925
|
+
const nothingFound = dirsRemoved.length === 0 && dirsKept.length === 0 && entriesReclaimed.length === 0 && entriesKept.length === 0;
|
|
4926
|
+
if (nothingFound) {
|
|
4927
|
+
console.log("Nothing to prune: no orphan pair directories or reclaimable entries found.");
|
|
4464
4928
|
return;
|
|
4465
4929
|
}
|
|
4466
|
-
if (
|
|
4467
|
-
console.log(
|
|
4468
|
-
for (const name of
|
|
4930
|
+
if (dirsRemoved.length > 0) {
|
|
4931
|
+
console.log(apply ? "Removed orphan pair directories:" : "Would remove orphan pair directories:");
|
|
4932
|
+
for (const name of dirsRemoved)
|
|
4469
4933
|
console.log(` ${name}`);
|
|
4470
|
-
} else {
|
|
4471
|
-
console.log(dryRun ? "No orphan pair directories to remove." : "No orphan pair directories removed.");
|
|
4472
4934
|
}
|
|
4473
|
-
if (
|
|
4935
|
+
if (entriesReclaimed.length > 0) {
|
|
4936
|
+
console.log(apply ? "Reclaimed registry entries:" : "Would reclaim registry entries:");
|
|
4937
|
+
for (const { pairId, slot, reason } of entriesReclaimed) {
|
|
4938
|
+
console.log(` ${pairId} (slot ${slot}) \u2014 ${reason}`);
|
|
4939
|
+
}
|
|
4940
|
+
}
|
|
4941
|
+
if (dirsRemoved.length === 0 && entriesReclaimed.length === 0) {
|
|
4942
|
+
console.log(apply ? "Nothing was reclaimed." : "Nothing to reclaim.");
|
|
4943
|
+
}
|
|
4944
|
+
const keptLines = [
|
|
4945
|
+
...dirsKept.map(({ name, reason }) => ` ${name} (${reason})`),
|
|
4946
|
+
...entriesKept.map(({ pairId, reason }) => ` ${pairId} (${reason})`)
|
|
4947
|
+
];
|
|
4948
|
+
if (keptLines.length > 0) {
|
|
4474
4949
|
console.log("Kept:");
|
|
4475
|
-
for (const
|
|
4476
|
-
console.log(
|
|
4950
|
+
for (const line of keptLines)
|
|
4951
|
+
console.log(line);
|
|
4477
4952
|
}
|
|
4478
|
-
if (
|
|
4953
|
+
if (!apply) {
|
|
4479
4954
|
console.log(`
|
|
4480
|
-
(dry run \u2014 nothing was deleted. Re-run
|
|
4955
|
+
(dry run \u2014 nothing was deleted. Re-run with --apply to reclaim.)`);
|
|
4481
4956
|
}
|
|
4482
4957
|
}
|
|
4483
4958
|
async function collectRows() {
|
|
4484
4959
|
const base = computeBaseDir();
|
|
4485
|
-
|
|
4960
|
+
let rows;
|
|
4961
|
+
try {
|
|
4962
|
+
rows = await Promise.all(listPairs(base).map((pair) => rowForPair(base, pair)));
|
|
4963
|
+
} catch (error) {
|
|
4964
|
+
if (!isRegistryCorruptError(error))
|
|
4965
|
+
throw error;
|
|
4966
|
+
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`);
|
|
4967
|
+
process.exitCode = 2;
|
|
4968
|
+
rows = await collectDiskScanRows(base);
|
|
4969
|
+
}
|
|
4486
4970
|
const legacy = detectLegacyRootDaemon(base);
|
|
4487
4971
|
if (legacy) {
|
|
4488
4972
|
rows.push({
|
|
@@ -4509,9 +4993,9 @@ async function rowForPair(base, pair) {
|
|
|
4509
4993
|
controlPort: ports.controlPort,
|
|
4510
4994
|
log: () => {}
|
|
4511
4995
|
});
|
|
4512
|
-
const [running,
|
|
4996
|
+
const [running, record] = await Promise.all([
|
|
4513
4997
|
lifecycle.isHealthy(),
|
|
4514
|
-
Promise.resolve(lifecycle.
|
|
4998
|
+
Promise.resolve(lifecycle.readDaemonRecord())
|
|
4515
4999
|
]);
|
|
4516
5000
|
const thread = readRawCurrentThread(stateDir);
|
|
4517
5001
|
return {
|
|
@@ -4522,7 +5006,42 @@ async function rowForPair(base, pair) {
|
|
|
4522
5006
|
source: pair.source,
|
|
4523
5007
|
cwd: pair.cwd,
|
|
4524
5008
|
running,
|
|
4525
|
-
pid: typeof
|
|
5009
|
+
pid: typeof record?.pid === "number" ? record.pid : null,
|
|
5010
|
+
threadId: thread?.threadId ?? null,
|
|
5011
|
+
threadStatus: thread?.status ?? null,
|
|
5012
|
+
threadUpdatedAt: thread?.updatedAt ?? null
|
|
5013
|
+
};
|
|
5014
|
+
}
|
|
5015
|
+
async function collectDiskScanRows(base) {
|
|
5016
|
+
const names = listPairDirsSafe2(base);
|
|
5017
|
+
return Promise.all(names.map((name) => rowForDiskScanDir(base, name)));
|
|
5018
|
+
}
|
|
5019
|
+
function listPairDirsSafe2(base) {
|
|
5020
|
+
try {
|
|
5021
|
+
return listPairDirs(base);
|
|
5022
|
+
} catch {
|
|
5023
|
+
return [];
|
|
5024
|
+
}
|
|
5025
|
+
}
|
|
5026
|
+
async function rowForDiskScanDir(base, dirName) {
|
|
5027
|
+
const stateDir = new StateDirResolver(join15(base, "pairs", dirName));
|
|
5028
|
+
const record = new DaemonLifecycle({ stateDir, controlPort: 0, log: () => {} }).readDaemonRecord();
|
|
5029
|
+
const ports = {
|
|
5030
|
+
appPort: record?.ports?.appPort ?? 0,
|
|
5031
|
+
proxyPort: record?.ports?.proxyPort ?? 0,
|
|
5032
|
+
controlPort: record?.ports?.controlPort ?? 0
|
|
5033
|
+
};
|
|
5034
|
+
const running = ports.controlPort > 0 ? await new DaemonLifecycle({ stateDir, controlPort: ports.controlPort, log: () => {} }).isHealthy() : false;
|
|
5035
|
+
const thread = readRawCurrentThread(stateDir);
|
|
5036
|
+
return {
|
|
5037
|
+
pairId: dirName,
|
|
5038
|
+
name: "-",
|
|
5039
|
+
slot: null,
|
|
5040
|
+
ports,
|
|
5041
|
+
source: "cwd",
|
|
5042
|
+
cwd: "-",
|
|
5043
|
+
running,
|
|
5044
|
+
pid: typeof record?.pid === "number" ? record.pid : null,
|
|
4526
5045
|
threadId: thread?.threadId ?? null,
|
|
4527
5046
|
threadStatus: thread?.status ?? null,
|
|
4528
5047
|
threadUpdatedAt: thread?.updatedAt ?? null
|
|
@@ -4588,6 +5107,215 @@ var init_pairs = __esm(() => {
|
|
|
4588
5107
|
init_thread_state();
|
|
4589
5108
|
init_kill();
|
|
4590
5109
|
});
|
|
5110
|
+
// src/budget/types.ts
|
|
5111
|
+
var STALE_MAX_AGE_SEC = 600;
|
|
5112
|
+
|
|
5113
|
+
// src/budget/budget-state.ts
|
|
5114
|
+
function isDecisionGrade(usage, now) {
|
|
5115
|
+
if (!usage)
|
|
5116
|
+
return false;
|
|
5117
|
+
const freshWindow = usage.fiveHour !== null && usage.fiveHour.resetEpoch > now || usage.weekly !== null && usage.weekly.resetEpoch > now;
|
|
5118
|
+
if (!freshWindow)
|
|
5119
|
+
return false;
|
|
5120
|
+
if (usage.fetchedAt > 0 && now - usage.fetchedAt > STALE_MAX_AGE_SEC)
|
|
5121
|
+
return false;
|
|
5122
|
+
return true;
|
|
5123
|
+
}
|
|
5124
|
+
var init_budget_state = () => {};
|
|
5125
|
+
|
|
5126
|
+
// src/budget/burn-view.ts
|
|
5127
|
+
function agentWeeklyFiveHourWindowsLeft(usage, now) {
|
|
5128
|
+
if (!usage || usage.stale || !usage.ok)
|
|
5129
|
+
return null;
|
|
5130
|
+
if (!isDecisionGrade(usage, now))
|
|
5131
|
+
return null;
|
|
5132
|
+
const weekly = usage.weekly;
|
|
5133
|
+
if (!weekly || weekly.resetEpoch <= now)
|
|
5134
|
+
return null;
|
|
5135
|
+
if (weekly.burnConfident !== true)
|
|
5136
|
+
return null;
|
|
5137
|
+
if (weekly.runwaySeconds === undefined)
|
|
5138
|
+
return null;
|
|
5139
|
+
return weekly.fiveHourWindowsLeft ?? null;
|
|
5140
|
+
}
|
|
5141
|
+
var init_burn_view = __esm(() => {
|
|
5142
|
+
init_budget_state();
|
|
5143
|
+
});
|
|
5144
|
+
|
|
5145
|
+
// src/budget/render.ts
|
|
5146
|
+
function resolveGuardHardHint(env = process.env) {
|
|
5147
|
+
const raw = env.AGENTBRIDGE_GUARD_HARD_HINT;
|
|
5148
|
+
if (raw === undefined || raw.trim() === "")
|
|
5149
|
+
return DEFAULT_GUARD_HARD_PCT;
|
|
5150
|
+
const parsed = Number(raw);
|
|
5151
|
+
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 100)
|
|
5152
|
+
return DEFAULT_GUARD_HARD_PCT;
|
|
5153
|
+
return parsed;
|
|
5154
|
+
}
|
|
5155
|
+
function formatEpoch(epochSeconds) {
|
|
5156
|
+
if (!epochSeconds || epochSeconds <= 0)
|
|
5157
|
+
return "\u672A\u77E5";
|
|
5158
|
+
return new Date(epochSeconds * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
|
|
5159
|
+
}
|
|
5160
|
+
function formatWindow(window, label) {
|
|
5161
|
+
if (!window)
|
|
5162
|
+
return `${label} \u672A\u77E5`;
|
|
5163
|
+
return `${label} ${window.util}%\uFF08\u91CD\u7F6E ${formatEpoch(window.resetEpoch)}\uFF09`;
|
|
5164
|
+
}
|
|
5165
|
+
function formatAgent(name, usage, snapshotAt) {
|
|
5166
|
+
if (!usage)
|
|
5167
|
+
return `${name}\uFF1A\u672A\u77E5\uFF08\u63A2\u6D4B\u4E0D\u53EF\u7528\uFF09`;
|
|
5168
|
+
const parts = [
|
|
5169
|
+
formatWindow(usage.fiveHour, "5h"),
|
|
5170
|
+
formatWindow(usage.weekly, "\u5468"),
|
|
5171
|
+
`\u95E8\u63A7 ${usage.gateUtil}%`,
|
|
5172
|
+
`\u9884\u8B66 ${usage.warnUtil}%`
|
|
5173
|
+
];
|
|
5174
|
+
if (usage.rateLimitedUntil > 0) {
|
|
5175
|
+
parts.push(`\u9650\u6D41\u81F3 ${formatEpoch(usage.rateLimitedUntil)}`);
|
|
5176
|
+
}
|
|
5177
|
+
if (usage.parsedVia === "positional") {
|
|
5178
|
+
parts.push("\u26A0\uFE0F \u7A97\u53E3\u8BC6\u522B\u4F7F\u7528\u4F4D\u7F6E\u515C\u5E95");
|
|
5179
|
+
}
|
|
5180
|
+
const ageSec = usage.fetchedAt > 0 ? snapshotAt - usage.fetchedAt : 0;
|
|
5181
|
+
if (ageSec > 300) {
|
|
5182
|
+
parts.push(`\u26A0\uFE0F \u6570\u636E\u91C7\u96C6\u4E8E ${Math.round(ageSec / 60)} \u5206\u949F\u524D`);
|
|
5183
|
+
} else if (usage.stale) {
|
|
5184
|
+
parts.push("\uFF08\u7F13\u5B58\u6570\u636E\uFF09");
|
|
5185
|
+
}
|
|
5186
|
+
return `${name}\uFF1A${parts.join(" \xB7 ")}`;
|
|
5187
|
+
}
|
|
5188
|
+
function formatDuration(seconds) {
|
|
5189
|
+
const totalMinutes = Math.max(0, Math.round(seconds / 60));
|
|
5190
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
5191
|
+
const minutes = totalMinutes % 60;
|
|
5192
|
+
if (hours === 0)
|
|
5193
|
+
return `${minutes}\u5206\u949F`;
|
|
5194
|
+
return `${hours}\u5C0F\u65F6${minutes}\u5206\u949F`;
|
|
5195
|
+
}
|
|
5196
|
+
function formatClockTime(epochSeconds) {
|
|
5197
|
+
const date = new Date(epochSeconds * 1000);
|
|
5198
|
+
const hh = String(date.getHours()).padStart(2, "0");
|
|
5199
|
+
const mm = String(date.getMinutes()).padStart(2, "0");
|
|
5200
|
+
return `${hh}:${mm}`;
|
|
5201
|
+
}
|
|
5202
|
+
function formatWindowRate(label, rate) {
|
|
5203
|
+
if (!rate)
|
|
5204
|
+
return null;
|
|
5205
|
+
if (!rate.confident)
|
|
5206
|
+
return `${label} \u91C7\u6837\u4E2D`;
|
|
5207
|
+
return `${label} \u2248${rate.pctPerHour.toFixed(2)}%/h`;
|
|
5208
|
+
}
|
|
5209
|
+
function formatRunwaySegment(runway, basisWindow, snapshotAt) {
|
|
5210
|
+
const truncatedByReset = basisWindow !== null && basisWindow.resetEpoch > 0 && snapshotAt + runway.seconds >= basisWindow.resetEpoch - RESET_TRUNCATION_EPSILON_SEC;
|
|
5211
|
+
const clock = runway.depletedAtEpoch ? formatClockTime(runway.depletedAtEpoch) : null;
|
|
5212
|
+
let clockNote;
|
|
5213
|
+
if (clock) {
|
|
5214
|
+
clockNote = truncatedByReset ? `\u81F3 ${clock} \u7A97\u53E3\u5237\u65B0\u5373\u622A\u65AD\uFF0C` : `\u81F3 ${clock}\uFF0C`;
|
|
5215
|
+
} else {
|
|
5216
|
+
clockNote = truncatedByReset ? "\u7A97\u53E3\u5237\u65B0\u5373\u622A\u65AD\uFF0C" : "";
|
|
5217
|
+
}
|
|
5218
|
+
return `\u7EA6\u53EF\u518D\u5DE5\u4F5C ${formatDuration(runway.seconds)}\uFF08${clockNote}${WINDOW_LABELS[runway.basis]}\u4E3A\u7EA6\u675F\uFF09`;
|
|
5219
|
+
}
|
|
5220
|
+
function formatBurnRateLine(name, usage, rates, runway, snapshotAt, guardHardPct) {
|
|
5221
|
+
const parts = [
|
|
5222
|
+
formatWindowRate("5h", rates.fiveHour),
|
|
5223
|
+
formatWindowRate("\u5468", rates.weekly)
|
|
5224
|
+
].filter((part) => part !== null);
|
|
5225
|
+
if (parts.length === 0 && !runway)
|
|
5226
|
+
return null;
|
|
5227
|
+
if (runway) {
|
|
5228
|
+
const basisWindow = usage ? usage[runway.basis] : null;
|
|
5229
|
+
parts.push(formatRunwaySegment(runway, basisWindow, snapshotAt));
|
|
5230
|
+
}
|
|
5231
|
+
if (guardHardPct !== null) {
|
|
5232
|
+
parts.push(`\u5916\u5C42 guard \u786C\u7EBF ${guardHardPct}%\uFF08v3 \u4E0D\u53EF\u8D8A\u8FC7\uFF1Brunway \u4E3A\u4E2D\u6027\u53E3\u5F84\uFF0CClaude \u4F1A\u5148\u5728\u786C\u7EBF\u88AB\u5916\u5C42\u505C\u4F4F\uFF09`);
|
|
5233
|
+
}
|
|
5234
|
+
return `${name} \u71C3\u5C3D\u7387\uFF1A${parts.join(" \xB7 ")}`;
|
|
5235
|
+
}
|
|
5236
|
+
function formatFiveHourWindowsLeftLine(snapshot) {
|
|
5237
|
+
const values = [];
|
|
5238
|
+
const claude = agentWeeklyFiveHourWindowsLeft(snapshot.claude, snapshot.updatedAt);
|
|
5239
|
+
const codex = agentWeeklyFiveHourWindowsLeft(snapshot.codex, snapshot.updatedAt);
|
|
5240
|
+
if (claude !== null)
|
|
5241
|
+
values.push(["Claude", claude]);
|
|
5242
|
+
if (codex !== null)
|
|
5243
|
+
values.push(["Codex", codex]);
|
|
5244
|
+
if (values.length === 0)
|
|
5245
|
+
return null;
|
|
5246
|
+
const unique = [...new Set(values.map(([, value]) => value.toFixed(1)))];
|
|
5247
|
+
if (unique.length === 1)
|
|
5248
|
+
return `\u6309\u5F53\u524D\u8282\u594F\uFF0C\u5468\u989D\u5EA6\u8FD8\u591F ~${unique[0]} \u4E2A 5h \u7A97\u53E3`;
|
|
5249
|
+
const byAgent = values.map(([name, value]) => `${name} ~${value.toFixed(1)}`).join(" / ");
|
|
5250
|
+
return `\u6309\u5F53\u524D\u8282\u594F\uFF0C\u5468\u989D\u5EA6\u8FD8\u591F ${byAgent} \u4E2A 5h \u7A97\u53E3`;
|
|
5251
|
+
}
|
|
5252
|
+
function renderBudgetSnapshot(snapshot, options = {}) {
|
|
5253
|
+
const guardHardPct = options.guardHardPct ?? resolveGuardHardHint();
|
|
5254
|
+
const lines = [];
|
|
5255
|
+
lines.push(`\u3010\u9884\u7B97\u5FEB\u7167 \xB7 \u8D26\u53F7\u7EA7\u3011\u9636\u6BB5\uFF1A${PHASE_LABELS[snapshot.phase]} \xB7 \u66F4\u65B0\u4E8E ${formatEpoch(snapshot.updatedAt)}`);
|
|
5256
|
+
lines.push(formatAgent("Claude", snapshot.claude, snapshot.updatedAt));
|
|
5257
|
+
lines.push(formatAgent("Codex", snapshot.codex, snapshot.updatedAt));
|
|
5258
|
+
if (snapshot.burnRate) {
|
|
5259
|
+
const claudeLine = formatBurnRateLine("Claude", snapshot.claude, snapshot.burnRate.claude, snapshot.runway?.claude ?? null, snapshot.updatedAt, guardHardPct);
|
|
5260
|
+
if (claudeLine)
|
|
5261
|
+
lines.push(claudeLine);
|
|
5262
|
+
const codexLine = formatBurnRateLine("Codex", snapshot.codex, snapshot.burnRate.codex, snapshot.runway?.codex ?? null, snapshot.updatedAt, null);
|
|
5263
|
+
if (codexLine)
|
|
5264
|
+
lines.push(codexLine);
|
|
5265
|
+
}
|
|
5266
|
+
const fiveHourWindowsLeftLine = formatFiveHourWindowsLeftLine(snapshot);
|
|
5267
|
+
if (fiveHourWindowsLeftLine)
|
|
5268
|
+
lines.push(fiveHourWindowsLeftLine);
|
|
5269
|
+
if (snapshot.claude && snapshot.codex) {
|
|
5270
|
+
const abs = Math.abs(snapshot.driftPct);
|
|
5271
|
+
if (abs > 0) {
|
|
5272
|
+
const heavier = snapshot.driftPct > 0 ? "Claude" : "Codex";
|
|
5273
|
+
const lighter = snapshot.driftPct > 0 ? "Codex" : "Claude";
|
|
5274
|
+
lines.push(`\u6F02\u79FB\uFF1A${heavier} \u6BD4 ${lighter} \u9AD8 ${abs} \u4E2A\u767E\u5206\u70B9`);
|
|
5275
|
+
} else {
|
|
5276
|
+
lines.push("\u6F02\u79FB\uFF1A\u53CC\u65B9\u6301\u5E73");
|
|
5277
|
+
}
|
|
5278
|
+
}
|
|
5279
|
+
if (snapshot.paused) {
|
|
5280
|
+
const resume = snapshot.resumeAfterEpoch ? `\uFF1B\u9884\u8BA1\u6062\u590D ${formatEpoch(snapshot.resumeAfterEpoch)}\uFF08\u4EE5\u5B9E\u6D4B\u4E3A\u51C6\uFF1B\u63D0\u524D\u5237\u65B0\u4F1A\u66F4\u65E9\u89E3\u9664\uFF09` : "";
|
|
5281
|
+
const reason = snapshot.pauseReason ?? "\u989D\u5EA6\u63A5\u8FD1\u8017\u5C3D";
|
|
5282
|
+
if (snapshot.pauseSide === "claude" && !snapshot.gateClosed) {
|
|
5283
|
+
lines.push(`\u63A5\u529B\u4E2D\uFF1AClaude \u4FA7\u989D\u5EA6\u8017\u5C3D\uFF0C\u5DF2\u4EA4\u63A5 Codex \u7EE7\u7EED\u63A8\u8FDB\uFF08\u95F8\u95E8\u5F00\u653E\uFF09 \u2014 ${reason}${resume}`);
|
|
5284
|
+
} else if (snapshot.pauseSide === "codex") {
|
|
5285
|
+
lines.push(`\u6682\u505C\uFF1ACodex \u4FA7\u989D\u5EA6\u8017\u5C3D\uFF08\u95F8\u95E8\u5173\u95ED\uFF0CClaude \u53EF solo \u63A8\u8FDB\u72EC\u7ACB\u90E8\u5206\uFF09 \u2014 ${reason}${resume}`);
|
|
5286
|
+
} else {
|
|
5287
|
+
lines.push(`\u6682\u505C\uFF1A\u53CC\u4FA7\u8054\u5408\u6682\u505C\uFF08\u95F8\u95E8\u5173\u95ED\uFF09 \u2014 ${reason}${resume}`);
|
|
5288
|
+
}
|
|
5289
|
+
} else {
|
|
5290
|
+
lines.push("\u6682\u505C\uFF1A\u5426");
|
|
5291
|
+
}
|
|
5292
|
+
if (snapshot.parallelRecommended) {
|
|
5293
|
+
lines.push("\u5E76\u884C\u5EFA\u8BAE\uFF1A\u989D\u5EA6\u5BCC\u4F59\u4E14\u4E34\u8FD1\u7ED3\u7B97\uFF0C\u5EFA\u8BAE\u62C6\u5206\u66F4\u591A\u5E76\u884C\u5B50\u4EFB\u52A1");
|
|
5294
|
+
}
|
|
5295
|
+
if (snapshot.codexTier !== "full") {
|
|
5296
|
+
lines.push(`Codex \u6863\u4F4D\uFF1A${snapshot.codexTier}`);
|
|
5297
|
+
}
|
|
5298
|
+
if (snapshot.claudeAdvice) {
|
|
5299
|
+
lines.push(`Claude \u5EFA\u8BAE\uFF1A${snapshot.claudeAdvice}`);
|
|
5300
|
+
}
|
|
5301
|
+
lines.push("\u6CE8\uFF1A\u767E\u5206\u6BD4\u4E3A\u8BA2\u9605\u8D26\u53F7\u7EA7\u7528\u91CF\uFF08\u540C\u673A\u5176\u4ED6\u4F1A\u8BDD\u5171\u4EAB\u540C\u4E00\u989D\u5EA6\u6C60\uFF09\u3002");
|
|
5302
|
+
return lines.join(`
|
|
5303
|
+
`);
|
|
5304
|
+
}
|
|
5305
|
+
var DEFAULT_GUARD_HARD_PCT = 92, WINDOW_LABELS, RESET_TRUNCATION_EPSILON_SEC = 60, PHASE_LABELS, BUDGET_UNAVAILABLE_TEXT = "\u9884\u7B97\u611F\u77E5\u4E0D\u53EF\u7528\uFF1A\u672A\u68C0\u6D4B\u5230 agent-quota-guard \u63A2\u9488\uFF08~/.budget-guard/bin/budget-probe\uFF09\u6216 budget \u529F\u80FD\u5DF2\u7981\u7528\u3002\u534F\u4F5C\u4E0D\u53D7\u5F71\u54CD\u3002";
|
|
5306
|
+
var init_render = __esm(() => {
|
|
5307
|
+
init_burn_view();
|
|
5308
|
+
WINDOW_LABELS = {
|
|
5309
|
+
fiveHour: "5h \u7A97\u53E3",
|
|
5310
|
+
weekly: "\u5468\u7A97\u53E3"
|
|
5311
|
+
};
|
|
5312
|
+
PHASE_LABELS = {
|
|
5313
|
+
normal: "normal\uFF08\u6B63\u5E38\uFF09",
|
|
5314
|
+
balance: "balance\uFF08\u9700\u5747\u8861\uFF09",
|
|
5315
|
+
parallel: "parallel\uFF08\u5EFA\u8BAE\u5E76\u884C\u63D0\u901F\uFF09",
|
|
5316
|
+
paused: "paused\uFF08\u9884\u7B97\u5E72\u9884\u4E2D\uFF09"
|
|
5317
|
+
};
|
|
5318
|
+
});
|
|
4591
5319
|
|
|
4592
5320
|
// src/daemon-status.ts
|
|
4593
5321
|
async function fetchDaemonStatus(port, path = "/healthz", timeoutMs = DAEMON_STATUS_FETCH_TIMEOUT_MS) {
|
|
@@ -4612,7 +5340,7 @@ import {
|
|
|
4612
5340
|
copyFileSync,
|
|
4613
5341
|
existsSync as existsSync14,
|
|
4614
5342
|
mkdirSync as mkdirSync7,
|
|
4615
|
-
readFileSync as
|
|
5343
|
+
readFileSync as readFileSync12
|
|
4616
5344
|
} from "fs";
|
|
4617
5345
|
import { dirname as dirname5, join as join16 } from "path";
|
|
4618
5346
|
function isKickoffText(text) {
|
|
@@ -4623,7 +5351,7 @@ function isKickoffText(text) {
|
|
|
4623
5351
|
function extractFirstRealUserMessage(rolloutPath) {
|
|
4624
5352
|
if (!existsSync14(rolloutPath))
|
|
4625
5353
|
return null;
|
|
4626
|
-
const raw =
|
|
5354
|
+
const raw = readFileSync12(rolloutPath, "utf-8");
|
|
4627
5355
|
for (const line of raw.split(`
|
|
4628
5356
|
`)) {
|
|
4629
5357
|
if (!line.trim())
|
|
@@ -4790,9 +5518,13 @@ var init_resume_pollution = __esm(() => {
|
|
|
4790
5518
|
var exports_doctor = {};
|
|
4791
5519
|
__export(exports_doctor, {
|
|
4792
5520
|
runDoctor: () => runDoctor,
|
|
4793
|
-
formatDoctorReport: () => formatDoctorReport
|
|
5521
|
+
formatDoctorReport: () => formatDoctorReport,
|
|
5522
|
+
evaluateBudgetStrategyGuard: () => evaluateBudgetStrategyGuard,
|
|
5523
|
+
evaluateArtifactAlignment: () => evaluateArtifactAlignment,
|
|
5524
|
+
describeBuildDrift: () => describeBuildDrift,
|
|
5525
|
+
V3_DEFAULT_TARGET_UTIL: () => V3_DEFAULT_TARGET_UTIL
|
|
4794
5526
|
});
|
|
4795
|
-
import { existsSync as existsSync15, readFileSync as
|
|
5527
|
+
import { existsSync as existsSync15, readFileSync as readFileSync13, readdirSync as readdirSync6, realpathSync as realpathSync3, statSync as statSync7 } from "fs";
|
|
4796
5528
|
import { join as join17 } from "path";
|
|
4797
5529
|
async function runDoctor(args = []) {
|
|
4798
5530
|
if (args[0] === "resume-pollution") {
|
|
@@ -4869,6 +5601,7 @@ function runResumePollution(args) {
|
|
|
4869
5601
|
}
|
|
4870
5602
|
async function buildDoctorReport(pair, registered) {
|
|
4871
5603
|
const cwd = process.cwd();
|
|
5604
|
+
const cli = cliInvocationName();
|
|
4872
5605
|
const env = inspectAgentBridgeEnv({ cwd, env: process.env });
|
|
4873
5606
|
const [health, ready] = registered ? await Promise.all([
|
|
4874
5607
|
fetchDaemonStatus(pair.ports.controlPort, "/healthz"),
|
|
@@ -4888,20 +5621,21 @@ async function buildDoctorReport(pair, registered) {
|
|
|
4888
5621
|
name: "pair registration",
|
|
4889
5622
|
status: registered ? "ok" : "warn",
|
|
4890
5623
|
detail: registered ? pair.manual ? "manual mode (explicit env)" : `registered as ${pair.pairId}` : `not registered yet \u2014 would be ${pair.pairId} (created on first launch)`,
|
|
4891
|
-
hint: registered ? undefined :
|
|
5624
|
+
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`
|
|
4892
5625
|
});
|
|
4893
5626
|
checks.push({
|
|
4894
5627
|
name: "env",
|
|
4895
5628
|
status: env.ok ? "ok" : "fail",
|
|
4896
5629
|
detail: env.ok ? "AgentBridge env matches cwd" : env.reasons.join("; "),
|
|
4897
|
-
hint: env.ok ? undefined :
|
|
5630
|
+
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`
|
|
4898
5631
|
});
|
|
4899
|
-
checks.push(configParseabilityCheck(cwd));
|
|
5632
|
+
checks.push(configParseabilityCheck(cwd, cli));
|
|
5633
|
+
checks.push(budgetStrategyGuardCheck(cwd));
|
|
4900
5634
|
checks.push({
|
|
4901
5635
|
name: "daemon health",
|
|
4902
5636
|
status: health ? "ok" : "warn",
|
|
4903
5637
|
detail: health ? `healthz reachable pid=${health.pid}` : registered ? `no daemon reachable on :${pair.ports.controlPort}` : "n/a \u2014 pair not registered",
|
|
4904
|
-
hint: health ? undefined :
|
|
5638
|
+
hint: health ? undefined : `daemon \u672A\u8FD0\u884C\u3002\u8FD0\u884C \`${cli} claude\`\uFF08\u6216 \`${cli} codex\`\uFF09\u4F1A\u81EA\u52A8\u542F\u52A8\u5B83\u3002`
|
|
4905
5639
|
});
|
|
4906
5640
|
checks.push({
|
|
4907
5641
|
name: "daemon readiness",
|
|
@@ -4909,18 +5643,26 @@ async function buildDoctorReport(pair, registered) {
|
|
|
4909
5643
|
detail: ready ? `ready thread=${ready.threadId ?? "none"}` : health ? "readyz is not OK" : "n/a \u2014 daemon not running",
|
|
4910
5644
|
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
|
|
4911
5645
|
});
|
|
5646
|
+
const appServerInfo = health?.appServerInfo ?? null;
|
|
5647
|
+
checks.push({
|
|
5648
|
+
name: "codex app-server",
|
|
5649
|
+
status: health ? "ok" : "skip",
|
|
5650
|
+
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",
|
|
5651
|
+
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
|
|
5652
|
+
});
|
|
5653
|
+
const drift = buildDrift === true ? describeBuildDrift(health?.build, BUILD_INFO, cli) : null;
|
|
4912
5654
|
checks.push({
|
|
4913
5655
|
name: "build drift",
|
|
4914
5656
|
status: buildDrift === false ? "ok" : buildDrift === true ? "fail" : "skip",
|
|
4915
|
-
detail: buildDrift === false ? `runtime matches launcher ${formatBuildInfo(BUILD_INFO)}` :
|
|
4916
|
-
hint:
|
|
5657
|
+
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)",
|
|
5658
|
+
hint: drift?.hint
|
|
4917
5659
|
});
|
|
4918
5660
|
checks.push(artifactAlignmentCheck());
|
|
4919
5661
|
checks.push({
|
|
4920
5662
|
name: "current thread",
|
|
4921
5663
|
status: usableThread ? "ok" : rawThread ? "warn" : registered ? "warn" : "skip",
|
|
4922
5664
|
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",
|
|
4923
|
-
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" +
|
|
5665
|
+
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
|
|
4924
5666
|
});
|
|
4925
5667
|
const pairProxyUrl = `ws://127.0.0.1:${pair.ports.proxyPort}`;
|
|
4926
5668
|
const managedTuis = listManagedCodexTuiProcesses();
|
|
@@ -4937,19 +5679,19 @@ async function buildDoctorReport(pair, registered) {
|
|
|
4937
5679
|
name: "codex tui (this pair)",
|
|
4938
5680
|
status: attachedHere.length > 0 ? "ok" : "warn",
|
|
4939
5681
|
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}`,
|
|
4940
|
-
hint: attachedHere.length > 0 ? undefined :
|
|
5682
|
+
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`
|
|
4941
5683
|
});
|
|
4942
5684
|
checks.push({
|
|
4943
5685
|
name: "codex tui (other pairs)",
|
|
4944
5686
|
status: attachedElsewhere.length > 0 ? "warn" : "ok",
|
|
4945
5687
|
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",
|
|
4946
|
-
hint: attachedElsewhere.length > 0 ?
|
|
5688
|
+
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
|
|
4947
5689
|
});
|
|
4948
5690
|
for (const [name, path] of [
|
|
4949
5691
|
["daemon log", pair.stateDir.logFile],
|
|
4950
5692
|
["codex wrapper log", pair.stateDir.codexWrapperLogFile]
|
|
4951
5693
|
]) {
|
|
4952
|
-
checks.push(logCheck(name, path));
|
|
5694
|
+
checks.push(logCheck(name, path, cli));
|
|
4953
5695
|
}
|
|
4954
5696
|
return {
|
|
4955
5697
|
cwd,
|
|
@@ -4970,61 +5712,102 @@ async function buildDoctorReport(pair, registered) {
|
|
|
4970
5712
|
checks
|
|
4971
5713
|
};
|
|
4972
5714
|
}
|
|
5715
|
+
function describeBuildDrift(runtime, launcher, cli = "abg") {
|
|
5716
|
+
const basis = runtimeContractComparisonBasis(runtime, launcher);
|
|
5717
|
+
const baseDetail = `runtime ${formatBuildInfo(runtime)} differs from launcher ${formatBuildInfo(launcher)}`;
|
|
5718
|
+
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";
|
|
5719
|
+
if (basis === "codeHash") {
|
|
5720
|
+
return { detail: `${baseDetail} [compared by codeHash \u2014 real code difference]`, hint: baseHint };
|
|
5721
|
+
}
|
|
5722
|
+
return {
|
|
5723
|
+
detail: `${baseDetail} [compared by commit stamp \u2014 legacy build without codeHash]`,
|
|
5724
|
+
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"
|
|
5725
|
+
};
|
|
5726
|
+
}
|
|
5727
|
+
function isUsableCodeHash(hash) {
|
|
5728
|
+
return typeof hash === "string" && hash.length > 0 && hash !== "source";
|
|
5729
|
+
}
|
|
5730
|
+
function evaluateArtifactAlignment(stamps) {
|
|
5731
|
+
if (stamps.length < 2) {
|
|
5732
|
+
return {
|
|
5733
|
+
name: "artifact alignment",
|
|
5734
|
+
status: "skip",
|
|
5735
|
+
detail: "n/a \u2014 fewer than two stamped artifacts found"
|
|
5736
|
+
};
|
|
5737
|
+
}
|
|
5738
|
+
if (stamps.every((stamp) => isUsableCodeHash(stamp.codeHash))) {
|
|
5739
|
+
const rendered2 = stamps.map((stamp) => `${stamp.label}=${stamp.codeHash}`).join(", ");
|
|
5740
|
+
if (new Set(stamps.map((stamp) => stamp.codeHash)).size === 1) {
|
|
5741
|
+
return { name: "artifact alignment", status: "ok", detail: `codeHash basis: ${rendered2}` };
|
|
5742
|
+
}
|
|
5743
|
+
return {
|
|
5744
|
+
name: "artifact alignment",
|
|
5745
|
+
status: "fail",
|
|
5746
|
+
detail: `deployed artifacts contain DIFFERENT code (codeHash basis): ${rendered2}`,
|
|
5747
|
+
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"
|
|
5748
|
+
};
|
|
5749
|
+
}
|
|
5750
|
+
const rendered = stamps.map((stamp) => `${stamp.label}=${stamp.commit}`).join(", ");
|
|
5751
|
+
if (new Set(stamps.map((stamp) => stamp.commit)).size === 1) {
|
|
5752
|
+
return {
|
|
5753
|
+
name: "artifact alignment",
|
|
5754
|
+
status: "ok",
|
|
5755
|
+
detail: `legacy commit-stamp basis: ${rendered}`
|
|
5756
|
+
};
|
|
5757
|
+
}
|
|
5758
|
+
return {
|
|
5759
|
+
name: "artifact alignment",
|
|
5760
|
+
status: "fail",
|
|
5761
|
+
detail: `deployed artifacts are at DIFFERENT builds (legacy commit-stamp basis): ${rendered}`,
|
|
5762
|
+
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"
|
|
5763
|
+
};
|
|
5764
|
+
}
|
|
4973
5765
|
function artifactAlignmentCheck() {
|
|
4974
5766
|
const stamps = [];
|
|
4975
5767
|
if (BUILD_INFO.commit !== "source") {
|
|
4976
|
-
stamps.push({
|
|
5768
|
+
stamps.push({
|
|
5769
|
+
label: `launcher(${BUILD_INFO.bundle})`,
|
|
5770
|
+
commit: BUILD_INFO.commit,
|
|
5771
|
+
codeHash: hasValidCodeHash(BUILD_INFO) ? BUILD_INFO.codeHash ?? null : null
|
|
5772
|
+
});
|
|
4977
5773
|
}
|
|
4978
5774
|
const bin = Bun.which("agentbridge") ?? Bun.which("abg");
|
|
4979
5775
|
if (bin) {
|
|
4980
5776
|
try {
|
|
4981
|
-
const
|
|
4982
|
-
if (
|
|
4983
|
-
stamps.push({ label: "global-cli",
|
|
5777
|
+
const stamp = extractBundleStamp(realpathSync3(bin));
|
|
5778
|
+
if (stamp)
|
|
5779
|
+
stamps.push({ label: "global-cli", ...stamp });
|
|
4984
5780
|
} catch {}
|
|
4985
5781
|
}
|
|
4986
5782
|
const cacheRoot = pluginCacheRoot();
|
|
4987
5783
|
try {
|
|
4988
5784
|
for (const version of readdirSync6(cacheRoot)) {
|
|
4989
|
-
const
|
|
4990
|
-
if (
|
|
4991
|
-
stamps.push({ label: `plugin-cache@${version}`,
|
|
5785
|
+
const stamp = extractBundleStamp(join17(cacheRoot, version, "server", "daemon.js"));
|
|
5786
|
+
if (stamp)
|
|
5787
|
+
stamps.push({ label: `plugin-cache@${version}`, ...stamp });
|
|
4992
5788
|
}
|
|
4993
5789
|
} catch {}
|
|
4994
5790
|
const repoBundle = join17(process.cwd(), "plugins", "agentbridge", "server", "daemon.js");
|
|
4995
5791
|
if (existsSync15(repoBundle)) {
|
|
4996
|
-
const
|
|
4997
|
-
if (
|
|
4998
|
-
stamps.push({ label: "repo-bundle",
|
|
4999
|
-
}
|
|
5000
|
-
if (stamps.length < 2) {
|
|
5001
|
-
return {
|
|
5002
|
-
name: "artifact alignment",
|
|
5003
|
-
status: "skip",
|
|
5004
|
-
detail: "n/a \u2014 fewer than two stamped artifacts found"
|
|
5005
|
-
};
|
|
5006
|
-
}
|
|
5007
|
-
const commits = new Set(stamps.map((s) => s.commit));
|
|
5008
|
-
const rendered = stamps.map((s) => `${s.label}=${s.commit}`).join(", ");
|
|
5009
|
-
if (commits.size === 1) {
|
|
5010
|
-
return { name: "artifact alignment", status: "ok", detail: rendered };
|
|
5792
|
+
const stamp = extractBundleStamp(repoBundle);
|
|
5793
|
+
if (stamp)
|
|
5794
|
+
stamps.push({ label: "repo-bundle", ...stamp });
|
|
5011
5795
|
}
|
|
5012
|
-
return
|
|
5013
|
-
name: "artifact alignment",
|
|
5014
|
-
status: "fail",
|
|
5015
|
-
detail: `deployed artifacts are at DIFFERENT builds: ${rendered}`,
|
|
5016
|
-
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"
|
|
5017
|
-
};
|
|
5796
|
+
return evaluateArtifactAlignment(stamps);
|
|
5018
5797
|
}
|
|
5019
|
-
function
|
|
5798
|
+
function extractBundleStamp(path) {
|
|
5020
5799
|
try {
|
|
5021
|
-
const
|
|
5022
|
-
|
|
5800
|
+
const text = readFileSync13(path, "utf-8");
|
|
5801
|
+
const commit = text.match(/commit:\s*defineString\("([^"]+)",\s*"source"\)/)?.[1] ?? null;
|
|
5802
|
+
if (!commit)
|
|
5803
|
+
return null;
|
|
5804
|
+
const codeHash = text.match(/codeHash:\s*defineString\("([^"]+)",\s*"source"\)/)?.[1] ?? null;
|
|
5805
|
+
return { commit, codeHash };
|
|
5023
5806
|
} catch {
|
|
5024
5807
|
return null;
|
|
5025
5808
|
}
|
|
5026
5809
|
}
|
|
5027
|
-
function configParseabilityCheck(cwd) {
|
|
5810
|
+
function configParseabilityCheck(cwd, cli) {
|
|
5028
5811
|
const desc = new ConfigService(cwd).describeConfig();
|
|
5029
5812
|
if (desc.state === "absent") {
|
|
5030
5813
|
return {
|
|
@@ -5038,7 +5821,7 @@ function configParseabilityCheck(cwd) {
|
|
|
5038
5821
|
name: "config.json",
|
|
5039
5822
|
status: "warn",
|
|
5040
5823
|
detail: `unparseable at ${desc.path} (${desc.reason}) \u2014 custom thresholds NOT in effect, using defaults`,
|
|
5041
|
-
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" +
|
|
5824
|
+
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`
|
|
5042
5825
|
};
|
|
5043
5826
|
}
|
|
5044
5827
|
return {
|
|
@@ -5047,7 +5830,34 @@ function configParseabilityCheck(cwd) {
|
|
|
5047
5830
|
detail: desc.customValues ? `parsed at ${desc.path} \u2014 custom values in effect` : `parsed at ${desc.path} \u2014 all values match defaults`
|
|
5048
5831
|
};
|
|
5049
5832
|
}
|
|
5050
|
-
function
|
|
5833
|
+
function evaluateBudgetStrategyGuard(strategy, guardHardPct, targetUtilPct = V3_DEFAULT_TARGET_UTIL) {
|
|
5834
|
+
if (strategy !== "maximize") {
|
|
5835
|
+
return {
|
|
5836
|
+
name: "budget strategy",
|
|
5837
|
+
status: "ok",
|
|
5838
|
+
detail: "strategy=conserve \u2014 v2-equivalent budget behavior (v3 maximize is opt-in)"
|
|
5839
|
+
};
|
|
5840
|
+
}
|
|
5841
|
+
if (guardHardPct >= targetUtilPct) {
|
|
5842
|
+
return {
|
|
5843
|
+
name: "budget strategy",
|
|
5844
|
+
status: "ok",
|
|
5845
|
+
detail: `strategy=maximize \u2014 outer guard hard line ${guardHardPct}% covers targetUtil ${targetUtilPct}%`
|
|
5846
|
+
};
|
|
5847
|
+
}
|
|
5848
|
+
return {
|
|
5849
|
+
name: "budget strategy",
|
|
5850
|
+
status: "warn",
|
|
5851
|
+
detail: `strategy=maximize but the outer quota-guard hard line (${guardHardPct}%) is below ` + `targetUtil (${targetUtilPct}%) \u2014 the ${guardHardPct}\u2192${targetUtilPct} band is unreachable for Claude`,
|
|
5852
|
+
hint: "v3 \u4E0D\u53EF\u8D8A\u8FC7\u5916\u5C42 quota-guard \u786C\u7EBF\uFF1AClaude \u4FA7\u8FBE\u5230 guard \u786C\u7EBF\u65F6\u8FDB\u7A0B\u4F1A\u88AB\u5916\u5C42\u5F3A\u505C\uFF0C" + `maximize \u7684 ${guardHardPct}%\u2192${targetUtilPct}% \u533A\u95F4\u5B9E\u9645\u70E7\u4E0D\u5230\u3002\u60F3\u771F\u6B63\u7528\u5230 targetUtil\uFF0C` + "\u9700\u81EA\u884C\u8C03\u9AD8 quota-guard \u7684 BUDGET_HARD\uFF08\u672C\u4ED3\u5E93\u4E0D\u4EE3\u6539\u5916\u5C42\u914D\u7F6E\uFF09\uFF1B\u5C55\u793A\u4FA7\u5DF2\u6309 guard \u7EBF\u6536\u53E3\u3002"
|
|
5853
|
+
};
|
|
5854
|
+
}
|
|
5855
|
+
function budgetStrategyGuardCheck(cwd) {
|
|
5856
|
+
const config = new ConfigService(cwd).loadOrDefault();
|
|
5857
|
+
const budget = applyBudgetEnvOverrides(config.budget);
|
|
5858
|
+
return evaluateBudgetStrategyGuard(budget.strategy, resolveGuardHardHint());
|
|
5859
|
+
}
|
|
5860
|
+
function logCheck(name, path, cli) {
|
|
5051
5861
|
if (!existsSync15(path)) {
|
|
5052
5862
|
return {
|
|
5053
5863
|
name,
|
|
@@ -5062,7 +5872,7 @@ function logCheck(name, path) {
|
|
|
5062
5872
|
name,
|
|
5063
5873
|
status: "warn",
|
|
5064
5874
|
detail: `${path} (${stat.size} bytes, oversized; stop the pair, rebuild/reinstall, then rotate or remove this log)`,
|
|
5065
|
-
hint:
|
|
5875
|
+
hint: `\u65E5\u5FD7\u8FC7\u5927\uFF1A\`${cli} kill\` \u505C\u6B62 pair \u540E\u5220\u9664\u8BE5\u6587\u4EF6\u518D\u91CD\u542F\u5373\u53EF\u3002`
|
|
5066
5876
|
};
|
|
5067
5877
|
}
|
|
5068
5878
|
return { name, status: "ok", detail: `${path} (${stat.size} bytes)` };
|
|
@@ -5096,11 +5906,13 @@ function printDoctorReport(report) {
|
|
|
5096
5906
|
console.log(line);
|
|
5097
5907
|
}
|
|
5098
5908
|
}
|
|
5099
|
-
var LARGE_LOG_WARN_BYTES;
|
|
5909
|
+
var LARGE_LOG_WARN_BYTES, V3_DEFAULT_TARGET_UTIL = 97;
|
|
5100
5910
|
var init_doctor = __esm(() => {
|
|
5101
5911
|
init_plugin_cache();
|
|
5102
5912
|
init_build_info();
|
|
5913
|
+
init_cli_invocation();
|
|
5103
5914
|
init_config_service();
|
|
5915
|
+
init_render();
|
|
5104
5916
|
init_env_guard();
|
|
5105
5917
|
init_pair_resolver();
|
|
5106
5918
|
init_thread_state();
|
|
@@ -5109,91 +5921,6 @@ var init_doctor = __esm(() => {
|
|
|
5109
5921
|
LARGE_LOG_WARN_BYTES = 100 * 1024 * 1024;
|
|
5110
5922
|
});
|
|
5111
5923
|
|
|
5112
|
-
// src/budget/render.ts
|
|
5113
|
-
function formatEpoch(epochSeconds) {
|
|
5114
|
-
if (!epochSeconds || epochSeconds <= 0)
|
|
5115
|
-
return "\u672A\u77E5";
|
|
5116
|
-
return new Date(epochSeconds * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
|
|
5117
|
-
}
|
|
5118
|
-
function formatWindow(window, label) {
|
|
5119
|
-
if (!window)
|
|
5120
|
-
return `${label} \u672A\u77E5`;
|
|
5121
|
-
return `${label} ${window.util}%\uFF08\u91CD\u7F6E ${formatEpoch(window.resetEpoch)}\uFF09`;
|
|
5122
|
-
}
|
|
5123
|
-
function formatAgent(name, usage, snapshotAt) {
|
|
5124
|
-
if (!usage)
|
|
5125
|
-
return `${name}\uFF1A\u672A\u77E5\uFF08\u63A2\u6D4B\u4E0D\u53EF\u7528\uFF09`;
|
|
5126
|
-
const parts = [
|
|
5127
|
-
formatWindow(usage.fiveHour, "5h"),
|
|
5128
|
-
formatWindow(usage.weekly, "\u5468"),
|
|
5129
|
-
`\u95E8\u63A7 ${usage.gateUtil}%`,
|
|
5130
|
-
`\u9884\u8B66 ${usage.warnUtil}%`
|
|
5131
|
-
];
|
|
5132
|
-
if (usage.rateLimitedUntil > 0) {
|
|
5133
|
-
parts.push(`\u9650\u6D41\u81F3 ${formatEpoch(usage.rateLimitedUntil)}`);
|
|
5134
|
-
}
|
|
5135
|
-
if (usage.parsedVia === "positional") {
|
|
5136
|
-
parts.push("\u26A0\uFE0F \u7A97\u53E3\u8BC6\u522B\u4F7F\u7528\u4F4D\u7F6E\u515C\u5E95");
|
|
5137
|
-
}
|
|
5138
|
-
const ageSec = usage.fetchedAt > 0 ? snapshotAt - usage.fetchedAt : 0;
|
|
5139
|
-
if (ageSec > 300) {
|
|
5140
|
-
parts.push(`\u26A0\uFE0F \u6570\u636E\u91C7\u96C6\u4E8E ${Math.round(ageSec / 60)} \u5206\u949F\u524D`);
|
|
5141
|
-
} else if (usage.stale) {
|
|
5142
|
-
parts.push("\uFF08\u7F13\u5B58\u6570\u636E\uFF09");
|
|
5143
|
-
}
|
|
5144
|
-
return `${name}\uFF1A${parts.join(" \xB7 ")}`;
|
|
5145
|
-
}
|
|
5146
|
-
function renderBudgetSnapshot(snapshot) {
|
|
5147
|
-
const lines = [];
|
|
5148
|
-
lines.push(`\u3010\u9884\u7B97\u5FEB\u7167 \xB7 \u8D26\u53F7\u7EA7\u3011\u9636\u6BB5\uFF1A${PHASE_LABELS[snapshot.phase]} \xB7 \u66F4\u65B0\u4E8E ${formatEpoch(snapshot.updatedAt)}`);
|
|
5149
|
-
lines.push(formatAgent("Claude", snapshot.claude, snapshot.updatedAt));
|
|
5150
|
-
lines.push(formatAgent("Codex", snapshot.codex, snapshot.updatedAt));
|
|
5151
|
-
if (snapshot.claude && snapshot.codex) {
|
|
5152
|
-
const abs = Math.abs(snapshot.driftPct);
|
|
5153
|
-
if (abs > 0) {
|
|
5154
|
-
const heavier = snapshot.driftPct > 0 ? "Claude" : "Codex";
|
|
5155
|
-
const lighter = snapshot.driftPct > 0 ? "Codex" : "Claude";
|
|
5156
|
-
lines.push(`\u6F02\u79FB\uFF1A${heavier} \u6BD4 ${lighter} \u9AD8 ${abs} \u4E2A\u767E\u5206\u70B9`);
|
|
5157
|
-
} else {
|
|
5158
|
-
lines.push("\u6F02\u79FB\uFF1A\u53CC\u65B9\u6301\u5E73");
|
|
5159
|
-
}
|
|
5160
|
-
}
|
|
5161
|
-
if (snapshot.paused) {
|
|
5162
|
-
const resume = snapshot.resumeAfterEpoch ? `\uFF1B\u9884\u8BA1\u6062\u590D ${formatEpoch(snapshot.resumeAfterEpoch)}\uFF08\u4EE5\u5B9E\u6D4B\u4E3A\u51C6\uFF1B\u63D0\u524D\u5237\u65B0\u4F1A\u66F4\u65E9\u89E3\u9664\uFF09` : "";
|
|
5163
|
-
const reason = snapshot.pauseReason ?? "\u989D\u5EA6\u63A5\u8FD1\u8017\u5C3D";
|
|
5164
|
-
if (snapshot.pauseSide === "claude" && !snapshot.gateClosed) {
|
|
5165
|
-
lines.push(`\u63A5\u529B\u4E2D\uFF1AClaude \u4FA7\u989D\u5EA6\u8017\u5C3D\uFF0C\u5DF2\u4EA4\u63A5 Codex \u7EE7\u7EED\u63A8\u8FDB\uFF08\u95F8\u95E8\u5F00\u653E\uFF09 \u2014 ${reason}${resume}`);
|
|
5166
|
-
} else if (snapshot.pauseSide === "codex") {
|
|
5167
|
-
lines.push(`\u6682\u505C\uFF1ACodex \u4FA7\u989D\u5EA6\u8017\u5C3D\uFF08\u95F8\u95E8\u5173\u95ED\uFF0CClaude \u53EF solo \u63A8\u8FDB\u72EC\u7ACB\u90E8\u5206\uFF09 \u2014 ${reason}${resume}`);
|
|
5168
|
-
} else {
|
|
5169
|
-
lines.push(`\u6682\u505C\uFF1A\u53CC\u4FA7\u8054\u5408\u6682\u505C\uFF08\u95F8\u95E8\u5173\u95ED\uFF09 \u2014 ${reason}${resume}`);
|
|
5170
|
-
}
|
|
5171
|
-
} else {
|
|
5172
|
-
lines.push("\u6682\u505C\uFF1A\u5426");
|
|
5173
|
-
}
|
|
5174
|
-
if (snapshot.parallelRecommended) {
|
|
5175
|
-
lines.push("\u5E76\u884C\u5EFA\u8BAE\uFF1A\u989D\u5EA6\u5BCC\u4F59\u4E14\u4E34\u8FD1\u7ED3\u7B97\uFF0C\u5EFA\u8BAE\u62C6\u5206\u66F4\u591A\u5E76\u884C\u5B50\u4EFB\u52A1");
|
|
5176
|
-
}
|
|
5177
|
-
if (snapshot.codexTier !== "full") {
|
|
5178
|
-
lines.push(`Codex \u6863\u4F4D\uFF1A${snapshot.codexTier}`);
|
|
5179
|
-
}
|
|
5180
|
-
if (snapshot.claudeAdvice) {
|
|
5181
|
-
lines.push(`Claude \u5EFA\u8BAE\uFF1A${snapshot.claudeAdvice}`);
|
|
5182
|
-
}
|
|
5183
|
-
lines.push("\u6CE8\uFF1A\u767E\u5206\u6BD4\u4E3A\u8BA2\u9605\u8D26\u53F7\u7EA7\u7528\u91CF\uFF08\u540C\u673A\u5176\u4ED6\u4F1A\u8BDD\u5171\u4EAB\u540C\u4E00\u989D\u5EA6\u6C60\uFF09\u3002");
|
|
5184
|
-
return lines.join(`
|
|
5185
|
-
`);
|
|
5186
|
-
}
|
|
5187
|
-
var PHASE_LABELS, BUDGET_UNAVAILABLE_TEXT = "\u9884\u7B97\u611F\u77E5\u4E0D\u53EF\u7528\uFF1A\u672A\u68C0\u6D4B\u5230 agent-quota-guard \u63A2\u9488\uFF08~/.budget-guard/bin/budget-probe\uFF09\u6216 budget \u529F\u80FD\u5DF2\u7981\u7528\u3002\u534F\u4F5C\u4E0D\u53D7\u5F71\u54CD\u3002";
|
|
5188
|
-
var init_render = __esm(() => {
|
|
5189
|
-
PHASE_LABELS = {
|
|
5190
|
-
normal: "normal\uFF08\u6B63\u5E38\uFF09",
|
|
5191
|
-
balance: "balance\uFF08\u9700\u5747\u8861\uFF09",
|
|
5192
|
-
parallel: "parallel\uFF08\u5EFA\u8BAE\u5E76\u884C\u63D0\u901F\uFF09",
|
|
5193
|
-
paused: "paused\uFF08\u9884\u7B97\u5E72\u9884\u4E2D\uFF09"
|
|
5194
|
-
};
|
|
5195
|
-
});
|
|
5196
|
-
|
|
5197
5924
|
// src/cli/budget.ts
|
|
5198
5925
|
var exports_budget = {};
|
|
5199
5926
|
__export(exports_budget, {
|
|
@@ -5201,6 +5928,7 @@ __export(exports_budget, {
|
|
|
5201
5928
|
});
|
|
5202
5929
|
async function runBudget(args) {
|
|
5203
5930
|
const json = args.includes("--json");
|
|
5931
|
+
const cli = cliInvocationName();
|
|
5204
5932
|
const { pairFlag } = parsePairFlag(args.filter((arg) => arg !== "--json"));
|
|
5205
5933
|
let resolution;
|
|
5206
5934
|
try {
|
|
@@ -5220,7 +5948,7 @@ async function runBudget(args) {
|
|
|
5220
5948
|
if (json) {
|
|
5221
5949
|
console.log(JSON.stringify({ ok: false, error: "pair_not_registered" }));
|
|
5222
5950
|
} else {
|
|
5223
|
-
console.error(
|
|
5951
|
+
console.error(`\u8BE5\u76EE\u5F55\u5C1A\u65E0 pair\uFF0C\u5148\u8FD0\u884C ${cli} claude`);
|
|
5224
5952
|
}
|
|
5225
5953
|
process.exit(1);
|
|
5226
5954
|
return;
|
|
@@ -5230,7 +5958,7 @@ async function runBudget(args) {
|
|
|
5230
5958
|
if (json) {
|
|
5231
5959
|
console.log(JSON.stringify({ ok: false, pairId: pair.pairId, error: "daemon_unreachable" }));
|
|
5232
5960
|
} else {
|
|
5233
|
-
console.error(`AgentBridge daemon \u672A\u8FD0\u884C\uFF08pair ${pair.pairId}\uFF0C\u63A7\u5236\u7AEF\u53E3 ${pair.ports.controlPort}\uFF09\u3002` +
|
|
5961
|
+
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`);
|
|
5234
5962
|
}
|
|
5235
5963
|
process.exit(1);
|
|
5236
5964
|
}
|
|
@@ -5242,6 +5970,7 @@ async function runBudget(args) {
|
|
|
5242
5970
|
console.log(status.budget ? renderBudgetSnapshot(status.budget) : BUDGET_UNAVAILABLE_TEXT);
|
|
5243
5971
|
}
|
|
5244
5972
|
var init_budget = __esm(() => {
|
|
5973
|
+
init_cli_invocation();
|
|
5245
5974
|
init_pair_resolver();
|
|
5246
5975
|
init_render();
|
|
5247
5976
|
});
|
|
@@ -5254,7 +5983,7 @@ __export(exports_logs, {
|
|
|
5254
5983
|
parseLogsArgs: () => parseLogsArgs,
|
|
5255
5984
|
followLog: () => followLog
|
|
5256
5985
|
});
|
|
5257
|
-
import { existsSync as existsSync16, readFileSync as
|
|
5986
|
+
import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
|
|
5258
5987
|
import { spawn as spawn4 } from "child_process";
|
|
5259
5988
|
function parseLogsArgs(args) {
|
|
5260
5989
|
let codex = false;
|
|
@@ -5347,7 +6076,7 @@ async function runLogs(args) {
|
|
|
5347
6076
|
function printTail(logPath, count, label, pairName) {
|
|
5348
6077
|
let text;
|
|
5349
6078
|
try {
|
|
5350
|
-
text =
|
|
6079
|
+
text = readFileSync14(logPath, "utf8");
|
|
5351
6080
|
} catch (err) {
|
|
5352
6081
|
console.error(`[agentbridge] failed to read ${label} for pair ${pairName}: ` + `${err instanceof Error ? err.message : String(err)} (${logPath})`);
|
|
5353
6082
|
process.exit(1);
|
|
@@ -5492,6 +6221,7 @@ async function main(command, restArgs) {
|
|
|
5492
6221
|
}
|
|
5493
6222
|
}
|
|
5494
6223
|
function printHelp() {
|
|
6224
|
+
const cli = cliInvocationName();
|
|
5495
6225
|
console.log(`
|
|
5496
6226
|
AgentBridge \u2014 Multi-agent collaboration bridge
|
|
5497
6227
|
|
|
@@ -5509,8 +6239,9 @@ Commands:
|
|
|
5509
6239
|
No target: print resume commands for this directory's last
|
|
5510
6240
|
Claude session + this pair's current Codex thread.
|
|
5511
6241
|
With target: resume that side directly.
|
|
5512
|
-
pairs [rm <name|id> | prune [--
|
|
5513
|
-
List pairs; remove one (rm), or
|
|
6242
|
+
pairs [rm <name|id> | prune [--apply]]
|
|
6243
|
+
List pairs; remove one (rm), or reclaim orphan dirs + stranded
|
|
6244
|
+
entries (prune previews by default; --apply to delete)
|
|
5514
6245
|
doctor [--json] Diagnose env, daemon, build drift, logs, and current thread
|
|
5515
6246
|
doctor resume-pollution [--apply] Find/fix old AgentBridge kickoff metadata
|
|
5516
6247
|
budget [--json] Show both agents' subscription quota snapshot (5h/weekly, drift, pause state)
|
|
@@ -5542,30 +6273,30 @@ Multi-pair:
|
|
|
5542
6273
|
contesting it \u2014 pick another --pair name (or kill the live one first).
|
|
5543
6274
|
|
|
5544
6275
|
Examples:
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
6276
|
+
${cli} init # First-time setup
|
|
6277
|
+
${cli} claude # Start the "main" pair for this directory
|
|
6278
|
+
${cli} codex # Connect Codex to this directory's "main" pair
|
|
6279
|
+
${cli} resume # Print resume commands for both sides
|
|
6280
|
+
${cli} resume claude # Resume the last Claude Code session here
|
|
6281
|
+
${cli} resume codex # Resume this pair's current Codex thread
|
|
6282
|
+
${cli} claude --safe # One launch without the max-permission default
|
|
6283
|
+
${cli} --pair work claude # Start a named pair "work" (this directory)
|
|
6284
|
+
${cli} --pair work codex # Connect Codex to the "work" pair
|
|
6285
|
+
${cli} --pair review claude # A second, parallel pair
|
|
6286
|
+
${cli} pairs # List all pairs and their ports/status
|
|
6287
|
+
${cli} pairs --threads # Include current thread mapping
|
|
6288
|
+
${cli} doctor --json # Emit a structured diagnostics report
|
|
6289
|
+
${cli} logs # Tail the last 100 lines of this pair's daemon log
|
|
6290
|
+
${cli} logs -f -n 200 # Follow the log, starting from the last 200 lines
|
|
6291
|
+
${cli} logs --codex # Tail the codex wrapper log instead
|
|
6292
|
+
${cli} --pair work logs # Tail the "work" pair's daemon log
|
|
6293
|
+
${cli} pairs rm work # Stop this directory's "work" pair and free its slot
|
|
6294
|
+
${cli} pairs rm work-1a2b3c4d # ...or by its full id (from that pair's directory)
|
|
6295
|
+
${cli} pairs prune # Preview reclaimable: orphan dirs + stranded entries (cwd-gone, dead, >1d)
|
|
6296
|
+
${cli} pairs prune --apply # ...actually delete the previewed dirs + entries
|
|
6297
|
+
${cli} --pair work kill # Stop only this directory's "work" pair
|
|
6298
|
+
${cli} kill # Stop this directory's pairs (+ any legacy-root daemon)
|
|
6299
|
+
${cli} kill all # Stop every pair in every directory (+ legacy-root)
|
|
5569
6300
|
`.trim());
|
|
5570
6301
|
}
|
|
5571
6302
|
function printVersion() {
|
|
@@ -5578,6 +6309,7 @@ function printVersion() {
|
|
|
5578
6309
|
}
|
|
5579
6310
|
var MARKETPLACE_NAME = "agentbridge", PLUGIN_NAME = "agentbridge", REFRESH_COMMANDS, NOTIFY_COMMANDS, PAIR_AWARE_COMMANDS;
|
|
5580
6311
|
var init_cli = __esm(() => {
|
|
6312
|
+
init_cli_invocation();
|
|
5581
6313
|
REFRESH_COMMANDS = new Set(["claude", "codex", "resume"]);
|
|
5582
6314
|
NOTIFY_COMMANDS = new Set(["claude", "codex", "init", "dev", "resume"]);
|
|
5583
6315
|
PAIR_AWARE_COMMANDS = new Set(["claude", "codex", "kill", "doctor", "budget", "resume", "logs"]);
|