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 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 when stable
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.1",
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.3.1"
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
- /** Caller must hold the lock. */
354
- async expire(ttlDays: number): Promise<Gate[]> {
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 cutoff = Date.now() - ttlDays * DAY_MS
357
- const expired = gates.filter((g) => Date.parse(g.lastSeen) < cutoff)
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
- this.gates = gates.filter((g) => Date.parse(g.lastSeen) >= cutoff)
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
- await store.log({ type: "expired", key: gate.key, tool: gate.tool })
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
  }