@raysonmeng/agentbridge 0.1.10 → 0.1.12
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/dist/cli.js +859 -421
- package/dist/daemon.js +968 -239
- package/package.json +1 -1
- package/plugins/agentbridge/.claude-plugin/plugin.json +1 -1
- package/plugins/agentbridge/server/bridge-server.js +375 -139
- package/plugins/agentbridge/server/daemon.js +968 -239
package/dist/cli.js
CHANGED
|
@@ -120,7 +120,7 @@ function parsePositiveIntEnv(name, fallback, log = () => {}, env = process.env)
|
|
|
120
120
|
var require_package = __commonJS((exports, module) => {
|
|
121
121
|
module.exports = {
|
|
122
122
|
name: "@raysonmeng/agentbridge",
|
|
123
|
-
version: "0.1.
|
|
123
|
+
version: "0.1.12",
|
|
124
124
|
description: "Bridge between Claude Code and Codex \u2014 bidirectional agent communication via MCP Channel + JSON-RPC",
|
|
125
125
|
type: "module",
|
|
126
126
|
packageManager: "bun@1.3.11",
|
|
@@ -323,6 +323,48 @@ import { join as join2 } from "path";
|
|
|
323
323
|
function isRecord(value) {
|
|
324
324
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
325
325
|
}
|
|
326
|
+
function isCoercibleNumber(value) {
|
|
327
|
+
if (typeof value === "number")
|
|
328
|
+
return Number.isFinite(value);
|
|
329
|
+
if (typeof value === "string")
|
|
330
|
+
return Number.isFinite(Number(value));
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
function findShapeViolation(raw) {
|
|
334
|
+
if ("idleShutdownSeconds" in raw && !isCoercibleNumber(raw.idleShutdownSeconds)) {
|
|
335
|
+
return "idleShutdownSeconds is present but not a number";
|
|
336
|
+
}
|
|
337
|
+
if ("budget" in raw) {
|
|
338
|
+
const budget = raw.budget;
|
|
339
|
+
if (!isRecord(budget)) {
|
|
340
|
+
return "budget is present but not an object";
|
|
341
|
+
}
|
|
342
|
+
const numericKeys = ["pauseAt", "resumeBelow", "pollSeconds", "syncDriftPct"];
|
|
343
|
+
for (const key of numericKeys) {
|
|
344
|
+
if (key in budget && !isCoercibleNumber(budget[key])) {
|
|
345
|
+
return `budget.${key} is present but not a number`;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if ("parallel" in budget) {
|
|
349
|
+
const parallel = budget.parallel;
|
|
350
|
+
if (!isRecord(parallel)) {
|
|
351
|
+
return "budget.parallel is present but not an object";
|
|
352
|
+
}
|
|
353
|
+
for (const key of ["minRemainingPct", "timeWindowSec"]) {
|
|
354
|
+
if (key in parallel && !isCoercibleNumber(parallel[key])) {
|
|
355
|
+
return `budget.parallel.${key} is present but not a number`;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
function hasCustomDecisionValues(config) {
|
|
363
|
+
const d = DEFAULT_CONFIG;
|
|
364
|
+
const b = config.budget;
|
|
365
|
+
const db = d.budget;
|
|
366
|
+
return config.idleShutdownSeconds !== d.idleShutdownSeconds || config.turnCoordination.attentionWindowSeconds !== d.turnCoordination.attentionWindowSeconds || config.codex.appPort !== d.codex.appPort || config.codex.proxyPort !== d.codex.proxyPort || b.enabled !== db.enabled || b.pollSeconds !== db.pollSeconds || b.pauseAt !== db.pauseAt || b.resumeBelow !== db.resumeBelow || b.syncDriftPct !== db.syncDriftPct || b.parallel.minRemainingPct !== db.parallel.minRemainingPct || b.parallel.timeWindowSec !== db.parallel.timeWindowSec || b.codexTierControl !== db.codexTierControl;
|
|
367
|
+
}
|
|
326
368
|
function normalizeInteger(value, fallback) {
|
|
327
369
|
if (typeof value === "number" && Number.isFinite(value))
|
|
328
370
|
return value;
|
|
@@ -358,35 +400,35 @@ function normalizeCodexOverride(raw) {
|
|
|
358
400
|
override.effort = raw.effort.trim();
|
|
359
401
|
return Object.keys(override).length > 0 ? override : null;
|
|
360
402
|
}
|
|
361
|
-
function normalizeCodexTiers(raw) {
|
|
403
|
+
function normalizeCodexTiers(raw, fallback = DEFAULT_BUDGET_CONFIG.codexTiers) {
|
|
362
404
|
const tiers = isRecord(raw) ? raw : {};
|
|
363
405
|
return {
|
|
364
406
|
full: normalizeCodexOverride(tiers.full),
|
|
365
|
-
balanced: normalizeCodexOverride(tiers.balanced) ??
|
|
366
|
-
eco: normalizeCodexOverride(tiers.eco) ??
|
|
407
|
+
balanced: normalizeCodexOverride(tiers.balanced) ?? fallback.balanced,
|
|
408
|
+
eco: normalizeCodexOverride(tiers.eco) ?? fallback.eco
|
|
367
409
|
};
|
|
368
410
|
}
|
|
369
|
-
function normalizeBudgetConfig(raw) {
|
|
411
|
+
function normalizeBudgetConfig(raw, fallback = DEFAULT_BUDGET_CONFIG) {
|
|
370
412
|
const budget = isRecord(raw) ? raw : {};
|
|
371
413
|
const parallel = isRecord(budget.parallel) ? budget.parallel : {};
|
|
372
|
-
const codexTiers = normalizeCodexTiers(budget.codexTiers);
|
|
373
|
-
let pauseAt = normalizeBoundedInteger(budget.pauseAt,
|
|
374
|
-
let resumeBelow = normalizeBoundedInteger(budget.resumeBelow,
|
|
414
|
+
const codexTiers = normalizeCodexTiers(budget.codexTiers, fallback.codexTiers);
|
|
415
|
+
let pauseAt = normalizeBoundedInteger(budget.pauseAt, fallback.pauseAt, 1, 100);
|
|
416
|
+
let resumeBelow = normalizeBoundedInteger(budget.resumeBelow, fallback.resumeBelow, 0, 99);
|
|
375
417
|
if (pauseAt <= resumeBelow) {
|
|
376
418
|
pauseAt = DEFAULT_BUDGET_CONFIG.pauseAt;
|
|
377
419
|
resumeBelow = DEFAULT_BUDGET_CONFIG.resumeBelow;
|
|
378
420
|
}
|
|
379
421
|
return {
|
|
380
|
-
enabled: normalizeBoolean(budget.enabled,
|
|
381
|
-
pollSeconds: normalizeBoundedInteger(budget.pollSeconds,
|
|
422
|
+
enabled: normalizeBoolean(budget.enabled, fallback.enabled),
|
|
423
|
+
pollSeconds: normalizeBoundedInteger(budget.pollSeconds, fallback.pollSeconds, 5, 3600),
|
|
382
424
|
pauseAt,
|
|
383
425
|
resumeBelow,
|
|
384
|
-
syncDriftPct: normalizeBoundedInteger(budget.syncDriftPct,
|
|
426
|
+
syncDriftPct: normalizeBoundedInteger(budget.syncDriftPct, fallback.syncDriftPct, 1, 100),
|
|
385
427
|
parallel: {
|
|
386
|
-
minRemainingPct: normalizeBoundedInteger(parallel.minRemainingPct,
|
|
387
|
-
timeWindowSec: normalizeBoundedInteger(parallel.timeWindowSec,
|
|
428
|
+
minRemainingPct: normalizeBoundedInteger(parallel.minRemainingPct, fallback.parallel.minRemainingPct, 1, 100),
|
|
429
|
+
timeWindowSec: normalizeBoundedInteger(parallel.timeWindowSec, fallback.parallel.timeWindowSec, 60, 604800)
|
|
388
430
|
},
|
|
389
|
-
codexTierControl: normalizeBoolean(budget.codexTierControl,
|
|
431
|
+
codexTierControl: normalizeBoolean(budget.codexTierControl, fallback.codexTierControl) && codexTiers.full !== null,
|
|
390
432
|
codexTiers
|
|
391
433
|
};
|
|
392
434
|
}
|
|
@@ -423,15 +465,59 @@ class ConfigService {
|
|
|
423
465
|
return existsSync2(this.configPath);
|
|
424
466
|
}
|
|
425
467
|
load() {
|
|
468
|
+
let raw;
|
|
426
469
|
try {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
470
|
+
raw = readFileSync2(this.configPath, "utf-8");
|
|
471
|
+
} catch (err) {
|
|
472
|
+
if (err?.code === "ENOENT") {
|
|
473
|
+
return { state: "absent" };
|
|
474
|
+
}
|
|
475
|
+
return { state: "corrupt", reason: `config.json is unreadable: ${err.message}` };
|
|
431
476
|
}
|
|
477
|
+
let parsed;
|
|
478
|
+
try {
|
|
479
|
+
parsed = JSON.parse(raw);
|
|
480
|
+
} catch (err) {
|
|
481
|
+
return {
|
|
482
|
+
state: "corrupt",
|
|
483
|
+
reason: `config.json is not valid JSON: ${err.message}`
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
if (!isRecord(parsed)) {
|
|
487
|
+
return { state: "corrupt", reason: "config.json is not a JSON object" };
|
|
488
|
+
}
|
|
489
|
+
const violation = findShapeViolation(parsed);
|
|
490
|
+
if (violation) {
|
|
491
|
+
return { state: "corrupt", reason: `config.json is shape-invalid: ${violation}` };
|
|
492
|
+
}
|
|
493
|
+
const config = normalizeConfig(parsed);
|
|
494
|
+
if (!config) {
|
|
495
|
+
return { state: "corrupt", reason: "config.json could not be normalized" };
|
|
496
|
+
}
|
|
497
|
+
return { state: "parsed", config };
|
|
498
|
+
}
|
|
499
|
+
loadOrDefault(log = NOOP_LOGGER) {
|
|
500
|
+
const result = this.load();
|
|
501
|
+
if (result.state === "parsed")
|
|
502
|
+
return result.config;
|
|
503
|
+
if (result.state === "corrupt") {
|
|
504
|
+
log(`config.json at ${this.configPath} is unusable (${result.reason}); ` + "falling back to defaults \u2014 your custom budget thresholds / idle-shutdown settings are NOT in effect. " + "Fix the file and restart to re-apply them.");
|
|
505
|
+
}
|
|
506
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
432
507
|
}
|
|
433
|
-
|
|
434
|
-
|
|
508
|
+
describeConfig() {
|
|
509
|
+
const result = this.load();
|
|
510
|
+
if (result.state === "absent") {
|
|
511
|
+
return { state: "absent", path: this.configPath, customValues: false };
|
|
512
|
+
}
|
|
513
|
+
if (result.state === "corrupt") {
|
|
514
|
+
return { state: "corrupt", path: this.configPath, reason: result.reason, customValues: false };
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
state: "parsed",
|
|
518
|
+
path: this.configPath,
|
|
519
|
+
customValues: hasCustomDecisionValues(result.config)
|
|
520
|
+
};
|
|
435
521
|
}
|
|
436
522
|
save(config) {
|
|
437
523
|
this.ensureConfigDir();
|
|
@@ -456,11 +542,11 @@ class ConfigService {
|
|
|
456
542
|
}
|
|
457
543
|
}
|
|
458
544
|
}
|
|
459
|
-
var DEFAULT_BUDGET_CONFIG, DEFAULT_CONFIG, CONFIG_DIR = ".agentbridge", CONFIG_FILE = "config.json";
|
|
545
|
+
var DEFAULT_BUDGET_CONFIG, DEFAULT_CONFIG, CONFIG_DIR = ".agentbridge", CONFIG_FILE = "config.json", NOOP_LOGGER = () => {};
|
|
460
546
|
var init_config_service = __esm(() => {
|
|
461
547
|
DEFAULT_BUDGET_CONFIG = {
|
|
462
548
|
enabled: true,
|
|
463
|
-
pollSeconds:
|
|
549
|
+
pollSeconds: 300,
|
|
464
550
|
pauseAt: 90,
|
|
465
551
|
resumeBelow: 30,
|
|
466
552
|
syncDriftPct: 10,
|
|
@@ -513,6 +599,30 @@ function registerMarketplace(marketplaceRoot) {
|
|
|
513
599
|
}
|
|
514
600
|
var init_pkg_root = () => {};
|
|
515
601
|
|
|
602
|
+
// src/cli/plugin-cache.ts
|
|
603
|
+
import { existsSync as existsSync4 } from "fs";
|
|
604
|
+
import { homedir as homedir2 } from "os";
|
|
605
|
+
import { join as join4, resolve } from "path";
|
|
606
|
+
function pluginCacheRoot(home = homedir2()) {
|
|
607
|
+
return join4(home, ".claude", "plugins", "cache", MARKETPLACE_NAME, PLUGIN_NAME);
|
|
608
|
+
}
|
|
609
|
+
function isInsideRepoCheckout(projectRoot) {
|
|
610
|
+
const buildScript = resolve(projectRoot, "scripts", "build-bundles.mjs");
|
|
611
|
+
return existsSync4(buildScript);
|
|
612
|
+
}
|
|
613
|
+
function shouldWarnMissingPluginCache(cacheExists) {
|
|
614
|
+
return !cacheExists;
|
|
615
|
+
}
|
|
616
|
+
var MARKETPLACE_STEPS;
|
|
617
|
+
var init_plugin_cache = __esm(() => {
|
|
618
|
+
init_cli();
|
|
619
|
+
MARKETPLACE_STEPS = [
|
|
620
|
+
`/plugin marketplace add raysonmeng/agent-bridge`,
|
|
621
|
+
`/plugin install ${PLUGIN_NAME}@${MARKETPLACE_NAME}`,
|
|
622
|
+
`/reload-plugins`
|
|
623
|
+
];
|
|
624
|
+
});
|
|
625
|
+
|
|
516
626
|
// src/marker-section.ts
|
|
517
627
|
function upsertMarkedSection(content, sectionId, section) {
|
|
518
628
|
const startMarker = MARKER_START(sectionId);
|
|
@@ -649,11 +759,12 @@ var exports_init = {};
|
|
|
649
759
|
__export(exports_init, {
|
|
650
760
|
writeCollaborationSections: () => writeCollaborationSections,
|
|
651
761
|
runInit: () => runInit,
|
|
762
|
+
pluginInstallFallbackGuidance: () => pluginInstallFallbackGuidance,
|
|
652
763
|
compareVersions: () => compareVersions
|
|
653
764
|
});
|
|
654
765
|
import { execSync, execFileSync as execFileSync2 } from "child_process";
|
|
655
766
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
656
|
-
import { join as
|
|
767
|
+
import { join as join5 } from "path";
|
|
657
768
|
async function runInit() {
|
|
658
769
|
console.log(`AgentBridge Init
|
|
659
770
|
`);
|
|
@@ -681,25 +792,54 @@ async function runInit() {
|
|
|
681
792
|
}
|
|
682
793
|
console.log("");
|
|
683
794
|
console.log("Installing AgentBridge plugin...");
|
|
795
|
+
let pluginInstalled = false;
|
|
684
796
|
try {
|
|
685
|
-
|
|
797
|
+
const packageRoot = findPackageRoot();
|
|
798
|
+
registerMarketplace(packageRoot);
|
|
686
799
|
execFileSync2("claude", ["plugin", "install", `${PLUGIN_NAME}@${MARKETPLACE_NAME}`], {
|
|
687
800
|
stdio: "inherit"
|
|
688
801
|
});
|
|
689
802
|
console.log(" Plugin installed successfully.");
|
|
803
|
+
pluginInstalled = true;
|
|
690
804
|
} catch {
|
|
691
805
|
console.log(" Plugin install skipped (marketplace registration or install failed).");
|
|
692
|
-
|
|
693
|
-
|
|
806
|
+
for (const line of pluginInstallFallbackGuidance(detectRepoCheckout())) {
|
|
807
|
+
console.log(line);
|
|
808
|
+
}
|
|
694
809
|
}
|
|
695
810
|
console.log("");
|
|
696
|
-
|
|
811
|
+
if (pluginInstalled) {
|
|
812
|
+
console.log(`Setup complete!
|
|
813
|
+
`);
|
|
814
|
+
} else {
|
|
815
|
+
console.log(`Setup incomplete \u2014 plugin not installed.
|
|
697
816
|
`);
|
|
817
|
+
process.exitCode = 1;
|
|
818
|
+
}
|
|
698
819
|
console.log("Next steps:");
|
|
699
820
|
console.log(" 1. If Claude Code is already running, execute /reload-plugins in your session");
|
|
700
821
|
console.log(" 2. Start Claude Code: agentbridge claude");
|
|
701
822
|
console.log(" 3. Start Codex TUI: agentbridge codex");
|
|
702
823
|
}
|
|
824
|
+
function detectRepoCheckout() {
|
|
825
|
+
try {
|
|
826
|
+
return isInsideRepoCheckout(findPackageRoot());
|
|
827
|
+
} catch {
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
function pluginInstallFallbackGuidance(insideRepo) {
|
|
832
|
+
if (insideRepo) {
|
|
833
|
+
return [
|
|
834
|
+
" You can install it later with:",
|
|
835
|
+
" abg dev # registers marketplace and installs plugin"
|
|
836
|
+
];
|
|
837
|
+
}
|
|
838
|
+
return [
|
|
839
|
+
" Install the plugin from Claude Code with these steps:",
|
|
840
|
+
...MARKETPLACE_STEPS.map((step) => ` ${step}`)
|
|
841
|
+
];
|
|
842
|
+
}
|
|
703
843
|
function checkBun() {
|
|
704
844
|
try {
|
|
705
845
|
const version = execSync("bun --version", { encoding: "utf-8" }).trim();
|
|
@@ -745,8 +885,8 @@ function checkCodex() {
|
|
|
745
885
|
function writeCollaborationSections(projectRoot) {
|
|
746
886
|
const results = [];
|
|
747
887
|
const files = [
|
|
748
|
-
{ name: "CLAUDE.md", path:
|
|
749
|
-
{ name: "AGENTS.md", path:
|
|
888
|
+
{ name: "CLAUDE.md", path: join5(projectRoot, "CLAUDE.md"), section: CLAUDE_MD_SECTION },
|
|
889
|
+
{ name: "AGENTS.md", path: join5(projectRoot, "AGENTS.md"), section: AGENTS_MD_SECTION }
|
|
750
890
|
];
|
|
751
891
|
for (const { name, path, section } of files) {
|
|
752
892
|
let existing = "";
|
|
@@ -781,6 +921,7 @@ var init_init = __esm(() => {
|
|
|
781
921
|
init_config_service();
|
|
782
922
|
init_cli();
|
|
783
923
|
init_pkg_root();
|
|
924
|
+
init_plugin_cache();
|
|
784
925
|
init_version_utils();
|
|
785
926
|
});
|
|
786
927
|
|
|
@@ -790,19 +931,17 @@ __export(exports_dev, {
|
|
|
790
931
|
runDev: () => runDev
|
|
791
932
|
});
|
|
792
933
|
import { execFileSync as execFileSync3, spawnSync } from "child_process";
|
|
793
|
-
import { resolve } from "path";
|
|
794
|
-
import { existsSync as
|
|
795
|
-
import { homedir as homedir2 } from "os";
|
|
934
|
+
import { resolve as resolve2 } from "path";
|
|
935
|
+
import { existsSync as existsSync5, cpSync, rmSync } from "fs";
|
|
796
936
|
async function runDev(args = []) {
|
|
797
937
|
console.log(`AgentBridge Dev Setup
|
|
798
938
|
`);
|
|
799
939
|
const skipBuild = args.includes("--skip-build");
|
|
800
940
|
const projectRoot = findPackageRoot();
|
|
801
|
-
const marketplacePath =
|
|
802
|
-
const pluginDir =
|
|
803
|
-
const pluginManifest =
|
|
804
|
-
|
|
805
|
-
if (!existsSync4(buildScript)) {
|
|
941
|
+
const marketplacePath = resolve2(projectRoot, ".claude-plugin", "marketplace.json");
|
|
942
|
+
const pluginDir = resolve2(projectRoot, "plugins", "agentbridge");
|
|
943
|
+
const pluginManifest = resolve2(pluginDir, ".claude-plugin", "plugin.json");
|
|
944
|
+
if (!isInsideRepoCheckout(projectRoot)) {
|
|
806
945
|
console.error(" ERROR: 'agentbridge dev' must run inside an AgentBridge repository checkout \u2014");
|
|
807
946
|
console.error(" the published package does not ship the build scripts.");
|
|
808
947
|
console.error("");
|
|
@@ -839,12 +978,12 @@ async function runDev(args = []) {
|
|
|
839
978
|
console.log(` \u2713 Plugin built successfully
|
|
840
979
|
`);
|
|
841
980
|
}
|
|
842
|
-
if (!
|
|
981
|
+
if (!existsSync5(pluginManifest)) {
|
|
843
982
|
console.error(` ERROR: Plugin manifest not found at ${pluginManifest}`);
|
|
844
983
|
console.error(" Run 'bun run build:plugin' first, or check your working tree.");
|
|
845
984
|
process.exit(1);
|
|
846
985
|
}
|
|
847
|
-
if (!
|
|
986
|
+
if (!existsSync5(marketplacePath)) {
|
|
848
987
|
console.error(` ERROR: Marketplace manifest not found at ${marketplacePath}`);
|
|
849
988
|
process.exit(1);
|
|
850
989
|
}
|
|
@@ -873,12 +1012,12 @@ Installing plugin...`);
|
|
|
873
1012
|
}
|
|
874
1013
|
console.log(`
|
|
875
1014
|
Syncing local plugin to cache...`);
|
|
876
|
-
const cacheDir =
|
|
877
|
-
if (
|
|
1015
|
+
const cacheDir = pluginCacheRoot();
|
|
1016
|
+
if (existsSync5(cacheDir)) {
|
|
878
1017
|
const versionDirs = Bun.spawnSync(["ls", cacheDir]).stdout.toString().trim().split(`
|
|
879
1018
|
`).filter(Boolean);
|
|
880
1019
|
for (const ver of versionDirs) {
|
|
881
|
-
const targetDir =
|
|
1020
|
+
const targetDir = resolve2(cacheDir, ver);
|
|
882
1021
|
rmSync(targetDir, { recursive: true, force: true });
|
|
883
1022
|
cpSync(pluginDir, targetDir, { recursive: true });
|
|
884
1023
|
console.log(` Synced to ${targetDir}`);
|
|
@@ -898,15 +1037,23 @@ Syncing local plugin to cache...`);
|
|
|
898
1037
|
var init_dev = __esm(() => {
|
|
899
1038
|
init_cli();
|
|
900
1039
|
init_pkg_root();
|
|
1040
|
+
init_plugin_cache();
|
|
901
1041
|
});
|
|
902
1042
|
|
|
903
1043
|
// src/control-protocol.ts
|
|
904
1044
|
var CLOSE_CODE_REPLACED = 4001, CLOSE_CODE_EVICTED_STALE = 4002, CLOSE_CODE_PROBE_IN_PROGRESS = 4003, CLOSE_CODE_PAIR_MISMATCH = 4004;
|
|
905
1045
|
|
|
1046
|
+
// src/interrupt-timing.ts
|
|
1047
|
+
var CLIENT_REPLY_TIMEOUT_MS = 15000, INTERRUPT_CLIENT_MARGIN_MS = 2000, MAX_INTERRUPT_TIMEOUT_MS;
|
|
1048
|
+
var init_interrupt_timing = __esm(() => {
|
|
1049
|
+
MAX_INTERRUPT_TIMEOUT_MS = CLIENT_REPLY_TIMEOUT_MS - INTERRUPT_CLIENT_MARGIN_MS;
|
|
1050
|
+
});
|
|
1051
|
+
|
|
906
1052
|
// src/daemon-client.ts
|
|
907
1053
|
import { EventEmitter } from "events";
|
|
908
1054
|
var nextSocketId = 0, DaemonClient;
|
|
909
1055
|
var init_daemon_client = __esm(() => {
|
|
1056
|
+
init_interrupt_timing();
|
|
910
1057
|
DaemonClient = class DaemonClient extends EventEmitter {
|
|
911
1058
|
url;
|
|
912
1059
|
options;
|
|
@@ -933,7 +1080,7 @@ var init_daemon_client = __esm(() => {
|
|
|
933
1080
|
this.ws = null;
|
|
934
1081
|
}
|
|
935
1082
|
const socketId = ++nextSocketId;
|
|
936
|
-
await new Promise((
|
|
1083
|
+
await new Promise((resolve3, reject) => {
|
|
937
1084
|
const ws = new WebSocket(this.url);
|
|
938
1085
|
let settled = false;
|
|
939
1086
|
ws.onopen = () => {
|
|
@@ -942,7 +1089,7 @@ var init_daemon_client = __esm(() => {
|
|
|
942
1089
|
this.wsId = socketId;
|
|
943
1090
|
this.attachSocketHandlers(ws, socketId);
|
|
944
1091
|
this.log(`ws#${socketId} opened and attached`);
|
|
945
|
-
|
|
1092
|
+
resolve3();
|
|
946
1093
|
};
|
|
947
1094
|
ws.onerror = () => {
|
|
948
1095
|
if (settled)
|
|
@@ -968,7 +1115,7 @@ var init_daemon_client = __esm(() => {
|
|
|
968
1115
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
969
1116
|
return null;
|
|
970
1117
|
}
|
|
971
|
-
return await new Promise((
|
|
1118
|
+
return await new Promise((resolve3) => {
|
|
972
1119
|
let settled = false;
|
|
973
1120
|
let timer = null;
|
|
974
1121
|
const cleanup = () => {
|
|
@@ -985,7 +1132,7 @@ var init_daemon_client = __esm(() => {
|
|
|
985
1132
|
};
|
|
986
1133
|
const finish = (value) => {
|
|
987
1134
|
cleanup();
|
|
988
|
-
|
|
1135
|
+
resolve3(value);
|
|
989
1136
|
};
|
|
990
1137
|
const onStatus = (status) => finish(status);
|
|
991
1138
|
const onRejected = () => finish(null);
|
|
@@ -1007,7 +1154,7 @@ var init_daemon_client = __esm(() => {
|
|
|
1007
1154
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1008
1155
|
return { connected: false, alive: false };
|
|
1009
1156
|
}
|
|
1010
|
-
return await new Promise((
|
|
1157
|
+
return await new Promise((resolve3) => {
|
|
1011
1158
|
let settled = false;
|
|
1012
1159
|
let timer = null;
|
|
1013
1160
|
const finish = (value) => {
|
|
@@ -1019,7 +1166,7 @@ var init_daemon_client = __esm(() => {
|
|
|
1019
1166
|
this.off("incumbentStatus", onStatus);
|
|
1020
1167
|
this.off("disconnect", onDisconnect);
|
|
1021
1168
|
this.off("rejected", onRejected);
|
|
1022
|
-
|
|
1169
|
+
resolve3(value);
|
|
1023
1170
|
};
|
|
1024
1171
|
const onStatus = (s) => finish(s);
|
|
1025
1172
|
const onDisconnect = () => finish({ connected: false, alive: false });
|
|
@@ -1047,23 +1194,24 @@ var init_daemon_client = __esm(() => {
|
|
|
1047
1194
|
this.ws = null;
|
|
1048
1195
|
this.rejectPendingReplies("Daemon connection closed");
|
|
1049
1196
|
}
|
|
1050
|
-
async sendReply(message, requireReply, onBusy) {
|
|
1197
|
+
async sendReply(message, requireReply, onBusy, idempotencyKey) {
|
|
1051
1198
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1052
1199
|
return { success: false, error: "AgentBridge daemon is not connected." };
|
|
1053
1200
|
}
|
|
1054
1201
|
const requestId = `reply_${Date.now()}_${this.nextRequestId++}`;
|
|
1055
|
-
return new Promise((
|
|
1202
|
+
return new Promise((resolve3) => {
|
|
1056
1203
|
const timer = setTimeout(() => {
|
|
1057
1204
|
this.pendingReplies.delete(requestId);
|
|
1058
|
-
|
|
1059
|
-
},
|
|
1060
|
-
this.pendingReplies.set(requestId, { resolve:
|
|
1205
|
+
resolve3({ success: false, error: "Timed out waiting for AgentBridge daemon reply." });
|
|
1206
|
+
}, CLIENT_REPLY_TIMEOUT_MS);
|
|
1207
|
+
this.pendingReplies.set(requestId, { resolve: resolve3, timer });
|
|
1061
1208
|
this.send({
|
|
1062
1209
|
type: "claude_to_codex",
|
|
1063
1210
|
requestId,
|
|
1064
1211
|
message,
|
|
1065
1212
|
...requireReply ? { requireReply: true } : {},
|
|
1066
|
-
...onBusy && onBusy !== "reject" ? { onBusy } : {}
|
|
1213
|
+
...onBusy && onBusy !== "reject" ? { onBusy } : {},
|
|
1214
|
+
...idempotencyKey ? { idempotencyKey } : {}
|
|
1067
1215
|
});
|
|
1068
1216
|
});
|
|
1069
1217
|
}
|
|
@@ -1086,9 +1234,23 @@ var init_daemon_client = __esm(() => {
|
|
|
1086
1234
|
return;
|
|
1087
1235
|
clearTimeout(pending.timer);
|
|
1088
1236
|
this.pendingReplies.delete(message.requestId);
|
|
1089
|
-
pending.resolve({
|
|
1237
|
+
pending.resolve({
|
|
1238
|
+
success: message.success,
|
|
1239
|
+
error: message.error,
|
|
1240
|
+
...message.code !== undefined ? { code: message.code } : {},
|
|
1241
|
+
...message.phase !== undefined ? { phase: message.phase } : {},
|
|
1242
|
+
...message.retryAfterMs !== undefined ? { retryAfterMs: message.retryAfterMs } : {}
|
|
1243
|
+
});
|
|
1090
1244
|
return;
|
|
1091
1245
|
}
|
|
1246
|
+
case "turn_started":
|
|
1247
|
+
this.emit("turnStarted", {
|
|
1248
|
+
requestId: message.requestId,
|
|
1249
|
+
...message.idempotencyKey !== undefined ? { idempotencyKey: message.idempotencyKey } : {},
|
|
1250
|
+
threadId: message.threadId,
|
|
1251
|
+
turnId: message.turnId
|
|
1252
|
+
});
|
|
1253
|
+
return;
|
|
1092
1254
|
case "status":
|
|
1093
1255
|
this.emit("status", message.status);
|
|
1094
1256
|
return;
|
|
@@ -1165,17 +1327,217 @@ function formatBuildInfo(build) {
|
|
|
1165
1327
|
var BUILD_INFO;
|
|
1166
1328
|
var init_build_info = __esm(() => {
|
|
1167
1329
|
BUILD_INFO = Object.freeze({
|
|
1168
|
-
version: defineString("0.1.
|
|
1169
|
-
commit: defineString("
|
|
1330
|
+
version: defineString("0.1.12", "0.0.0-source"),
|
|
1331
|
+
commit: defineString("eec6018", "source"),
|
|
1170
1332
|
bundle: defineBundle("dist"),
|
|
1171
1333
|
contractVersion: defineNumber(1, CONTRACT_VERSION)
|
|
1172
1334
|
});
|
|
1173
1335
|
});
|
|
1174
1336
|
|
|
1337
|
+
// src/process-lifecycle.ts
|
|
1338
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
1339
|
+
import { basename } from "path";
|
|
1340
|
+
function parsePsProcessList(output) {
|
|
1341
|
+
const entries = [];
|
|
1342
|
+
for (const line of output.split(/\r?\n/)) {
|
|
1343
|
+
const match = line.match(/^\s*(\d+)\s+(.+?)\s*$/);
|
|
1344
|
+
if (!match)
|
|
1345
|
+
continue;
|
|
1346
|
+
const pid = Number.parseInt(match[1], 10);
|
|
1347
|
+
if (!Number.isFinite(pid))
|
|
1348
|
+
continue;
|
|
1349
|
+
entries.push({ pid, command: match[2] });
|
|
1350
|
+
}
|
|
1351
|
+
return entries;
|
|
1352
|
+
}
|
|
1353
|
+
function invokesCodexBinary(command) {
|
|
1354
|
+
const tokens = command.trim().split(/\s+/);
|
|
1355
|
+
const exe = tokens[0] ? basename(tokens[0]) : "";
|
|
1356
|
+
if (exe === "codex")
|
|
1357
|
+
return true;
|
|
1358
|
+
if ((exe === "node" || exe === "bun") && tokens[1]) {
|
|
1359
|
+
return basename(tokens[1]) === "codex";
|
|
1360
|
+
}
|
|
1361
|
+
return false;
|
|
1362
|
+
}
|
|
1363
|
+
function commandMatchesManagedCodexTui(command, proxyUrl) {
|
|
1364
|
+
if (!invokesCodexBinary(command))
|
|
1365
|
+
return false;
|
|
1366
|
+
if (!command.includes("tui_app_server"))
|
|
1367
|
+
return false;
|
|
1368
|
+
const remoteUrl = extractRemoteUrl(command);
|
|
1369
|
+
if (!remoteUrl)
|
|
1370
|
+
return false;
|
|
1371
|
+
if (!proxyUrl)
|
|
1372
|
+
return true;
|
|
1373
|
+
return remoteTargetsProxy(remoteUrl, proxyUrl);
|
|
1374
|
+
}
|
|
1375
|
+
function findManagedCodexTuiProcessesFromList(processes, proxyUrl) {
|
|
1376
|
+
return processes.filter((entry) => commandMatchesManagedCodexTui(entry.command, proxyUrl));
|
|
1377
|
+
}
|
|
1378
|
+
function findManagedCodexTuiProcesses(proxyUrl) {
|
|
1379
|
+
try {
|
|
1380
|
+
const output = execFileSync4("ps", ["-axo", "pid=,command="], { encoding: "utf-8" });
|
|
1381
|
+
return findManagedCodexTuiProcessesFromList(parsePsProcessList(output), proxyUrl).filter((entry) => entry.pid !== process.pid);
|
|
1382
|
+
} catch {
|
|
1383
|
+
return [];
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
function listManagedCodexTuiProcessesFromList(processes) {
|
|
1387
|
+
return processes.filter((entry) => commandMatchesManagedCodexTui(entry.command)).map((entry) => ({ ...entry, remoteUrl: extractRemoteUrl(entry.command) }));
|
|
1388
|
+
}
|
|
1389
|
+
function listManagedCodexTuiProcesses() {
|
|
1390
|
+
try {
|
|
1391
|
+
const output = execFileSync4("ps", ["-axo", "pid=,command="], { encoding: "utf-8" });
|
|
1392
|
+
return listManagedCodexTuiProcessesFromList(parsePsProcessList(output)).filter((entry) => entry.pid !== process.pid);
|
|
1393
|
+
} catch {
|
|
1394
|
+
return [];
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
function listBridgeFrontendProcessesFromList(processes) {
|
|
1398
|
+
return processes.filter((entry) => /(?:^|[\s/\\])bridge-server\.js(?:\s|$)/.test(entry.command) && (entry.command.includes("agentbridge") || entry.command.includes("agent_bridge")));
|
|
1399
|
+
}
|
|
1400
|
+
function listBridgeFrontendProcesses() {
|
|
1401
|
+
try {
|
|
1402
|
+
const output = execFileSync4("ps", ["-axo", "pid=,command="], { encoding: "utf-8" });
|
|
1403
|
+
return listBridgeFrontendProcessesFromList(parsePsProcessList(output)).filter((entry) => entry.pid !== process.pid);
|
|
1404
|
+
} catch {
|
|
1405
|
+
return [];
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
function commandForPid(pid) {
|
|
1409
|
+
try {
|
|
1410
|
+
return execFileSync4("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
|
|
1411
|
+
} catch {
|
|
1412
|
+
return null;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
function pidLooksAlive(pid) {
|
|
1416
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
1417
|
+
return false;
|
|
1418
|
+
try {
|
|
1419
|
+
process.kill(pid, 0);
|
|
1420
|
+
return true;
|
|
1421
|
+
} catch (err) {
|
|
1422
|
+
return err?.code === "EPERM";
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
function isAgentBridgeDaemon(pid, lookup = commandForPid) {
|
|
1426
|
+
const cmd = lookup(pid);
|
|
1427
|
+
if (cmd === null)
|
|
1428
|
+
return false;
|
|
1429
|
+
const hasDaemonEntry = /(?:^|[\s/\\])[\w.-]*-?daemon\.(?:ts|js)(?:\s|$)/.test(cmd);
|
|
1430
|
+
const hasAgentbridge = cmd.includes("agentbridge") || cmd.includes("agent_bridge");
|
|
1431
|
+
return hasDaemonEntry && hasAgentbridge;
|
|
1432
|
+
}
|
|
1433
|
+
function isAgentBridgeProcess(pid, lookup = commandForPid) {
|
|
1434
|
+
const cmd = lookup(pid);
|
|
1435
|
+
if (cmd === null)
|
|
1436
|
+
return false;
|
|
1437
|
+
return cmd.includes("agentbridge") || cmd.includes("agent_bridge");
|
|
1438
|
+
}
|
|
1439
|
+
function terminateProcessSync(pid, options = {}) {
|
|
1440
|
+
const gracefulTimeoutMs = options.gracefulTimeoutMs ?? 2000;
|
|
1441
|
+
const target = options.processGroup && process.platform !== "win32" ? -pid : pid;
|
|
1442
|
+
const label = options.processGroup && process.platform !== "win32" ? `process group ${pid}` : `pid ${pid}`;
|
|
1443
|
+
try {
|
|
1444
|
+
process.kill(target, "SIGTERM");
|
|
1445
|
+
options.log?.(`Sent SIGTERM to ${label}`);
|
|
1446
|
+
} catch {
|
|
1447
|
+
return !isProcessAlive(pid);
|
|
1448
|
+
}
|
|
1449
|
+
if (waitForExitSync(pid, gracefulTimeoutMs))
|
|
1450
|
+
return true;
|
|
1451
|
+
try {
|
|
1452
|
+
process.kill(target, "SIGKILL");
|
|
1453
|
+
options.log?.(`Sent SIGKILL to ${label}`);
|
|
1454
|
+
} catch {}
|
|
1455
|
+
return waitForExitSync(pid, 500);
|
|
1456
|
+
}
|
|
1457
|
+
function waitForExitSync(pid, timeoutMs) {
|
|
1458
|
+
const deadline = Date.now() + timeoutMs;
|
|
1459
|
+
while (Date.now() < deadline) {
|
|
1460
|
+
if (!isProcessAlive(pid))
|
|
1461
|
+
return true;
|
|
1462
|
+
sleepSync(50);
|
|
1463
|
+
}
|
|
1464
|
+
return !isProcessAlive(pid);
|
|
1465
|
+
}
|
|
1466
|
+
function sleepSync(ms) {
|
|
1467
|
+
const buffer = new SharedArrayBuffer(4);
|
|
1468
|
+
const view = new Int32Array(buffer);
|
|
1469
|
+
Atomics.wait(view, 0, 0, ms);
|
|
1470
|
+
}
|
|
1471
|
+
function extractRemoteUrl(command) {
|
|
1472
|
+
const equals = command.match(/(?:^|\s)--remote=([^\s]+)/);
|
|
1473
|
+
if (equals)
|
|
1474
|
+
return equals[1];
|
|
1475
|
+
const separate = command.match(/(?:^|\s)--remote\s+([^\s]+)/);
|
|
1476
|
+
return separate?.[1] ?? null;
|
|
1477
|
+
}
|
|
1478
|
+
function remoteTargetsProxy(remoteUrl, proxyUrl) {
|
|
1479
|
+
try {
|
|
1480
|
+
const remote = new URL(remoteUrl);
|
|
1481
|
+
const proxy = new URL(proxyUrl);
|
|
1482
|
+
return remote.protocol === proxy.protocol && remote.hostname === proxy.hostname && remote.port === proxy.port && normalizePath(remote.pathname) === normalizePath(proxy.pathname);
|
|
1483
|
+
} catch {
|
|
1484
|
+
return remoteUrl === proxyUrl;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
function normalizePath(pathname) {
|
|
1488
|
+
return pathname === "" ? "/" : pathname;
|
|
1489
|
+
}
|
|
1490
|
+
var isProcessAlive;
|
|
1491
|
+
var init_process_lifecycle = __esm(() => {
|
|
1492
|
+
isProcessAlive = pidLooksAlive;
|
|
1493
|
+
});
|
|
1494
|
+
|
|
1175
1495
|
// src/daemon-lifecycle.ts
|
|
1176
|
-
import { spawn
|
|
1177
|
-
import { existsSync as
|
|
1496
|
+
import { spawn } from "child_process";
|
|
1497
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, statSync, unlinkSync, writeFileSync as writeFileSync4, openSync, closeSync, constants } from "fs";
|
|
1178
1498
|
import { fileURLToPath } from "url";
|
|
1499
|
+
function isReuseVerdict(verdict) {
|
|
1500
|
+
return verdict === "reuse" || verdict === "reuse-despite-drift";
|
|
1501
|
+
}
|
|
1502
|
+
function classifyDaemon(expectedPairId, status, buildInfo) {
|
|
1503
|
+
if (!status) {
|
|
1504
|
+
return { verdict: "unreachable", reason: "daemon status is unavailable or unparseable" };
|
|
1505
|
+
}
|
|
1506
|
+
const reportedPairId = status.pairId;
|
|
1507
|
+
if (!expectedPairId && reportedPairId != null) {
|
|
1508
|
+
return {
|
|
1509
|
+
verdict: "manual-conflict",
|
|
1510
|
+
reason: `manual mode must not adopt registered pair ${reportedPairId}`
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
if (expectedPairId) {
|
|
1514
|
+
if (reportedPairId == null) {
|
|
1515
|
+
return {
|
|
1516
|
+
verdict: "replace-foreign",
|
|
1517
|
+
reason: `pair ${expectedPairId} found daemon without pair identity`
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
if (reportedPairId !== expectedPairId) {
|
|
1521
|
+
return {
|
|
1522
|
+
verdict: "replace-foreign",
|
|
1523
|
+
reason: `pair ${expectedPairId} found daemon for pair ${reportedPairId}`
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
if (!sameRuntimeContract(status.build, buildInfo)) {
|
|
1528
|
+
if (compatibleContractVersion(status.build, buildInfo) && status.tuiConnected === true) {
|
|
1529
|
+
return {
|
|
1530
|
+
verdict: "reuse-despite-drift",
|
|
1531
|
+
reason: "runtime build drift has a compatible contract and a live Codex TUI is attached"
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
return {
|
|
1535
|
+
verdict: "replace-drifted",
|
|
1536
|
+
reason: `runtime build ${formatBuildInfo(status.build)} does not match launcher ${formatBuildInfo(buildInfo)}`
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
return { verdict: "reuse", reason: "daemon pair and runtime contract match" };
|
|
1540
|
+
}
|
|
1179
1541
|
|
|
1180
1542
|
class DaemonLifecycle {
|
|
1181
1543
|
stateDir;
|
|
@@ -1208,52 +1570,37 @@ class DaemonLifecycle {
|
|
|
1208
1570
|
return null;
|
|
1209
1571
|
}
|
|
1210
1572
|
}
|
|
1211
|
-
|
|
1212
|
-
const
|
|
1213
|
-
if (
|
|
1214
|
-
return
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
const reported = status.pairId;
|
|
1218
|
-
if (reported == null)
|
|
1219
|
-
return true;
|
|
1220
|
-
return reported !== expected;
|
|
1221
|
-
}
|
|
1222
|
-
isRegisteredPairDaemonInManualMode(status) {
|
|
1223
|
-
return !this.expectedPairId && status?.pairId != null;
|
|
1224
|
-
}
|
|
1225
|
-
isBuildDrifted(status) {
|
|
1226
|
-
if (process.env.AGENTBRIDGE_ALLOW_BUILD_DRIFT === "1")
|
|
1227
|
-
return false;
|
|
1228
|
-
const runtime = status?.build;
|
|
1229
|
-
if (!runtime)
|
|
1230
|
-
return true;
|
|
1231
|
-
return !sameRuntimeContract(runtime, BUILD_INFO);
|
|
1573
|
+
classifyDaemon(status) {
|
|
1574
|
+
const classification = classifyDaemon(this.expectedPairId, status, BUILD_INFO);
|
|
1575
|
+
if (process.env.AGENTBRIDGE_ALLOW_BUILD_DRIFT === "1" && (classification.verdict === "replace-drifted" || classification.verdict === "unreachable")) {
|
|
1576
|
+
return { verdict: "reuse", reason: "build drift replacement disabled by AGENTBRIDGE_ALLOW_BUILD_DRIFT" };
|
|
1577
|
+
}
|
|
1578
|
+
return classification;
|
|
1232
1579
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
return false;
|
|
1236
|
-
return status?.tuiConnected === true;
|
|
1580
|
+
manualConflictError(status) {
|
|
1581
|
+
return new Error(`Control port ${this.controlPort} is owned by registered pair ${status?.pairId}. ` + `This session has no pair identity (manual mode) and will not reuse or replace it \u2014 ` + `start with \`agentbridge claude\` from that pair's directory, or set AGENTBRIDGE_CONTROL_PORT to a free port.`);
|
|
1237
1582
|
}
|
|
1238
1583
|
async ensureRunning() {
|
|
1239
1584
|
if (await this.isHealthy()) {
|
|
1240
1585
|
const status = await this.fetchStatus();
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
this.log(`Daemon on control port ${this.controlPort} is running build ${formatBuildInfo(status?.build)} ` + `(launcher ${formatBuildInfo(BUILD_INFO)}) but a live Codex TUI is attached \u2014 reusing instead of ` + `replacing; the new build is picked up at the next restart (abg kill, then relaunch)`);
|
|
1252
|
-
} else {
|
|
1586
|
+
const classification = this.classifyDaemon(status);
|
|
1587
|
+
switch (classification.verdict) {
|
|
1588
|
+
case "manual-conflict":
|
|
1589
|
+
throw this.manualConflictError(status);
|
|
1590
|
+
case "replace-foreign":
|
|
1591
|
+
this.log(`Control port ${this.controlPort} held by a daemon for pair ${status?.pairId ?? "<none>"}, ` + `but this pair is ${this.expectedPairId} \u2014 replacing foreign daemon`);
|
|
1592
|
+
await this.replaceUnhealthyDaemon(status?.pid);
|
|
1593
|
+
return;
|
|
1594
|
+
case "replace-drifted":
|
|
1595
|
+
case "unreachable":
|
|
1253
1596
|
this.log(`Daemon on control port ${this.controlPort} is running build ${formatBuildInfo(status?.build)} ` + `but launcher is ${formatBuildInfo(BUILD_INFO)} \u2014 replacing drifted daemon`);
|
|
1254
1597
|
await this.replaceUnhealthyDaemon(status?.pid);
|
|
1255
1598
|
return;
|
|
1256
|
-
|
|
1599
|
+
case "reuse-despite-drift":
|
|
1600
|
+
this.log(`Daemon on control port ${this.controlPort} is running build ${formatBuildInfo(status?.build)} ` + `(launcher ${formatBuildInfo(BUILD_INFO)}) but a live Codex TUI is attached \u2014 reusing instead of ` + `replacing; the new build is picked up at the next restart (abg kill, then relaunch)`);
|
|
1601
|
+
break;
|
|
1602
|
+
case "reuse":
|
|
1603
|
+
break;
|
|
1257
1604
|
}
|
|
1258
1605
|
try {
|
|
1259
1606
|
await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
|
|
@@ -1267,7 +1614,7 @@ class DaemonLifecycle {
|
|
|
1267
1614
|
const existingPid = this.readPid();
|
|
1268
1615
|
if (existingPid) {
|
|
1269
1616
|
if (isProcessAlive(existingPid)) {
|
|
1270
|
-
if (
|
|
1617
|
+
if (isAgentBridgeDaemon(existingPid)) {
|
|
1271
1618
|
try {
|
|
1272
1619
|
await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
|
|
1273
1620
|
return;
|
|
@@ -1283,14 +1630,17 @@ class DaemonLifecycle {
|
|
|
1283
1630
|
}
|
|
1284
1631
|
await this.withStartupLockStrict(async (locked) => {
|
|
1285
1632
|
if (!locked) {
|
|
1286
|
-
this.
|
|
1287
|
-
await this.waitForReadyAndOurs();
|
|
1633
|
+
await this.waitForContendedStartupLock();
|
|
1288
1634
|
return;
|
|
1289
1635
|
}
|
|
1290
1636
|
if (await this.isHealthy()) {
|
|
1291
1637
|
const status = await this.fetchStatus();
|
|
1292
|
-
|
|
1293
|
-
|
|
1638
|
+
const classification = this.classifyDaemon(status);
|
|
1639
|
+
if (classification.verdict === "manual-conflict") {
|
|
1640
|
+
throw this.manualConflictError(status);
|
|
1641
|
+
}
|
|
1642
|
+
if (!isReuseVerdict(classification.verdict)) {
|
|
1643
|
+
this.log(`Daemon on control port ${this.controlPort} is not reusable under startup lock ` + `(pair=${status?.pairId ?? "<none>"}, build=${formatBuildInfo(status?.build)}, ` + `reason=${classification.reason}) \u2014 replacing`);
|
|
1294
1644
|
await this.kill(3000, status?.pid);
|
|
1295
1645
|
} else {
|
|
1296
1646
|
try {
|
|
@@ -1318,7 +1668,7 @@ class DaemonLifecycle {
|
|
|
1318
1668
|
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
1319
1669
|
if (await this.isHealthy())
|
|
1320
1670
|
return;
|
|
1321
|
-
await new Promise((
|
|
1671
|
+
await new Promise((resolve3) => setTimeout(resolve3, delayMs));
|
|
1322
1672
|
}
|
|
1323
1673
|
throw new Error(`Timed out waiting for AgentBridge daemon health on ${this.healthUrl}`);
|
|
1324
1674
|
}
|
|
@@ -1334,7 +1684,7 @@ class DaemonLifecycle {
|
|
|
1334
1684
|
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
1335
1685
|
if (await this.isReady())
|
|
1336
1686
|
return;
|
|
1337
|
-
await new Promise((
|
|
1687
|
+
await new Promise((resolve3) => setTimeout(resolve3, delayMs));
|
|
1338
1688
|
}
|
|
1339
1689
|
throw new Error(`Timed out waiting for AgentBridge daemon readiness on ${this.readyUrl}`);
|
|
1340
1690
|
}
|
|
@@ -1342,11 +1692,15 @@ class DaemonLifecycle {
|
|
|
1342
1692
|
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
1343
1693
|
if (await this.isReady()) {
|
|
1344
1694
|
const status = await this.fetchStatus();
|
|
1345
|
-
|
|
1695
|
+
const classification = this.classifyDaemon(status);
|
|
1696
|
+
if (classification.verdict === "manual-conflict") {
|
|
1697
|
+
throw this.manualConflictError(status);
|
|
1698
|
+
}
|
|
1699
|
+
if (isReuseVerdict(classification.verdict)) {
|
|
1346
1700
|
return;
|
|
1347
1701
|
}
|
|
1348
1702
|
}
|
|
1349
|
-
await new Promise((
|
|
1703
|
+
await new Promise((resolve3) => setTimeout(resolve3, delayMs));
|
|
1350
1704
|
}
|
|
1351
1705
|
throw new Error(`Timed out waiting for AgentBridge daemon readiness+identity on ${this.readyUrl} (control port ${this.controlPort})`);
|
|
1352
1706
|
}
|
|
@@ -1400,7 +1754,7 @@ class DaemonLifecycle {
|
|
|
1400
1754
|
} catch {}
|
|
1401
1755
|
}
|
|
1402
1756
|
wasKilled() {
|
|
1403
|
-
return
|
|
1757
|
+
return existsSync6(this.stateDir.killedFile);
|
|
1404
1758
|
}
|
|
1405
1759
|
launch() {
|
|
1406
1760
|
this.stateDir.ensure();
|
|
@@ -1424,13 +1778,16 @@ class DaemonLifecycle {
|
|
|
1424
1778
|
async replaceUnhealthyDaemon(statusPid) {
|
|
1425
1779
|
await this.withStartupLockStrict(async (locked) => {
|
|
1426
1780
|
if (!locked) {
|
|
1427
|
-
this.
|
|
1428
|
-
await this.waitForReadyAndOurs();
|
|
1781
|
+
await this.waitForContendedStartupLock();
|
|
1429
1782
|
return;
|
|
1430
1783
|
}
|
|
1431
1784
|
if (await this.isHealthy()) {
|
|
1432
1785
|
const status = await this.fetchStatus();
|
|
1433
|
-
|
|
1786
|
+
const classification = this.classifyDaemon(status);
|
|
1787
|
+
if (classification.verdict === "manual-conflict") {
|
|
1788
|
+
throw this.manualConflictError(status);
|
|
1789
|
+
}
|
|
1790
|
+
if (isReuseVerdict(classification.verdict)) {
|
|
1434
1791
|
try {
|
|
1435
1792
|
await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
|
|
1436
1793
|
return;
|
|
@@ -1443,6 +1800,10 @@ class DaemonLifecycle {
|
|
|
1443
1800
|
await this.waitForReady();
|
|
1444
1801
|
});
|
|
1445
1802
|
}
|
|
1803
|
+
async waitForContendedStartupLock() {
|
|
1804
|
+
this.log("Another process holds the startup lock, waiting for readiness+identity...");
|
|
1805
|
+
await this.waitForReadyAndOurs();
|
|
1806
|
+
}
|
|
1446
1807
|
async withStartupLockStrict(fn) {
|
|
1447
1808
|
const locked = this.acquireLockStrict();
|
|
1448
1809
|
try {
|
|
@@ -1478,7 +1839,7 @@ class DaemonLifecycle {
|
|
|
1478
1839
|
this.releaseLock();
|
|
1479
1840
|
return this.acquireLockStrict(true);
|
|
1480
1841
|
}
|
|
1481
|
-
if (Number.isFinite(holderPid) && this.lockAgeMs() > LOCK_IDENTITY_GRACE_MS && !
|
|
1842
|
+
if (Number.isFinite(holderPid) && this.lockAgeMs() > LOCK_IDENTITY_GRACE_MS && !isAgentBridgeProcess(holderPid)) {
|
|
1482
1843
|
this.log(`Startup lock is ${Math.round(this.lockAgeMs() / 1000)}s old and holder pid ${holderPid} ` + `is an unrelated process (pid recycled), reclaiming`);
|
|
1483
1844
|
this.releaseLock();
|
|
1484
1845
|
return this.acquireLockStrict(true);
|
|
@@ -1499,14 +1860,6 @@ class DaemonLifecycle {
|
|
|
1499
1860
|
return 0;
|
|
1500
1861
|
}
|
|
1501
1862
|
}
|
|
1502
|
-
isAgentBridgeProcess(pid) {
|
|
1503
|
-
try {
|
|
1504
|
-
const cmd = execFileSync4("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
|
|
1505
|
-
return cmd.includes("agentbridge") || cmd.includes("agent_bridge");
|
|
1506
|
-
} catch {
|
|
1507
|
-
return false;
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
1863
|
releaseLock() {
|
|
1511
1864
|
try {
|
|
1512
1865
|
unlinkSync(this.stateDir.lockFile);
|
|
@@ -1524,7 +1877,7 @@ class DaemonLifecycle {
|
|
|
1524
1877
|
this.cleanup();
|
|
1525
1878
|
return false;
|
|
1526
1879
|
}
|
|
1527
|
-
if (!
|
|
1880
|
+
if (!isAgentBridgeDaemon(pid)) {
|
|
1528
1881
|
this.log(`Pid ${pid} is alive but is NOT an AgentBridge daemon \u2014 refusing to kill. Cleaning up stale pid file.`);
|
|
1529
1882
|
this.cleanup();
|
|
1530
1883
|
return false;
|
|
@@ -1543,7 +1896,7 @@ class DaemonLifecycle {
|
|
|
1543
1896
|
this.cleanup();
|
|
1544
1897
|
return true;
|
|
1545
1898
|
}
|
|
1546
|
-
await new Promise((
|
|
1899
|
+
await new Promise((resolve3) => setTimeout(resolve3, 200));
|
|
1547
1900
|
}
|
|
1548
1901
|
this.log(`Daemon pid ${pid} did not stop gracefully, sending SIGKILL`);
|
|
1549
1902
|
try {
|
|
@@ -1552,16 +1905,6 @@ class DaemonLifecycle {
|
|
|
1552
1905
|
this.cleanup();
|
|
1553
1906
|
return true;
|
|
1554
1907
|
}
|
|
1555
|
-
isDaemonProcess(pid) {
|
|
1556
|
-
try {
|
|
1557
|
-
const cmd = execFileSync4("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
|
|
1558
|
-
const hasDaemonEntry = /(?:^|[\s/\\])[\w.-]*-?daemon\.(?:ts|js)(?:\s|$)/.test(cmd);
|
|
1559
|
-
const hasAgentbridge = cmd.includes("agentbridge") || cmd.includes("agent_bridge");
|
|
1560
|
-
return hasDaemonEntry && hasAgentbridge;
|
|
1561
|
-
} catch {
|
|
1562
|
-
return false;
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
1908
|
cleanup() {
|
|
1566
1909
|
this.removePidFile();
|
|
1567
1910
|
this.removeStatusFile();
|
|
@@ -1576,17 +1919,10 @@ async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
|
|
|
1576
1919
|
clearTimeout(timer);
|
|
1577
1920
|
}
|
|
1578
1921
|
}
|
|
1579
|
-
function isProcessAlive(pid) {
|
|
1580
|
-
try {
|
|
1581
|
-
process.kill(pid, 0);
|
|
1582
|
-
return true;
|
|
1583
|
-
} catch {
|
|
1584
|
-
return false;
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
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;
|
|
1588
1923
|
var init_daemon_lifecycle = __esm(() => {
|
|
1589
1924
|
init_build_info();
|
|
1925
|
+
init_process_lifecycle();
|
|
1590
1926
|
DEFAULT_DAEMON_ENTRY = import.meta.url.endsWith(".ts") ? "./daemon.ts" : "./daemon.js";
|
|
1591
1927
|
DAEMON_ENTRY = process.env.AGENTBRIDGE_DAEMON_ENTRY || DEFAULT_DAEMON_ENTRY;
|
|
1592
1928
|
DAEMON_PATH = fileURLToPath(new URL(DAEMON_ENTRY, import.meta.url));
|
|
@@ -1598,7 +1934,7 @@ var init_daemon_lifecycle = __esm(() => {
|
|
|
1598
1934
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
1599
1935
|
import {
|
|
1600
1936
|
closeSync as closeSync2,
|
|
1601
|
-
existsSync as
|
|
1937
|
+
existsSync as existsSync7,
|
|
1602
1938
|
fsyncSync,
|
|
1603
1939
|
linkSync,
|
|
1604
1940
|
lstatSync,
|
|
@@ -1616,7 +1952,7 @@ import {
|
|
|
1616
1952
|
import { createServer } from "net";
|
|
1617
1953
|
import { createHash, randomUUID } from "crypto";
|
|
1618
1954
|
import { hostname, userInfo } from "os";
|
|
1619
|
-
import { basename, join as
|
|
1955
|
+
import { basename as basename2, join as join6, resolve as resolve3, sep } from "path";
|
|
1620
1956
|
function portsForSlot(slot) {
|
|
1621
1957
|
if (!Number.isInteger(slot) || slot < 0) {
|
|
1622
1958
|
throw new PairError("PAIR_ID_INVALID", `Invalid slot: ${slot}`);
|
|
@@ -1654,14 +1990,14 @@ function pickLowestFreeSlot(entries) {
|
|
|
1654
1990
|
return slot;
|
|
1655
1991
|
}
|
|
1656
1992
|
function pairsDir(base) {
|
|
1657
|
-
return
|
|
1993
|
+
return join6(base, "pairs");
|
|
1658
1994
|
}
|
|
1659
1995
|
function registryPath(base) {
|
|
1660
|
-
return
|
|
1996
|
+
return join6(pairsDir(base), REGISTRY_FILE_NAME);
|
|
1661
1997
|
}
|
|
1662
1998
|
function readRegistry(base) {
|
|
1663
1999
|
const path = registryPath(base);
|
|
1664
|
-
if (!
|
|
2000
|
+
if (!existsSync7(path))
|
|
1665
2001
|
return { version: 1, pairs: [] };
|
|
1666
2002
|
let parsed;
|
|
1667
2003
|
try {
|
|
@@ -1711,7 +2047,7 @@ function writeRegistry(base, reg) {
|
|
|
1711
2047
|
renameSync(tmp, target);
|
|
1712
2048
|
}
|
|
1713
2049
|
function lockFilePath(base) {
|
|
1714
|
-
return
|
|
2050
|
+
return join6(pairsDir(base), LOCK_FILE_NAME);
|
|
1715
2051
|
}
|
|
1716
2052
|
function readLockOwner(lockFile) {
|
|
1717
2053
|
try {
|
|
@@ -1723,16 +2059,6 @@ function readLockOwner(lockFile) {
|
|
|
1723
2059
|
return null;
|
|
1724
2060
|
}
|
|
1725
2061
|
}
|
|
1726
|
-
function pidLooksAlive(pid) {
|
|
1727
|
-
if (!Number.isInteger(pid) || pid <= 0)
|
|
1728
|
-
return false;
|
|
1729
|
-
try {
|
|
1730
|
-
process.kill(pid, 0);
|
|
1731
|
-
return true;
|
|
1732
|
-
} catch (err) {
|
|
1733
|
-
return err?.code === "EPERM";
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
2062
|
function lockFileAgeMs(lockFile) {
|
|
1737
2063
|
try {
|
|
1738
2064
|
return Date.now() - statSync2(lockFile).mtimeMs;
|
|
@@ -1755,7 +2081,7 @@ function safeUid() {
|
|
|
1755
2081
|
}
|
|
1756
2082
|
}
|
|
1757
2083
|
function sleep(ms) {
|
|
1758
|
-
return new Promise((
|
|
2084
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1759
2085
|
}
|
|
1760
2086
|
function lockIsStale(lockFile) {
|
|
1761
2087
|
const owner = readLockOwner(lockFile);
|
|
@@ -1866,17 +2192,9 @@ async function withRegistryLock(base, fn) {
|
|
|
1866
2192
|
await sleep(25 + Math.floor(Math.random() * 50));
|
|
1867
2193
|
}
|
|
1868
2194
|
}
|
|
1869
|
-
function isDaemonProcess(pid) {
|
|
1870
|
-
try {
|
|
1871
|
-
const cmd = execFileSync5("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
|
|
1872
|
-
return cmd.includes("daemon") && (cmd.includes("agentbridge") || cmd.includes("agent_bridge"));
|
|
1873
|
-
} catch {
|
|
1874
|
-
return false;
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
2195
|
function detectLegacyRootDaemon(base) {
|
|
1878
|
-
const rootPidFile =
|
|
1879
|
-
if (!
|
|
2196
|
+
const rootPidFile = join6(base, "daemon.pid");
|
|
2197
|
+
if (!existsSync7(rootPidFile))
|
|
1880
2198
|
return null;
|
|
1881
2199
|
let pid;
|
|
1882
2200
|
try {
|
|
@@ -1885,16 +2203,16 @@ function detectLegacyRootDaemon(base) {
|
|
|
1885
2203
|
} catch {
|
|
1886
2204
|
return null;
|
|
1887
2205
|
}
|
|
1888
|
-
if (!Number.isFinite(pid) || !pidLooksAlive(pid) || !
|
|
2206
|
+
if (!Number.isFinite(pid) || !pidLooksAlive(pid) || !isAgentBridgeDaemon(pid))
|
|
1889
2207
|
return null;
|
|
1890
2208
|
return { pid, controlPort: LEGACY_ROOT_CONTROL_PORT };
|
|
1891
2209
|
}
|
|
1892
2210
|
function probePortFree(port) {
|
|
1893
|
-
return new Promise((
|
|
2211
|
+
return new Promise((resolve4) => {
|
|
1894
2212
|
const server = createServer();
|
|
1895
|
-
server.once("error", () =>
|
|
2213
|
+
server.once("error", () => resolve4(false));
|
|
1896
2214
|
server.once("listening", () => {
|
|
1897
|
-
server.close(() =>
|
|
2215
|
+
server.close(() => resolve4(true));
|
|
1898
2216
|
});
|
|
1899
2217
|
server.listen(port, "127.0.0.1");
|
|
1900
2218
|
});
|
|
@@ -1971,7 +2289,7 @@ async function resolvePair(base, opts) {
|
|
|
1971
2289
|
pairId: entry.pairId,
|
|
1972
2290
|
slot,
|
|
1973
2291
|
ports,
|
|
1974
|
-
stateDir:
|
|
2292
|
+
stateDir: join6(pairsDir(base), entry.pairId),
|
|
1975
2293
|
name: entry.name ?? name,
|
|
1976
2294
|
entry,
|
|
1977
2295
|
warning
|
|
@@ -1979,7 +2297,7 @@ async function resolvePair(base, opts) {
|
|
|
1979
2297
|
}
|
|
1980
2298
|
async function removeAllocatedPairIfUnchanged(base, pairId, slot) {
|
|
1981
2299
|
await withRegistryLock(base, () => {
|
|
1982
|
-
if (
|
|
2300
|
+
if (existsSync7(pairDirPath(base, pairId)) || pairDirDaemonAlive(base, pairId))
|
|
1983
2301
|
return;
|
|
1984
2302
|
const reg = readRegistry(base);
|
|
1985
2303
|
const nextPairs = reg.pairs.filter((pair) => !(pair.pairId === pairId && pair.slot === slot));
|
|
@@ -1990,19 +2308,19 @@ async function removeAllocatedPairIfUnchanged(base, pairId, slot) {
|
|
|
1990
2308
|
}
|
|
1991
2309
|
function pairDirPath(base, pairId) {
|
|
1992
2310
|
const id = validatePairId(pairId);
|
|
1993
|
-
return
|
|
2311
|
+
return join6(pairsDir(base), id);
|
|
1994
2312
|
}
|
|
1995
2313
|
function removePairDir(base, pairId) {
|
|
1996
2314
|
const id = validatePairId(pairId);
|
|
1997
2315
|
const root = pairsDir(base);
|
|
1998
|
-
const dir =
|
|
1999
|
-
const canonicalRoot =
|
|
2000
|
-
const canonicalDir =
|
|
2316
|
+
const dir = join6(root, id);
|
|
2317
|
+
const canonicalRoot = resolve3(root);
|
|
2318
|
+
const canonicalDir = resolve3(dir);
|
|
2001
2319
|
if (canonicalDir === canonicalRoot || !canonicalDir.startsWith(canonicalRoot + sep)) {
|
|
2002
2320
|
throw new PairError("PAIR_ID_INVALID", `Refusing to remove a pair dir outside ${canonicalRoot}: ${canonicalDir}`, { pairId });
|
|
2003
2321
|
}
|
|
2004
2322
|
assertPairsRootNotSymlinked(root);
|
|
2005
|
-
if (!
|
|
2323
|
+
if (!existsSync7(canonicalDir))
|
|
2006
2324
|
return false;
|
|
2007
2325
|
rmSync2(canonicalDir, { recursive: true, force: true });
|
|
2008
2326
|
return true;
|
|
@@ -2020,22 +2338,22 @@ function assertPairsRootNotSymlinked(root) {
|
|
|
2020
2338
|
}
|
|
2021
2339
|
function listPairDirs(base) {
|
|
2022
2340
|
const root = pairsDir(base);
|
|
2023
|
-
if (!
|
|
2341
|
+
if (!existsSync7(root))
|
|
2024
2342
|
return [];
|
|
2025
2343
|
if (lstatSync(root).isSymbolicLink())
|
|
2026
2344
|
return [];
|
|
2027
2345
|
return readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
2028
2346
|
}
|
|
2029
2347
|
function pairDirDaemonAlive(base, pairId) {
|
|
2030
|
-
const dir =
|
|
2348
|
+
const dir = join6(pairsDir(base), pairId);
|
|
2031
2349
|
const pids = [];
|
|
2032
2350
|
try {
|
|
2033
|
-
const pid = Number.parseInt(readFileSync5(
|
|
2351
|
+
const pid = Number.parseInt(readFileSync5(join6(dir, "daemon.pid"), "utf-8").trim(), 10);
|
|
2034
2352
|
if (Number.isFinite(pid))
|
|
2035
2353
|
pids.push(pid);
|
|
2036
2354
|
} catch {}
|
|
2037
2355
|
try {
|
|
2038
|
-
const status = JSON.parse(readFileSync5(
|
|
2356
|
+
const status = JSON.parse(readFileSync5(join6(dir, "status.json"), "utf-8"));
|
|
2039
2357
|
if (typeof status?.pid === "number")
|
|
2040
2358
|
pids.push(status.pid);
|
|
2041
2359
|
} catch {}
|
|
@@ -2071,6 +2389,7 @@ async function removeUnregisteredPairDir(base, pairId) {
|
|
|
2071
2389
|
}
|
|
2072
2390
|
var PAIR_BASE_PORT = 4500, PAIR_SLOT_STRIDE = 10, PAIR_ID_REGEX, DEFAULT_PAIR_NAME = "main", 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;
|
|
2073
2391
|
var init_pair_registry = __esm(() => {
|
|
2392
|
+
init_process_lifecycle();
|
|
2074
2393
|
PAIR_ID_REGEX = /^[A-Za-z0-9._-]{1,64}$/;
|
|
2075
2394
|
WINDOWS_RESERVED_RE = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
|
2076
2395
|
PairError = class PairError extends Error {
|
|
@@ -2167,7 +2486,7 @@ var init_env_guard = __esm(() => {
|
|
|
2167
2486
|
|
|
2168
2487
|
// src/pair-resolver.ts
|
|
2169
2488
|
import { realpathSync as realpathSync2 } from "fs";
|
|
2170
|
-
import { join as
|
|
2489
|
+
import { join as join7, resolve as resolve4 } from "path";
|
|
2171
2490
|
function computeBaseDir() {
|
|
2172
2491
|
return process.env.AGENTBRIDGE_BASE_DIR || process.env.AGENTBRIDGE_STATE_DIR || StateDirResolver.platformBaseDir();
|
|
2173
2492
|
}
|
|
@@ -2294,7 +2613,7 @@ function resolvePairReadOnly(pairFlag) {
|
|
|
2294
2613
|
pairId: entry.pairId,
|
|
2295
2614
|
slot: entry.slot,
|
|
2296
2615
|
ports: portsForEntry(entry),
|
|
2297
|
-
stateDir: new StateDirResolver(
|
|
2616
|
+
stateDir: new StateDirResolver(join7(base, "pairs", entry.pairId)),
|
|
2298
2617
|
name: entry.name ?? name,
|
|
2299
2618
|
manual: false
|
|
2300
2619
|
}
|
|
@@ -2307,7 +2626,7 @@ function resolvePairReadOnly(pairFlag) {
|
|
|
2307
2626
|
pairId,
|
|
2308
2627
|
slot: null,
|
|
2309
2628
|
ports: { appPort: 0, proxyPort: 0, controlPort: 0 },
|
|
2310
|
-
stateDir: new StateDirResolver(
|
|
2629
|
+
stateDir: new StateDirResolver(join7(base, "pairs", pairId)),
|
|
2311
2630
|
name,
|
|
2312
2631
|
manual: false
|
|
2313
2632
|
}
|
|
@@ -2337,7 +2656,7 @@ function portsForEntry(entry) {
|
|
|
2337
2656
|
return portsForSlot(entry.slot);
|
|
2338
2657
|
}
|
|
2339
2658
|
function canonicalizeCwd(cwd) {
|
|
2340
|
-
const absolute =
|
|
2659
|
+
const absolute = resolve4(cwd);
|
|
2341
2660
|
try {
|
|
2342
2661
|
return realpathSync2.native(absolute);
|
|
2343
2662
|
} catch {
|
|
@@ -2354,8 +2673,8 @@ var init_pair_resolver = __esm(() => {
|
|
|
2354
2673
|
});
|
|
2355
2674
|
|
|
2356
2675
|
// src/trace-log.ts
|
|
2357
|
-
import { appendFileSync, mkdirSync as mkdirSync4 } from "fs";
|
|
2358
|
-
import { join as
|
|
2676
|
+
import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync3 } from "fs";
|
|
2677
|
+
import { join as join8 } from "path";
|
|
2359
2678
|
function pickRelevantEnv(env) {
|
|
2360
2679
|
const picked = {};
|
|
2361
2680
|
for (const [key, value] of Object.entries(env)) {
|
|
@@ -2390,7 +2709,7 @@ function redactArgv(argv) {
|
|
|
2390
2709
|
}
|
|
2391
2710
|
function traceLogPath(cwd, timestamp) {
|
|
2392
2711
|
const day = timestamp.slice(0, 10);
|
|
2393
|
-
return
|
|
2712
|
+
return join8(cwd, ".agentbridge", "logs", `trace-${day}.jsonl`);
|
|
2394
2713
|
}
|
|
2395
2714
|
function appendTraceEvent(input) {
|
|
2396
2715
|
const timestamp = input.timestamp ?? new Date().toISOString();
|
|
@@ -2404,11 +2723,39 @@ function appendTraceEvent(input) {
|
|
|
2404
2723
|
...input.env ? { env: pickRelevantEnv(input.env) } : {},
|
|
2405
2724
|
...input.data ? { data: redactData(input.data) } : {}
|
|
2406
2725
|
};
|
|
2407
|
-
|
|
2726
|
+
const logsDir = join8(input.cwd, ".agentbridge", "logs");
|
|
2727
|
+
const isNewDayFile = !existsSync8(path);
|
|
2728
|
+
mkdirSync4(logsDir, { recursive: true });
|
|
2729
|
+
if (isNewDayFile) {
|
|
2730
|
+
pruneOldTraceLogs(logsDir, path, Date.parse(timestamp));
|
|
2731
|
+
}
|
|
2408
2732
|
appendFileSync(path, JSON.stringify(event) + `
|
|
2409
2733
|
`, "utf-8");
|
|
2410
2734
|
return path;
|
|
2411
2735
|
}
|
|
2736
|
+
function pruneOldTraceLogs(logsDir, keepPath, nowMs) {
|
|
2737
|
+
if (!Number.isFinite(nowMs))
|
|
2738
|
+
return;
|
|
2739
|
+
const cutoff = nowMs - TRACE_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
|
2740
|
+
let entries;
|
|
2741
|
+
try {
|
|
2742
|
+
entries = readdirSync2(logsDir);
|
|
2743
|
+
} catch {
|
|
2744
|
+
return;
|
|
2745
|
+
}
|
|
2746
|
+
for (const name of entries) {
|
|
2747
|
+
if (!TRACE_FILE_RE.test(name))
|
|
2748
|
+
continue;
|
|
2749
|
+
const filePath = join8(logsDir, name);
|
|
2750
|
+
if (filePath === keepPath)
|
|
2751
|
+
continue;
|
|
2752
|
+
try {
|
|
2753
|
+
if (statSync3(filePath).mtimeMs < cutoff) {
|
|
2754
|
+
unlinkSync3(filePath);
|
|
2755
|
+
}
|
|
2756
|
+
} catch {}
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2412
2759
|
function isEnvSnapshot(key, value) {
|
|
2413
2760
|
return /env$/i.test(key) && !!value && typeof value === "object" && !Array.isArray(value);
|
|
2414
2761
|
}
|
|
@@ -2434,8 +2781,9 @@ function redactData(value, key = "") {
|
|
|
2434
2781
|
}
|
|
2435
2782
|
return value;
|
|
2436
2783
|
}
|
|
2437
|
-
var SECRET_KEY_RE, SECRET_ARG_RE, RELEVANT_ENV_RE;
|
|
2784
|
+
var TRACE_RETENTION_DAYS = 7, TRACE_FILE_RE, SECRET_KEY_RE, SECRET_ARG_RE, RELEVANT_ENV_RE;
|
|
2438
2785
|
var init_trace_log = __esm(() => {
|
|
2786
|
+
TRACE_FILE_RE = /^trace-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
2439
2787
|
SECRET_KEY_RE = /(token|secret|password|passwd|api[_-]?key|auth|cookie|session)/i;
|
|
2440
2788
|
SECRET_ARG_RE = /^--?(?:token|secret|password|passwd|apikey|api-key|api_key|auth|cookie|session)(?:=.*)?$/i;
|
|
2441
2789
|
RELEVANT_ENV_RE = /^(AGENTBRIDGE_|CODEX_)/;
|
|
@@ -2484,10 +2832,12 @@ var init_max_permissions = __esm(() => {
|
|
|
2484
2832
|
// src/cli/claude.ts
|
|
2485
2833
|
var exports_claude = {};
|
|
2486
2834
|
__export(exports_claude, {
|
|
2835
|
+
warnIfPluginCacheMissing: () => warnIfPluginCacheMissing,
|
|
2487
2836
|
runClaude: () => runClaude,
|
|
2488
2837
|
checkOwnedFlagConflicts: () => checkOwnedFlagConflicts
|
|
2489
2838
|
});
|
|
2490
2839
|
import { spawn as spawn2 } from "child_process";
|
|
2840
|
+
import { existsSync as existsSync9 } from "fs";
|
|
2491
2841
|
async function runClaude(args) {
|
|
2492
2842
|
const originalEnv = { ...process.env };
|
|
2493
2843
|
const envGuardResult = guardAgentBridgeEnv({
|
|
@@ -2525,6 +2875,7 @@ async function runClaude(args) {
|
|
|
2525
2875
|
}
|
|
2526
2876
|
await assertPairNotLive(lifecycle, pair);
|
|
2527
2877
|
lifecycle.clearKilled();
|
|
2878
|
+
warnIfPluginCacheMissing();
|
|
2528
2879
|
const channelEntry = `plugin:${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
2529
2880
|
if (permissionPlan.inject) {
|
|
2530
2881
|
console.error(`[agentbridge] running with ${CLAUDE_MAX_PERMISSION_FLAG} (default; opt out with --safe or AGENTBRIDGE_SAFE=1)`);
|
|
@@ -2552,6 +2903,18 @@ async function runClaude(args) {
|
|
|
2552
2903
|
process.exit(1);
|
|
2553
2904
|
});
|
|
2554
2905
|
}
|
|
2906
|
+
function warnIfPluginCacheMissing(cacheRoot = pluginCacheRoot(), log = (msg) => console.error(msg)) {
|
|
2907
|
+
let cacheExists;
|
|
2908
|
+
try {
|
|
2909
|
+
cacheExists = existsSync9(cacheRoot);
|
|
2910
|
+
} catch {
|
|
2911
|
+
return false;
|
|
2912
|
+
}
|
|
2913
|
+
if (!shouldWarnMissingPluginCache(cacheExists))
|
|
2914
|
+
return false;
|
|
2915
|
+
log("[agentbridge] \u26A0\uFE0F Plugin not installed (no plugin cache found). Run `abg init`, " + `or in Claude Code: ${MARKETPLACE_STEPS.join(" \u2192 ")}. Launching anyway\u2026`);
|
|
2916
|
+
return true;
|
|
2917
|
+
}
|
|
2555
2918
|
function traceCliStart(event, args, originalEnv, envGuardAction, pair) {
|
|
2556
2919
|
try {
|
|
2557
2920
|
appendTraceEvent({
|
|
@@ -2628,6 +2991,7 @@ function checkOwnedFlagConflicts(args, commandName, ownedFlags) {
|
|
|
2628
2991
|
var OWNED_FLAGS;
|
|
2629
2992
|
var init_claude = __esm(() => {
|
|
2630
2993
|
init_cli();
|
|
2994
|
+
init_plugin_cache();
|
|
2631
2995
|
init_daemon_client();
|
|
2632
2996
|
init_daemon_lifecycle();
|
|
2633
2997
|
init_build_info();
|
|
@@ -2639,11 +3003,11 @@ var init_claude = __esm(() => {
|
|
|
2639
3003
|
});
|
|
2640
3004
|
|
|
2641
3005
|
// src/agents-contract.ts
|
|
2642
|
-
import { existsSync as
|
|
2643
|
-
import { join as
|
|
3006
|
+
import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
|
|
3007
|
+
import { join as join9 } from "path";
|
|
2644
3008
|
function checkAgentsMdContract(cwd) {
|
|
2645
|
-
const path =
|
|
2646
|
-
const exists =
|
|
3009
|
+
const path = join9(cwd, "AGENTS.md");
|
|
3010
|
+
const exists = existsSync10(path);
|
|
2647
3011
|
let content = "";
|
|
2648
3012
|
if (exists) {
|
|
2649
3013
|
try {
|
|
@@ -2674,8 +3038,8 @@ function isFreshAgentsMdContract(content) {
|
|
|
2674
3038
|
var init_agents_contract = () => {};
|
|
2675
3039
|
|
|
2676
3040
|
// src/wrapper-exit-observability.ts
|
|
2677
|
-
import { readFileSync as readFileSync7, readdirSync as
|
|
2678
|
-
import { join as
|
|
3041
|
+
import { readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
3042
|
+
import { join as join10 } from "path";
|
|
2679
3043
|
function discoverNativeChildPid(launcherPid, run) {
|
|
2680
3044
|
try {
|
|
2681
3045
|
const out = run("pgrep", ["-P", String(launcherPid)]);
|
|
@@ -2697,16 +3061,6 @@ function readTurnInProgress(statusFilePath, read = (p) => readFileSync7(p, "utf-
|
|
|
2697
3061
|
return null;
|
|
2698
3062
|
}
|
|
2699
3063
|
}
|
|
2700
|
-
function defaultIsPidAlive(pid) {
|
|
2701
|
-
if (pid <= 0)
|
|
2702
|
-
return false;
|
|
2703
|
-
try {
|
|
2704
|
-
process.kill(pid, 0);
|
|
2705
|
-
return true;
|
|
2706
|
-
} catch (err) {
|
|
2707
|
-
return err.code === "EPERM";
|
|
2708
|
-
}
|
|
2709
|
-
}
|
|
2710
3064
|
function refineCleanExitClassification(turnInProgress) {
|
|
2711
3065
|
if (turnInProgress === true)
|
|
2712
3066
|
return "exit_0_during_turn";
|
|
@@ -2714,12 +3068,12 @@ function refineCleanExitClassification(turnInProgress) {
|
|
|
2714
3068
|
return "exit_0_idle";
|
|
2715
3069
|
return "exit_0_turn_unknown";
|
|
2716
3070
|
}
|
|
2717
|
-
function findCodexSqliteLog(codexHome, fs = { readdir:
|
|
3071
|
+
function findCodexSqliteLog(codexHome, fs = { readdir: readdirSync3, stat: statSync4 }) {
|
|
2718
3072
|
try {
|
|
2719
3073
|
const entries = fs.readdir(codexHome).filter((name) => /^logs.*\.sqlite$/.test(String(name)));
|
|
2720
3074
|
let best = null;
|
|
2721
3075
|
for (const name of entries) {
|
|
2722
|
-
const path =
|
|
3076
|
+
const path = join10(codexHome, String(name));
|
|
2723
3077
|
try {
|
|
2724
3078
|
const mtime = fs.stat(path).mtimeMs;
|
|
2725
3079
|
if (!best || mtime > best.mtime)
|
|
@@ -2753,7 +3107,11 @@ function captureTuiLogTail(options) {
|
|
|
2753
3107
|
return `(tui log tail capture failed: ${err instanceof Error ? err.message : String(err)})`;
|
|
2754
3108
|
}
|
|
2755
3109
|
}
|
|
2756
|
-
var
|
|
3110
|
+
var defaultIsPidAlive;
|
|
3111
|
+
var init_wrapper_exit_observability = __esm(() => {
|
|
3112
|
+
init_process_lifecycle();
|
|
3113
|
+
defaultIsPidAlive = pidLooksAlive;
|
|
3114
|
+
});
|
|
2757
3115
|
|
|
2758
3116
|
// src/pair-command.ts
|
|
2759
3117
|
function pairScopedCommand(cmd) {
|
|
@@ -2775,15 +3133,15 @@ var init_pair_command = __esm(() => {
|
|
|
2775
3133
|
});
|
|
2776
3134
|
|
|
2777
3135
|
// src/rotating-log.ts
|
|
2778
|
-
import { appendFileSync as appendFileSync2, existsSync as
|
|
3136
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync11, renameSync as renameSync2, statSync as statSync5, unlinkSync as unlinkSync4 } from "fs";
|
|
2779
3137
|
import { dirname as dirname2 } from "path";
|
|
2780
|
-
function appendRotatingLog(path, content, options = {}) {
|
|
3138
|
+
function appendRotatingLog(path, content, options = {}, fsOps = REAL_FS_OPS) {
|
|
2781
3139
|
const maxBytes = options.maxBytes ?? positiveIntFromEnv("AGENTBRIDGE_LOG_MAX_BYTES", DEFAULT_MAX_BYTES);
|
|
2782
3140
|
const keep = options.keep ?? positiveIntFromEnv("AGENTBRIDGE_LOG_ROTATE_KEEP", DEFAULT_KEEP);
|
|
2783
|
-
if (!
|
|
3141
|
+
if (!fsOps.existsSync(dirname2(path)))
|
|
2784
3142
|
return;
|
|
2785
|
-
rotateIfNeeded(path, Buffer.byteLength(content), maxBytes, keep);
|
|
2786
|
-
|
|
3143
|
+
rotateIfNeeded(path, Buffer.byteLength(content), maxBytes, keep, fsOps);
|
|
3144
|
+
fsOps.appendFileSync(path, content, "utf-8");
|
|
2787
3145
|
}
|
|
2788
3146
|
function positiveIntFromEnv(name, fallback) {
|
|
2789
3147
|
const value = process.env[name];
|
|
@@ -2792,30 +3150,53 @@ function positiveIntFromEnv(name, fallback) {
|
|
|
2792
3150
|
const parsed = Number(value);
|
|
2793
3151
|
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
2794
3152
|
}
|
|
2795
|
-
function
|
|
3153
|
+
function isEnoent(error) {
|
|
3154
|
+
return !!error && error.code === "ENOENT";
|
|
3155
|
+
}
|
|
3156
|
+
function renameIfPresent(from, to, fsOps) {
|
|
3157
|
+
try {
|
|
3158
|
+
fsOps.renameSync(from, to);
|
|
3159
|
+
} catch (error) {
|
|
3160
|
+
if (!isEnoent(error))
|
|
3161
|
+
throw error;
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
function unlinkIfPresent(path, fsOps) {
|
|
3165
|
+
try {
|
|
3166
|
+
fsOps.unlinkSync(path);
|
|
3167
|
+
} catch (error) {
|
|
3168
|
+
if (!isEnoent(error))
|
|
3169
|
+
throw error;
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
function rotateIfNeeded(path, incomingBytes, maxBytes, keep, fsOps) {
|
|
2796
3173
|
if (!Number.isFinite(maxBytes) || maxBytes <= 0 || keep <= 0)
|
|
2797
3174
|
return;
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
3175
|
+
let size;
|
|
3176
|
+
try {
|
|
3177
|
+
size = fsOps.statSync(path).size;
|
|
3178
|
+
} catch (error) {
|
|
3179
|
+
if (isEnoent(error))
|
|
3180
|
+
return;
|
|
3181
|
+
throw error;
|
|
3182
|
+
}
|
|
2801
3183
|
if (size + incomingBytes <= maxBytes)
|
|
2802
3184
|
return;
|
|
2803
3185
|
for (let index = keep;index >= 1; index--) {
|
|
2804
3186
|
const current = `${path}.${index}`;
|
|
2805
3187
|
const next = `${path}.${index + 1}`;
|
|
2806
|
-
if (!existsSync8(current))
|
|
2807
|
-
continue;
|
|
2808
3188
|
if (index === keep) {
|
|
2809
|
-
|
|
3189
|
+
unlinkIfPresent(current, fsOps);
|
|
2810
3190
|
} else {
|
|
2811
|
-
|
|
3191
|
+
renameIfPresent(current, next, fsOps);
|
|
2812
3192
|
}
|
|
2813
3193
|
}
|
|
2814
|
-
|
|
3194
|
+
renameIfPresent(path, `${path}.1`, fsOps);
|
|
2815
3195
|
}
|
|
2816
|
-
var DEFAULT_MAX_BYTES, DEFAULT_KEEP = 3;
|
|
3196
|
+
var DEFAULT_MAX_BYTES, DEFAULT_KEEP = 3, REAL_FS_OPS;
|
|
2817
3197
|
var init_rotating_log = __esm(() => {
|
|
2818
3198
|
DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
3199
|
+
REAL_FS_OPS = { statSync: statSync5, renameSync: renameSync2, unlinkSync: unlinkSync4, appendFileSync: appendFileSync2, existsSync: existsSync11 };
|
|
2819
3200
|
});
|
|
2820
3201
|
|
|
2821
3202
|
// src/stderr-ring-buffer.ts
|
|
@@ -2868,20 +3249,20 @@ var init_stderr_ring_buffer = __esm(() => {
|
|
|
2868
3249
|
|
|
2869
3250
|
// src/thread-state.ts
|
|
2870
3251
|
import {
|
|
2871
|
-
existsSync as
|
|
3252
|
+
existsSync as existsSync12,
|
|
2872
3253
|
mkdirSync as mkdirSync5,
|
|
2873
|
-
readdirSync as
|
|
3254
|
+
readdirSync as readdirSync4,
|
|
2874
3255
|
readFileSync as readFileSync8,
|
|
2875
3256
|
renameSync as renameSync3,
|
|
2876
3257
|
writeFileSync as writeFileSync6
|
|
2877
3258
|
} from "fs";
|
|
2878
3259
|
import { homedir as homedir3 } from "os";
|
|
2879
|
-
import { basename as
|
|
3260
|
+
import { basename as basename3, dirname as dirname3, join as join11 } from "path";
|
|
2880
3261
|
function nowIso() {
|
|
2881
3262
|
return new Date().toISOString();
|
|
2882
3263
|
}
|
|
2883
3264
|
function codexHome(env = process.env) {
|
|
2884
|
-
return env.CODEX_HOME && env.CODEX_HOME.length > 0 ? env.CODEX_HOME :
|
|
3265
|
+
return env.CODEX_HOME && env.CODEX_HOME.length > 0 ? env.CODEX_HOME : join11(homedir3(), ".codex");
|
|
2885
3266
|
}
|
|
2886
3267
|
function atomicWriteJson(path, value) {
|
|
2887
3268
|
mkdirSync5(dirname3(path), { recursive: true });
|
|
@@ -2900,8 +3281,8 @@ function readRawCurrentThread(stateDir) {
|
|
|
2900
3281
|
return null;
|
|
2901
3282
|
}
|
|
2902
3283
|
function findCodexRolloutFile(threadId, env = process.env, maxEntries = 20000) {
|
|
2903
|
-
const sessionsDir =
|
|
2904
|
-
if (!threadId || !
|
|
3284
|
+
const sessionsDir = join11(codexHome(env), "sessions");
|
|
3285
|
+
if (!threadId || !existsSync12(sessionsDir))
|
|
2905
3286
|
return null;
|
|
2906
3287
|
const exactName = `rollout-${threadId}.jsonl`;
|
|
2907
3288
|
const stack = [sessionsDir];
|
|
@@ -2910,20 +3291,20 @@ function findCodexRolloutFile(threadId, env = process.env, maxEntries = 20000) {
|
|
|
2910
3291
|
const dir = stack.pop();
|
|
2911
3292
|
let entries;
|
|
2912
3293
|
try {
|
|
2913
|
-
entries =
|
|
3294
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
2914
3295
|
} catch {
|
|
2915
3296
|
continue;
|
|
2916
3297
|
}
|
|
2917
3298
|
for (const entry of entries) {
|
|
2918
3299
|
visited++;
|
|
2919
|
-
const path =
|
|
3300
|
+
const path = join11(dir, entry.name);
|
|
2920
3301
|
if (entry.isDirectory()) {
|
|
2921
3302
|
stack.push(path);
|
|
2922
3303
|
continue;
|
|
2923
3304
|
}
|
|
2924
3305
|
if (!entry.isFile())
|
|
2925
3306
|
continue;
|
|
2926
|
-
const name =
|
|
3307
|
+
const name = basename3(entry.name);
|
|
2927
3308
|
if (name === exactName || name.startsWith("rollout-") && name.endsWith(".jsonl") && name.includes(threadId)) {
|
|
2928
3309
|
return path;
|
|
2929
3310
|
}
|
|
@@ -2941,7 +3322,7 @@ function readUsableCurrentThread(identity, env = process.env) {
|
|
|
2941
3322
|
return null;
|
|
2942
3323
|
if (state.cwd !== identity.cwd)
|
|
2943
3324
|
return null;
|
|
2944
|
-
if (state.rolloutPath &&
|
|
3325
|
+
if (state.rolloutPath && existsSync12(state.rolloutPath))
|
|
2945
3326
|
return state;
|
|
2946
3327
|
const rolloutPath = findCodexRolloutFile(state.threadId, env);
|
|
2947
3328
|
if (!rolloutPath)
|
|
@@ -2957,145 +3338,6 @@ function readUsableCurrentThread(identity, env = process.env) {
|
|
|
2957
3338
|
}
|
|
2958
3339
|
var init_thread_state = () => {};
|
|
2959
3340
|
|
|
2960
|
-
// src/process-lifecycle.ts
|
|
2961
|
-
import { execFileSync as execFileSync6 } from "child_process";
|
|
2962
|
-
import { basename as basename3 } from "path";
|
|
2963
|
-
function parsePsProcessList(output) {
|
|
2964
|
-
const entries = [];
|
|
2965
|
-
for (const line of output.split(/\r?\n/)) {
|
|
2966
|
-
const match = line.match(/^\s*(\d+)\s+(.+?)\s*$/);
|
|
2967
|
-
if (!match)
|
|
2968
|
-
continue;
|
|
2969
|
-
const pid = Number.parseInt(match[1], 10);
|
|
2970
|
-
if (!Number.isFinite(pid))
|
|
2971
|
-
continue;
|
|
2972
|
-
entries.push({ pid, command: match[2] });
|
|
2973
|
-
}
|
|
2974
|
-
return entries;
|
|
2975
|
-
}
|
|
2976
|
-
function invokesCodexBinary(command) {
|
|
2977
|
-
const tokens = command.trim().split(/\s+/);
|
|
2978
|
-
const exe = tokens[0] ? basename3(tokens[0]) : "";
|
|
2979
|
-
if (exe === "codex")
|
|
2980
|
-
return true;
|
|
2981
|
-
if ((exe === "node" || exe === "bun") && tokens[1]) {
|
|
2982
|
-
return basename3(tokens[1]) === "codex";
|
|
2983
|
-
}
|
|
2984
|
-
return false;
|
|
2985
|
-
}
|
|
2986
|
-
function commandMatchesManagedCodexTui(command, proxyUrl) {
|
|
2987
|
-
if (!invokesCodexBinary(command))
|
|
2988
|
-
return false;
|
|
2989
|
-
if (!command.includes("tui_app_server"))
|
|
2990
|
-
return false;
|
|
2991
|
-
const remoteUrl = extractRemoteUrl(command);
|
|
2992
|
-
if (!remoteUrl)
|
|
2993
|
-
return false;
|
|
2994
|
-
if (!proxyUrl)
|
|
2995
|
-
return true;
|
|
2996
|
-
return remoteTargetsProxy(remoteUrl, proxyUrl);
|
|
2997
|
-
}
|
|
2998
|
-
function findManagedCodexTuiProcessesFromList(processes, proxyUrl) {
|
|
2999
|
-
return processes.filter((entry) => commandMatchesManagedCodexTui(entry.command, proxyUrl));
|
|
3000
|
-
}
|
|
3001
|
-
function findManagedCodexTuiProcesses(proxyUrl) {
|
|
3002
|
-
try {
|
|
3003
|
-
const output = execFileSync6("ps", ["-axo", "pid=,command="], { encoding: "utf-8" });
|
|
3004
|
-
return findManagedCodexTuiProcessesFromList(parsePsProcessList(output), proxyUrl).filter((entry) => entry.pid !== process.pid);
|
|
3005
|
-
} catch {
|
|
3006
|
-
return [];
|
|
3007
|
-
}
|
|
3008
|
-
}
|
|
3009
|
-
function listManagedCodexTuiProcessesFromList(processes) {
|
|
3010
|
-
return processes.filter((entry) => commandMatchesManagedCodexTui(entry.command)).map((entry) => ({ ...entry, remoteUrl: extractRemoteUrl(entry.command) }));
|
|
3011
|
-
}
|
|
3012
|
-
function listManagedCodexTuiProcesses() {
|
|
3013
|
-
try {
|
|
3014
|
-
const output = execFileSync6("ps", ["-axo", "pid=,command="], { encoding: "utf-8" });
|
|
3015
|
-
return listManagedCodexTuiProcessesFromList(parsePsProcessList(output)).filter((entry) => entry.pid !== process.pid);
|
|
3016
|
-
} catch {
|
|
3017
|
-
return [];
|
|
3018
|
-
}
|
|
3019
|
-
}
|
|
3020
|
-
function listBridgeFrontendProcessesFromList(processes) {
|
|
3021
|
-
return processes.filter((entry) => /(?:^|[\s/\\])bridge-server\.js(?:\s|$)/.test(entry.command) && (entry.command.includes("agentbridge") || entry.command.includes("agent_bridge")));
|
|
3022
|
-
}
|
|
3023
|
-
function listBridgeFrontendProcesses() {
|
|
3024
|
-
try {
|
|
3025
|
-
const output = execFileSync6("ps", ["-axo", "pid=,command="], { encoding: "utf-8" });
|
|
3026
|
-
return listBridgeFrontendProcessesFromList(parsePsProcessList(output)).filter((entry) => entry.pid !== process.pid);
|
|
3027
|
-
} catch {
|
|
3028
|
-
return [];
|
|
3029
|
-
}
|
|
3030
|
-
}
|
|
3031
|
-
function isProcessAlive2(pid) {
|
|
3032
|
-
try {
|
|
3033
|
-
process.kill(pid, 0);
|
|
3034
|
-
return true;
|
|
3035
|
-
} catch {
|
|
3036
|
-
return false;
|
|
3037
|
-
}
|
|
3038
|
-
}
|
|
3039
|
-
function commandForPid(pid) {
|
|
3040
|
-
try {
|
|
3041
|
-
return execFileSync6("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
|
|
3042
|
-
} catch {
|
|
3043
|
-
return null;
|
|
3044
|
-
}
|
|
3045
|
-
}
|
|
3046
|
-
function terminateProcessSync(pid, options = {}) {
|
|
3047
|
-
const gracefulTimeoutMs = options.gracefulTimeoutMs ?? 2000;
|
|
3048
|
-
const target = options.processGroup && process.platform !== "win32" ? -pid : pid;
|
|
3049
|
-
const label = options.processGroup && process.platform !== "win32" ? `process group ${pid}` : `pid ${pid}`;
|
|
3050
|
-
try {
|
|
3051
|
-
process.kill(target, "SIGTERM");
|
|
3052
|
-
options.log?.(`Sent SIGTERM to ${label}`);
|
|
3053
|
-
} catch {
|
|
3054
|
-
return !isProcessAlive2(pid);
|
|
3055
|
-
}
|
|
3056
|
-
if (waitForExitSync(pid, gracefulTimeoutMs))
|
|
3057
|
-
return true;
|
|
3058
|
-
try {
|
|
3059
|
-
process.kill(target, "SIGKILL");
|
|
3060
|
-
options.log?.(`Sent SIGKILL to ${label}`);
|
|
3061
|
-
} catch {}
|
|
3062
|
-
return waitForExitSync(pid, 500);
|
|
3063
|
-
}
|
|
3064
|
-
function waitForExitSync(pid, timeoutMs) {
|
|
3065
|
-
const deadline = Date.now() + timeoutMs;
|
|
3066
|
-
while (Date.now() < deadline) {
|
|
3067
|
-
if (!isProcessAlive2(pid))
|
|
3068
|
-
return true;
|
|
3069
|
-
sleepSync(50);
|
|
3070
|
-
}
|
|
3071
|
-
return !isProcessAlive2(pid);
|
|
3072
|
-
}
|
|
3073
|
-
function sleepSync(ms) {
|
|
3074
|
-
const buffer = new SharedArrayBuffer(4);
|
|
3075
|
-
const view = new Int32Array(buffer);
|
|
3076
|
-
Atomics.wait(view, 0, 0, ms);
|
|
3077
|
-
}
|
|
3078
|
-
function extractRemoteUrl(command) {
|
|
3079
|
-
const equals = command.match(/(?:^|\s)--remote=([^\s]+)/);
|
|
3080
|
-
if (equals)
|
|
3081
|
-
return equals[1];
|
|
3082
|
-
const separate = command.match(/(?:^|\s)--remote\s+([^\s]+)/);
|
|
3083
|
-
return separate?.[1] ?? null;
|
|
3084
|
-
}
|
|
3085
|
-
function remoteTargetsProxy(remoteUrl, proxyUrl) {
|
|
3086
|
-
try {
|
|
3087
|
-
const remote = new URL(remoteUrl);
|
|
3088
|
-
const proxy = new URL(proxyUrl);
|
|
3089
|
-
return remote.protocol === proxy.protocol && remote.hostname === proxy.hostname && remote.port === proxy.port && normalizePath(remote.pathname) === normalizePath(proxy.pathname);
|
|
3090
|
-
} catch {
|
|
3091
|
-
return remoteUrl === proxyUrl;
|
|
3092
|
-
}
|
|
3093
|
-
}
|
|
3094
|
-
function normalizePath(pathname) {
|
|
3095
|
-
return pathname === "" ? "/" : pathname;
|
|
3096
|
-
}
|
|
3097
|
-
var init_process_lifecycle = () => {};
|
|
3098
|
-
|
|
3099
3341
|
// src/cli/codex.ts
|
|
3100
3342
|
var exports_codex = {};
|
|
3101
3343
|
__export(exports_codex, {
|
|
@@ -3104,23 +3346,23 @@ __export(exports_codex, {
|
|
|
3104
3346
|
parseAgentBridgeCodexArgs: () => parseAgentBridgeCodexArgs,
|
|
3105
3347
|
buildCodexArgs: () => buildCodexArgs
|
|
3106
3348
|
});
|
|
3107
|
-
import { spawn as spawn3, execSync as execSync2, execFileSync as
|
|
3349
|
+
import { spawn as spawn3, execSync as execSync2, execFileSync as execFileSync6 } from "child_process";
|
|
3108
3350
|
import {
|
|
3109
3351
|
openSync as openSync3,
|
|
3110
3352
|
writeSync,
|
|
3111
3353
|
closeSync as closeSync3,
|
|
3112
3354
|
writeFileSync as writeFileSync7,
|
|
3113
3355
|
readFileSync as readFileSync9,
|
|
3114
|
-
unlinkSync as
|
|
3115
|
-
existsSync as
|
|
3356
|
+
unlinkSync as unlinkSync5,
|
|
3357
|
+
existsSync as existsSync13,
|
|
3116
3358
|
mkdirSync as mkdirSync6
|
|
3117
3359
|
} from "fs";
|
|
3118
3360
|
import { homedir as homedir4 } from "os";
|
|
3119
|
-
import { dirname as dirname4, join as
|
|
3361
|
+
import { dirname as dirname4, join as join12 } from "path";
|
|
3120
3362
|
function appendWrapperLog(path, entry) {
|
|
3121
3363
|
try {
|
|
3122
3364
|
const dir = dirname4(path);
|
|
3123
|
-
if (!
|
|
3365
|
+
if (!existsSync13(dir)) {
|
|
3124
3366
|
mkdirSync6(dir, { recursive: true });
|
|
3125
3367
|
}
|
|
3126
3368
|
appendRotatingLog(path, `[${new Date().toISOString()}] ${entry}
|
|
@@ -3287,7 +3529,7 @@ async function runCodex(args) {
|
|
|
3287
3529
|
if (status?.proxyUrl) {
|
|
3288
3530
|
proxyUrl = status.proxyUrl;
|
|
3289
3531
|
} else {
|
|
3290
|
-
const fallbackProxyPort = process.env.CODEX_PROXY_PORT ?? String(new ConfigService().loadOrDefault().codex.proxyPort);
|
|
3532
|
+
const fallbackProxyPort = process.env.CODEX_PROXY_PORT ?? String(new ConfigService().loadOrDefault((msg) => console.error(`[agentbridge] ${msg}`)).codex.proxyPort);
|
|
3291
3533
|
proxyUrl = `ws://127.0.0.1:${fallbackProxyPort}`;
|
|
3292
3534
|
console.error(`[agentbridge] No daemon status found, using fallback proxy port: ${proxyUrl}`);
|
|
3293
3535
|
}
|
|
@@ -3377,7 +3619,7 @@ async function runCodex(args) {
|
|
|
3377
3619
|
let attempts = 0;
|
|
3378
3620
|
const discover = () => {
|
|
3379
3621
|
attempts += 1;
|
|
3380
|
-
nativeChildPid = discoverNativeChildPid(launcherPid, (cmd, args2) =>
|
|
3622
|
+
nativeChildPid = discoverNativeChildPid(launcherPid, (cmd, args2) => execFileSync6(cmd, args2, { encoding: "utf-8", timeout: 2000 }));
|
|
3381
3623
|
if (nativeChildPid !== null) {
|
|
3382
3624
|
appendWrapperLog(wrapperLogPath, `native child pid=${nativeChildPid} (launcher pid=${launcherPid})`);
|
|
3383
3625
|
return;
|
|
@@ -3407,7 +3649,7 @@ async function runCodex(args) {
|
|
|
3407
3649
|
return;
|
|
3408
3650
|
cleanedTuiPid = true;
|
|
3409
3651
|
try {
|
|
3410
|
-
|
|
3652
|
+
unlinkSync5(stateDir.tuiPidFile);
|
|
3411
3653
|
} catch {}
|
|
3412
3654
|
}
|
|
3413
3655
|
function requestChildTermination(reason) {
|
|
@@ -3481,9 +3723,9 @@ async function runCodex(args) {
|
|
|
3481
3723
|
classification = refineCleanExitClassification(readTurnInProgress(stateDir.statusFile));
|
|
3482
3724
|
}
|
|
3483
3725
|
const tuiLogTail = captureTuiLogTail({
|
|
3484
|
-
codexHome:
|
|
3726
|
+
codexHome: join12(homedir4(), ".codex"),
|
|
3485
3727
|
nativePid: nativeChildPid,
|
|
3486
|
-
run: (cmd, args2) =>
|
|
3728
|
+
run: (cmd, args2) => execFileSync6(cmd, args2, { encoding: "utf-8", timeout: 2000 })
|
|
3487
3729
|
});
|
|
3488
3730
|
appendWrapperLog(wrapperLogPath, [
|
|
3489
3731
|
`exit: code=${code ?? "null"} signal=${signal ?? "null"} runtime_ms=${runtimeMs} pid=${child.pid ?? "unknown"} native_pid=${nativeChildPid ?? "unknown"} classification=${classification}`,
|
|
@@ -3535,14 +3777,14 @@ function traceCliStart2(event, args, originalEnv, envGuardAction, pair) {
|
|
|
3535
3777
|
function guardNoLiveManagedTui(stateDir, proxyUrl) {
|
|
3536
3778
|
const pid = readTuiPid(stateDir);
|
|
3537
3779
|
if (pid) {
|
|
3538
|
-
if (!
|
|
3780
|
+
if (!isProcessAlive(pid)) {
|
|
3539
3781
|
try {
|
|
3540
|
-
|
|
3782
|
+
unlinkSync5(stateDir.tuiPidFile);
|
|
3541
3783
|
} catch {}
|
|
3542
3784
|
} else if (!isManagedCodexTuiProcess(pid, proxyUrl)) {
|
|
3543
3785
|
appendWrapperLog(stateDir.codexWrapperLogFile, `stale tui pid file pointed at unmanaged live pid=${pid}; removing`);
|
|
3544
3786
|
try {
|
|
3545
|
-
|
|
3787
|
+
unlinkSync5(stateDir.tuiPidFile);
|
|
3546
3788
|
} catch {}
|
|
3547
3789
|
} else {
|
|
3548
3790
|
console.error(`[agentbridge] This pair already has a managed Codex TUI running (pid ${pid}).`);
|
|
@@ -3589,7 +3831,7 @@ async function waitForProxyReady(proxyUrl, maxRetries = 20, delayMs = 100) {
|
|
|
3589
3831
|
return;
|
|
3590
3832
|
}
|
|
3591
3833
|
} catch {}
|
|
3592
|
-
await new Promise((
|
|
3834
|
+
await new Promise((resolve5) => setTimeout(resolve5, delayMs));
|
|
3593
3835
|
}
|
|
3594
3836
|
throw new Error(`Timed out waiting for Codex proxy readiness on ${healthUrl}`);
|
|
3595
3837
|
}
|
|
@@ -3638,17 +3880,17 @@ var init_codex = __esm(() => {
|
|
|
3638
3880
|
});
|
|
3639
3881
|
|
|
3640
3882
|
// src/claude-session.ts
|
|
3641
|
-
import { readdirSync as
|
|
3883
|
+
import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
|
|
3642
3884
|
import { homedir as homedir5 } from "os";
|
|
3643
|
-
import { join as
|
|
3885
|
+
import { join as join13 } from "path";
|
|
3644
3886
|
function encodeClaudeProjectDir(cwd) {
|
|
3645
3887
|
return cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
3646
3888
|
}
|
|
3647
|
-
function findLatestClaudeSession(cwd, claudeHome = process.env.CLAUDE_CONFIG_DIR ||
|
|
3648
|
-
const dir =
|
|
3889
|
+
function findLatestClaudeSession(cwd, claudeHome = process.env.CLAUDE_CONFIG_DIR || join13(homedir5(), ".claude")) {
|
|
3890
|
+
const dir = join13(claudeHome, "projects", encodeClaudeProjectDir(cwd));
|
|
3649
3891
|
let entries;
|
|
3650
3892
|
try {
|
|
3651
|
-
entries =
|
|
3893
|
+
entries = readdirSync5(dir);
|
|
3652
3894
|
} catch {
|
|
3653
3895
|
return null;
|
|
3654
3896
|
}
|
|
@@ -3659,10 +3901,10 @@ function findLatestClaudeSession(cwd, claudeHome = process.env.CLAUDE_CONFIG_DIR
|
|
|
3659
3901
|
const sessionId = name.slice(0, -".jsonl".length);
|
|
3660
3902
|
if (!SESSION_ID_PATTERN.test(sessionId))
|
|
3661
3903
|
continue;
|
|
3662
|
-
const file =
|
|
3904
|
+
const file = join13(dir, name);
|
|
3663
3905
|
let mtimeMs;
|
|
3664
3906
|
try {
|
|
3665
|
-
const st =
|
|
3907
|
+
const st = statSync6(file);
|
|
3666
3908
|
if (!st.isFile())
|
|
3667
3909
|
continue;
|
|
3668
3910
|
mtimeMs = st.mtimeMs;
|
|
@@ -3768,8 +4010,8 @@ __export(exports_kill, {
|
|
|
3768
4010
|
runKill: () => runKill,
|
|
3769
4011
|
formatKillReport: () => formatKillReport
|
|
3770
4012
|
});
|
|
3771
|
-
import { readFileSync as readFileSync10, unlinkSync as
|
|
3772
|
-
import { join as
|
|
4013
|
+
import { readFileSync as readFileSync10, unlinkSync as unlinkSync6 } from "fs";
|
|
4014
|
+
import { join as join14 } from "path";
|
|
3773
4015
|
async function runKill(args = []) {
|
|
3774
4016
|
const argError = validateKillArgs(args);
|
|
3775
4017
|
if (argError === "help") {
|
|
@@ -3821,7 +4063,7 @@ async function runKill(args = []) {
|
|
|
3821
4063
|
for (const dirName of listPairDirsSafe(base)) {
|
|
3822
4064
|
if (registeredIds.has(dirName))
|
|
3823
4065
|
continue;
|
|
3824
|
-
const stateDir = new StateDirResolver(
|
|
4066
|
+
const stateDir = new StateDirResolver(join14(base, "pairs", dirName));
|
|
3825
4067
|
results.push(await stopStateDir(`${dirName} (unregistered)`, stateDir, portsFromStateDir(stateDir)));
|
|
3826
4068
|
}
|
|
3827
4069
|
const legacy = detectLegacyRootDaemon(base);
|
|
@@ -3906,7 +4148,7 @@ No arguments stop this directory's registered pairs and any legacy-root daemon.
|
|
|
3906
4148
|
}
|
|
3907
4149
|
async function stopPairEntry(base, pair) {
|
|
3908
4150
|
const ports = portsForEntry(pair);
|
|
3909
|
-
const stateDir = new StateDirResolver(
|
|
4151
|
+
const stateDir = new StateDirResolver(join14(base, "pairs", pair.pairId));
|
|
3910
4152
|
return stopStateDir(pair.pairId, stateDir, ports);
|
|
3911
4153
|
}
|
|
3912
4154
|
function listPairDirsSafe(base) {
|
|
@@ -4041,7 +4283,7 @@ async function killManagedCodexTui(stateDir, proxyUrl, log, gracefulTimeoutMs =
|
|
|
4041
4283
|
if (!pid) {
|
|
4042
4284
|
log("No Codex TUI pid file found");
|
|
4043
4285
|
removeTuiPidFile(stateDir);
|
|
4044
|
-
} else if (!
|
|
4286
|
+
} else if (!isProcessAlive(pid)) {
|
|
4045
4287
|
log(`Codex TUI pid ${pid} is not alive, cleaning up stale pid file`);
|
|
4046
4288
|
removeTuiPidFile(stateDir);
|
|
4047
4289
|
} else if (!isManagedCodexTuiProcess2(pid, proxyUrl)) {
|
|
@@ -4075,7 +4317,7 @@ function readTuiPid2(stateDir) {
|
|
|
4075
4317
|
}
|
|
4076
4318
|
function removeTuiPidFile(stateDir) {
|
|
4077
4319
|
try {
|
|
4078
|
-
|
|
4320
|
+
unlinkSync6(stateDir.tuiPidFile);
|
|
4079
4321
|
} catch {}
|
|
4080
4322
|
}
|
|
4081
4323
|
function isManagedCodexTuiProcess2(pid, proxyUrl) {
|
|
@@ -4095,7 +4337,7 @@ var exports_pairs = {};
|
|
|
4095
4337
|
__export(exports_pairs, {
|
|
4096
4338
|
runPairs: () => runPairs
|
|
4097
4339
|
});
|
|
4098
|
-
import { join as
|
|
4340
|
+
import { join as join15 } from "path";
|
|
4099
4341
|
async function runPairs(args = []) {
|
|
4100
4342
|
const [command, ...rest] = args;
|
|
4101
4343
|
if (command === "rm") {
|
|
@@ -4261,7 +4503,7 @@ async function collectRows() {
|
|
|
4261
4503
|
}
|
|
4262
4504
|
async function rowForPair(base, pair) {
|
|
4263
4505
|
const ports = portsForEntry(pair);
|
|
4264
|
-
const stateDir = new StateDirResolver(
|
|
4506
|
+
const stateDir = new StateDirResolver(join15(base, "pairs", pair.pairId));
|
|
4265
4507
|
const lifecycle = new DaemonLifecycle({
|
|
4266
4508
|
stateDir,
|
|
4267
4509
|
controlPort: ports.controlPort,
|
|
@@ -4368,18 +4610,18 @@ var DAEMON_STATUS_FETCH_TIMEOUT_MS = 1000;
|
|
|
4368
4610
|
import { Database } from "bun:sqlite";
|
|
4369
4611
|
import {
|
|
4370
4612
|
copyFileSync,
|
|
4371
|
-
existsSync as
|
|
4613
|
+
existsSync as existsSync14,
|
|
4372
4614
|
mkdirSync as mkdirSync7,
|
|
4373
4615
|
readFileSync as readFileSync11
|
|
4374
4616
|
} from "fs";
|
|
4375
|
-
import { dirname as dirname5, join as
|
|
4617
|
+
import { dirname as dirname5, join as join16 } from "path";
|
|
4376
4618
|
function isKickoffText(text) {
|
|
4377
4619
|
if (!text)
|
|
4378
4620
|
return false;
|
|
4379
4621
|
return KICKOFF_FINGERPRINTS.some((fingerprint) => text.includes(fingerprint));
|
|
4380
4622
|
}
|
|
4381
4623
|
function extractFirstRealUserMessage(rolloutPath) {
|
|
4382
|
-
if (!
|
|
4624
|
+
if (!existsSync14(rolloutPath))
|
|
4383
4625
|
return null;
|
|
4384
4626
|
const raw = readFileSync11(rolloutPath, "utf-8");
|
|
4385
4627
|
for (const line of raw.split(`
|
|
@@ -4403,8 +4645,8 @@ function extractFirstRealUserMessage(rolloutPath) {
|
|
|
4403
4645
|
}
|
|
4404
4646
|
function scanResumePollution(options = {}) {
|
|
4405
4647
|
const codexHome2 = options.codexHome ?? codexHome();
|
|
4406
|
-
const dbPath = options.dbPath ??
|
|
4407
|
-
if (!
|
|
4648
|
+
const dbPath = options.dbPath ?? join16(codexHome2, "state_5.sqlite");
|
|
4649
|
+
if (!existsSync14(dbPath)) {
|
|
4408
4650
|
return { codexHome: codexHome2, dbPath, scanned: 0, candidates: [], applied: 0, renamed: 0, deleted: 0 };
|
|
4409
4651
|
}
|
|
4410
4652
|
const db = options.apply ? new Database(dbPath) : new Database(dbPath, { readonly: true });
|
|
@@ -4497,12 +4739,12 @@ function scanResumePollution(options = {}) {
|
|
|
4497
4739
|
}
|
|
4498
4740
|
function backupCodexStateFiles(dbPath, now = new Date().toISOString()) {
|
|
4499
4741
|
const safeStamp = now.replace(/[:.]/g, "-");
|
|
4500
|
-
const base =
|
|
4742
|
+
const base = join16(dirname5(dbPath), "agentbridge-backups", `resume-pollution-${safeStamp}`);
|
|
4501
4743
|
mkdirSync7(base, { recursive: true });
|
|
4502
4744
|
for (const path of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
4503
|
-
if (!
|
|
4745
|
+
if (!existsSync14(path))
|
|
4504
4746
|
continue;
|
|
4505
|
-
const target =
|
|
4747
|
+
const target = join16(base, path.split("/").pop());
|
|
4506
4748
|
mkdirSync7(dirname5(target), { recursive: true });
|
|
4507
4749
|
copyFileSync(path, target);
|
|
4508
4750
|
}
|
|
@@ -4550,9 +4792,8 @@ __export(exports_doctor, {
|
|
|
4550
4792
|
runDoctor: () => runDoctor,
|
|
4551
4793
|
formatDoctorReport: () => formatDoctorReport
|
|
4552
4794
|
});
|
|
4553
|
-
import { existsSync as
|
|
4554
|
-
import {
|
|
4555
|
-
import { join as join16 } from "path";
|
|
4795
|
+
import { existsSync as existsSync15, readFileSync as readFileSync12, readdirSync as readdirSync6, realpathSync as realpathSync3, statSync as statSync7 } from "fs";
|
|
4796
|
+
import { join as join17 } from "path";
|
|
4556
4797
|
async function runDoctor(args = []) {
|
|
4557
4798
|
if (args[0] === "resume-pollution") {
|
|
4558
4799
|
runResumePollution(args.slice(1));
|
|
@@ -4655,6 +4896,7 @@ async function buildDoctorReport(pair, registered) {
|
|
|
4655
4896
|
detail: env.ok ? "AgentBridge env matches cwd" : env.reasons.join("; "),
|
|
4656
4897
|
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 `agentbridge claude`\uFF0C\u4E0D\u8981\u590D\u7528\u5176\u4ED6\u76EE\u5F55\u7684\u4F1A\u8BDD\u73AF\u5883\u3002"
|
|
4657
4898
|
});
|
|
4899
|
+
checks.push(configParseabilityCheck(cwd));
|
|
4658
4900
|
checks.push({
|
|
4659
4901
|
name: "daemon health",
|
|
4660
4902
|
status: health ? "ok" : "warn",
|
|
@@ -4741,16 +4983,16 @@ function artifactAlignmentCheck() {
|
|
|
4741
4983
|
stamps.push({ label: "global-cli", commit });
|
|
4742
4984
|
} catch {}
|
|
4743
4985
|
}
|
|
4744
|
-
const cacheRoot =
|
|
4986
|
+
const cacheRoot = pluginCacheRoot();
|
|
4745
4987
|
try {
|
|
4746
|
-
for (const version of
|
|
4747
|
-
const commit = extractBundleCommit(
|
|
4988
|
+
for (const version of readdirSync6(cacheRoot)) {
|
|
4989
|
+
const commit = extractBundleCommit(join17(cacheRoot, version, "server", "daemon.js"));
|
|
4748
4990
|
if (commit)
|
|
4749
4991
|
stamps.push({ label: `plugin-cache@${version}`, commit });
|
|
4750
4992
|
}
|
|
4751
4993
|
} catch {}
|
|
4752
|
-
const repoBundle =
|
|
4753
|
-
if (
|
|
4994
|
+
const repoBundle = join17(process.cwd(), "plugins", "agentbridge", "server", "daemon.js");
|
|
4995
|
+
if (existsSync15(repoBundle)) {
|
|
4754
4996
|
const commit = extractBundleCommit(repoBundle);
|
|
4755
4997
|
if (commit)
|
|
4756
4998
|
stamps.push({ label: "repo-bundle", commit });
|
|
@@ -4782,8 +5024,31 @@ function extractBundleCommit(path) {
|
|
|
4782
5024
|
return null;
|
|
4783
5025
|
}
|
|
4784
5026
|
}
|
|
5027
|
+
function configParseabilityCheck(cwd) {
|
|
5028
|
+
const desc = new ConfigService(cwd).describeConfig();
|
|
5029
|
+
if (desc.state === "absent") {
|
|
5030
|
+
return {
|
|
5031
|
+
name: "config.json",
|
|
5032
|
+
status: "ok",
|
|
5033
|
+
detail: `no project config at ${desc.path} \u2014 built-in defaults in effect`
|
|
5034
|
+
};
|
|
5035
|
+
}
|
|
5036
|
+
if (desc.state === "corrupt") {
|
|
5037
|
+
return {
|
|
5038
|
+
name: "config.json",
|
|
5039
|
+
status: "warn",
|
|
5040
|
+
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" + "\u4FEE\u6B63\u8BE5\u6587\u4EF6\u7684 JSON \u8BED\u6CD5/\u5B57\u6BB5\u7C7B\u578B\u540E\u91CD\u542F `agentbridge claude` \u5373\u53EF\u91CD\u65B0\u751F\u6548\u3002"
|
|
5042
|
+
};
|
|
5043
|
+
}
|
|
5044
|
+
return {
|
|
5045
|
+
name: "config.json",
|
|
5046
|
+
status: "ok",
|
|
5047
|
+
detail: desc.customValues ? `parsed at ${desc.path} \u2014 custom values in effect` : `parsed at ${desc.path} \u2014 all values match defaults`
|
|
5048
|
+
};
|
|
5049
|
+
}
|
|
4785
5050
|
function logCheck(name, path) {
|
|
4786
|
-
if (!
|
|
5051
|
+
if (!existsSync15(path)) {
|
|
4787
5052
|
return {
|
|
4788
5053
|
name,
|
|
4789
5054
|
status: "warn",
|
|
@@ -4791,7 +5056,7 @@ function logCheck(name, path) {
|
|
|
4791
5056
|
hint: "\u65E5\u5FD7\u4F1A\u5728\u76F8\u5E94\u8FDB\u7A0B\u9996\u6B21\u542F\u52A8\u65F6\u521B\u5EFA\uFF1B\u8FDB\u7A0B\u4ECE\u672A\u542F\u52A8\u8FC7\u65F6\u8FD9\u662F\u6B63\u5E38\u7684\u3002"
|
|
4792
5057
|
};
|
|
4793
5058
|
}
|
|
4794
|
-
const stat =
|
|
5059
|
+
const stat = statSync7(path);
|
|
4795
5060
|
if (stat.size > LARGE_LOG_WARN_BYTES) {
|
|
4796
5061
|
return {
|
|
4797
5062
|
name,
|
|
@@ -4833,7 +5098,9 @@ function printDoctorReport(report) {
|
|
|
4833
5098
|
}
|
|
4834
5099
|
var LARGE_LOG_WARN_BYTES;
|
|
4835
5100
|
var init_doctor = __esm(() => {
|
|
5101
|
+
init_plugin_cache();
|
|
4836
5102
|
init_build_info();
|
|
5103
|
+
init_config_service();
|
|
4837
5104
|
init_env_guard();
|
|
4838
5105
|
init_pair_resolver();
|
|
4839
5106
|
init_thread_state();
|
|
@@ -4865,6 +5132,9 @@ function formatAgent(name, usage, snapshotAt) {
|
|
|
4865
5132
|
if (usage.rateLimitedUntil > 0) {
|
|
4866
5133
|
parts.push(`\u9650\u6D41\u81F3 ${formatEpoch(usage.rateLimitedUntil)}`);
|
|
4867
5134
|
}
|
|
5135
|
+
if (usage.parsedVia === "positional") {
|
|
5136
|
+
parts.push("\u26A0\uFE0F \u7A97\u53E3\u8BC6\u522B\u4F7F\u7528\u4F4D\u7F6E\u515C\u5E95");
|
|
5137
|
+
}
|
|
4868
5138
|
const ageSec = usage.fetchedAt > 0 ? snapshotAt - usage.fetchedAt : 0;
|
|
4869
5139
|
if (ageSec > 300) {
|
|
4870
5140
|
parts.push(`\u26A0\uFE0F \u6570\u636E\u91C7\u96C6\u4E8E ${Math.round(ageSec / 60)} \u5206\u949F\u524D`);
|
|
@@ -4976,6 +5246,160 @@ var init_budget = __esm(() => {
|
|
|
4976
5246
|
init_render();
|
|
4977
5247
|
});
|
|
4978
5248
|
|
|
5249
|
+
// src/cli/logs.ts
|
|
5250
|
+
var exports_logs = {};
|
|
5251
|
+
__export(exports_logs, {
|
|
5252
|
+
tailLines: () => tailLines,
|
|
5253
|
+
runLogs: () => runLogs,
|
|
5254
|
+
parseLogsArgs: () => parseLogsArgs,
|
|
5255
|
+
followLog: () => followLog
|
|
5256
|
+
});
|
|
5257
|
+
import { existsSync as existsSync16, readFileSync as readFileSync13 } from "fs";
|
|
5258
|
+
import { spawn as spawn4 } from "child_process";
|
|
5259
|
+
function parseLogsArgs(args) {
|
|
5260
|
+
let codex = false;
|
|
5261
|
+
let follow = false;
|
|
5262
|
+
let lines = DEFAULT_LINES;
|
|
5263
|
+
for (let i = 0;i < args.length; i++) {
|
|
5264
|
+
const a = args[i];
|
|
5265
|
+
if (a === "--codex") {
|
|
5266
|
+
codex = true;
|
|
5267
|
+
continue;
|
|
5268
|
+
}
|
|
5269
|
+
if (a === "-f" || a === "--follow") {
|
|
5270
|
+
follow = true;
|
|
5271
|
+
continue;
|
|
5272
|
+
}
|
|
5273
|
+
if (a === "-n" || a === "--lines") {
|
|
5274
|
+
const next = args[i + 1];
|
|
5275
|
+
if (next === undefined) {
|
|
5276
|
+
throw new Error(`${a} requires a positive integer (e.g. ${a} 200)`);
|
|
5277
|
+
}
|
|
5278
|
+
lines = parsePositiveInt(next, a);
|
|
5279
|
+
i++;
|
|
5280
|
+
continue;
|
|
5281
|
+
}
|
|
5282
|
+
if (a.startsWith("-n")) {
|
|
5283
|
+
lines = parsePositiveInt(a.slice(2), "-n");
|
|
5284
|
+
continue;
|
|
5285
|
+
}
|
|
5286
|
+
if (a.startsWith("--lines=")) {
|
|
5287
|
+
lines = parsePositiveInt(a.slice("--lines=".length), "--lines");
|
|
5288
|
+
continue;
|
|
5289
|
+
}
|
|
5290
|
+
throw new Error(`Unknown logs flag: ${a}`);
|
|
5291
|
+
}
|
|
5292
|
+
return { codex, follow, lines };
|
|
5293
|
+
}
|
|
5294
|
+
function parsePositiveInt(raw, flag) {
|
|
5295
|
+
if (!/^\d+$/.test(raw)) {
|
|
5296
|
+
throw new Error(`${flag} must be a positive integer, got "${raw}"`);
|
|
5297
|
+
}
|
|
5298
|
+
const n = Number.parseInt(raw, 10);
|
|
5299
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
5300
|
+
throw new Error(`${flag} must be a positive integer, got "${raw}"`);
|
|
5301
|
+
}
|
|
5302
|
+
return n;
|
|
5303
|
+
}
|
|
5304
|
+
function tailLines(text, count) {
|
|
5305
|
+
const body = text.endsWith(`
|
|
5306
|
+
`) ? text.slice(0, -1) : text;
|
|
5307
|
+
if (body.length === 0)
|
|
5308
|
+
return [];
|
|
5309
|
+
const all = body.split(`
|
|
5310
|
+
`);
|
|
5311
|
+
return all.length <= count ? all : all.slice(all.length - count);
|
|
5312
|
+
}
|
|
5313
|
+
async function runLogs(args) {
|
|
5314
|
+
const { pairFlag } = parsePairFlag(args);
|
|
5315
|
+
const rest = stripPairTokens(args);
|
|
5316
|
+
let options;
|
|
5317
|
+
try {
|
|
5318
|
+
options = parseLogsArgs(rest);
|
|
5319
|
+
} catch (err) {
|
|
5320
|
+
console.error(`[agentbridge] ${err instanceof Error ? err.message : String(err)}`);
|
|
5321
|
+
process.exit(1);
|
|
5322
|
+
return;
|
|
5323
|
+
}
|
|
5324
|
+
let resolution;
|
|
5325
|
+
try {
|
|
5326
|
+
resolution = resolvePairReadOnly(pairFlag);
|
|
5327
|
+
} catch (err) {
|
|
5328
|
+
console.error(`[agentbridge] ${err instanceof Error ? err.message : String(err)}`);
|
|
5329
|
+
process.exit(1);
|
|
5330
|
+
return;
|
|
5331
|
+
}
|
|
5332
|
+
const { pair } = resolution;
|
|
5333
|
+
const logPath = options.codex ? pair.stateDir.codexWrapperLogFile : pair.stateDir.logFile;
|
|
5334
|
+
const logLabel = options.codex ? "codex wrapper log" : "daemon log";
|
|
5335
|
+
if (!existsSync16(logPath)) {
|
|
5336
|
+
const which = options.codex ? "codex wrapper log" : "daemon log";
|
|
5337
|
+
console.error(`no ${which} for pair ${pair.name} yet \u2014 start it with \`abg claude\` (${logPath})`);
|
|
5338
|
+
process.exit(1);
|
|
5339
|
+
return;
|
|
5340
|
+
}
|
|
5341
|
+
if (options.follow) {
|
|
5342
|
+
await followLog(logPath, options.lines);
|
|
5343
|
+
return;
|
|
5344
|
+
}
|
|
5345
|
+
printTail(logPath, options.lines, logLabel, pair.name);
|
|
5346
|
+
}
|
|
5347
|
+
function printTail(logPath, count, label, pairName) {
|
|
5348
|
+
let text;
|
|
5349
|
+
try {
|
|
5350
|
+
text = readFileSync13(logPath, "utf8");
|
|
5351
|
+
} catch (err) {
|
|
5352
|
+
console.error(`[agentbridge] failed to read ${label} for pair ${pairName}: ` + `${err instanceof Error ? err.message : String(err)} (${logPath})`);
|
|
5353
|
+
process.exit(1);
|
|
5354
|
+
return;
|
|
5355
|
+
}
|
|
5356
|
+
const lines = tailLines(text, count);
|
|
5357
|
+
for (const line of lines)
|
|
5358
|
+
console.log(line);
|
|
5359
|
+
}
|
|
5360
|
+
function followLog(logPath, count) {
|
|
5361
|
+
return new Promise((resolvePromise) => {
|
|
5362
|
+
const child = spawn4("tail", ["-f", "-n", String(count), logPath], {
|
|
5363
|
+
stdio: "inherit"
|
|
5364
|
+
});
|
|
5365
|
+
child.on("error", (err) => {
|
|
5366
|
+
console.error(`[agentbridge] failed to follow log: ${err.message}`);
|
|
5367
|
+
process.exit(1);
|
|
5368
|
+
});
|
|
5369
|
+
child.on("exit", (code, signal) => {
|
|
5370
|
+
if (signal === "SIGINT" || signal === "SIGTERM") {
|
|
5371
|
+
resolvePromise();
|
|
5372
|
+
return;
|
|
5373
|
+
}
|
|
5374
|
+
if (code != null && code !== 0) {
|
|
5375
|
+
process.exit(code);
|
|
5376
|
+
return;
|
|
5377
|
+
}
|
|
5378
|
+
resolvePromise();
|
|
5379
|
+
});
|
|
5380
|
+
});
|
|
5381
|
+
}
|
|
5382
|
+
function stripPairTokens(args) {
|
|
5383
|
+
const out = [];
|
|
5384
|
+
for (let i = 0;i < args.length; i++) {
|
|
5385
|
+
const a = args[i];
|
|
5386
|
+
if (a === "--pair") {
|
|
5387
|
+
const next = args[i + 1];
|
|
5388
|
+
if (next !== undefined && !next.startsWith("-"))
|
|
5389
|
+
i++;
|
|
5390
|
+
continue;
|
|
5391
|
+
}
|
|
5392
|
+
if (a.startsWith("--pair="))
|
|
5393
|
+
continue;
|
|
5394
|
+
out.push(a);
|
|
5395
|
+
}
|
|
5396
|
+
return out;
|
|
5397
|
+
}
|
|
5398
|
+
var DEFAULT_LINES = 100;
|
|
5399
|
+
var init_logs = __esm(() => {
|
|
5400
|
+
init_pair_resolver();
|
|
5401
|
+
});
|
|
5402
|
+
|
|
4979
5403
|
// src/cli.ts
|
|
4980
5404
|
function parseTopLevel(args) {
|
|
4981
5405
|
const pairTokens = [];
|
|
@@ -5048,6 +5472,10 @@ async function main(command, restArgs) {
|
|
|
5048
5472
|
const { runBudget: runBudget2 } = await Promise.resolve().then(() => (init_budget(), exports_budget));
|
|
5049
5473
|
await runBudget2(restArgs);
|
|
5050
5474
|
break;
|
|
5475
|
+
case "logs":
|
|
5476
|
+
const { runLogs: runLogs2 } = await Promise.resolve().then(() => (init_logs(), exports_logs));
|
|
5477
|
+
await runLogs2(restArgs);
|
|
5478
|
+
break;
|
|
5051
5479
|
case "--help":
|
|
5052
5480
|
case "-h":
|
|
5053
5481
|
case undefined:
|
|
@@ -5086,6 +5514,9 @@ Commands:
|
|
|
5086
5514
|
doctor [--json] Diagnose env, daemon, build drift, logs, and current thread
|
|
5087
5515
|
doctor resume-pollution [--apply] Find/fix old AgentBridge kickoff metadata
|
|
5088
5516
|
budget [--json] Show both agents' subscription quota snapshot (5h/weekly, drift, pause state)
|
|
5517
|
+
logs [--codex] [-f] [-n N]
|
|
5518
|
+
Tail this pair's daemon log (or the codex wrapper log with
|
|
5519
|
+
--codex). -n N: last N lines (default 100). -f: follow/stream.
|
|
5089
5520
|
kill [all | --all | --pair <name|id>]
|
|
5090
5521
|
Stop this directory's pairs (default), every pair (all/--all), or one (--pair)
|
|
5091
5522
|
|
|
@@ -5124,6 +5555,10 @@ Examples:
|
|
|
5124
5555
|
abg pairs # List all pairs and their ports/status
|
|
5125
5556
|
abg pairs --threads # Include current thread mapping
|
|
5126
5557
|
abg doctor --json # Emit a structured diagnostics report
|
|
5558
|
+
abg logs # Tail the last 100 lines of this pair's daemon log
|
|
5559
|
+
abg logs -f -n 200 # Follow the log, starting from the last 200 lines
|
|
5560
|
+
abg logs --codex # Tail the codex wrapper log instead
|
|
5561
|
+
abg --pair work logs # Tail the "work" pair's daemon log
|
|
5127
5562
|
abg pairs rm work # Stop this directory's "work" pair and free its slot
|
|
5128
5563
|
abg pairs rm work-1a2b3c4d # ...or by its full id (from that pair's directory)
|
|
5129
5564
|
abg pairs prune --dry-run # Preview orphan pair dirs (no registry entry, not live)
|
|
@@ -5145,7 +5580,7 @@ var MARKETPLACE_NAME = "agentbridge", PLUGIN_NAME = "agentbridge", REFRESH_COMMA
|
|
|
5145
5580
|
var init_cli = __esm(() => {
|
|
5146
5581
|
REFRESH_COMMANDS = new Set(["claude", "codex", "resume"]);
|
|
5147
5582
|
NOTIFY_COMMANDS = new Set(["claude", "codex", "init", "dev", "resume"]);
|
|
5148
|
-
PAIR_AWARE_COMMANDS = new Set(["claude", "codex", "kill", "doctor", "budget", "resume"]);
|
|
5583
|
+
PAIR_AWARE_COMMANDS = new Set(["claude", "codex", "kill", "doctor", "budget", "resume", "logs"]);
|
|
5149
5584
|
if (import.meta.main) {
|
|
5150
5585
|
const { command, restArgs } = parseTopLevel(process.argv.slice(2));
|
|
5151
5586
|
main(command, restArgs).catch((err) => {
|
|
@@ -5158,6 +5593,9 @@ init_cli();
|
|
|
5158
5593
|
|
|
5159
5594
|
export {
|
|
5160
5595
|
parseTopLevel,
|
|
5596
|
+
REFRESH_COMMANDS,
|
|
5161
5597
|
PLUGIN_NAME,
|
|
5598
|
+
PAIR_AWARE_COMMANDS,
|
|
5599
|
+
NOTIFY_COMMANDS,
|
|
5162
5600
|
MARKETPLACE_NAME
|
|
5163
5601
|
};
|