opencode-dejavu 2.3.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +2 -2
- package/index.ts +4 -2
- package/package.json +1 -1
- package/src/store.ts +27 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.4.0 — 2026-08-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Noise TTL: weak one-off patterns (below the promotion threshold, never enforced) expire after 7 days instead of 60 — memory is for recurring mistakes, not one-shot noise.
|
|
7
|
+
- Correction lifecycle signal: an expired gate that had a correction and zero recurrences after promotion logs `retired-healed` — the mechanical "the teaching worked"; doctor reports such gates as TEACHING.
|
|
8
|
+
|
|
9
|
+
### Notes
|
|
10
|
+
- V2 plugin API migration awaits upstream: `tool.execute.error` (opencode issue #27900) is drafted but unmerged — the event-stream scan remains the file-tool failure channel until then.
|
|
11
|
+
|
|
3
12
|
## 2.3.1 — 2026-08-24
|
|
4
13
|
|
|
5
14
|
### Fixed (adversarial + security review round)
|
package/README.md
CHANGED
|
@@ -112,11 +112,11 @@ bun run typecheck # tsc --noEmit (index.ts + src/**)
|
|
|
112
112
|
bun test/smoke.ts # behavioral smoke test, no framework needed
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
Tunables are named constants at the top of `index.ts` and `src/store.ts`: `PROMOTE_COUNT` (3), `PROMOTE_COUNT_PROBE` (5), `PROMOTE_SESSIONS` (2), `GLOBAL_PROJECTS` (2), `TTL_DAYS` (60), `REVIEW_FIRES` (10).
|
|
115
|
+
Tunables are named constants at the top of `index.ts` and `src/store.ts`: `PROMOTE_COUNT` (3), `PROMOTE_COUNT_PROBE` (5), `PROMOTE_SESSIONS` (2), `GLOBAL_PROJECTS` (2), `TTL_DAYS` (60), `NOISE_TTL_DAYS` (7), `REVIEW_FIRES` (10), `MAX_GATES` (2000).
|
|
116
116
|
|
|
117
117
|
## Roadmap
|
|
118
118
|
|
|
119
|
-
- v2: recurrence-after-gate reporting command; V2 plugin API error hooks
|
|
119
|
+
- v2: recurrence-after-gate reporting command; V2 plugin API error hooks — `tool.execute.error` is drafted upstream (opencode issue #27900) but unmerged; the event-stream scan remains the file-tool failure channel until it lands
|
|
120
120
|
- v3: auto-proposal of ast-grep rules for statically detectable patterns (repo-level CI gates)
|
|
121
121
|
|
|
122
122
|
## License
|
package/index.ts
CHANGED
|
@@ -19,6 +19,8 @@ import { GateStore, Stores, type Gate, PLUGIN_VERSION } from "./src/store"
|
|
|
19
19
|
const GLOBAL_PROJECTS = 2
|
|
20
20
|
/** gates expire when the pattern has not recurred for this many days */
|
|
21
21
|
const TTL_DAYS = 60
|
|
22
|
+
/** weak one-off patterns (below promotion threshold, never enforced) rot this fast */
|
|
23
|
+
const NOISE_TTL_DAYS = 7
|
|
22
24
|
/** how often a long-lived process re-runs expiry */
|
|
23
25
|
const TTL_INTERVAL_MS = 6 * 60 * 60 * 1000
|
|
24
26
|
/** a gate firing this often without killing the error gets flagged for review */
|
|
@@ -91,7 +93,7 @@ export const Dejavu: Plugin = async ({ directory, client }) => {
|
|
|
91
93
|
try {
|
|
92
94
|
await stores.reconcileAll(GLOBAL_PROJECTS)
|
|
93
95
|
await stores.migrate()
|
|
94
|
-
await stores.expireAll(TTL_DAYS)
|
|
96
|
+
await stores.expireAll(TTL_DAYS, NOISE_TTL_DAYS)
|
|
95
97
|
await stores.rotateLogs()
|
|
96
98
|
await stores.logAll({ type: "init", key: "dejavu", version: PLUGIN_VERSION })
|
|
97
99
|
await logClient("info", `dejavu initialized v${PLUGIN_VERSION}`)
|
|
@@ -104,7 +106,7 @@ export const Dejavu: Plugin = async ({ directory, client }) => {
|
|
|
104
106
|
// Long-lived processes re-run expiry periodically.
|
|
105
107
|
const ttlTimer = setInterval(() => {
|
|
106
108
|
// expiry is best-effort; the timer keeps running regardless
|
|
107
|
-
stores.expireAll(TTL_DAYS).catch(() => {})
|
|
109
|
+
stores.expireAll(TTL_DAYS, NOISE_TTL_DAYS).catch(() => {})
|
|
108
110
|
}, TTL_INTERVAL_MS)
|
|
109
111
|
;(ttlTimer as { unref?: () => void }).unref?.()
|
|
110
112
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-dejavu",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Cross-session memory prosthesis for OpenCode: detects recurring tool-call failures and promotes them into enforced gates. Remind first, block on same-session repeat offense.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
package/src/store.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { canBlock, fuzzySimilar, FUZZY_MAX_LEN, scrubSecrets } from "./patterns"
|
|
|
4
4
|
import { coerceGateShape, repairGate } from "./validate"
|
|
5
5
|
|
|
6
6
|
/** Bumped on behavior changes; stamped into init log events so stale sessions are visible. */
|
|
7
|
-
export const PLUGIN_VERSION = "2.
|
|
7
|
+
export const PLUGIN_VERSION = "2.4.0"
|
|
8
8
|
|
|
9
9
|
export interface Gate {
|
|
10
10
|
/** sha1 signature prefix — the pattern identity */
|
|
@@ -71,6 +71,7 @@ export type LogEventType =
|
|
|
71
71
|
| "repaired"
|
|
72
72
|
| "quarantined"
|
|
73
73
|
| "degraded"
|
|
74
|
+
| "retired-healed"
|
|
74
75
|
|
|
75
76
|
export interface LogEvent {
|
|
76
77
|
type: LogEventType
|
|
@@ -350,13 +351,23 @@ export class GateStore {
|
|
|
350
351
|
})
|
|
351
352
|
}
|
|
352
353
|
|
|
353
|
-
/**
|
|
354
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Caller must hold the lock. Weak one-off patterns (below the promotion
|
|
356
|
+
* threshold, never enforced) rot faster than proven ones — a pattern that
|
|
357
|
+
* never recurred enough to matter is noise, not memory.
|
|
358
|
+
*/
|
|
359
|
+
async expire(ttlDays: number, noiseTtlDays: number): Promise<Gate[]> {
|
|
355
360
|
const gates = await this.load(true)
|
|
356
|
-
const
|
|
357
|
-
const expired = gates.filter((g) =>
|
|
361
|
+
const now = Date.now()
|
|
362
|
+
const expired = gates.filter((g) => {
|
|
363
|
+
const ttl = g.status === "blocking" || g.count >= PROMOTE_COUNT ? ttlDays : noiseTtlDays
|
|
364
|
+
return Date.parse(g.lastSeen) < now - ttl * DAY_MS
|
|
365
|
+
})
|
|
358
366
|
if (expired.length === 0) return []
|
|
359
|
-
|
|
367
|
+
const expiredKeys = new Set(expired.map((g) => g.key))
|
|
368
|
+
this.gates = gates.filter((g) => !expiredKeys.has(g.key))
|
|
369
|
+
this.keyIndex = null
|
|
370
|
+
this.blockingCache = null
|
|
360
371
|
await this.save()
|
|
361
372
|
return expired
|
|
362
373
|
}
|
|
@@ -596,12 +607,19 @@ export class Stores {
|
|
|
596
607
|
}
|
|
597
608
|
}
|
|
598
609
|
|
|
599
|
-
async expireAll(ttlDays: number): Promise<void> {
|
|
610
|
+
async expireAll(ttlDays: number, noiseTtlDays: number): Promise<void> {
|
|
600
611
|
for (const store of this.scopes()) {
|
|
601
612
|
await store.runLocked(async () => {
|
|
602
|
-
const expired = await store.expire(ttlDays)
|
|
613
|
+
const expired = await store.expire(ttlDays, noiseTtlDays)
|
|
603
614
|
for (const gate of expired) {
|
|
604
|
-
|
|
615
|
+
// Correction lifecycle: a corrected gate that never recurred after
|
|
616
|
+
// promotion means the pattern died out — the mechanical signal that
|
|
617
|
+
// the teaching worked.
|
|
618
|
+
if (gate.correction !== undefined && gate.recurredAfterGate === 0) {
|
|
619
|
+
await store.log({ type: "retired-healed", key: gate.key, tool: gate.tool, snippet: gate.correction.slice(0, 200) })
|
|
620
|
+
} else {
|
|
621
|
+
await store.log({ type: "expired", key: gate.key, tool: gate.tool })
|
|
622
|
+
}
|
|
605
623
|
}
|
|
606
624
|
})
|
|
607
625
|
}
|