From b0127c24eb1964078f20baeabd34ac2cde3092fc Mon Sep 17 00:00:00 2001 From: canvas-bot Date: Tue, 16 Jun 2026 21:13:35 +0400 Subject: [PATCH] fix: silence Mission 404 storm + Assistant 422 noise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three sources of red errors in scene walk: 1. Mission/Approvals: GET /api/runtime/transactions/{key} 404 storm. The runtime backend's single-transaction endpoint is broken (returns 500 'transaction_read_failed: All connection attempts failed' even on valid IDs from the list endpoint, and 404 for work-item IDs that don't exist in the runtime DB at all). Fix in buildScenarios.ts: keep the headlineTx fetch (still tries to enrich) but synthesize a RuntimeTransaction shape from work-item data via workItemToRt() when the API call returns null. Skip the 3x 'recent' GETs entirely — they were always 404ing. Fix in store.ts pollLiveTick: .catch(() => null) on the headline transaction call so polling never surfaces backend brokenness. 2. Assistant: POST /api/ea2/apply-batch 422/404 when remembering. Vault edge attach can race with vault creation OR target a doc that hasn't propagated. Wrap in try/catch with console.warn so the failure is visible in logs but doesn't fire as a red console error on every turn. --- qa/_full_dogfood.mjs | 96 +++++++++++++++++++++++++++++++++++++++ src/lib/agentMemory.ts | 29 +++++++----- src/lib/buildScenarios.ts | 25 ++++++++-- src/state/store.ts | 5 +- 4 files changed, 138 insertions(+), 17 deletions(-) create mode 100644 qa/_full_dogfood.mjs diff --git a/qa/_full_dogfood.mjs b/qa/_full_dogfood.mjs new file mode 100644 index 0000000..0987610 --- /dev/null +++ b/qa/_full_dogfood.mjs @@ -0,0 +1,96 @@ +// Walk every scene, capture all console + network errors, list which scene each error belongs to. +import { chromium } from "playwright"; + +const URL = "https://canvas.flow-master.ai/"; +const b = await chromium.launch({ headless: true }); +const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } }); +const p = await ctx.newPage(); + +const events = []; +let currentScene = "boot"; +p.on("console", m => { + if (m.type() === "error") events.push({ scene: currentScene, type: "console.error", text: m.text().slice(0, 250) }); + if (m.type() === "warning") events.push({ scene: currentScene, type: "console.warn", text: m.text().slice(0, 250) }); +}); +p.on("pageerror", e => events.push({ scene: currentScene, type: "pageerror", text: e.message.slice(0, 250) })); +p.on("response", r => { + const u = r.url(); + if (u.includes("/api/") && r.status() >= 400) { + events.push({ scene: currentScene, type: `http.${r.status()}`, text: `${r.request().method()} ${u.replace("https://canvas.flow-master.ai","")}` }); + } +}); + +await ctx.addInitScript(() => localStorage.setItem("fm.canvas.tour.seen", String(Date.now()))); + +currentScene = "landing"; +await p.goto(URL, { waitUntil: "domcontentloaded" }); +await p.waitForTimeout(2500); +await p.locator(".persona-chip").filter({ hasText: /Mariana/ }).click(); +await p.waitForSelector(".topbar", { timeout: 30000 }); +await p.waitForTimeout(2500); + +const scenes = ["Mission", "Approvals", "Runs", "Studio", "Chat", "Assistant"]; +for (const scene of scenes) { + currentScene = scene; + console.log(`\n=== ${scene} ===`); + try { + await p.locator(".tab").filter({ hasText: new RegExp(scene, "i") }).first().click(); + await p.waitForTimeout(3500); + // for Studio, try clicking Confirm step 2 + if (scene === "Studio") { + const ta = p.locator("textarea").first(); + if (await ta.count() > 0) { + await ta.fill("dogfood scene-walk test"); + const draftBtn = p.locator("button.btn-primary").filter({ hasText: /draft/i }).first(); + if (await draftBtn.count() > 0) { + await draftBtn.click(); + await p.waitForTimeout(5000); + } + const confirmBtn = p.locator("button.btn-primary").filter({ hasText: /confirm/i }).first(); + if (await confirmBtn.count() > 0) { + await confirmBtn.click(); + await p.waitForTimeout(6000); + } + } + } + if (scene === "Assistant") { + const txt = p.locator(".agent-composer textarea").first(); + if (await txt.count() > 0) { + await txt.fill("list processes"); + await txt.press("Enter"); + await p.waitForTimeout(6000); + } + } + if (scene === "Chat") { + // try to click first thread + const thread = p.locator(".thread-row, .chat-thread, .chat-list-item").first(); + if (await thread.count() > 0) { + await thread.click(); + await p.waitForTimeout(3000); + } + } + } catch (e) { + events.push({ scene, type: "test.error", text: e.message.slice(0, 200) }); + } +} + +await b.close(); + +// Group by scene +const grouped = {}; +for (const ev of events) { + grouped[ev.scene] = grouped[ev.scene] || []; + grouped[ev.scene].push(ev); +} +for (const scene of Object.keys(grouped)) { + console.log(`\n--- ${scene}: ${grouped[scene].length} events ---`); + // dedup + const seen = new Set(); + for (const ev of grouped[scene]) { + const key = `${ev.type}::${ev.text}`; + if (seen.has(key)) continue; + seen.add(key); + console.log(` [${ev.type}] ${ev.text}`); + } +} +console.log(`\nTOTAL: ${events.length} events across ${Object.keys(grouped).length} scenes`); diff --git a/src/lib/agentMemory.ts b/src/lib/agentMemory.ts index b8d18d5..5f20160 100644 --- a/src/lib/agentMemory.ts +++ b/src/lib/agentMemory.ts @@ -83,17 +83,24 @@ export const agentMemory = { }); if (!doc.ok) return null; const mem = await doc.json(); - await fetch(`${api.config.baseUrl}/api/ea2/apply-batch`, { - method: "POST", - headers: authHeaders(), - body: JSON.stringify({ - request_id: newRequestId(), - ops: [ - { op: "create_edge", edge_coll: "defines", from: `flow/${vaultKey}`, to: `flow/${mem._key}`, role: "presentation" }, - ], - }), - signal, - }); + try { + const edgeRes = await fetch(`${api.config.baseUrl}/api/ea2/apply-batch`, { + method: "POST", + headers: authHeaders(), + body: JSON.stringify({ + request_id: newRequestId(), + ops: [ + { op: "create_edge", edge_coll: "defines", from: `flow/${vaultKey}`, to: `flow/${mem._key}`, role: "presentation" }, + ], + }), + signal, + }); + if (!edgeRes.ok) { + console.warn(`[memory] vault edge attach returned ${edgeRes.status} for ${mem._key} (recall may miss this entry)`); + } + } catch (e) { + console.warn(`[memory] vault edge attach threw: ${(e as Error).message}`); + } return mem._key; }, diff --git a/src/lib/buildScenarios.ts b/src/lib/buildScenarios.ts index cf91c5c..40e4400 100644 --- a/src/lib/buildScenarios.ts +++ b/src/lib/buildScenarios.ts @@ -191,6 +191,20 @@ function buildScenarioFromGraph( }; } +function workItemToRt(w: WorkItem | undefined): RuntimeTransaction | null { + if (!w?.transaction_id) return null; + return { + transaction_id: w.transaction_id, + status: w.status, + active_step: w.active_step_display_name + ? { display_name: w.active_step_display_name } + : undefined, + available_actions: [], + created_at: w.created_at, + business_subject: w.business_subject, + }; +} + function avgCycle(cases: WorkItem[]): string { const ages = cases.map((c) => c.age_days ?? 0).filter((a) => a > 0); if (!ages.length) return "—"; @@ -258,11 +272,12 @@ export async function buildLiveScenariosFromApi(signal?: AbortSignal): Promise<{ const recentCandidates = c.cases .filter((w) => w.transaction_id && w.transaction_id !== headlineCase?.transaction_id) .slice(0, 3); - const [headlineRt, ...recentResults] = await Promise.all([ - headlineCase?.transaction_id ? api.transaction(headlineCase.transaction_id, signal) : Promise.resolve(null), - ...recentCandidates.map((w) => api.transaction(w.transaction_id, signal)), - ]); - const recent = recentResults.filter((r): r is RuntimeTransaction => r != null); + const headlineRt = headlineCase?.transaction_id + ? (await api.transaction(headlineCase.transaction_id, signal)) ?? workItemToRt(headlineCase) + : null; + const recent: RuntimeTransaction[] = recentCandidates + .map((w) => workItemToRt(w)) + .filter((r): r is RuntimeTransaction => r != null); return { bucket: c, graph, headlineRt, recent }; }), ); diff --git a/src/state/store.ts b/src/state/store.ts index a25ff06..e56c9cc 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -313,7 +313,10 @@ export const useApp = create((set, get) => { const sc = s.scenarios.find((x) => x.id === s.scenarioId); let headlineRt = null; if (sc?.headlineTx) { - headlineRt = await api.transaction(sc.headlineTx); + // Best-effort: the per-transaction endpoint can 500/404 when the + // runtime DB is unreachable. Don't surface that as a polling error — + // existing headlineRt in scenario.raw is still valid for display. + headlineRt = await api.transaction(sc.headlineTx).catch(() => null); } const updatedScenarios = s.scenarios.map((scen) => { if (!scen.live) return scen;