Skip to content

Commit 909d6c7

Browse files
committed
docs: document concise compatibility limitations
1 parent c697f63 commit 909d6c7

4 files changed

Lines changed: 86 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- Remove absorbed/superseded keys from rejected set to avoid duplicate rejection tracking.
1717
- Memory quality evaluation fixtures covering accepted durable facts and rejected noisy facts.
1818
- Sharper compaction memory extraction prompt with concrete good/bad memory examples.
19+
- Pending journal retention: max 50 entries, 30-day TTL, automatic pruning on save.
20+
- Plugin capability test to catch missing OpenCode hooks before release.
21+
- CI workflow for weekly OpenCode plugin API compatibility testing.
1922

2023
### Fixed
2124

@@ -27,12 +30,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2730

2831
### Changed
2932

30-
- Deferred pending journal safety cap implementation (see TODO in `src/pending-journal.ts`).
3133
- Clarified superseded accounting semantics: P0 emits events only, does not archive newly superseded records.
3234
- README structure was streamlined around the automatic memory flow and ongoing memory-quality work.
3335
- Architecture docs now describe `Memory candidates:` as the primary extraction format and XML candidate blocks as legacy.
3436
- Superpowers implementation plans are no longer tracked in git.
3537

38+
### Known Limitations
39+
40+
- Compatibility is tested against OpenCode plugin API `>=1.2.0 <2.0.0`.
41+
- Credential redaction is best-effort; do not store secrets.
42+
- This is working memory, not semantic search.
43+
- Multi-process writes to the same workspace are not fully serialized.
44+
3645
## [1.2.3] - 2026-04-26
3746

3847
### Added

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,17 @@ npm run typecheck
214214

215215
## Requirements
216216

217-
- OpenCode >= 1.0.0
217+
- OpenCode plugin API `>=1.2.0 <2.0.0`
218218
- Node.js >= 18.0.0
219219

220+
## Limitations
221+
222+
- Requires OpenCode plugin API `>=1.2.0 <2.0.0`; OpenCode hook changes may break compatibility.
223+
- Not a secret manager. Credential redaction is best-effort. Do not store secrets.
224+
- Working memory only. No semantic search, embeddings, or vector knowledge base.
225+
- Other prompt or compaction plugins may conflict depending on plugin order.
226+
- Multiple OpenCode processes on the same workspace may race on local files.
227+
220228
## License
221229

222230
MIT License. See [LICENSE](LICENSE) for details.

src/pending-journal.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,27 @@ function dedupeByText(entries: LongTermMemoryEntry[]): LongTermMemoryEntry[] {
5151
return result;
5252
}
5353

54-
function isStaleEntry(entry: LongTermMemoryEntry, maxAgeDays: number): boolean {
55-
const createdAt = entry.createdAt ? new Date(entry.createdAt).getTime() : NaN;
54+
/**
55+
* Get the effective timestamp for an entry, preferring updatedAt over createdAt.
56+
* Returns 0 if both are invalid/missing.
57+
*/
58+
function entryTime(entry: LongTermMemoryEntry): number {
5659
const updatedAt = entry.updatedAt ? new Date(entry.updatedAt).getTime() : NaN;
60+
if (!Number.isNaN(updatedAt)) return updatedAt;
61+
62+
const createdAt = entry.createdAt ? new Date(entry.createdAt).getTime() : NaN;
63+
if (!Number.isNaN(createdAt)) return createdAt;
64+
65+
return 0;
66+
}
67+
68+
function isStaleEntry(entry: LongTermMemoryEntry, maxAgeDays: number): boolean {
69+
const time = entryTime(entry);
5770

58-
// If both timestamps are invalid, treat as stale
59-
if (Number.isNaN(createdAt) && Number.isNaN(updatedAt)) {
60-
return true;
61-
}
71+
// If timestamp is 0 (both invalid), treat as stale
72+
if (time === 0) return true;
6273

63-
// Use createdAt as primary age timestamp
64-
const ageMs = Date.now() - (Number.isNaN(createdAt) ? updatedAt : createdAt);
74+
const ageMs = Date.now() - time;
6575
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
6676

6777
return ageMs > maxAgeMs;
@@ -78,21 +88,17 @@ function applyRetention(
7888
// 2. Remove stale entries
7989
const freshEntries = deduped.filter(entry => !isStaleEntry(entry, maxAgeDays));
8090

81-
// 3. Sort by createdAt descending (newest first) for cap
91+
// 3. Sort by entryTime descending (newest first) for cap, using updatedAt then createdAt
8292
const sorted = [...freshEntries].sort((a, b) => {
83-
const aTime = a.createdAt ? new Date(a.createdAt).getTime() : 0;
84-
const bTime = b.createdAt ? new Date(b.createdAt).getTime() : 0;
85-
return bTime - aTime;
93+
return entryTime(b) - entryTime(a);
8694
});
8795

8896
// 4. Keep maxEntries newest
8997
const capped = sorted.slice(0, maxEntries);
9098

9199
// 5. Restore stable order (oldest-to-newest) for consistency with existing code
92100
return capped.sort((a, b) => {
93-
const aTime = a.createdAt ? new Date(a.createdAt).getTime() : 0;
94-
const bTime = b.createdAt ? new Date(b.createdAt).getTime() : 0;
95-
return aTime - bTime;
101+
return entryTime(a) - entryTime(b);
96102
});
97103
}
98104

tests/pending-journal.test.ts

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ describe("pending journal retention", () => {
193193
);
194194
});
195195

196-
it("savePendingJournal keeps explicit entries even if old", async () => {
196+
it("savePendingJournal prunes stale entries regardless of source", async () => {
197197
const now = new Date();
198198
const staleDate = new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000);
199199

@@ -223,12 +223,51 @@ describe("pending journal retention", () => {
223223

224224
const loaded = await loadPendingJournal(testDir);
225225

226-
// Both explicit and compaction entries past maxAgeDays should be pruned
227-
// Currently retention doesn't differentiate by source
228-
// This test documents current behavior
229-
assert.ok(
230-
loaded.entries.length <= 2,
231-
"Entries should be within cap"
226+
// Both explicit and compaction entries past maxAgeDays are pruned
227+
// Retention does not differentiate by source
228+
assert.strictEqual(
229+
loaded.entries.length,
230+
0,
231+
"Stale entries should be pruned regardless of source"
232+
);
233+
});
234+
235+
it("savePendingJournal uses updatedAt when createdAt is missing", async () => {
236+
const now = new Date();
237+
const freshDate = new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000);
238+
const staleDate = new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000);
239+
240+
const entries: LongTermMemoryEntry[] = [
241+
{
242+
type: "decision",
243+
text: "Entry with missing createdAt but fresh updatedAt",
244+
source: "compaction",
245+
createdAt: "", // invalid
246+
updatedAt: freshDate.toISOString(),
247+
},
248+
{
249+
type: "decision",
250+
text: "Entry with missing createdAt and stale updatedAt",
251+
source: "compaction",
252+
createdAt: "", // invalid
253+
updatedAt: staleDate.toISOString(),
254+
},
255+
];
256+
257+
await savePendingJournal(testDir, {
258+
version: 1,
259+
workspace: { root: testDir, key: "test" },
260+
entries,
261+
updatedAt: now.toISOString(),
262+
});
263+
264+
const loaded = await loadPendingJournal(testDir);
265+
266+
// Fresh entry should be kept, stale entry should be pruned
267+
assert.strictEqual(loaded.entries.length, 1);
268+
assert.strictEqual(
269+
loaded.entries[0].text,
270+
"Entry with missing createdAt but fresh updatedAt"
232271
);
233272
});
234273
});

0 commit comments

Comments
 (0)