From 923a64e735c11ec09edd97a560125332f60ed9f4 Mon Sep 17 00:00:00 2001 From: aaron Date: Sat, 22 Aug 2026 18:14:12 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20live-runs=20injection=20=E2=80=94=20summ?= =?UTF-8?q?arize=20backlog,=20flag=20zombies=20stale=20(#506)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runs block listed every run dir individually: a 120-line wall of finished entries burying the signal, and any run without FINISHED reported as 'solving' with no age check — an Aug-14 zombie was presented to the user as 'one run still solving'. - live runs (unfinished, < 12h): one line each, with age - older unfinished: STALE — no FINISHED since , 'probably dead, do not present as live' - finished: ONE backlog line (count · best F · latest) + pointer to the full history in the runs dir - version 0.2.4 -> 0.2.5 (shipped plugin content changes; keeps installed-vsix dir resolution unambiguous) Closes #506. --- .../extension/opencode-plugin/stack_state.ts | 68 ++++++++++++++--- packages/extension/package.json | 2 +- packages/extension/test/stack_state.test.ts | 74 ++++++++++++++++++- 3 files changed, 133 insertions(+), 11 deletions(-) diff --git a/packages/extension/opencode-plugin/stack_state.ts b/packages/extension/opencode-plugin/stack_state.ts index 777767d9..be86dc66 100644 --- a/packages/extension/opencode-plugin/stack_state.ts +++ b/packages/extension/opencode-plugin/stack_state.ts @@ -221,39 +221,89 @@ function buildActiveProblemBlock(): string { return desc; } -/** Compact live-runs block: one-line-per-run with status. */ +/** A run "solving" with no FINISHED marker this many hours after its + * timestamped id started is a ZOMBIE (crashed server, killed process) — + * report it as stale, never as live work. Run ids are rYYYYMMDD-HHMMSSZ-hash. */ +const SOLVING_STALE_HOURS = 12; + +/** Age in hours parsed from the run id's timestamp, else undefined. */ +function runAgeHours(runName: string): number | undefined { + const m = runName.match(/^r(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})Z-/); + if (!m) return undefined; + const t = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]); + if (Number.isNaN(t)) return undefined; + return (Date.now() - t) / 3_600_000; +} + +/** Compact live-runs block: LIVE runs individually (with age), zombies called + * out as stale, and the finished backlog as ONE summary line (count + best + * F + latest). The full history lives in the runs dir — 120 individually + * listed finished runs drowned exactly the signal the greeting needs. */ function buildLiveRunsBlock(): string { const root = runsRoot(); - const lines: string[] = []; try { if (!fs.existsSync(root)) return ""; + const live: string[] = []; + const stale: string[] = []; + const done: { name: string; lab: string; fidelity: number | undefined }[] = []; + const labs = fs.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()); for (const lab of labs) { const labDir = path.join(root, lab.name); const runs = fs.readdirSync(labDir, { withFileTypes: true }).filter((d) => d.isDirectory()); for (const run of runs) { const runDir = path.join(labDir, run.name); - const solved = !fs.existsSync(path.join(runDir, "FINISHED")); - let fidelity: string | undefined; + const finished = fs.existsSync(path.join(runDir, "FINISHED")); + let fidelity: number | undefined; + let fidelityStr: string | undefined; const resultPath = path.join(runDir, "result.toml"); if (fs.existsSync(resultPath)) { try { const content = fs.readFileSync(resultPath, "utf8"); const m = content.match(/fidelity\s*=\s*([\d.eE+-]+)/); - if (m) fidelity = parseFloat(m[1]).toFixed(6); + if (m) { + fidelity = parseFloat(m[1]); + fidelityStr = fidelity.toFixed(6); + } } catch { /* skip */ } } - const status = solved ? "solving" : fidelity ? `done (F=${fidelity})` : "done"; - lines.push(`- ${run.name} @ ${lab.name}: ${status}`); + if (finished) { + done.push({ name: run.name, lab: lab.name, fidelity }); + } else { + const ageH = runAgeHours(run.name); + if (ageH !== undefined && ageH > SOLVING_STALE_HOURS) { + const since = run.name.slice(1, 9).replace(/^(\d{4})(\d{2})(\d{2})$/, "$1-$2-$3"); + stale.push(`- ${run.name} @ ${lab.name}: STALE — no FINISHED since ${since} (probably dead, do not present as live)`); + } else { + const age = ageH !== undefined ? (ageH < 1 ? `${Math.max(1, Math.round(ageH * 60))}m` : `${Math.round(ageH)}h`) : "age unknown"; + live.push(`- ${run.name} @ ${lab.name}: solving (${age} old)`); + } + } + } + } + + const lines: string[] = []; + if (live.length > 0 || stale.length > 0 || done.length > 0) { + lines.push("**live runs**"); + lines.push(...live, ...stale); + if (done.length > 0) { + const best = done.reduce((acc, d) => (d.fidelity !== undefined && (acc === undefined || d.fidelity > acc) ? d.fidelity : acc), undefined); + const withF = done.filter((d) => d.fidelity !== undefined); + const latest = done[done.length - 1]; + const bits = [ + `${done.length} finished`, + best !== undefined ? `best F=${best.toFixed(6)}` : null, + withF.length > 0 ? `latest ${latest.name}${latest.fidelity !== undefined ? ` (F=${latest.fidelity.toFixed(6)})` : ""}` : null, + ].filter(Boolean); + lines.push(`- backlog: ${bits.join(" · ")} — full history in the runs dir`); } } + return lines.length > 0 ? lines.join("\n") : ""; } catch { return ""; // optional — silent on error } - - return lines.length > 0 ? "**live runs**\n" + lines.join("\n") : ""; } // ── Fleet state ────────────────────────────────────────────────────────────── diff --git a/packages/extension/package.json b/packages/extension/package.json index 4c1c7218..23f3ac80 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -2,7 +2,7 @@ "name": "amicode", "displayName": "Amicode", "description": "Open autonomous research in VS Code \u2014 vaults, fleet, and live solves for quantum control and physical intelligence.", - "version": "0.2.4", + "version": "0.2.5", "publisher": "harmoniqs", "license": "Apache-2.0", "icon": "media/icon.png", diff --git a/packages/extension/test/stack_state.test.ts b/packages/extension/test/stack_state.test.ts index 4b659753..42dc4258 100644 --- a/packages/extension/test/stack_state.test.ts +++ b/packages/extension/test/stack_state.test.ts @@ -300,12 +300,84 @@ describe("caps + composition", () => { }); }); +// ── Live-runs block: live/stale/summary ────────────────────────────────────── + +describe("buildLiveRunsBlock (live individually, zombies flagged, backlog summarized)", () => { + function mkRun(lab: string, name: string, opts: { finished?: boolean; fidelity?: number } = {}): void { + const dir = path.join(lab, name); + fs.mkdirSync(dir, { recursive: true }); + if (opts.finished) fs.writeFileSync(path.join(dir, "FINISHED"), ""); + if (opts.fidelity !== undefined) { + fs.writeFileSync(path.join(dir, "result.toml"), `fidelity = ${opts.fidelity}\n`); + } + } + function runsBlockWith(runs: (labDir: string) => void): string { + const root = mkTmp("runs-"); + const lab = path.join(root, "default"); + fs.mkdirSync(lab, { recursive: true }); + runs(lab); + const stubs = stubAllSeams({ runsDir: root }); + try { + const block = buildStackStateBlock() ?? ""; + const m = block.match(/\*\*live runs\*\*[\s\S]*?(?=\n\n|$)/); + return m ? m[0] : ""; + } finally { + restoreSeams(stubs); + } + } + const now = new Date(); + const stamp = (hoursAgo: number): string => { + const t = new Date(now.getTime() - hoursAgo * 3_600_000); + const p = (n: number, w = 2) => String(n).padStart(w, "0"); + return `r${t.getUTCFullYear()}${p(t.getUTCMonth() + 1)}${p(t.getUTCDate())}-${p(t.getUTCHours())}${p(t.getUTCMinutes())}${p(t.getUTCSeconds())}Z-x`; + }; + + it("a fresh unfinished run is LIVE with its age; a days-old one is STALE, never 'solving'", () => { + const s = runsBlockWith((lab) => { + mkRun(lab, stamp(0.2)); // 12 min ago + mkRun(lab, stamp(24 * 8)); // 8 days ago — zombie + }); + expect(s).toContain("solving ("); + expect(s).toMatch(/STALE — no FINISHED since \d{4}-\d{2}-\d{2}/); + expect(s).toContain("do not present as live"); + // the zombie line must NOT read as solving + const zombieLine = s.split("\n").find((l) => l.includes("STALE")); + expect(zombieLine).not.toContain("solving"); + }); + it("finished runs collapse to ONE backlog line: count, best F, latest", () => { + const s = runsBlockWith((lab) => { + mkRun(lab, stamp(30), { finished: true, fidelity: 0.999 }); + mkRun(lab, stamp(20), { finished: true, fidelity: 0.999979 }); + mkRun(lab, stamp(10), { finished: true, fidelity: 0.99 }); + mkRun(lab, stamp(5), { finished: true }); // finished, no result.toml + }); + const backlog = s.split("\n").filter((l) => l.startsWith("- backlog:")); + expect(backlog.length).toBe(1); + expect(backlog[0]).toContain("4 finished"); + expect(backlog[0]).toContain("best F=0.999979"); + expect(backlog[0]).toContain("full history in the runs dir"); + // no individually listed done runs + expect(s.split("\n").filter((l) => l.startsWith("- ") && /: done/.test(l)).length).toBe(0); + }); + it("no runs at all → no live-runs section", () => { + const root = mkTmp("runs-"); + fs.mkdirSync(path.join(root, "default"), { recursive: true }); + const stubs = stubAllSeams({ runsDir: root }); + try { + expect(buildStackStateBlock() ?? "").not.toContain("**live runs**"); + } finally { + restoreSeams(stubs); + } + }); +}); + // ── Env-seam plumbing ──────────────────────────────────────────────────────── interface SeamOpts { vaultsRoot?: string; fleetConfig?: string; fleetStatus?: string; + runsDir?: string; /** Prebuilt fixture vault flavor for the golden-text cases. */ vault?: "profile" | "knowledge" | "demos" | "memory"; } @@ -364,7 +436,7 @@ function stubAllSeams(opts: SeamOpts): Record { process.env.AMICODE_OPS_DIR = ops; // no solver-mode.json → piccolo/ready → no section process.env.AMICODE_CONNECTIONS_FILE = path.join(conn, "absent.json"); // not connected process.env.AMICODE_PROBLEMS_DIR = problems; // no active problem - process.env.AMICODE_RUNS_DIR = runs; // no runs + process.env.AMICODE_RUNS_DIR = opts.runsDir ?? runs; // no runs unless a fixture is passed return saved; }