diff --git a/api/package.json b/api/package.json
index de294d2d..20207b9a 100644
--- a/api/package.json
+++ b/api/package.json
@@ -16,7 +16,7 @@
"db:cleanup": "node dist/cron/dbCleanup.js",
"funnel-report": "node dist/scripts/funnelReport.js",
"npm:audit": "npm audit --json > /tmp/audit.json && echo \"Audit complete\"",
-"test": "TS_NODE_TRANSPILE_ONLY=1 node --loader ts-node/esm tests/wallet-provisioning.test.js && node --loader ts-node/esm tests/ssrf.test.js && node tests/integration.test.js && node tests/pages.test.js && node tests/x402-v1-passthrough.test.mjs && node tests/model-cost.test.mjs && node tests/session-pricing.test.mjs && node tests/prompt-moderation.test.mjs && node tests/critical-regressions.test.mjs && node tests/unsubscribe.test.mjs && node tests/reactivation-render.test.mjs && node tests/outreach-active-devs.test.mjs && node tests/credit-alert-dedup.test.mjs && node tests/verify-activation.test.mjs && node tests/signup-firstcall.test.mjs && node tests/oauth-signup-cta.test.mjs && node tests/x402-sell-copy.test.mjs && node tests/verify-resend.test.mjs && node tests/intent-funnel.test.mjs && node tests/credit-email-buylinks.test.mjs",
+"test": "TS_NODE_TRANSPILE_ONLY=1 node --loader ts-node/esm tests/wallet-provisioning.test.js && node --loader ts-node/esm tests/ssrf.test.js && node tests/integration.test.js && node tests/pages.test.js && node tests/x402-v1-passthrough.test.mjs && node tests/model-cost.test.mjs && node tests/session-pricing.test.mjs && node tests/ai-generate-provider-errors.test.mjs && node tests/prompt-moderation.test.mjs && node tests/critical-regressions.test.mjs && node tests/unsubscribe.test.mjs && node tests/reactivation-render.test.mjs && node tests/outreach-active-devs.test.mjs && node tests/credit-alert-dedup.test.mjs && node tests/verify-activation.test.mjs && node tests/signup-firstcall.test.mjs && node tests/oauth-signup-cta.test.mjs && node tests/x402-sell-copy.test.mjs && node tests/verify-resend.test.mjs && node tests/intent-funnel.test.mjs && node tests/credit-email-buylinks.test.mjs",
"test:integration": "node tests/integration.test.js",
"test:verify-activation": "node tests/verify-activation.test.mjs",
"test:verify-resend": "node tests/verify-resend.test.mjs",
diff --git a/api/src/assets/dashboardHtml.ts b/api/src/assets/dashboardHtml.ts
index 40c72f26..b2cce090 100644
--- a/api/src/assets/dashboardHtml.ts
+++ b/api/src/assets/dashboardHtml.ts
@@ -76,8 +76,12 @@ export const DASHBOARD_HTML = `
.depleted-banner { background:rgba(248,113,113,0.10); border:1px solid rgba(248,113,113,0.35); border-radius:14px; padding:18px 20px; display:flex; align-items:center; justify-content:space-between; gap:16px; margin-bottom:16px; }
.depleted-text { font-size:14px; color:rgba(255,255,255,0.85); }
.depleted-text strong { color:#f87171; }
- .verify-banner { background:rgba(34,211,238,0.07); border:1px solid rgba(34,211,238,0.25); border-radius:14px; padding:14px 20px; display:flex; align-items:center; gap:12px; margin-bottom:16px; font-size:13px; color:rgba(255,255,255,0.75); }
+ .verify-banner { background:rgba(34,211,238,0.07); border:1px solid rgba(34,211,238,0.25); border-radius:14px; padding:14px 20px; display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:16px; font-size:13px; color:rgba(255,255,255,0.75); }
.verify-banner strong { color:#22d3ee; }
+ .verify-actions { display:flex; align-items:center; gap:10px; flex-wrap:wrap; justify-content:flex-end; }
+ .verify-btn { height:34px; padding:0 12px; border-radius:8px; border:1px solid rgba(34,211,238,0.35); background:rgba(34,211,238,0.12); color:#22d3ee; font-size:12px; font-weight:700; font-family:inherit; cursor:pointer; white-space:nowrap; }
+ .verify-btn:disabled { opacity:0.55; cursor:not-allowed; }
+ .verify-status { font-size:12px; color:rgba(255,255,255,0.55); min-height:16px; }
/* STATUS */
.status-tag { font-size:12px; color:var(--muted); font-family:"JetBrains Mono",monospace; }
@media (max-width:600px) {
@@ -196,6 +200,10 @@ export const DASHBOARD_HTML = `
Your email isn't verified yet — verify it to unlock your 0 pending credits. Use the verification link we emailed you at signup.
+
+
+
+
@@ -336,6 +344,14 @@ export const DASHBOARD_HTML = `
var pending = data.pending_credits ?? 0;
if (data.email_verified === false && pending > 0) {
document.getElementById("pending-credits").textContent = pending.toLocaleString();
+ var accountEmail = (typeof data.email === "string") ? data.email : "";
+ var resendBtn = document.getElementById("resend-verify-btn");
+ var resendStatus = document.getElementById("verify-resend-status");
+ if (resendStatus) resendStatus.textContent = "";
+ if (resendBtn) {
+ resendBtn.disabled = !accountEmail;
+ resendBtn.onclick = function() { resendVerificationEmail(accountEmail); };
+ }
document.getElementById("verify-banner").style.display = "flex";
} else {
document.getElementById("verify-banner").style.display = "none";
@@ -383,6 +399,30 @@ export const DASHBOARD_HTML = `
}
}
+ async function resendVerificationEmail(email) {
+ var btn = document.getElementById("resend-verify-btn");
+ var st = document.getElementById("verify-resend-status");
+ if (!email || !btn || !st) return;
+ btn.disabled = true;
+ btn.textContent = "Sending...";
+ st.textContent = "";
+ try {
+ var resp = await fetch("/v1/agent/verify-email/resend", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ email: email })
+ });
+ var data;
+ try { data = await resp.json(); } catch(_) { data = {}; }
+ st.textContent = data.message || (resp.ok ? "Requested — check your inbox." : "Could not resend right now.");
+ } catch(_) {
+ st.textContent = "Could not reach the server — try again shortly.";
+ } finally {
+ btn.disabled = false;
+ btn.textContent = "Resend email";
+ }
+ }
+
function showKeyEntryFallback() {
document.getElementById("loading-card").style.display = "none";
document.getElementById("key-entry-card").style.display = "block";
diff --git a/api/src/lib/sessionContext.ts b/api/src/lib/sessionContext.ts
index 8ae2ad01..415d19fc 100644
--- a/api/src/lib/sessionContext.ts
+++ b/api/src/lib/sessionContext.ts
@@ -21,6 +21,15 @@ export interface SessionContextMessage {
content: string;
}
+export const DEFAULT_SESSION_CONTEXT_MAX_CHARS = 40000;
+
+export function parseSessionContextMaxChars(raw: string | undefined): number {
+ const value = (raw ?? String(DEFAULT_SESSION_CONTEXT_MAX_CHARS)).trim();
+ if (!/^\d+$/.test(value)) return DEFAULT_SESSION_CONTEXT_MAX_CHARS;
+ const parsed = Number(value);
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : DEFAULT_SESSION_CONTEXT_MAX_CHARS;
+}
+
export function trimSessionContext(
messages: T[],
maxChars: number,
diff --git a/api/src/routes/agent.ts b/api/src/routes/agent.ts
index 3fafb1d1..3eeb5f94 100644
--- a/api/src/routes/agent.ts
+++ b/api/src/routes/agent.ts
@@ -354,6 +354,7 @@ router.get("/usage", requireAuth, async (req: AuthedRequest, res: Response): Pro
res.json({
ok: true,
agent_id: agent.id,
+ email: agent.email,
credits_remaining: agent.credits,
calls_today: callsToday,
total_calls: agent.totalCalls,
diff --git a/api/src/routes/tools/index.ts b/api/src/routes/tools/index.ts
index 250d667a..b1c2c3f0 100644
--- a/api/src/routes/tools/index.ts
+++ b/api/src/routes/tools/index.ts
@@ -7,7 +7,7 @@ import { config } from "../../config.js";
import { validateUrl, safeAxiosGet, safeFetch, safeAxiosRequest } from "../../lib/ssrf.js";
import { prisma } from "../../lib/prisma.js";
import { applyModelCost, modelCostMultiplier } from "../../lib/modelCost.js";
-import { trimSessionContext } from "../../lib/sessionContext.js";
+import { parseSessionContextMaxChars, trimSessionContext } from "../../lib/sessionContext.js";
import { moderateGenerationPrompt } from "../../lib/promptModeration.js";
import { readArrayBufferWithLimit, ResponseTooLargeError } from "../../utils/responseBody.js";
import { enforcementTierForAccount } from "../../lib/tiers.js";
@@ -187,6 +187,27 @@ function byokAdjustedCost(req: Request, cost: number, headerNames: readonly stri
return hasByokKeys(req, headerNames) ? Math.max(1, Math.ceil(cost * 0.2)) : cost;
}
+async function readProviderJson(resp: globalThis.Response): Promise> {
+ return (await resp.json().catch(() => ({}))) as Record;
+}
+
+function providerFailureStatus(status: number): number {
+ return status === 429 ? 429 : 502;
+}
+
+function providerFailureCode(provider: string, status: number): string {
+ return status === 429 ? "rate_limited" : `${provider}_error`;
+}
+
+function providerFailureMessage(label: string, status: number, data: Record): string {
+ const detail = typeof data.error?.message === "string"
+ ? data.error.message
+ : typeof data.message === "string"
+ ? data.message
+ : "";
+ return detail ? `${label} API error: ${detail}` : `${label} API returned ${status}`;
+}
+
function extractJsonObject(text: string): string | null {
const cleaned = text.replace(/```json|```/g, "").trim();
const match = cleaned.match(/\{[\s\S]*\}/);
@@ -1274,8 +1295,16 @@ router.post("/ai-generate", ...toolMiddleware("ai-generate"), async (req: Authed
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${openaiKey}` },
body: JSON.stringify({ model, max_tokens: maxTok, messages: [...(system ? [{ role: "system", content: system }] : []), { role: "user", content: prompt }] }),
});
- const data = await resp.json() as { choices?: Array<{ message?: { content?: string } }>; usage?: { prompt_tokens?: number; completion_tokens?: number } };
+ const data = await readProviderJson(resp) as { choices?: Array<{ message?: { content?: string } }>; usage?: { prompt_tokens?: number; completion_tokens?: number }; error?: { message?: string }; message?: string };
+ if (!resp.ok) {
+ res.status(providerFailureStatus(resp.status)).json({ ok: false, error: providerFailureCode("openai", resp.status), message: providerFailureMessage("OpenAI", resp.status, data), request_id: reqId() });
+ return;
+ }
const text = data.choices?.[0]?.message?.content ?? "";
+ if (!text) {
+ res.status(502).json({ ok: false, error: "openai_error", message: "OpenAI returned an empty response", request_id: reqId() });
+ return;
+ }
const _u = { input_tokens: data.usage?.prompt_tokens ?? 0, output_tokens: data.usage?.completion_tokens ?? 0 };
res.json({ ok: true, text, model, ...(resolvedMode ? { mode: resolvedMode } : {}), provider: "openai", usage: _u, word_count: text.split(/\s+/).filter(Boolean).length, char_count: text.length, sentence_count: text.split(/[.!?]+/).filter((s: string) => s.trim()).length, estimated_cost_usd: (_u.input_tokens * 0.000003 + _u.output_tokens * 0.000015).toFixed(6), response_format: "structured", arch_tools_version: "1.9.0", processed_at: new Date().toISOString(), ...(byokProvider === "openai" ? { byok: true, byok_provider: "openai" } : {}), request_id: reqId() });
return;
@@ -1291,8 +1320,16 @@ router.post("/ai-generate", ...toolMiddleware("ai-generate"), async (req: Authed
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ contents: [{ parts: [{ text: fullPrompt }] }], generationConfig: { maxOutputTokens: maxTok } }),
});
- const data = await resp.json() as { candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }>; usageMetadata?: { promptTokenCount?: number; candidatesTokenCount?: number } };
+ const data = await readProviderJson(resp) as { candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }>; usageMetadata?: { promptTokenCount?: number; candidatesTokenCount?: number }; error?: { message?: string }; message?: string };
+ if (!resp.ok) {
+ res.status(providerFailureStatus(resp.status)).json({ ok: false, error: providerFailureCode("google", resp.status), message: providerFailureMessage("Google", resp.status, data), request_id: reqId() });
+ return;
+ }
const text = data.candidates?.[0]?.content?.parts?.[0]?.text ?? "";
+ if (!text) {
+ res.status(502).json({ ok: false, error: "google_error", message: "Google returned an empty response", request_id: reqId() });
+ return;
+ }
const _ug = { input_tokens: data.usageMetadata?.promptTokenCount ?? 0, output_tokens: data.usageMetadata?.candidatesTokenCount ?? 0 };
res.json({ ok: true, text, model, ...(resolvedMode ? { mode: resolvedMode } : {}), provider: "google", usage: _ug, word_count: text.split(/\s+/).filter(Boolean).length, char_count: text.length, sentence_count: text.split(/[.!?]+/).filter((s: string) => s.trim()).length, estimated_cost_usd: (_ug.input_tokens * 0.000001 + _ug.output_tokens * 0.000004).toFixed(6), response_format: "structured", arch_tools_version: "1.9.0", processed_at: new Date().toISOString(), ...(byokProvider === "google" ? { byok: true, byok_provider: "google" } : {}), request_id: reqId() });
return;
@@ -1307,8 +1344,16 @@ router.post("/ai-generate", ...toolMiddleware("ai-generate"), async (req: Authed
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${xaiKey}` },
body: JSON.stringify({ model, max_tokens: maxTok, messages: [...(system ? [{ role: "system", content: system }] : []), { role: "user", content: prompt }] }),
});
- const data = await resp.json() as { choices?: Array<{ message?: { content?: string } }>; usage?: { prompt_tokens?: number; completion_tokens?: number } };
+ const data = await readProviderJson(resp) as { choices?: Array<{ message?: { content?: string } }>; usage?: { prompt_tokens?: number; completion_tokens?: number }; error?: { message?: string }; message?: string };
+ if (!resp.ok) {
+ res.status(providerFailureStatus(resp.status)).json({ ok: false, error: providerFailureCode("xai", resp.status), message: providerFailureMessage("xAI", resp.status, data), request_id: reqId() });
+ return;
+ }
const text = data.choices?.[0]?.message?.content ?? "";
+ if (!text) {
+ res.status(502).json({ ok: false, error: "xai_error", message: "xAI returned an empty response", request_id: reqId() });
+ return;
+ }
const _ux = { input_tokens: data.usage?.prompt_tokens ?? 0, output_tokens: data.usage?.completion_tokens ?? 0 };
res.json({ ok: true, text, model, ...(resolvedMode ? { mode: resolvedMode } : {}), provider: "xai", usage: _ux, word_count: text.split(/\s+/).filter(Boolean).length, char_count: text.length, sentence_count: text.split(/[.!?]+/).filter((s: string) => s.trim()).length, estimated_cost_usd: (_ux.input_tokens * 0.000005 + _ux.output_tokens * 0.000015).toFixed(6), response_format: "structured", arch_tools_version: "1.9.0", processed_at: new Date().toISOString(), ...(byokProvider === "xai" ? { byok: true, byok_provider: "xai" } : {}), request_id: reqId() });
return;
@@ -3071,7 +3116,7 @@ router.post("/session-message", ...toolMiddleware("session-message"), async (req
// a loop. Trim oldest-first to SESSION_CONTEXT_MAX_CHARS (env-tunable,
// default 40000); the newest message is always sent. Stored history is
// unchanged — only the window sent upstream is trimmed.
- const SESSION_CONTEXT_MAX_CHARS = parseInt(process.env.SESSION_CONTEXT_MAX_CHARS ?? "40000", 10);
+ const SESSION_CONTEXT_MAX_CHARS = parseSessionContextMaxChars(process.env.SESSION_CONTEXT_MAX_CHARS);
const { window: upstreamMessages, truncated: contextTruncated } =
trimSessionContext(session.messages, SESSION_CONTEXT_MAX_CHARS);
diff --git a/api/tests/ai-generate-provider-errors.test.mjs b/api/tests/ai-generate-provider-errors.test.mjs
new file mode 100644
index 00000000..f0c05389
--- /dev/null
+++ b/api/tests/ai-generate-provider-errors.test.mjs
@@ -0,0 +1,61 @@
+/**
+ * ai-generate provider failure regression coverage.
+ *
+ * Guards the paid-empty-success bug: OpenAI/Gemini/xAI non-2xx or empty
+ * responses must not be coerced into { ok:true, text:"" }, because successful
+ * responses are charged/logged by the credit finalizer.
+ *
+ * Run: cd api && node tests/ai-generate-provider-errors.test.mjs
+ */
+import assert from "assert";
+import fs from "fs";
+import path from "path";
+import { fileURLToPath } from "url";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const toolsSrc = fs.readFileSync(path.join(__dirname, "..", "src", "routes", "tools", "index.ts"), "utf8");
+
+let failures = 0;
+function test(name, fn) {
+ try { fn(); console.log(` ✓ ${name}`); }
+ catch (e) { failures++; console.error(` ✗ ${name}: ${e.message}`); }
+}
+
+function route(name) {
+ const start = toolsSrc.indexOf(`router.post("/${name}"`);
+ assert.ok(start >= 0, `missing route for ${name}`);
+ const end = toolsSrc.indexOf("router.post(", start + 1);
+ return toolsSrc.slice(start, end > start ? end : undefined);
+}
+
+const aiGenerateRoute = route("ai-generate");
+
+console.log("ai-generate provider failure handling:");
+
+test("shared helpers convert provider failures into non-2xx API responses", () => {
+ assert.match(toolsSrc, /async function readProviderJson\(resp: globalThis\.Response\)/);
+ assert.match(toolsSrc, /function providerFailureStatus\(status: number\): number/);
+ assert.match(toolsSrc, /status === 429 \? "rate_limited" : `\$\{provider\}_error`/);
+});
+
+for (const [provider, label, emptyMessage] of [
+ ["openai", "OpenAI", "OpenAI returned an empty response"],
+ ["google", "Google", "Google returned an empty response"],
+ ["xai", "xAI", "xAI returned an empty response"],
+]) {
+ test(`${provider}: checks resp.ok before success`, () => {
+ const providerBlock = aiGenerateRoute.slice(aiGenerateRoute.indexOf(`provider: "${provider}"`) - 900, aiGenerateRoute.indexOf(`provider: "${provider}"`) + 500);
+ assert.match(providerBlock, /if \(!resp\.ok\)/, `${provider} branch must check provider HTTP status`);
+ assert.match(providerBlock, new RegExp(`providerFailureCode\\("${provider}", resp\\.status\\)`));
+ assert.match(providerBlock, new RegExp(`providerFailureMessage\\("${label}", resp\\.status, data\\)`));
+ });
+
+ test(`${provider}: empty provider output is not a paid success`, () => {
+ assert.ok(aiGenerateRoute.includes(emptyMessage), `${provider} empty response message missing`);
+ assert.ok(!new RegExp(`provider: "${provider}"[\\s\\S]{0,500}const text = data\\.[\\s\\S]*?\\?\\? "";[\\s\\S]{0,250}res\\.json\\(\\{ ok: true`).test(aiGenerateRoute),
+ `${provider} must not immediately turn missing output into ok:true`);
+ });
+}
+
+if (failures) { console.error(`\n${failures} failure(s)`); process.exit(1); }
+console.log("\nAll ai-generate provider-error tests passed.");
diff --git a/api/tests/intent-funnel.test.mjs b/api/tests/intent-funnel.test.mjs
index 2a244fb3..d4479f2d 100644
--- a/api/tests/intent-funnel.test.mjs
+++ b/api/tests/intent-funnel.test.mjs
@@ -162,13 +162,17 @@ async function main() {
assert.ok(DASHBOARD_HTML.includes('id="depleted-banner"'), "zero-balance banner missing");
assert.ok(DASHBOARD_HTML.includes('id="upgrade-banner"'), "low-balance banner missing");
assert.ok(DASHBOARD_HTML.includes('id="verify-banner"'), "verify banner missing");
+ assert.ok(DASHBOARD_HTML.includes('id="resend-verify-btn"'), "verify resend button missing");
assert.ok(DASHBOARD_HTML.includes('(cr === 0) ? "flex" : "none"'));
assert.ok(DASHBOARD_HTML.includes('(cr > 0 && cr < 50) ? "flex" : "none"'));
assert.ok(DASHBOARD_HTML.includes('data.email_verified === false && pending > 0'));
+ assert.ok(DASHBOARD_HTML.includes('resendVerificationEmail(accountEmail)'), "verify banner must wire an explicit resend action");
+ assert.ok(DASHBOARD_HTML.includes('/v1/agent/verify-email/resend'), "dashboard must call the neutral resend endpoint");
assert.ok(DASHBOARD_HTML.includes('href="/pricing?pack=starter"'));
});
- test("/v1/agent/usage exposes email_verified + pending_credits", () => {
+ test("/v1/agent/usage exposes email + email_verified + pending_credits", () => {
const agentSrc = fs.readFileSync(src("routes", "agent.ts"), "utf-8");
+ assert.ok(agentSrc.includes("email: agent.email"));
assert.ok(agentSrc.includes("email_verified: verification?.emailVerified ?? true"));
assert.ok(agentSrc.includes("pending_credits: verification?.pendingCredits ?? 0"));
});
diff --git a/api/tests/session-pricing.test.mjs b/api/tests/session-pricing.test.mjs
index 6b32dce6..15c50c5e 100644
--- a/api/tests/session-pricing.test.mjs
+++ b/api/tests/session-pricing.test.mjs
@@ -12,7 +12,7 @@ import assert from "assert";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
-import { trimSessionContext } from "../dist/lib/sessionContext.js";
+import { DEFAULT_SESSION_CONTEXT_MAX_CHARS, parseSessionContextMaxChars, trimSessionContext } from "../dist/lib/sessionContext.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const toolsSrc = fs.readFileSync(path.join(__dirname, "..", "src", "routes", "tools", "index.ts"), "utf8");
@@ -81,6 +81,24 @@ test("stored history is not mutated by trimming", () => {
assert.deepStrictEqual(messages, copy);
});
+console.log("\nContext cap env parsing:");
+
+test("default context cap is the safe 40k budget", () => {
+ assert.strictEqual(DEFAULT_SESSION_CONTEXT_MAX_CHARS, 40000);
+ assert.strictEqual(parseSessionContextMaxChars(undefined), 40000);
+});
+
+for (const bad of ["", " ", "abc", "40000x", "-1", "0", String(Number.MAX_SAFE_INTEGER + 1)]) {
+ test(`invalid SESSION_CONTEXT_MAX_CHARS=${JSON.stringify(bad)} falls back to default`, () => {
+ assert.strictEqual(parseSessionContextMaxChars(bad), DEFAULT_SESSION_CONTEXT_MAX_CHARS);
+ });
+}
+
+test("valid SESSION_CONTEXT_MAX_CHARS is honored", () => {
+ assert.strictEqual(parseSessionContextMaxChars("25000"), 25000);
+ assert.strictEqual(parseSessionContextMaxChars(" 12000 "), 12000);
+});
+
console.log("\nsession-message route shape:");
const sessionMessageRoute = route("session-message");
@@ -110,7 +128,8 @@ test("x402-paid session-create cannot store a premium model", () => {
test("upstream calls send the trimmed window, not the raw stored history", () => {
assert.match(sessionMessageRoute, /trimSessionContext\(session\.messages, SESSION_CONTEXT_MAX_CHARS\)/);
- assert.match(sessionMessageRoute, /SESSION_CONTEXT_MAX_CHARS = parseInt\(process\.env\.SESSION_CONTEXT_MAX_CHARS \?\? "40000", 10\)/);
+ assert.match(sessionMessageRoute, /SESSION_CONTEXT_MAX_CHARS = parseSessionContextMaxChars\(process\.env\.SESSION_CONTEXT_MAX_CHARS\)/);
+ assert.ok(!/SESSION_CONTEXT_MAX_CHARS = parseInt/.test(sessionMessageRoute), "bare parseInt would let NaN disable trimming");
assert.match(sessionMessageRoute, /messages: upstreamMessages\.map/);
assert.match(sessionMessageRoute, /\.\.\.upstreamMessages\.map/);
assert.ok(!/messages: session\.messages\.map/.test(sessionMessageRoute), "raw history must not be sent to Anthropic");