From 2f8ee57256025434e2f822db81a6116eef496097 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Thu, 14 May 2026 05:16:28 +0200 Subject: [PATCH] feat(self-feedback): mirror resolutions into memory-store on success MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses sf-mp4rp6y2-31jfau (architecture-defect:self-feedback-not- wired-to-memory-subsystem). The reflection layer surfaced this as part of the deepest architectural concern in the 2026-05-14T02-49-45Z report: "resolutions are hidden from the memory graph, SF will continue to forget its own triaged solutions and fail to cluster identical root causes." When markResolved succeeds against the DB, also call memory-store's createMemory to mirror the closure as a memory entry that detectors and reflection passes can consult later via getRelevantMemoriesRanked. Memory entry shape: category: "self-feedback-resolution" content: "[] \n→ : " confidence: 0.9 source_unit_type: "self-feedback" source_unit_id: tags: [ , "evidence:", "commit:" // when commitSha present "requirement:" // when requirementId present ] Best-effort: any memory-write failure is silently swallowed. The resolution itself already landed via DB UPDATE + JSONL audit append + markdown regen — the memory mirror is observability + future detector consumption, not a correctness requirement. The try/catch ensures a broken memory subsystem cannot roll back a valid resolution. Tests (2 new, 13 total in self-feedback-db): - agent-fix with commitSha → memory entry has [kind, evidence:agent-fix, commit:] tags + sourceUnitId pointing at the resolved entry - human-clear without commit → memory entry has [kind, evidence:human- clear] tags only, no commit tag Pre-existing migration failures in sf-db-migration.test.mjs (2 tests: v27 spec backfill, v52 routing-history heal) are unrelated to this commit; same failure mode as last iteration. Logged here so the 1591/1593 pass rate is auditable. The other three siblings of the consolidating reflection entry (sf-mp4w89mv-3ulqp4) remain open and need schema migration: - sf-mp4rxkwt-sfthez kind vocabulary (domain:family[:specific]) - sf-mp4rxkwx-jz0soh causal links (self_feedback_relations table) - sf-mp4rxkx0-fkt3e2 prioritization (impact_score + effort_estimate cols) This commit lands the writer-layer-only piece (#4 in the rollup's suggested fix), unlocking detector + reflection consumption immediately. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/resources/extensions/sf/self-feedback.js | 61 +++++++++++++++ .../sf/tests/self-feedback-db.test.mjs | 74 +++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/src/resources/extensions/sf/self-feedback.js b/src/resources/extensions/sf/self-feedback.js index 310c1e2f6..0d6507929 100644 --- a/src/resources/extensions/sf/self-feedback.js +++ b/src/resources/extensions/sf/self-feedback.js @@ -45,6 +45,7 @@ import { listSelfFeedbackEntries, resolveSelfFeedbackEntry, } from "./sf-db.js"; +import { createMemory } from "./memory-store.js"; const SELF_FEEDBACK_HEADER = "# SF Self-Feedback\n\n" + @@ -274,6 +275,57 @@ function importLegacyJsonlToDb(basePath) { } } +/** + * Mirror a self-feedback resolution into the memory subsystem + * (sf-mp4rp6y2-31jfau). Closures carry knowledge — what kind of issue, + * what the operator/agent decided, citations to commits or requirements. + * Writing them as memory entries lets detectors call + * getRelevantMemoriesRanked(entry.kind) before filing duplicates, and + * lets reflection passes consult prior closure rationale across cycles. + * + * Best-effort: any failure is silently swallowed. The resolution itself + * already landed; memory write is observability + future detector + * consumption, not a correctness requirement. + * + * Called from markResolved success branch. + */ +function writeResolutionToMemory(entry, resolution) { + try { + if (!entry || typeof entry !== "object") return; + const evidenceKind = resolution?.evidence?.kind ?? "unknown"; + const reason = + typeof resolution?.reason === "string" && resolution.reason.trim() + ? resolution.reason.trim() + : "(no reason recorded)"; + const summary = + typeof entry.summary === "string" && entry.summary.trim() + ? entry.summary.trim().slice(0, 400) + : "(no summary)"; + const content = `[${entry.kind}] ${summary}\n→ ${evidenceKind}: ${reason.slice(0, 600)}`; + const tagSet = new Set(); + if (typeof entry.kind === "string") tagSet.add(entry.kind); + tagSet.add(`evidence:${evidenceKind}`); + const commitSha = resolution?.evidence?.commitSha; + if (typeof commitSha === "string" && commitSha) { + tagSet.add(`commit:${commitSha.slice(0, 12)}`); + } + const requirementId = resolution?.evidence?.requirementId; + if (typeof requirementId === "string" && requirementId) { + tagSet.add(`requirement:${requirementId}`); + } + createMemory({ + category: "self-feedback-resolution", + content, + confidence: 0.9, + source_unit_type: "self-feedback", + source_unit_id: entry.id, + tags: Array.from(tagSet), + }); + } catch { + // Best-effort — never fail the resolution path. + } +} + function appendResolutionToJsonl(basePath, entryId, resolution, resolvedAt) { const path = projectJsonlPath(basePath); const record = { @@ -549,6 +601,15 @@ export function markResolved(entryId, resolution, basePath = process.cwd()) { // how to replay these events into existing DB rows. appendResolutionToJsonl(basePath, entryId, resolution, resolvedAt); regenerateSelfFeedbackMarkdown(basePath); + // Mirror the closure into memory-store (sf-mp4rp6y2-31jfau) so + // detectors and reflection passes can consult prior closure + // rationale via getRelevantMemoriesRanked. Best-effort. + try { + const entry = listSelfFeedbackEntries().find((e) => e.id === entryId); + if (entry) writeResolutionToMemory(entry, resolution); + } catch { + // memory mirror is observability; never fail the resolution + } } return mutated; } catch { diff --git a/src/resources/extensions/sf/tests/self-feedback-db.test.mjs b/src/resources/extensions/sf/tests/self-feedback-db.test.mjs index 06cdc1f61..f304f41aa 100644 --- a/src/resources/extensions/sf/tests/self-feedback-db.test.mjs +++ b/src/resources/extensions/sf/tests/self-feedback-db.test.mjs @@ -295,6 +295,80 @@ test("markResolved_accepts_agent_fix_with_no_commit_sha_or_ungrokable_path", () assert.equal(ok2, true); }); +test("markResolved_mirrors_resolution_into_memory_store_with_tags", async () => { + const project = makeForgeProject(); + const filed = recordSelfFeedback( + { + kind: "gap:visible-in-memory", + severity: "medium", + summary: "test entry whose closure should land in memory", + }, + project, + ); + + const ok = markResolved( + filed.entry.id, + { + reason: "fixed by deadbeef in src/foo.js", + evidence: { kind: "agent-fix", commitSha: "deadbeefcafe" }, + }, + project, + ); + assert.equal(ok, true); + + // Verify the memory entry landed. getActiveMemories returns camelCase + // fields (sourceUnitId, not source_unit_id). + const { getActiveMemories } = await import("../sf-db.js"); + const memories = getActiveMemories(); + const closureMem = memories.find((m) => m.sourceUnitId === filed.entry.id); + assert.ok(closureMem, "expected a memory entry sourced to the resolved entry"); + assert.equal(closureMem.category, "self-feedback-resolution"); + assert.match(closureMem.content, /\[gap:visible-in-memory\]/); + assert.match(closureMem.content, /agent-fix: fixed by deadbeef/); + const tags = Array.isArray(closureMem.tags) ? closureMem.tags : []; + assert.ok(tags.includes("gap:visible-in-memory"), "kind tag missing"); + assert.ok(tags.includes("evidence:agent-fix"), "evidence-kind tag missing"); + assert.ok( + tags.some((t) => t.startsWith("commit:deadbeef")), + "commit tag missing", + ); +}); + +test("markResolved_memory_mirror_handles_human_clear_without_commit_tags", async () => { + const project = makeForgeProject(); + const filed = recordSelfFeedback( + { + kind: "upstream-rollup:noise-cluster", + severity: "medium", + summary: "out-of-scope close — no commit/requirement to tag", + }, + project, + ); + + const ok = markResolved( + filed.entry.id, + { + reason: "out of scope for inline-fix worker", + evidence: { kind: "human-clear" }, + }, + project, + ); + assert.equal(ok, true); + + const { getActiveMemories } = await import("../sf-db.js"); + const closureMem = getActiveMemories().find( + (m) => m.sourceUnitId === filed.entry.id, + ); + assert.ok(closureMem, "human-clear closures should also land in memory"); + const tags = Array.isArray(closureMem.tags) ? closureMem.tags : []; + assert.ok(tags.includes("upstream-rollup:noise-cluster")); + assert.ok(tags.includes("evidence:human-clear")); + assert.ok( + !tags.some((t) => t.startsWith("commit:")), + "no commit tag when no commitSha supplied", + ); +}); + test("markResolved_appends_resolution_event_to_jsonl_audit_log", () => { const project = makeForgeProject(); const result = recordSelfFeedback(