fix: silence Mission 404 storm + Assistant 422 noise
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:
@@ -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
@@ -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;
|
||||
},
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user