Files
2026-07-13 13:36:53 +08:00

155 lines
5.1 KiB
JavaScript

/**
* Push the session-continuity test harder: many turns + larger payloads
* to force the gateway into context compression, which (per upstream
* NousResearch/hermes-agent#16938) can rotate the agent's session_id.
*
* Snapshot state.db AFTER EACH TURN — if a new session row appears
* mid-conversation, we've reproduced the proliferation seen in Marat's
* screenshot. Otherwise this conversation should stay in ONE row.
*
* Variant knobs:
* TURNS — default 12
* PAYLOAD_KB — default 4 (each user message includes this much filler text)
*/
const { attach } = require("./e2e-attach");
const { execFileSync } = require("child_process");
const path = require("path");
const os = require("os");
const STATE_DB = path.join(
os.homedir(),
"AppData",
"Local",
"hermes",
"state.db",
);
const N_TURNS = parseInt(process.env.TURNS || "12", 10);
const PAYLOAD_KB = parseInt(process.env.PAYLOAD_KB || "4", 10);
function snapshotSessions() {
const py = `
import sqlite3, json
db = sqlite3.connect(r"${STATE_DB.replace(/\\/g, "\\\\")}")
total = db.execute("SELECT COUNT(*) FROM sessions").fetchone()[0]
rows = [
{"id": r[0], "started_at": r[1], "message_count": r[2], "model": r[3]}
for r in db.execute(
"SELECT id, started_at, message_count, model FROM sessions ORDER BY started_at DESC LIMIT 50"
)
]
print(json.dumps({"total": total, "rows": rows}))
`;
const out = execFileSync("python", ["-c", py], { encoding: "utf-8" });
return JSON.parse(out);
}
function fillerText(kb) {
// Lorem-like deterministic filler so the request keeps changing but the
// context grows predictably.
const word = "context-filler-token-9c4a ";
const targetChars = kb * 1024;
const repeats = Math.ceil(targetChars / word.length);
return word.repeat(repeats).slice(0, targetChars);
}
async function waitForChatIdle(page, prevAgentBubbleCount) {
await page.waitForFunction(
() => document.querySelector("button.chat-stop-btn") !== null,
{ timeout: 10_000, polling: 100 },
);
await page.waitForFunction(
() => document.querySelector("button.chat-stop-btn") === null,
{ timeout: 300_000, polling: 250 },
);
await page.waitForFunction(
(prev) =>
document.querySelectorAll(".chat-bubble-agent").length > prev,
prevAgentBubbleCount,
{ timeout: 10_000, polling: 100 },
);
await new Promise((r) => setTimeout(r, 500));
}
async function countAgentBubbles(page) {
return await page.evaluate(
() => document.querySelectorAll(".chat-bubble-agent").length,
);
}
(async () => {
const { browser, page } = await attach();
console.log(`[setup] turns: ${N_TURNS}, payload per turn: ${PAYLOAD_KB} KB`);
await page.click("button.chat-clear-btn");
await new Promise((r) => setTimeout(r, 300));
const before = snapshotSessions();
const beforeIds = new Set(before.rows.map((r) => r.id));
console.log(`[baseline] state.db sessions: ${before.total}`);
console.log();
let prevAgentCount = await countAgentBubbles(page);
const newRowsByTurn = [];
const filler = fillerText(PAYLOAD_KB);
for (let i = 1; i <= N_TURNS; i++) {
const msg = `[Turn ${i}/${N_TURNS}] Reply with the single token PONG${i}. Context appended (ignore): ${filler}`;
process.stdout.write(`[turn ${String(i).padStart(2)}/${N_TURNS}] sending (${(msg.length / 1024).toFixed(1)} KB)... `);
const t0 = Date.now();
await page.fill("textarea.chat-input", msg);
await page.keyboard.press("Enter");
await waitForChatIdle(page, prevAgentCount);
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
prevAgentCount = await countAgentBubbles(page);
const after = snapshotSessions();
const newRows = after.rows.filter((r) => !beforeIds.has(r.id));
newRowsByTurn.push({
turn: i,
elapsed,
newRowCount: newRows.length,
newRows: newRows.map((r) => ({ id: r.id, msgs: r.message_count })),
});
const rowSummary =
newRows.length === 0
? "(no new row yet?)"
: newRows
.map((r) => `${r.id.slice(0, 30)}…(msgs=${r.message_count})`)
.join(" | ");
console.log(`${elapsed}s | new rows from chat: ${newRows.length}${rowSummary}`);
}
console.log();
const finalAfter = snapshotSessions();
const finalNew = finalAfter.rows.filter((r) => !beforeIds.has(r.id));
console.log(`[final] state.db sessions: ${finalAfter.total} (+${finalAfter.total - before.total})`);
console.log(`[final] new rows from this conversation:`);
for (const r of finalNew) {
console.log(` ${r.id} msgs=${r.message_count} ${r.model}`);
}
console.log();
// Track when (or if) a SECOND row appears
const splitTurn = newRowsByTurn.find((t) => t.newRowCount > 1);
if (finalNew.length > 1) {
console.log(
`[VERDICT] 🔴 PROLIFERATION REPRODUCED. ${finalNew.length} session rows for ${N_TURNS} turns. ` +
`Split first observed at turn ${splitTurn?.turn ?? "?"}.`,
);
} else {
console.log(
`[VERDICT] ✅ NO proliferation. 1 row across all ${N_TURNS} turns with ${finalNew[0]?.message_count ?? "?"} messages.`,
);
}
await browser.close();
})().catch((e) => {
console.error("FAILED:", e.stack || e.message || e);
process.exit(1);
});