155 lines
5.1 KiB
JavaScript
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);
|
|
});
|