perf(agent): cap recall + 2s deadline so LLM fallback always fires
build-and-publish / test (push) Has been cancelled
build-and-publish / image (push) Has been cancelled

Trace showed every Assistant submit triggered 60+ sequential
GET /api/ea2/flow/{key} requests via agentMemory.recall before
llmClient.chat ran. With a slow EA2 demo, recall took longer
than the test wait so the LLM fetch never started, the agent
reply never rendered, and [llm] telemetry never logged.

Two fixes:

1. agentMemory.listAll(cap) - default 200, recall uses 30.
   Edges are returned newest-first by EA2 so the 30-cap keeps the
   most recent vault entries and drops the long-tail history that
   was pinning the network.

2. routeAgentInput wraps recall in a 2s AbortController. If recall
   doesn't finish in 2s, hints[] stays empty and llmFallback runs
   immediately. Recall is best-effort context, never a blocker.
This commit is contained in:
canvas-bot
2026-06-15 21:42:58 +04:00
parent 3ebfaf81ae
commit 83a6b334c3
2 changed files with 10 additions and 4 deletions
+4 -3
View File
@@ -97,7 +97,7 @@ export const agentMemory = {
return mem._key; return mem._key;
}, },
async listAll(email: string, signal?: AbortSignal): Promise<Memory[]> { async listAll(email: string, signal?: AbortSignal, cap = 200): Promise<Memory[]> {
const vaultKey = vaultKeyFor(email); const vaultKey = vaultKeyFor(email);
const ok = await ensureVault(vaultKey, email, signal); const ok = await ensureVault(vaultKey, email, signal);
if (!ok) return []; if (!ok) return [];
@@ -107,7 +107,8 @@ export const agentMemory = {
const memKeys = (edges as any[]) const memKeys = (edges as any[])
.filter((e) => e?.role === "presentation") .filter((e) => e?.role === "presentation")
.map((e) => (e._to || "").split("/").pop()) .map((e) => (e._to || "").split("/").pop())
.filter(Boolean) as string[]; .filter(Boolean)
.slice(0, cap) as string[];
const memos = await Promise.all( const memos = await Promise.all(
memKeys.map(async (k) => { memKeys.map(async (k) => {
try { try {
@@ -132,7 +133,7 @@ export const agentMemory = {
* Score = count of unique stem matches. Ties broken by recency. * Score = count of unique stem matches. Ties broken by recency.
*/ */
async recall(email: string, query: string, limit = 5, signal?: AbortSignal): Promise<Memory[]> { async recall(email: string, query: string, limit = 5, signal?: AbortSignal): Promise<Memory[]> {
const all = await agentMemory.listAll(email, signal); const all = await agentMemory.listAll(email, signal, 30);
if (!query.trim() || all.length === 0) return all.slice(0, limit); if (!query.trim() || all.length === 0) return all.slice(0, limit);
const stems = new Set( const stems = new Set(
query.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2) query.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2)
+6 -1
View File
@@ -240,7 +240,12 @@ export async function routeAgentInput(text: string, ctx: ToolContext): Promise<T
} }
} }
let hints: Memory[] = []; let hints: Memory[] = [];
try { hints = await agentMemory.recall(ctx.userEmail, trimmed, 3); } catch { /* ignore */ } try {
const recallDeadline = new AbortController();
const recallTimer = setTimeout(() => recallDeadline.abort(), 2000);
hints = await agentMemory.recall(ctx.userEmail, trimmed, 3, recallDeadline.signal);
clearTimeout(recallTimer);
} catch { /* recall is best-effort; never blocks the user */ }
const llmReply = await llmFallback(trimmed, ctx, hints); const llmReply = await llmFallback(trimmed, ctx, hints);
if (llmReply) return llmReply; if (llmReply) return llmReply;
if (hints.length > 0) { if (hints.length > 0) {