docs: document concise compatibility limitations

This commit is contained in:
Ralph Chang
2026-04-27 18:55:12 +08:00
parent d6875aac1b
commit 6a1fa525dc
4 changed files with 86 additions and 24 deletions
+10 -1
View File
@@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Remove absorbed/superseded keys from rejected set to avoid duplicate rejection tracking.
- Memory quality evaluation fixtures covering accepted durable facts and rejected noisy facts.
- Sharper compaction memory extraction prompt with concrete good/bad memory examples.
- Pending journal retention: max 50 entries, 30-day TTL, automatic pruning on save.
- Plugin capability test to catch missing OpenCode hooks before release.
- CI workflow for weekly OpenCode plugin API compatibility testing.
### Fixed
@@ -27,12 +30,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Deferred pending journal safety cap implementation (see TODO in `src/pending-journal.ts`).
- Clarified superseded accounting semantics: P0 emits events only, does not archive newly superseded records.
- README structure was streamlined around the automatic memory flow and ongoing memory-quality work.
- Architecture docs now describe `Memory candidates:` as the primary extraction format and XML candidate blocks as legacy.
- Superpowers implementation plans are no longer tracked in git.
### Known Limitations
- Compatibility is tested against OpenCode plugin API `>=1.2.0 <2.0.0`.
- Credential redaction is best-effort; do not store secrets.
- This is working memory, not semantic search.
- Multi-process writes to the same workspace are not fully serialized.
## [1.2.3] - 2026-04-26
### Added
+9 -1
View File
@@ -214,9 +214,17 @@ npm run typecheck
## Requirements
- OpenCode >= 1.0.0
- OpenCode plugin API `>=1.2.0 <2.0.0`
- Node.js >= 18.0.0
## Limitations
- Requires OpenCode plugin API `>=1.2.0 <2.0.0`; OpenCode hook changes may break compatibility.
- Not a secret manager. Credential redaction is best-effort. Do not store secrets.
- Working memory only. No semantic search, embeddings, or vector knowledge base.
- Other prompt or compaction plugins may conflict depending on plugin order.
- Multiple OpenCode processes on the same workspace may race on local files.
## License
MIT License. See [LICENSE](LICENSE) for details.
+21 -15
View File
@@ -51,17 +51,27 @@ function dedupeByText(entries: LongTermMemoryEntry[]): LongTermMemoryEntry[] {
return result;
}
function isStaleEntry(entry: LongTermMemoryEntry, maxAgeDays: number): boolean {
const createdAt = entry.createdAt ? new Date(entry.createdAt).getTime() : NaN;
/**
* Get the effective timestamp for an entry, preferring updatedAt over createdAt.
* Returns 0 if both are invalid/missing.
*/
function entryTime(entry: LongTermMemoryEntry): number {
const updatedAt = entry.updatedAt ? new Date(entry.updatedAt).getTime() : NaN;
if (!Number.isNaN(updatedAt)) return updatedAt;
const createdAt = entry.createdAt ? new Date(entry.createdAt).getTime() : NaN;
if (!Number.isNaN(createdAt)) return createdAt;
return 0;
}
function isStaleEntry(entry: LongTermMemoryEntry, maxAgeDays: number): boolean {
const time = entryTime(entry);
// If both timestamps are invalid, treat as stale
if (Number.isNaN(createdAt) && Number.isNaN(updatedAt)) {
return true;
}
// If timestamp is 0 (both invalid), treat as stale
if (time === 0) return true;
// Use createdAt as primary age timestamp
const ageMs = Date.now() - (Number.isNaN(createdAt) ? updatedAt : createdAt);
const ageMs = Date.now() - time;
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
return ageMs > maxAgeMs;
@@ -78,11 +88,9 @@ function applyRetention(
// 2. Remove stale entries
const freshEntries = deduped.filter(entry => !isStaleEntry(entry, maxAgeDays));
// 3. Sort by createdAt descending (newest first) for cap
// 3. Sort by entryTime descending (newest first) for cap, using updatedAt then createdAt
const sorted = [...freshEntries].sort((a, b) => {
const aTime = a.createdAt ? new Date(a.createdAt).getTime() : 0;
const bTime = b.createdAt ? new Date(b.createdAt).getTime() : 0;
return bTime - aTime;
return entryTime(b) - entryTime(a);
});
// 4. Keep maxEntries newest
@@ -90,9 +98,7 @@ function applyRetention(
// 5. Restore stable order (oldest-to-newest) for consistency with existing code
return capped.sort((a, b) => {
const aTime = a.createdAt ? new Date(a.createdAt).getTime() : 0;
const bTime = b.createdAt ? new Date(b.createdAt).getTime() : 0;
return aTime - bTime;
return entryTime(a) - entryTime(b);
});
}
+46 -7
View File
@@ -193,7 +193,7 @@ describe("pending journal retention", () => {
);
});
it("savePendingJournal keeps explicit entries even if old", async () => {
it("savePendingJournal prunes stale entries regardless of source", async () => {
const now = new Date();
const staleDate = new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000);
@@ -223,12 +223,51 @@ describe("pending journal retention", () => {
const loaded = await loadPendingJournal(testDir);
// Both explicit and compaction entries past maxAgeDays should be pruned
// Currently retention doesn't differentiate by source
// This test documents current behavior
assert.ok(
loaded.entries.length <= 2,
"Entries should be within cap"
// Both explicit and compaction entries past maxAgeDays are pruned
// Retention does not differentiate by source
assert.strictEqual(
loaded.entries.length,
0,
"Stale entries should be pruned regardless of source"
);
});
it("savePendingJournal uses updatedAt when createdAt is missing", async () => {
const now = new Date();
const freshDate = new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000);
const staleDate = new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000);
const entries: LongTermMemoryEntry[] = [
{
type: "decision",
text: "Entry with missing createdAt but fresh updatedAt",
source: "compaction",
createdAt: "", // invalid
updatedAt: freshDate.toISOString(),
},
{
type: "decision",
text: "Entry with missing createdAt and stale updatedAt",
source: "compaction",
createdAt: "", // invalid
updatedAt: staleDate.toISOString(),
},
];
await savePendingJournal(testDir, {
version: 1,
workspace: { root: testDir, key: "test" },
entries,
updatedAt: now.toISOString(),
});
const loaded = await loadPendingJournal(testDir);
// Fresh entry should be kept, stale entry should be pruned
assert.strictEqual(loaded.entries.length, 1);
assert.strictEqual(
loaded.entries[0].text,
"Entry with missing createdAt but fresh updatedAt"
);
});
});