mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 13:17:40 +02:00
[eric] router: the history pruner announces itself at boot and the backend warns when it is missing, so it can never disable itself in silence
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PNA9xhjw5SkfuB2tJ5KwCj
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
b683d88c34
commit
81811d3c3e
@@ -100,19 +100,26 @@ const _http = require('http');
|
||||
|
||||
const TARGET_HOSTS = new Set(['api.openai.com', 'api.anthropic.com']);
|
||||
|
||||
// ENG-418: continuous history pruning rides the same interceptor. Loaded lazily and fail-open so
|
||||
// a missing or broken module costs the prune, never the request.
|
||||
// ENG-418: continuous history pruning rides the same interceptor. Fail-open: a missing or broken
|
||||
// module costs the prune, never the request. Loaded EAGERLY so the verdict line lands in the router's
|
||||
// start log at boot, where the backend reads it; a lazy load only spoke on the first request, which
|
||||
// left a dead pruner indistinguishable from a quiet one until a chat had already paid for it.
|
||||
let _prune = null;
|
||||
function historyPrune(bodyStr) {
|
||||
function loadHistoryPrune() {
|
||||
try {
|
||||
if (_prune === null) {
|
||||
_prune = require(require('path').join(__dirname, '9router_history_prune.js'));
|
||||
try { process.stderr.write('[history-prune] installed\n'); } catch (_) {}
|
||||
}
|
||||
return _prune.maybePrune(bodyStr);
|
||||
_prune = require(require('path').join(__dirname, '9router_history_prune.js'));
|
||||
try { process.stderr.write('[history-prune] installed\n'); } catch (_) {}
|
||||
} catch (_) {
|
||||
_prune = { maybePrune: (b) => b };
|
||||
try { process.stderr.write('[history-prune] FAILED to load; requests pass through unpruned\n'); } catch (_) {}
|
||||
}
|
||||
}
|
||||
loadHistoryPrune();
|
||||
function historyPrune(bodyStr) {
|
||||
try {
|
||||
return _prune.maybePrune(bodyStr);
|
||||
} catch (_) {
|
||||
try { process.stderr.write('[history-prune] request passed through unpruned (transform threw)\n'); } catch (_) {}
|
||||
return bodyStr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""The router's history pruner must never disable itself in silence.
|
||||
|
||||
`9router_gpt5_patch.js` loads the pruner eagerly and writes one verdict line to stderr at boot. In a
|
||||
packaged build that stderr lands only in the router start log, which nothing read, so a pruner that
|
||||
failed to load looked identical to one that was working. This reads the verdict after a successful
|
||||
start and says, loudly, what every chat on the machine just lost.
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
from typing import Literal
|
||||
from typeguard import typechecked
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
HistoryPruneState = Literal["installed", "failed", "unknown"]
|
||||
P_INSTALLED_LINE = "[history-prune] installed"
|
||||
P_FAILED_LINE = "[history-prune] FAILED to load"
|
||||
|
||||
|
||||
@typechecked
|
||||
def history_prune_state(log_path: str) -> HistoryPruneState:
|
||||
try:
|
||||
with open(log_path, "rb") as f:
|
||||
text = f.read(200_000).decode("utf-8", errors="replace")
|
||||
except OSError:
|
||||
return "unknown"
|
||||
if P_FAILED_LINE in text:
|
||||
return "failed"
|
||||
if P_INSTALLED_LINE in text:
|
||||
return "installed"
|
||||
return "unknown"
|
||||
|
||||
|
||||
@typechecked
|
||||
def report_history_prune_state(log_path: str, packaged: bool) -> HistoryPruneState:
|
||||
"""Packaged builds capture the router's stderr, so silence there means the patch never loaded (the
|
||||
`--require` flag is dropped when the patch file is missing). Dev sends stderr to DEVNULL, so
|
||||
silence there is just silence."""
|
||||
state = history_prune_state(log_path)
|
||||
if state == "installed":
|
||||
logger.info("9Router history pruner installed")
|
||||
return state
|
||||
if not packaged:
|
||||
logger.debug("9Router history pruner state unknown (stderr not captured in dev)")
|
||||
return state
|
||||
logger.warning(
|
||||
"9Router history pruner %s: every chat on this machine now resends its FULL tool history "
|
||||
"on every step, so long chats will hit the context wall (the autocompact-thrash class). "
|
||||
"Router start log: %s",
|
||||
"FAILED to load" if state == "failed" else "never announced itself (patch not loaded?)",
|
||||
log_path,
|
||||
)
|
||||
try:
|
||||
from backend.apps.service.client import submit_diagnostic
|
||||
submit_diagnostic({"kind": "router", "subkind": f"history_prune_{state}", "log_path": log_path})
|
||||
except Exception:
|
||||
logger.debug("submit_diagnostic history_prune_state failed", exc_info=True)
|
||||
return state
|
||||
@@ -26,6 +26,7 @@ import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import httpx
|
||||
from backend.apps.nine_router.history_prune_state import report_history_prune_state
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -667,6 +668,7 @@ async def p_ensure_running_impl():
|
||||
await asyncio.sleep(0.5)
|
||||
if is_running():
|
||||
logger.info("9Router started successfully")
|
||||
report_history_prune_state(p_cap_path, p_is_packaged)
|
||||
return
|
||||
# Verify-at-boot: it never answered. Report with the captured tail + the exit code (non-None = it crashed; None = wedged or just slow).
|
||||
p_report_start_failure(
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""The router's pruner may never disable itself in silence (CLAUDE.md, row 6)."""
|
||||
import logging
|
||||
import re
|
||||
from backend.apps.nine_router import history_prune_state as hps
|
||||
|
||||
|
||||
def p_log(tmp_path, text: str) -> str:
|
||||
p = tmp_path / "start.log"
|
||||
p.write_bytes(text.encode())
|
||||
return str(p)
|
||||
|
||||
|
||||
def p_capture(monkeypatch):
|
||||
records: list = []
|
||||
handler = logging.Handler()
|
||||
handler.emit = records.append
|
||||
hps.logger.addHandler(handler)
|
||||
hps.logger.setLevel(logging.DEBUG)
|
||||
monkeypatch.setattr(hps, "logger", hps.logger)
|
||||
return records, handler
|
||||
|
||||
|
||||
def test_reads_all_three_verdicts(tmp_path) -> None:
|
||||
assert hps.history_prune_state(p_log(tmp_path, "boot\n[history-prune] installed\n")) == "installed"
|
||||
assert hps.history_prune_state(p_log(tmp_path, "[history-prune] FAILED to load; requests pass through unpruned\n")) == "failed"
|
||||
assert hps.history_prune_state(p_log(tmp_path, "boot, nothing about pruning\n")) == "unknown"
|
||||
assert hps.history_prune_state(str(tmp_path / "missing.log")) == "unknown"
|
||||
|
||||
|
||||
def test_a_failed_pruner_in_a_packaged_build_warns_and_names_what_stopped(tmp_path, monkeypatch) -> None:
|
||||
records, handler = p_capture(monkeypatch)
|
||||
try:
|
||||
state = hps.report_history_prune_state(p_log(tmp_path, "[history-prune] FAILED to load; requests pass through unpruned\n"), packaged=True)
|
||||
finally:
|
||||
hps.logger.removeHandler(handler)
|
||||
assert state == "failed"
|
||||
warns = [r for r in records if r.levelno >= logging.WARNING]
|
||||
assert warns, "the fallback must be LOUD"
|
||||
msg = warns[0].getMessage()
|
||||
assert "FULL tool history" in msg and "context wall" in msg
|
||||
|
||||
|
||||
def test_silence_in_a_packaged_build_is_also_a_warning(tmp_path, monkeypatch) -> None:
|
||||
records, handler = p_capture(monkeypatch)
|
||||
try:
|
||||
state = hps.report_history_prune_state(p_log(tmp_path, "ready\n"), packaged=True)
|
||||
finally:
|
||||
hps.logger.removeHandler(handler)
|
||||
assert state == "unknown"
|
||||
assert [r for r in records if r.levelno >= logging.WARNING], "a patch that never announced itself did not load"
|
||||
|
||||
|
||||
def test_installed_and_dev_silence_are_quiet(tmp_path, monkeypatch) -> None:
|
||||
records, handler = p_capture(monkeypatch)
|
||||
try:
|
||||
hps.report_history_prune_state(p_log(tmp_path, "[history-prune] installed\n"), packaged=True)
|
||||
hps.report_history_prune_state(p_log(tmp_path, "nothing\n"), packaged=False)
|
||||
finally:
|
||||
hps.logger.removeHandler(handler)
|
||||
assert not [r for r in records if r.levelno >= logging.WARNING], "dev captures no stderr, so silence there is not a verdict"
|
||||
|
||||
|
||||
def test_the_report_is_wired_onto_the_start_success_path() -> None:
|
||||
src = open("backend/apps/nine_router/process.py").read()
|
||||
ok = src.index('logger.info("9Router started successfully")')
|
||||
call = src.index("report_history_prune_state(p_cap_path, p_is_packaged)")
|
||||
ret = src.index("return", call)
|
||||
assert ok < call < ret, "the verdict is read right after a successful start, before the function returns"
|
||||
|
||||
|
||||
def test_the_patch_loads_the_pruner_eagerly_not_lazily() -> None:
|
||||
src = open("backend/apps/agents/9router_gpt5_patch.js").read()
|
||||
assert re.search(r"^loadHistoryPrune\(\);", src, re.M), "a lazy load only speaks on the first request; the boot log would stay empty"
|
||||
assert src.index("loadHistoryPrune();") < src.index("function historyPrune(")
|
||||
@@ -225,3 +225,37 @@ test('old SMALL results age too, newest stay sacred (the 32% floor from the real
|
||||
assert.match(res[i].content[0].text, /nothing to commit/, 'a recent small IS the answer and must survive');
|
||||
}
|
||||
});
|
||||
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const pathMod = require('node:path');
|
||||
const PATCH_SRC = pathMod.join(__dirname, '..', 'backend', 'apps', 'agents', '9router_gpt5_patch.js');
|
||||
|
||||
function bootPatchWith(pruneModuleSource) {
|
||||
const dir = fs.mkdtempSync(pathMod.join(os.tmpdir(), 'osw-patch-'));
|
||||
fs.copyFileSync(PATCH_SRC, pathMod.join(dir, '9router_gpt5_patch.js'));
|
||||
if (pruneModuleSource !== null) fs.writeFileSync(pathMod.join(dir, '9router_history_prune.js'), pruneModuleSource);
|
||||
const r = spawnSync(process.execPath, ['--require', pathMod.join(dir, '9router_gpt5_patch.js'), '-e', 'process.stdout.write("booted")'], { encoding: 'utf8' });
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
return r;
|
||||
}
|
||||
|
||||
test('the pruner announces itself at LOAD, before any request (liveness, not just correctness)', () => {
|
||||
const real = fs.readFileSync(pathMod.join(__dirname, '..', 'backend', 'apps', 'agents', '9router_history_prune.js'), 'utf8');
|
||||
const r = bootPatchWith(real);
|
||||
assert.strictEqual(r.stdout, 'booted');
|
||||
assert.match(r.stderr, /\[history-prune\] installed/, 'the installed line must land at boot, with zero requests sent');
|
||||
});
|
||||
|
||||
test('a broken pruner module says so at boot and the router still boots (fail-open, loudly)', () => {
|
||||
const r = bootPatchWith('throw new Error("simulated broken pruner");');
|
||||
assert.strictEqual(r.stdout, 'booted', 'a dead pruner must never take the router down with it');
|
||||
assert.match(r.stderr, /\[history-prune\] FAILED to load; requests pass through unpruned/);
|
||||
});
|
||||
|
||||
test('a MISSING pruner module is the same story as a broken one', () => {
|
||||
const r = bootPatchWith(null);
|
||||
assert.strictEqual(r.stdout, 'booted');
|
||||
assert.match(r.stderr, /FAILED to load/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user