From 901625e2423453e82ead89baab4dc1bf1914a8ed Mon Sep 17 00:00:00 2001 From: graphlemon Date: Thu, 6 Aug 2026 01:29:42 +0800 Subject: [PATCH] graphlemon: code_diff artifact for task 3605272e-fe33-48cc-b3f3-e0c0ba26f970 --- .graphlemon/runs/3605272e-fe33-48cc-b3f3-e0c0ba26f970/code_diff.patch | 513 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 513 insertions(+), 0 deletions(-) create mode 100644 .graphlemon/runs/3605272e-fe33-48cc-b3f3-e0c0ba26f970/code_diff.patch diff --git a/.graphlemon/runs/3605272e-fe33-48cc-b3f3-e0c0ba26f970/code_diff.patch b/.graphlemon/runs/3605272e-fe33-48cc-b3f3-e0c0ba26f970/code_diff.patch new file mode 100644 index 0000000..06ee7da --- /dev/null +++ b/.graphlemon/runs/3605272e-fe33-48cc-b3f3-e0c0ba26f970/code_diff.patch @@ -0,0 +1,513 @@ +diff --git a/frontend/app.js b/frontend/app.js +index 2f17236..da50ca9 100644 +--- a/frontend/app.js ++++ b/frontend/app.js +@@ -104,6 +104,11 @@ const ARTIFACT_DETAIL_RE = /^#\/artifacts\/([^/]+)$/; + // reaching renderTaskDetail/mountTaskDetail with a bogus org_node that would + // otherwise need its own defensive checks further down. + const ORG_NODE_DETAIL_RE = /^#\/tasks\/([^/]+)\/org\/(planner|coder|validator)$/; ++// v_run_detail (U-046, 2026-08-06 SDD): "运行历史" (`#gc_runs_list`) ++// previously had nowhere real to send a click -- it just called ++// onSwitchToTask to switch v_graph_console's own task selector. This is the ++// actual run-detail page/route that closes that gap. ++const RUN_DETAIL_RE = /^#\/runs\/([^/]+)$/; + + let currentRouteCleanup = null; + function registerRouteCleanup(fn) { +@@ -124,6 +129,7 @@ function render() { + const orgNodeMatch = hash.match(ORG_NODE_DETAIL_RE); + const skillMatch = hash.match(SKILL_AUDIT_RE); + const artifactMatch = hash.match(ARTIFACT_DETAIL_RE); ++ const runMatch = hash.match(RUN_DETAIL_RE); + if (orgNodeMatch) { + // Reuses v_task_detail itself (same render/mount) with the org_node + // carried through as a second arg -- no separate view/route handler. +@@ -140,6 +146,9 @@ function render() { + // with the artifact_id pre-selected -- no separate detail view. + route = { render: renderArtifactBrowser, mount: mountArtifactBrowser }; + routeArgs = [artifactMatch[1]]; ++ } else if (runMatch) { ++ route = { render: renderRunDetail, mount: mountRunDetail }; ++ routeArgs = [runMatch[1]]; + } + } + if (!route) route = { render: () => "

Not found

" + hash + "

" }; +@@ -1468,6 +1477,122 @@ function mountTaskDetail(taskId, initialOrgNode) { + }); + } + ++// ---- v_run_detail (`#/runs/:run_id`, U-046 2026-08-06 SDD): single Work ++// Graph Run detail -- every work_node_execution the run contains, each with ++// its own log (live c_ws_log_tail while running, static c_execution_log_get ++// once terminal). Reuses renderGraphVisualizerPanel/mountLogViewer ++// unmodified, same as v_task_detail's todo tree above; the one DOM-id ++// contract mountLogViewer relies on (`#todo-detail- ++// .log-autoscroll`) is reproduced here via renderRunNodeListHtml so the ++// existing auto-scroll checkbox keeps working without touching ++// mountLogViewer itself. ++ ++function renderRunNodeListHtml(run) { ++ if (!run.nodes.length) return '
该运行尚无节点
'; ++ const items = run.nodes.map((wn) => { ++ const status = wn.status; ++ return ( ++ '
  • ' ++ + '
    ' ++ + '' ++ + '' + escapeHtml(wn.node_id) + "" ++ + '' + escapeHtml(status) + " · " + escapeHtml(wn.subagent_type) ++ + " · retry_count=" + wn.retry_count + " · node_exec_id=" + wn.node_exec_id + "" ++ + "
    " ++ + '
    ' ++ + (wn.reflection_notes ? "reflection_notes: " + escapeHtml(wn.reflection_notes) + "
    " : "") ++ + '
    ' ++ + "
    " ++ + "
  • " ++ ); ++ }).join(""); ++ return '"; ++} ++ ++function renderRunDetail(runId) { ++ if (!getCurrentProjectId()) return renderNoProjectSelected(); ++ return ` ++
    ++ ++
    加载中…
    ++
    ++ `; ++} ++ ++function mountRunDetail(runId) { ++ const projectId = getCurrentProjectId(); ++ if (!projectId) return; ++ const content = document.getElementById("rd_content"); ++ ++ // Same "track open log viewers, close them on re-render/route-leave" ++ // pattern as mountTaskDetail/mountGraphConsole above. ++ const activeLogViewers = new Map(); ++ function closeAllLogViewers() { ++ for (const close of activeLogViewers.values()) close(); ++ activeLogViewers.clear(); ++ } ++ ++ // The event socket needs task_id, which we only learn from the run's own ++ // response -- opened lazily on the first successful render, not before. ++ let socket = null; ++ ++ function renderDetail(run) { ++ closeAllLogViewers(); ++ content.innerHTML = ( ++ "

    " + escapeHtml(run.org_node) + " Work Graph Run

    " ++ + '
    ' ++ + "
    RUN_ID" + escapeHtml(run.run_id) + "
    " ++ + "
    STATUS" + escapeHtml(run.status) + "
    " ++ + "
    STARTED_AT" + escapeHtml(run.started_at || "") + "
    " ++ + "
    COMPLETED_AT" + escapeHtml(run.completed_at || "") + "
    " ++ + '
    TASK' + escapeHtml(run.task_id) + "
    " ++ + "
    " ++ + renderGraphVisualizerPanel(run) ++ + '
    ' ++ + '

    节点列表

    ' + renderRunNodeListHtml(run) + "
    " ++ ); ++ ++ for (const row of content.querySelectorAll(".todo-row[data-node-exec-id]")) { ++ row.addEventListener("click", () => { ++ const nodeExecId = row.dataset.nodeExecId; ++ if (!nodeExecId) return; ++ const detail = document.getElementById("todo-detail-" + nodeExecId); ++ if (!detail) return; ++ detail.classList.toggle("open"); ++ if (detail.classList.contains("open") && !activeLogViewers.has(nodeExecId)) { ++ const slot = document.getElementById("log-slot-" + nodeExecId); ++ slot.innerHTML = renderLogViewerHtml(nodeExecId); ++ activeLogViewers.set(nodeExecId, mountLogViewer(nodeExecId, detail.dataset.status)); ++ } ++ }); ++ } ++ mountGraphVisualizerTooltips(content); ++ ++ if (!socket) { ++ socket = connectEventSocket(projectId, run.task_id, { ++ onEvent: () => refresh(), ++ onStatusChange: () => {}, ++ }); ++ } ++ } ++ ++ async function refresh() { ++ const resp = await fetch("/api/work-graph-runs/" + encodeURIComponent(runId)); ++ if (resp.status === 404) { ++ content.innerHTML = '
    运行不存在
    '; ++ return; ++ } ++ renderDetail(await resp.json()); ++ } ++ ++ refresh(); ++ ++ registerRouteCleanup(() => { ++ if (socket) socket.close(); ++ closeAllLogViewers(); ++ }); ++} ++ + // ---- v_graph_console (`#/graph`, 2026-08-03 SDD): Org/Work Graph + // visualization + Planner output overview. FR-1: independent Org Graph + // topology (not `org-strip`, see renderOrgGraphTopology below). FR-2/3: +@@ -1525,9 +1650,11 @@ function renderArtifactSummaryList(artifacts, types, emptyText) { + // #/artifacts / #/tasks with no filter pre-applied -- v_artifact_browser + // and v_task_board don't parse URL query params today, and adding that is + // out of scope for this page (SDD §2.5's explicit, recorded trade-off). +-// Graph run rows are the one exception: clicking a row switches this same +-// page's own task switcher, since there's no separate "run detail" route. +-async function mountArtifactsOverviewPanel(container, projectId, { onSwitchToTask }) { ++// Graph run rows: U-046 fixed the previous shortcut (switching this same ++// page's own task switcher via onSwitchToTask, because there was no ++// separate "run detail" route) to navigate to the real v_run_detail page ++// (#/runs/:run_id) instead. ++async function mountArtifactsOverviewPanel(container, projectId) { + container.innerHTML = '

    Planner 产出总览

    加载中…
    '; + const [artifacts, runs, tasks] = await Promise.all([ + fetch("/api/artifacts?project_id=" + encodeURIComponent(projectId)).then((r) => r.json()), +@@ -1540,7 +1667,7 @@ async function mountArtifactsOverviewPanel(container, projectId, { onSwitchToTas + + const runsHtml = runs.length === 0 ? '
    暂无 Graph 运行记录
    ' : ( + '" + ); +@@ -1564,8 +1691,10 @@ async function mountArtifactsOverviewPanel(container, projectId, { onSwitchToTas + + const runsListEl = document.getElementById("gc_runs_list"); + if (runsListEl) { +- for (const li of runsListEl.querySelectorAll("li[data-run-task-id]")) { +- li.addEventListener("click", () => onSwitchToTask(li.dataset.runTaskId)); ++ for (const li of runsListEl.querySelectorAll("li[data-run-id]")) { ++ li.addEventListener("click", () => { ++ window.location.hash = "#/runs/" + encodeURIComponent(li.dataset.runId); ++ }); + } + } + +@@ -1639,12 +1768,7 @@ function mountGraphConsole() { + } + + function refreshArtifactsOverview() { +- mountArtifactsOverviewPanel(artifactsOverviewEl, projectId, { +- onSwitchToTask: (taskId) => { +- taskSelect.value = taskId; +- loadTaskAndRender(taskId); +- }, +- }); ++ mountArtifactsOverviewPanel(artifactsOverviewEl, projectId); + } + + async function loadTaskAndRender(taskId) { +diff --git a/frontend/test_run_detail_smoke.js b/frontend/test_run_detail_smoke.js +new file mode 100644 +index 0000000..58befa9 +--- /dev/null ++++ b/frontend/test_run_detail_smoke.js +@@ -0,0 +1,117 @@ ++// v_run_detail (`#/runs/:run_id`, U-046 2026-08-06 SDD) smoke test. Same ++// no-framework/no-fixture convention as frontend/test_graph_console_smoke.js ++// and frontend/test_org_click_through_smoke.js (see ++// .claude/agent-memory/validator/feedback_no_js_test_framework.md) -- loads ++// app.js in a bare vm.Context and asserts on generated HTML strings / regex ++// matches for the new top-level pieces: ++// - RUN_DETAIL_RE: the new #/runs/:run_id route. ++// - renderRunNodeListHtml: the run-detail node list, including the ++// todo-detail- DOM-id contract mountLogViewer relies on ++// (`#todo-detail- .log-autoscroll`) and the empty-run case. ++// - mountArtifactsOverviewPanel's now-fixed click target: `#gc_runs_list` ++// rows carry data-run-id (not the old data-run-task-id), so a real ++// click can build "#/runs/" + run_id instead of only ever switching ++// the task selector. ++// ++// Run: node frontend/test_run_detail_smoke.js ++ ++"use strict"; ++const fs = require("fs"); ++const path = require("path"); ++const vm = require("vm"); ++const assert = require("assert"); ++ ++function makeFakeDiv() { ++ let text = ""; ++ return { ++ set textContent(v) { text = v == null ? "" : String(v); }, ++ get textContent() { return text; }, ++ get innerHTML() { ++ return text.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); ++ }, ++ }; ++} ++ ++const sandbox = { ++ document: { ++ createElement: () => makeFakeDiv(), ++ getElementById: () => null, ++ querySelectorAll: () => [], ++ addEventListener: () => {}, ++ }, ++ window: { ++ location: { protocol: "http:", host: "localhost", hash: "" }, ++ addEventListener: () => {}, ++ }, ++ localStorage: { getItem: () => null, setItem: () => {} }, ++ console, ++}; ++vm.createContext(sandbox); ++ ++const src = fs.readFileSync(path.join(__dirname, "app.js"), "utf8"); ++// Top-level `const` regexes stay lexically scoped to the script and never ++// reach the sandbox object (same Annex B quirk the other smoke tests ++// already document) -- re-export what this test needs. ++const augmentedSrc = src + "\nglobalThis.RUN_DETAIL_RE = RUN_DETAIL_RE;\n"; ++new vm.Script(augmentedSrc, { filename: "app.js" }).runInContext(sandbox); ++ ++let assertionCount = 0; ++function check(condition, message) { ++ assertionCount++; ++ assert.ok(condition, message); ++} ++ ++// ---- 1. RUN_DETAIL_RE: happy / boundary / failure ---- ++{ ++ const legal = "#/runs/run-abc-123".match(sandbox.RUN_DETAIL_RE); ++ check(legal, "legal run_id must match"); ++ check(legal[1] === "run-abc-123", "captured group must be the run_id"); ++ ++ check(!"#/runs/".match(sandbox.RUN_DETAIL_RE), "missing run_id segment must not match"); ++ check(!"#/runs/abc/extra".match(sandbox.RUN_DETAIL_RE), "extra path segments must not match"); ++ check(!"#/tasks/abc".match(sandbox.RUN_DETAIL_RE), "an unrelated route must not match"); ++} ++ ++// ---- 2. renderRunNodeListHtml: node list + todo-detail- DOM contract ---- ++{ ++ const run = { ++ run_id: "run-1", org_node: "coder", status: "running", ++ nodes: [ ++ { ++ node_exec_id: "exec-1", node_id: "n1", subagent_type: "coder", status: "running", ++ retry_count: 0, reflection_notes: null, ++ }, ++ { ++ node_exec_id: "exec-2", node_id: "n2", subagent_type: "coder", status: "done", ++ retry_count: 1, reflection_notes: "retried once after a transient error", ++ }, ++ ], ++ }; ++ const html = sandbox.renderRunNodeListHtml(run); ++ check(html.includes('data-node-exec-id="exec-1"'), "each row must carry its node_exec_id for the click handler"); ++ check(html.includes('id="todo-detail-exec-1"'), "must reproduce the todo-detail- id mountLogViewer's querySelector needs"); ++ check(html.includes('data-status="running"'), "todo-detail wrapper must carry the node's live status"); ++ check(html.includes('id="log-slot-exec-2"'), "must reserve a log-viewer-slot per node, same as v_task_detail's todo tree"); ++ check(html.includes("retried once after a transient error"), "reflection_notes must be shown when present"); ++ check(!html.includes("reflection_notes:
    "), "no stray reflection_notes line when it's null (exec-1)"); ++ ++ const emptyHtml = sandbox.renderRunNodeListHtml({ nodes: [] }); ++ check(emptyHtml.includes("该运行尚无节点"), "a run with zero nodes must render an explicit empty state, not blank/undefined"); ++} ++ ++// ---- 3. gc_runs_list rows: run-detail click target (2026-08-06 fix) ---- ++{ ++ // mountArtifactsOverviewPanel itself does an async fetch and can't run in ++ // this sandbox (no `fetch`), but the row-id contract it depends on is a ++ // pure string-building concern worth pinning directly: replicate the ++ // exact
  • markup mountArtifactsOverviewPanel emits for one run and ++ // confirm it carries data-run-id (not the removed data-run-task-id). ++ const escapeHtml = sandbox.escapeHtml; ++ const r = { org_node: "coder", run_id: "run-1234567890", status: "done", node_count: 3 }; ++ const li = '
  • ' + escapeHtml(r.org_node) + " · run " ++ + r.run_id.slice(0, 8) + "" + escapeHtml(r.status) + " · " + r.node_count + " 节点
  • "; ++ check(li.includes('data-run-id="run-1234567890"'), "run row must carry the real run_id for #/runs/:run_id navigation"); ++ check(!li.includes("data-run-task-id"), "the old task-switcher click target must be gone"); ++} ++ ++console.log("frontend/test_run_detail_smoke.js: all " + assertionCount + " assertions passed"); +diff --git a/src/backend_server.py b/src/backend_server.py +index 470de0d..07b9551 100644 +--- a/src/backend_server.py ++++ b/src/backend_server.py +@@ -130,6 +130,7 @@ _LOG_SUFFIX = "/log" + _ARTIFACTS_PREFIX = "/api/artifacts/" + _SKILLS_PREFIX = "/api/skills/" + _DEPLOYMENTS_PREFIX = "/api/deployments/" ++_WORK_GRAPH_RUNS_PREFIX = "/api/work-graph-runs/" + + + class _Handler(BaseHTTPRequestHandler): +@@ -180,6 +181,11 @@ class _Handler(BaseHTTPRequestHandler): + if parsed.path == "/api/work-graph-runs": + self._handle_work_graph_run_list(parsed.query) + return ++ if parsed.path.startswith(_WORK_GRAPH_RUNS_PREFIX): ++ run_id = parsed.path[len(_WORK_GRAPH_RUNS_PREFIX):] ++ if run_id and "/" not in run_id: ++ self._handle_work_graph_run_get(run_id) ++ return + if parsed.path.startswith(_ARTIFACTS_PREFIX): + artifact_id = parsed.path[len(_ARTIFACTS_PREFIX):] + if artifact_id and "/" not in artifact_id: +@@ -666,6 +672,20 @@ class _Handler(BaseHTTPRequestHandler): + else: + self._send_json(200, result) + ++ def _handle_work_graph_run_get(self, run_id): ++ """c_work_graph_run_get (U-046, GET /api/work-graph-runs/:run_id): ++ v_run_detail's single-run lookup -- the list endpoint above has no ++ per-run detail shape, and v_run_detail needs the full node list ++ (same shape as task_query.get_task's embedded work_graph_runs[i]) ++ to reuse renderGraphVisualizerPanel/mountLogViewer unmodified.""" ++ try: ++ with self.server.conn_lock: ++ result = work_graph_run_query.get_work_graph_run(self.server.conn, run_id) ++ except work_graph_run_query.WorkGraphRunNotFoundError: ++ self._send_json(404, {"error": "NOT_FOUND"}) ++ else: ++ self._send_json(200, result) ++ + def _handle_artifact_get(self, artifact_id): + try: + with self.server.conn_lock: +diff --git a/src/test_backend_server.py b/src/test_backend_server.py +index 5dc6071..8f58afe 100644 +--- a/src/test_backend_server.py ++++ b/src/test_backend_server.py +@@ -1354,6 +1354,19 @@ class WorkGraphRunListRouteTest(unittest.TestCase): + self.assertEqual(status, 200) + self.assertEqual(body, []) + ++ def test_get_returns_run_with_nodes(self): ++ status, body = self._get("/api/work-graph-runs/run-1") ++ self.assertEqual(status, 200) ++ self.assertEqual(body["run_id"], "run-1") ++ self.assertEqual(body["task_id"], "task-1") ++ self.assertEqual(body["org_node"], "coder") ++ self.assertEqual(body["nodes"], []) ++ ++ def test_get_nonexistent_run_returns_404(self): ++ status, body = self._get("/api/work-graph-runs/no-such-run") ++ self.assertEqual(status, 404) ++ self.assertEqual(body["error"], "NOT_FOUND") ++ + + class DeploymentRouteTest(unittest.TestCase): + """U-043: c_deployment_list/c_deployment_current/c_deployment_get for +diff --git a/src/test_work_graph_run_query.py b/src/test_work_graph_run_query.py +index 152e2c8..cc32498 100644 +--- a/src/test_work_graph_run_query.py ++++ b/src/test_work_graph_run_query.py +@@ -118,5 +118,33 @@ class ListWorkGraphRunsTest(WorkGraphRunQueryTestBase): + self.assertEqual(result, []) + + ++class GetWorkGraphRunTest(WorkGraphRunQueryTestBase): ++ """U-046: c_work_graph_run_get -- v_run_detail's single-run lookup.""" ++ ++ def test_returns_run_with_embedded_nodes(self): ++ self._insert_run("run-1", "coder", status="running", node_count=2) ++ result = work_graph_run_query.get_work_graph_run(self.conn, "run-1") ++ self.assertEqual(result["run_id"], "run-1") ++ self.assertEqual(result["task_id"], "task-1") ++ self.assertEqual(result["org_node"], "coder") ++ self.assertEqual(result["status"], "running") ++ self.assertEqual(result["graph_manifest"], {}) ++ self.assertEqual(len(result["nodes"]), 2) ++ self.assertEqual( ++ {n["node_exec_id"] for n in result["nodes"]}, ++ {"run-1-node-0", "run-1-node-1"}, ++ ) ++ self.assertEqual(result["nodes"][0]["status"], "done") ++ ++ def test_nonexistent_run_raises(self): ++ with self.assertRaises(work_graph_run_query.WorkGraphRunNotFoundError): ++ work_graph_run_query.get_work_graph_run(self.conn, "no-such-run") ++ ++ def test_run_with_no_nodes_returns_empty_list(self): ++ self._insert_run("run-1", "planner", node_count=0) ++ result = work_graph_run_query.get_work_graph_run(self.conn, "run-1") ++ self.assertEqual(result["nodes"], []) ++ ++ + if __name__ == "__main__": + unittest.main() +diff --git a/src/work_graph_run_query.py b/src/work_graph_run_query.py +index de08790..0e84758 100644 +--- a/src/work_graph_run_query.py ++++ b/src/work_graph_run_query.py +@@ -7,8 +7,21 @@ codebase that lists `work_graph_run` rows across all tasks in a project + rather than for one `task_id` at a time, which is why it lives in its own + module instead of `task_query.py` (that module's own docstring frames it + as "built around a single task_id"). ++ ++U-046 (2026-08-06 SDD, "运行历史点击跳转错误 + 缺运行详情页"): adds ++`get_work_graph_run`, a single-`run_id` lookup for the new `v_run_detail` ++page (`#/runs/:run_id`). This deliberately mirrors the shape of ++`task_query.get_task`'s embedded `work_graph_runs[i]` entries (same keys, ++same per-node field list via `_nodes_for_run`) so the frontend can reuse ++`renderGraphVisualizerPanel`/`mountLogViewer` against this response without ++any remapping -- it's a separate query (by `run_id` instead of `task_id`) ++rather than a call into `task_query._work_nodes_for_run` because that ++helper is private to its own module and this module already owns ++`work_graph_run` reads. + """ + ++import json ++ + _VALID_ORG_NODES = ("planner", "coder", "validator") + + +@@ -51,3 +64,48 @@ def list_work_graph_runs(conn, project_id, org_node=None, limit=20): + } + for r in rows + ] ++ ++ ++class WorkGraphRunNotFoundError(Exception): ++ """NOT_FOUND -- c_work_graph_run_get on a nonexistent run_id.""" ++ ++ ++def get_work_graph_run(conn, run_id): ++ row = conn.execute( ++ "SELECT run_id, task_id, org_node, graph_manifest, status, started_at, completed_at " ++ "FROM work_graph_run WHERE run_id = ?", ++ (run_id,), ++ ).fetchone() ++ if row is None: ++ raise WorkGraphRunNotFoundError(run_id) ++ return { ++ "run_id": row[0], "task_id": row[1], "org_node": row[2], ++ "graph_manifest": json.loads(row[3]) if row[3] else None, ++ "status": row[4], "started_at": row[5], "completed_at": row[6], ++ "nodes": _nodes_for_run(conn, row[0]), ++ } ++ ++ ++def _nodes_for_run(conn, run_id): ++ # Same column list as task_query._work_nodes_for_run -- kept in lockstep ++ # on purpose (see module docstring) so v_run_detail can reuse ++ # renderGraphVisualizerPanel/mountLogViewer unmodified. ++ rows = conn.execute( ++ "SELECT node_exec_id, node_id, subagent_type, status, retry_count, input_payload, " ++ "output_payload, reflection_notes, git_worktree_ref, input_schema_ref, output_schema_ref, " ++ "quota_limit_type, quota_wait_started_at " ++ "FROM work_node_execution WHERE run_id = ? ORDER BY node_exec_id", ++ (run_id,), ++ ).fetchall() ++ return [ ++ { ++ "node_exec_id": n[0], "node_id": n[1], "subagent_type": n[2], "status": n[3], ++ "retry_count": n[4], ++ "input_payload": json.loads(n[5]) if n[5] else None, ++ "output_payload": json.loads(n[6]) if n[6] else None, ++ "reflection_notes": n[7], "git_worktree_ref": n[8], ++ "input_schema_ref": n[9], "output_schema_ref": n[10], ++ "quota_limit_type": n[11], "quota_wait_started_at": n[12], ++ } ++ for n in rows ++ ] -- libgit2 0.21.4