feat(self-feedback): mirror resolutions into memory-store on success
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: "[<entry.kind>] <entry.summary>\n→ <evidence.kind>: <reason>"
confidence: 0.9
source_unit_type: "self-feedback"
source_unit_id: <entryId>
tags: [
<entry.kind>,
"evidence:<evidence.kind>",
"commit:<sha-12-prefix>" // when commitSha present
"requirement:<reqId>" // 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:<sha-prefix>] 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) <noreply@anthropic.com>
This commit is contained in:
parent
6a88ad2f00
commit
2f8ee57256
2 changed files with 135 additions and 0 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue