diff --git a/src/nemoclaw.ts b/src/nemoclaw.ts index 2782eaa85f..b46288f3f4 100644 --- a/src/nemoclaw.ts +++ b/src/nemoclaw.ts @@ -1067,6 +1067,10 @@ async function credentialsCommand(args) { process.exit(1); } +/** + * Inspect gateway logs for known Telegram conflict signatures without blocking + * the broader status command when the probe cannot run. + */ function checkMessagingBridgeHealth(sandboxName, channels) { // Only Telegram currently emits a recognizable conflict signature in the // gateway log. Discord/Slack have similar single-consumer constraints but @@ -1078,7 +1082,7 @@ function checkMessagingBridgeHealth(sandboxName, channels) { try { const result = spawnSync( getOpenshellBinary(), - ["sandbox", "exec", sandboxName, "sh", "-c", script], + ["sandbox", "exec", "-n", sandboxName, "--", "sh", "-c", script], { encoding: "utf-8", timeout: 3000, stdio: ["ignore", "pipe", "pipe"] }, ); const count = Number.parseInt((result.stdout || "").trim(), 10); @@ -1126,12 +1130,15 @@ function backfillAndFindOverlaps() { } } +/** + * Read a short tail of the gateway log for degraded messaging diagnostics. + */ function readGatewayLog(sandboxName) { const { spawnSync } = require("child_process"); try { const result = spawnSync( getOpenshellBinary(), - ["sandbox", "exec", sandboxName, "sh", "-c", "tail -n 10 /tmp/gateway.log 2>/dev/null"], + ["sandbox", "exec", "-n", sandboxName, "--", "sh", "-c", "tail -n 10 /tmp/gateway.log 2>/dev/null"], { encoding: "utf-8", timeout: 3000, stdio: ["ignore", "pipe", "pipe"] }, ); const output = (result.stdout || "").trim(); diff --git a/test/cli.test.ts b/test/cli.test.ts index 6f8f3592f9..70f01d0ce5 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -295,6 +295,66 @@ describe("CLI dispatch", () => { expect(fs.readFileSync(markerFile, "utf8")).not.toContain("--follow"); }); + it("uses named sandbox exec for bridge status helpers", () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-status-messaging-")); + const localBin = path.join(home, "bin"); + const registryDir = path.join(home, ".nemoclaw"); + const markerFile = path.join(home, "openshell.log"); + fs.mkdirSync(localBin, { recursive: true }); + fs.mkdirSync(registryDir, { recursive: true }); + fs.writeFileSync( + path.join(registryDir, "sandboxes.json"), + JSON.stringify({ + sandboxes: { + alpha: { + name: "alpha", + model: "test-model", + provider: "nvidia-prod", + gpuEnabled: false, + policies: [], + messagingChannels: ["telegram"], + agent: "hermes", + }, + }, + defaultSandbox: "alpha", + }), + { mode: 0o600 }, + ); + fs.writeFileSync( + path.join(localBin, "openshell"), + [ + "#!/usr/bin/env bash", + `marker_file=${JSON.stringify(markerFile)}`, + 'printf \'%s\\n\' "$*" >> "$marker_file"', + 'if [ "$1" = "sandbox" ] && [ "$2" = "exec" ]; then', + ' if [ "$8" = "tail -n 200 /tmp/gateway.log 2>/dev/null | grep -cE \\"getUpdates conflict|409[[:space:]:]+Conflict\\" || true" ]; then', + " echo 1", + " exit 0", + " fi", + ' if [ "$8" = "tail -n 10 /tmp/gateway.log 2>/dev/null" ]; then', + " echo 'getUpdates conflict'", + " exit 0", + " fi", + "fi", + "exit 0", + ].join("\n"), + { mode: 0o755 }, + ); + + const r = runWithEnv("status", { + HOME: home, + PATH: `${localBin}:${process.env.PATH || ""}`, + }); + + const log = fs.readFileSync(markerFile, "utf8"); + expect(r.code).toBe(0); + expect(log).toContain( + 'sandbox exec -n alpha -- sh -c tail -n 200 /tmp/gateway.log 2>/dev/null | grep -cE "getUpdates conflict|409[[:space:]:]+Conflict" || true', + ); + expect(log).toContain("sandbox exec -n alpha -- sh -c tail -n 10 /tmp/gateway.log 2>/dev/null"); + expect(log).not.toContain("sandbox exec alpha sh -c"); + }); + it("destroys the gateway runtime when the last sandbox is removed", () => { const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-destroy-last-")); const localBin = path.join(home, "bin");