Skip to content

Commit 8da39c7

Browse files
committed
fix(memory): address quality cleanup audit findings
1 parent e8c95a6 commit 8da39c7

9 files changed

Lines changed: 267 additions & 63 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,6 @@ pnpm-lock.yaml
5151

5252
# Superpowers local planning artifacts
5353
docs/superpowers/plans/
54+
55+
# Local migration dry-run roots
56+
scripts/dev/dry-run-roots.local.txt

CHANGELOG.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [1.4.0] - 2026-04-28
99

10-
### Memory Quality Cleanup
10+
### Added
11+
12+
- Local migration audit log for the `2026-04-28-quality-cleanup` migration:
13+
`~/.local/share/opencode-working-memory/migration-logs/2026-04-28-quality-cleanup.jsonl`.
14+
- Local extraction rejection log for rejected compaction memory candidates:
15+
`~/.local/share/opencode-working-memory/extraction-rejections.jsonl`.
16+
- Sanitized real-workspace regression fixtures for memory cleanup migration behavior.
17+
18+
### Changed
1119

12-
- Unified quality gate for compaction memory candidates and cleanup checks.
20+
- Unified memory quality rules in a shared quality gate for compaction memory candidates and cleanup checks.
1321
- Rewritten compaction memory prompt to reduce over-production of low-quality memories.
14-
- Conservative one-time quality cleanup migration (`2026-04-28-quality-cleanup`) that supersedes only high-confidence garbage patterns: progress snapshots, raw errors, commit/CI snapshots, temporary status notes, active file snapshots, code/API signatures, path-heavy entries, and empty entries.
15-
- Soft heuristic failures (`bad_feedback`, `bad_decision`) are intentionally excluded from automatic migration cleanup to protect durable declarative memories such as branding rules, API facts, release rules, and architecture decisions.
16-
- Migration audit log: `~/.local/share/opencode-working-memory/migration-logs/2026-04-28-quality-cleanup.jsonl`.
17-
- Extraction rejection log: `~/.local/share/opencode-working-memory/extraction-rejections.jsonl`.
22+
- Changed quality cleanup migration to be conservative: it supersedes only high-confidence garbage patterns, including progress snapshots, raw errors, commit/CI snapshots, temporary status notes, active file snapshots, code/API signatures, path-heavy entries, and empty entries.
23+
- Soft heuristic failures (`bad_feedback`, `bad_decision`) are intentionally excluded from automatic migration cleanup to protect durable declarative memories such as branding rules, API facts, release rules, user workflow preferences, and architecture decisions.
1824

1925
### Recovery note
2026

RELEASE_NOTES.md

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,69 @@
44

55
### Memory Quality Cleanup
66

7-
This minor release automatically improves memory quality for all existing users on upgrade. Low-quality compaction memories are identified and superseded without requiring manual cleanup.
7+
This release improves automatic workspace memory quality without risking broad cleanup of useful existing memories.
8+
9+
The quality gate is now shared across compaction extraction and migration checks, the compaction prompt is stricter about what should become durable memory, and the one-time migration is intentionally conservative.
810

911
### What Changed
1012

11-
- **Unified quality gate**: All memory types (feedback, decision, project, reference) now share the same quality rules instead of only project entries having a quality check.
12-
- **Hardened compaction prompt**: The model is explicitly instructed that most compactions should produce zero memories, with clear good/bad examples.
13-
- **Auto-supersede migration**: On first load after upgrade, existing low-quality `compaction` memories are automatically marked as `superseded` with quality tags. Explicit and manual memories are never affected.
13+
- **Unified quality rules**: memory quality checks now live in one shared module and apply consistently across feedback, decisions, project facts, and references.
14+
- **Stricter compaction output**: the compaction prompt now tells the model to save fewer memories and prefer durable facts, user preferences, architecture decisions, and hard-to-rediscover references.
15+
- **Conservative migration cleanup**: the `2026-04-28-quality-cleanup` migration only supersedes high-confidence garbage patterns, not every rejected memory.
16+
- **Audit logs**: automatic migration cleanup writes local JSONL audit records so superseded entries can be inspected and restored.
17+
- **Extraction rejection logs**: newly rejected compaction candidates are logged locally to help calibrate future quality rules.
18+
- **Regression coverage**: migration behavior is tested against sanitized real-workspace patterns to prevent mass false positives from coming back.
1419

1520
### What Gets Cleaned Up
1621

17-
Low-quality memory patterns that are now rejected/superseded:
22+
The migration may supersede existing `source: "compaction"` memories only when they match hard garbage patterns:
1823

19-
- Progress snapshots: "Wave 1 completed successfully", "180 tests passed"
20-
- Session-internal notes: "The assistant reviewed feedback and updated the plan"
21-
- Implementation notes: "Implemented X in plugin.ts"
22-
- Commit/CI references: "Commit a762e86 contains the fix"
24+
- Empty entries
25+
- Progress snapshots, such as "Wave 1 completed successfully"
26+
- Test or suite count snapshots, such as "180 tests passed"
2327
- Raw errors and stack traces
24-
- Temporary status: "Currently running npm test"
28+
- Commit or CI snapshots
29+
- Temporary status notes, such as "Currently running npm test"
30+
- Active file snapshots
31+
- Code or API signatures
32+
- Path-heavy entries that are just rediscoverable file lists
33+
34+
### What Is Protected
35+
36+
The migration does not supersede entries whose only issue is a soft heuristic failure, such as:
37+
38+
- `bad_feedback`
39+
- `bad_decision`
40+
41+
This protects useful declarative memories like:
42+
43+
- Product branding rules
44+
- API facts
45+
- Release rules
46+
- Architecture decisions
47+
- User workflow preferences
48+
49+
Explicit and manual memories are also protected.
2550

2651
### Migration Behavior
2752

28-
- Runs exactly once per workspace (idempotent, non-destructive)
29-
- Only affects `source: "compaction"` entries
30-
- Explicit/manual memories are protected
31-
- Superseded entries retain `status: "superseded"` and quality tags for audit
32-
- No user action required
53+
- Runs once per workspace.
54+
- Only affects active `source: "compaction"` entries.
55+
- Marks matching entries as `status: "superseded"` instead of deleting them.
56+
- Adds `quality_cleanup` and `quality:<reason>` tags to superseded entries.
57+
- Writes audit logs to:
58+
`~/.local/share/opencode-working-memory/migration-logs/2026-04-28-quality-cleanup.jsonl`
59+
- Writes extraction rejection logs to:
60+
`~/.local/share/opencode-working-memory/extraction-rejections.jsonl`
61+
62+
### Recovery
63+
64+
If a useful memory is superseded, inspect the migration audit log and restore the entry by changing its status back to `"active"` in the workspace's `workspace-memory.json`.
3365

3466
### Upgrade Notes
3567

3668
- No configuration changes required.
37-
- Existing workspace memory files are automatically cleaned on first load.
69+
- Existing workspace memory files remain compatible.
3870
- The OpenCode config entry stays the same:
3971

4072
```json
@@ -45,7 +77,7 @@ Low-quality memory patterns that are now rejected/superseded:
4577

4678
### Validation
4779

48-
- `npm test` (196 tests)
80+
- `npm test`
4981
- `npm run typecheck`
5082

5183
---

scripts/dev/dry-run-migration.ts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,45 @@
1+
/**
2+
* Local helper to trigger migration on workspace roots.
3+
*
4+
* Usage:
5+
* MIGRATION_DRY_RUN_ROOTS=/path/a:/path/b bun run scripts/dev/dry-run-migration.ts
6+
*
7+
* Or create a local file (gitignored):
8+
* echo "/path/to/workspace1" > scripts/dev/dry-run-roots.local.txt
9+
* echo "/path/to/workspace2" >> scripts/dev/dry-run-roots.local.txt
10+
* bun run scripts/dev/dry-run-migration.ts
11+
*/
12+
13+
import { existsSync } from "node:fs";
14+
import { readFile } from "node:fs/promises";
15+
import { join } from "node:path";
116
import { loadWorkspaceMemory } from "../../src/workspace-memory.ts";
217

3-
const roots = [
4-
"/Users/sd_wo/work/opencode-working-memory",
5-
"/Users/sd_wo/Documents/projects/Pre-cancer-atlas",
6-
"/Users/sd_wo/work/opencode-record",
7-
"/Users/sd_wo/work/pathology-agent-reports",
8-
"/Users/sd_wo/work/pathology-extraction",
9-
];
18+
async function getRoots(): Promise<string[]> {
19+
// Priority 1: environment variable
20+
const envRoots = process.env.MIGRATION_DRY_RUN_ROOTS;
21+
if (envRoots) {
22+
return envRoots.split(":").filter(root => root.length > 0);
23+
}
24+
25+
// Priority 2: local file
26+
const localFile = join(import.meta.dirname, "dry-run-roots.local.txt");
27+
if (existsSync(localFile)) {
28+
const content = await readFile(localFile, "utf8");
29+
return content.trim().split("\n").filter(root => root.length > 0);
30+
}
31+
32+
// No roots configured
33+
console.log("No workspace roots configured.");
34+
console.log("Set MIGRATION_DRY_RUN_ROOTS=/path/a:/path/b or create dry-run-roots.local.txt");
35+
return [];
36+
}
37+
38+
const roots = await getRoots();
39+
40+
if (roots.length === 0) {
41+
process.exit(0);
42+
}
1043

1144
for (const root of roots) {
1245
console.log(`Loading workspace memory: ${root}`);

src/extractors.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,15 @@ async function logExtractionRejection(entry: ExtractionRejectionLogEntry): Promi
248248
}
249249
}
250250

251+
function redactSensitiveText(text: string): string {
252+
return text
253+
.replace(/bearer\s+[a-zA-Z0-9._-]+/gi, "bearer [REDACTED]")
254+
.replace(/token[=:]\s*[a-zA-Z0-9._-]+/gi, "token=[REDACTED]")
255+
.replace(/password[=:]\s*[a-zA-Z0-9._-]+/gi, "password=[REDACTED]")
256+
.replace(/secret[=:]\s*[a-zA-Z0-9._-]+/gi, "secret=[REDACTED]")
257+
.replace(/api[-_]?key[=:]\s*[a-zA-Z0-9._-]+/gi, "api_key=[REDACTED]");
258+
}
259+
251260
function shouldAcceptWorkspaceMemoryCandidate(
252261
entry: {
253262
type: LongTermType;
@@ -278,7 +287,7 @@ function shouldAcceptWorkspaceMemoryCandidate(
278287
void logExtractionRejection({
279288
timestamp: new Date().toISOString(),
280289
type: entry.type,
281-
text,
290+
text: redactSensitiveText(text),
282291
reasons: quality.reasons,
283292
source: "compaction",
284293
});

src/workspace-memory.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,14 +208,23 @@ export async function normalizeWorkspaceMemoryWithAccounting(
208208
// One-time migrations for legacy/low-quality snapshot violations.
209209
// Run quality cleanup first so hard violations receive quality audit tags
210210
// before the older P0 project-only cleanup marks progress snapshots.
211+
const beforeQualityCleanup = result;
211212
const qualityCleanup = runMigrationQualityCleanup(result, nowIso);
212213
result = qualityCleanup.store;
214+
let skipRemainingMigrations = false;
213215
if (qualityCleanup.events.length > 0) {
214-
await appendQualityCleanupMigrationLog(qualityCleanup.events).catch(error => {
216+
try {
217+
await appendQualityCleanupMigrationLog(qualityCleanup.events);
218+
} catch (error) {
215219
console.error("[memory] failed to write quality cleanup migration log:", error);
216-
});
220+
console.error("[memory] aborting migration to maintain audit trail integrity");
221+
result = beforeQualityCleanup;
222+
skipRemainingMigrations = true;
223+
}
224+
}
225+
if (!skipRemainingMigrations) {
226+
result = runMigrationP0Cleanup(result, nowIso);
217227
}
218-
result = runMigrationP0Cleanup(result, nowIso);
219228

220229
// P0 accounting only considers active entries. Entries that were already
221230
// superseded before this normalization are preserved in storage; entries that

tests/extractors.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,35 @@ Memory candidates:
324324
}
325325
});
326326

327+
test("parseWorkspaceMemoryCandidates redacts secrets in extraction rejection log", async () => {
328+
const dataHome = await mkdtemp(join(tmpdir(), "wm-extraction-redact-data-"));
329+
const previousXdgDataHome = process.env.XDG_DATA_HOME;
330+
process.env.XDG_DATA_HOME = dataHome;
331+
332+
try {
333+
const summary = `
334+
Memory candidates:
335+
- reference TypeError: bearer sk_test token=tok123 password=pass123 secret=sec123 api_key=key123
336+
`;
337+
338+
const items = parseWorkspaceMemoryCandidates(summary);
339+
340+
assert.equal(items.length, 0);
341+
const logPath = join(dataHome, "opencode-working-memory", "extraction-rejections.jsonl");
342+
const lines = (await waitForFile(logPath)).trim().split("\n");
343+
assert.equal(lines.length, 1);
344+
const event = JSON.parse(lines[0]);
345+
assert.equal(
346+
event.text,
347+
"TypeError: bearer [REDACTED] token=[REDACTED] password=[REDACTED] secret=[REDACTED] api_key=[REDACTED]",
348+
);
349+
} finally {
350+
if (previousXdgDataHome === undefined) delete process.env.XDG_DATA_HOME;
351+
else process.env.XDG_DATA_HOME = previousXdgDataHome;
352+
await rm(dataHome, { recursive: true, force: true });
353+
}
354+
});
355+
327356
test("parseWorkspaceMemoryCandidates rejects exact file count snapshots", () => {
328357
const summary = `
329358
Memory candidates:

0 commit comments

Comments
 (0)