opencode-self-improvement 0.8.4 → 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/README.md +2 -0
- package/package.json +1 -1
- package/src/index.ts +20 -7
- package/src/sessions.ts +34 -0
- package/src/undo.ts +19 -2
package/README.md
CHANGED
|
@@ -302,6 +302,8 @@ Run `/repo-init` once in every repo you work in. It:
|
|
|
302
302
|
Ongoing use is just the memory tools. Ask the agent to remember something and it
|
|
303
303
|
sticks across sessions; check what's stored with `memory_list`, refine with
|
|
304
304
|
`memory_update`, retract with `memory_forget`, roll back with `memory_undo`.
|
|
305
|
+
`memory_undo` only lists and restores its own `<stamp>-YYYY-MM-DD.logfmt`
|
|
306
|
+
snapshots; any other file in `snapshots/` is ignored.
|
|
305
307
|
|
|
306
308
|
`memory_update` takes a `query` that picks the row: its id, a clock fragment such
|
|
307
309
|
as `00:11:25.867`, or a unique substring of its text. Omit it to update the newest
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-self-improvement",
|
|
3
|
-
"version": "0.8.
|
|
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,
|
|
@@ -399,7 +400,7 @@ const impl = {
|
|
|
399
400
|
editor.add(validated(UndoArgs, {
|
|
400
401
|
name: "memory_undo",
|
|
401
402
|
description:
|
|
402
|
-
"List memory snapshots, or restore one (rollback). Only snapshots inside this store's snapshots directory are accepted: pass a bare name from the list or its full path. SAFE BY DEFAULT: restore refuses if the current file has rows added or edited since the snapshot, and audited human/tool deletions stay deleted. Re-run with merge=true to restore AND keep divergent rows, or force=true for an explicit exact rollback. Blocklisted rows remain inactive in every mode.",
|
|
403
|
+
"List memory snapshots, or restore one (rollback). Only snapshots inside this store's snapshots directory are accepted: pass a bare name from the list or its full path. Non-standard files in snapshots/ are not listed or restorable. SAFE BY DEFAULT: restore refuses if the current file has rows added or edited since the snapshot, and audited human/tool deletions stay deleted. Re-run with merge=true to restore AND keep divergent rows, or force=true for an explicit exact rollback. Blocklisted rows remain inactive in every mode.",
|
|
403
404
|
options: direct,
|
|
404
405
|
async execute(args): Promise<{ content: string }> {
|
|
405
406
|
if (args.snapshot) {
|
|
@@ -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
|
|
608
|
-
//
|
|
609
|
-
// prompt/context/compaction hooks) — so the system prefix, and with it
|
|
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
|
|
613
|
-
// even when the change happened during a tool loop with no new
|
|
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,
|
package/src/sessions.ts
ADDED
|
@@ -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
|
+
}
|
package/src/undo.ts
CHANGED
|
@@ -19,6 +19,23 @@ import { snapshotFiles } from "./memory.ts";
|
|
|
19
19
|
|
|
20
20
|
const SNAPSHOT_NAME = /^(\d+)-(\d{4}-\d{2}-\d{2}\.logfmt)$/;
|
|
21
21
|
|
|
22
|
+
function matchSnapshotName(name: string): RegExpExecArray | null {
|
|
23
|
+
const match = SNAPSHOT_NAME.exec(name);
|
|
24
|
+
if (!match) return null;
|
|
25
|
+
const original = match[2];
|
|
26
|
+
if (!original) return null;
|
|
27
|
+
const year = Number(original.slice(0, 4));
|
|
28
|
+
const month = Number(original.slice(5, 7));
|
|
29
|
+
const day = Number(original.slice(8, 10));
|
|
30
|
+
const date = new Date(0);
|
|
31
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
32
|
+
return date.getUTCFullYear() === year &&
|
|
33
|
+
date.getUTCMonth() === month - 1 &&
|
|
34
|
+
date.getUTCDate() === day
|
|
35
|
+
? match
|
|
36
|
+
: null;
|
|
37
|
+
}
|
|
38
|
+
|
|
22
39
|
export interface SnapshotInfo {
|
|
23
40
|
file: string;
|
|
24
41
|
name: string;
|
|
@@ -31,7 +48,7 @@ export async function listSnapshots(): Promise<SnapshotInfo[]> {
|
|
|
31
48
|
const out: SnapshotInfo[] = [];
|
|
32
49
|
try {
|
|
33
50
|
for await (const f of glob.scan(snapshotDir())) {
|
|
34
|
-
if (!
|
|
51
|
+
if (!matchSnapshotName(f)) continue;
|
|
35
52
|
const info = Bun.file(join(snapshotDir(), f));
|
|
36
53
|
out.push({
|
|
37
54
|
file: join(snapshotDir(), f),
|
|
@@ -83,7 +100,7 @@ async function resolveSnapshot(
|
|
|
83
100
|
memoryRootDir: string,
|
|
84
101
|
): Promise<{ path: string; original: string }> {
|
|
85
102
|
const name = basename(snapshot);
|
|
86
|
-
const match =
|
|
103
|
+
const match = matchSnapshotName(name);
|
|
87
104
|
const original = match?.[2];
|
|
88
105
|
const stamp = match ? Number(match[1]) : Number.NaN;
|
|
89
106
|
if (!original || !Number.isSafeInteger(stamp)) {
|