fix: silence Mission 404 storm + Assistant 422 noise
build-and-publish / test (push) Has been cancelled
build-and-publish / image (push) Has been cancelled

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.
This commit is contained in:
canvas-bot
2026-06-16 21:13:35 +04:00
parent 76ce4c23b2
commit b0127c24eb
4 changed files with 138 additions and 17 deletions
+96
View File
@@ -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`);
+18 -11
View File
@@ -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;
},
+20 -5
View File
@@ -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 };
}),
);
+4 -1
View File
@@ -313,7 +313,10 @@ export const useApp = create<AppState>((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;