mirror of
https://github.com/sdwolf4103/opencode-working-memory.git
synced 2026-06-02 06:19:36 +02:00
docs: document concise compatibility limitations
This commit is contained in:
+10
-1
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user