opencode-self-improvement 0.8.5 → 0.8.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-self-improvement",
3
- "version": "0.8.5",
3
+ "version": "0.8.6",
4
4
  "description": "Durable memory for opencode: remember facts, decisions, and commands across sessions, plus a one-command per-repo setup (/repo-init).",
5
5
  "main": "index.ts",
6
6
  "exports": {
package/src/index.ts CHANGED
@@ -29,6 +29,7 @@ import { runWriteGate, type GateContext, type WriteCandidate } from "./gates.ts"
29
29
  import { pruneMemory } from "./prune.ts";
30
30
  import { exportMemories, findStaleOpen, importMemories } from "./portable.ts";
31
31
  import { listSnapshots, restoreSnapshot } from "./undo.ts";
32
+ import { SESSION_CAP, SessionLru } from "./sessions.ts";
32
33
  import { record } from "./evidence.ts";
33
34
  import {
34
35
  capToBudget,
@@ -604,13 +605,15 @@ const impl = {
604
605
  });
605
606
 
606
607
  // Per-session frozen injection epochs: the relevance block plus the store
607
- // fingerprint it was computed against. Recomputed ONLY on the first prompt
608
- // of a session, when injectable rows change, or after a compaction (see the
609
- // prompt/context/compaction hooks) — so the system prefix, and with it the
610
- // provider prompt cache, survives ordinary requests.
608
+ // fingerprint it was computed against. Recomputed on the first prompt of a
609
+ // tracked session, when injectable rows change, or after a compaction (see
610
+ // the prompt/context/compaction hooks) — so the system prefix, and with it
611
+ // the provider prompt cache, survives ordinary requests. The shared LRU
612
+ // below may evict the whole session state; a later prompt starts it fresh.
611
613
  const epoch = new Map<string, { version: string; block: string; at: string; reason: "first" | "store_changed" }>();
612
- // Latest user prompt per session. A changed store is re-scored against it
613
- // even when the change happened during a tool loop with no new prompt.
614
+ // Latest user prompt per tracked session. A changed store is re-scored
615
+ // against it even when the change happened during a tool loop with no new
616
+ // prompt. It is evicted together with the epoch and session ledgers.
614
617
  const latestPrompt = new Map<string, string>();
615
618
  // Recomputes since server start: every one invalidated a prompt cache, so
616
619
  // it is the honest cost number the surfaces show (status files / tools).
@@ -622,6 +625,13 @@ const impl = {
622
625
  // (memory_status, in-process).
623
626
  const sessionStats = new Map<string, SessionStats>();
624
627
  const measuredTurns = new Map<string, Measured>();
628
+ // Every map above is keyed by session and dropped together on eviction.
629
+ const sessions = new SessionLru(SESSION_CAP, (sid) => {
630
+ epoch.delete(sid);
631
+ latestPrompt.delete(sid);
632
+ sessionStats.delete(sid);
633
+ measuredTurns.delete(sid);
634
+ });
625
635
  const statsFor = (sid: string): SessionStats => {
626
636
  let st = sessionStats.get(sid);
627
637
  if (!st) {
@@ -739,6 +749,7 @@ const impl = {
739
749
  // Route store reads in this hook through the right session's scope.
740
750
  setCurrent({ sessionID: event?.sessionID });
741
751
  const sid = event?.sessionID;
752
+ if (typeof sid === "string") sessions.touch(sid);
742
753
  const mems = await injectableRows();
743
754
  if (typeof sid === "string") await refreshEpoch(sid, mems);
744
755
  const parts: string[] = [];
@@ -773,6 +784,7 @@ const impl = {
773
784
  setCurrent({ sessionID: event?.sessionID });
774
785
  const sid = event?.sessionID;
775
786
  if (typeof sid === "string") {
787
+ sessions.touch(sid);
776
788
  markTurn(sid);
777
789
  await checkNewSession(sid);
778
790
  await bindSessionFromMessages(sid);
@@ -839,6 +851,7 @@ const impl = {
839
851
  const sid = info.sessionID;
840
852
  const t = info.tokens;
841
853
  if (typeof sid !== "string" || !t) continue;
854
+ sessions.touch(sid);
842
855
  measuredTurns.set(sid, {
843
856
  at: new Date().toISOString(),
844
857
  input: Number(t.input) || 0,
@@ -0,0 +1,34 @@
1
+ // Bounded per-session state for a long-lived server. The plugin keeps several
2
+ // session-keyed maps (epoch, latest prompt, stats, measured usage); a resident
3
+ // `opencode serve` would otherwise hold an entry per session it ever saw. One
4
+ // recency list owns eviction so the maps are always dropped together: an
5
+ // evicted session simply starts over (no block until its next prompt, then a
6
+ // "first" epoch), which costs one recompute and never serves stale state.
7
+ // Same cap as repokey.ts's session-scope cache.
8
+ export const SESSION_CAP = 64;
9
+
10
+ export class SessionLru {
11
+ private readonly order = new Set<string>();
12
+ private readonly cap: number;
13
+ private readonly onEvict: (sid: string) => void;
14
+
15
+ constructor(cap: number, onEvict: (sid: string) => void) {
16
+ this.cap = cap;
17
+ this.onEvict = onEvict;
18
+ }
19
+
20
+ // Mark a session as just used; evicts the least recently used beyond the cap.
21
+ touch(sid: string): void {
22
+ this.order.delete(sid);
23
+ this.order.add(sid);
24
+ while (this.order.size > this.cap) {
25
+ const oldest = this.order.values().next().value as string;
26
+ this.order.delete(oldest);
27
+ this.onEvict(oldest);
28
+ }
29
+ }
30
+
31
+ get size(): number {
32
+ return this.order.size;
33
+ }
34
+ }