From e776e6b3a6bc4c8b3e756a561f8a8b6e2c339428 Mon Sep 17 00:00:00 2001 From: aihop <4408344+hhyykk@users.noreply.github.com> Date: Tue, 13 Oct 2026 10:01:07 +0000 Subject: [PATCH] perf(explore): hash only findings emitted in turn context Signed-off-by: aihop <4408344+hhyykk@users.noreply.github.com> --- .../capabilities/explore_turn_context.ts | 19 +++--- .../explore_turn_context.test.ts | 58 +++++++++++++++++++ 2 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 tests/control_plane_ts/explore_turn_context.test.ts diff --git a/loopx/control_plane/capabilities/explore_turn_context.ts b/loopx/control_plane/capabilities/explore_turn_context.ts index 6b7149c5a5..6ae9527ae1 100644 --- a/loopx/control_plane/capabilities/explore_turn_context.ts +++ b/loopx/control_plane/capabilities/explore_turn_context.ts @@ -13,6 +13,10 @@ function compact(row: JsonObject, fields: string[]): JsonObject { return Object.fromEntries(fields.filter(key => row[key] !== undefined) .map(key => [key, typeof row[key] === "string" ? (row[key] as string).slice(0, 240) : row[key]])); } +function findingDigest(row: JsonObject, findingSources?: Map): JsonObject { + const source = findingSources?.get(String(row.finding_id)); + return source ? {source_sha256: createHash("sha256").update(JSON.stringify(source)).digest("hex")} : {}; +} function evidenceCount(audit: JsonObject, kind: string, detailCount: number): number { if (audit.status_counts == null) return detailCount; const counts = requireJsonObject(audit.status_counts, "Explore evidence status counts"); @@ -26,7 +30,7 @@ function evidenceCount(audit: JsonObject, kind: string, detailCount: number): nu } return Math.max(detailCount, total); } -function branchContext(row: JsonObject, findingDigests?: Map): JsonObject { +function branchContext(row: JsonObject, findingSources?: Map): JsonObject { const result = compact(row, ["todo_id", "text", "branch_role", "score", "confidence", "selection_status", "status", "actionable_open", "resume_when", "resume_ready"]); if (row.typed_evidence_audit == null) return result; @@ -44,7 +48,7 @@ function branchContext(row: JsonObject, findingDigests?: Map): J omitted_unknown_node_refs: Math.max(0, unknown.length - 8), nodes: nodes.slice(0, 3).map(node => compact(node, ["node_id", "title", "status"])), findings: findings.slice(0, 3).map(finding => ({...compact(finding, ["finding_id", "node_id", "finding", "status"]), - ...(findingDigests?.has(String(finding.finding_id)) ? {source_sha256: findingDigests.get(String(finding.finding_id))} : {})})), + ...findingDigest(finding, findingSources)})), relevant_edges: edges.slice(0, 3).map(edge => compact(edge, ["from_node", "to_node", "edge_type"])), omitted_audit_nodes: Math.max(0, nodes.length - 3), omitted_audit_findings: Math.max(0, evidenceCount(audit, "findings", findings.length) - 3), @@ -68,8 +72,9 @@ export function projectExploreTurnContext(params: JsonObject): JsonObject { if (params.include_finding_digests != null && typeof params.include_finding_digests !== "boolean") { throw new EffectRuntimeRequestError("include_finding_digests must be a boolean"); } - const findingDigests = params.include_finding_digests === true ? new Map(findings.map(row => - [String(row.finding_id), createHash("sha256").update(JSON.stringify(row)).digest("hex")])) : undefined; + // Retain full sources, but hash only findings emitted by the bounded view. + const findingSources = params.include_finding_digests === true ? new Map(findings.map(row => + [String(row.finding_id), row])) : undefined; const branches = rows(plan.selected_branches); const rejected = rows(plan.rejected_candidates); const frontier = rows(projection.frontier); @@ -127,14 +132,14 @@ export function projectExploreTurnContext(params: JsonObject): JsonObject { recent_nodes: nodes.slice(0, 3).map(row => compact(row, ["node_id", "title", "status", "blocked_reason"])), // The canonical evidence projection calls the finding's title `finding`. recent_findings: findings.slice(0, 3).map(row => ({...compact({...row, title: row.finding}, ["finding_id", "node_id", "title", "status"]), - ...(findingDigests?.has(String(row.finding_id)) ? {source_sha256: findingDigests.get(String(row.finding_id))} : {})})), + ...findingDigest(row, findingSources)})), // Full scoped summaries; progressive reads can raise the page size. // These retain the observation AND applicability; clipping away conditions // could turn a bounded refutation into a blanket route ban. writeback_results: visibleResults .map(row => ({...compact(row, ["finding_id", "node_id", "finding", "status", "evidence_refs"]), summary: String(row.summary ?? "").slice(0, EXPLORE_WRITEBACK_SUMMARY_LIMIT), - ...(findingDigests?.has(String(row.finding_id)) ? {source_sha256: findingDigests.get(String(row.finding_id))} : {})})), + ...findingDigest(row, findingSources)})), result_page: { total: resultRows.length, offset: resultOffset, limit: resultLimit, remaining: remainingResults, revision: resultRevision, @@ -163,7 +168,7 @@ export function projectExploreTurnContext(params: JsonObject): JsonObject { harness: harness ? { orchestration_gate: gate, candidate_count: plan.candidate_count ?? 0, - selected_branches: branches.slice(0, 3).map(branch => branchContext(branch, findingDigests)), + selected_branches: branches.slice(0, 3).map(branch => branchContext(branch, findingSources)), omitted_selected_branches: Math.max(0, branches.length - 3), rejected_candidates: rejected.slice(0, 3).map(branch => branchContext(branch)), omitted_rejected_candidates: Math.max(0, diff --git a/tests/control_plane_ts/explore_turn_context.test.ts b/tests/control_plane_ts/explore_turn_context.test.ts new file mode 100644 index 0000000000..e436a1fe7e --- /dev/null +++ b/tests/control_plane_ts/explore_turn_context.test.ts @@ -0,0 +1,58 @@ +import assert from "node:assert/strict"; +import {createHash} from "node:crypto"; +import test from "node:test"; +import type {JsonObject} from "../../loopx/control_plane/effect_program.ts"; +import {projectExploreTurnContext} from "../../loopx/control_plane/capabilities/explore_turn_context.ts"; + +for (const includeDigests of [false, true]) { + test(`paged and audited findings retain complete source digests: ${includeDigests}`, () => { + const findings = Array.from({length: 20}, (_, index) => ({ + finding_id: `finding-${index}`, node_id: `node-${index}`, + finding: `Scoped finding ${index}`, status: "confirmed", + summary: "Applicable only to the tested inputs. ".repeat(20), + evidence_refs: [`validation:case-${index}`], + tags: index < 10 ? ["writeback-result"] : [], + })); + const request = { + goal_id: "research", agent_id: "worker", route: ["loopx"], + graph_enabled: true, harness_gate: {enabled: true}, + include_finding_digests: includeDigests, result_limit: 2, + projection: {nodes: [], findings}, + plan: {selected_branches: [{todo_id: "audit-old-finding", typed_evidence_audit: { + requested_node_refs: ["node-19"], + findings: [{finding_id: "finding-19", node_id: "node-19", + finding: "Compact audit text", status: "confirmed"}], + }}]}, + }; + const first = projectExploreTurnContext(request); + const page = projectExploreTurnContext({...request, result_offset: 2, + result_revision: (first.graph as JsonObject).result_page.revision}); + const graph = page.graph as JsonObject; + const audit = (page.harness as JsonObject).selected_branches[0].typed_evidence_audit; + assert.deepEqual(graph.recent_findings.map((row: JsonObject) => row.finding_id), + ["finding-0", "finding-1", "finding-2"]); + assert.deepEqual(graph.writeback_results.map((row: JsonObject) => row.finding_id), + ["finding-2", "finding-3"]); + assert.equal(audit.findings[0].finding_id, "finding-19"); + for (const row of [...graph.recent_findings, ...graph.writeback_results, ...audit.findings]) { + const source = findings.find(finding => finding.finding_id === row.finding_id)!; + if (includeDigests) { + assert.equal(row.source_sha256, + createHash("sha256").update(JSON.stringify(source)).digest("hex")); + } else { + assert.equal(Object.hasOwn(row, "source_sha256"), false); + } + } + // The audit omits this source text; a new read must still bind its revision. + findings[19].summary += "Additional applicability constraint."; + const updated = projectExploreTurnContext(request); + const updatedFinding = (updated.harness as JsonObject).selected_branches[0].typed_evidence_audit.findings[0]; + if (includeDigests) { + assert.notEqual(updatedFinding.source_sha256, audit.findings[0].source_sha256); + assert.equal(updatedFinding.source_sha256, + createHash("sha256").update(JSON.stringify(findings[19])).digest("hex")); + } else { + assert.deepEqual(updatedFinding, audit.findings[0]); + } + }); +}