opencode-self-improvement 0.6.2 → 0.6.3
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 +1 -1
- package/src/index.ts +33 -9
- package/src/memory.ts +7 -4
- package/src/repokey.ts +48 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-self-improvement",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
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
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
onCompacting,
|
|
63
63
|
} from "./triggers.ts";
|
|
64
64
|
import { getCurrent, setCurrent } from "./state.ts";
|
|
65
|
-
import { resolveRepo, setRepoScope } from "./repokey.ts";
|
|
65
|
+
import { resolveRepo, setRepoScope, rememberSessionDir, sessionDir } from "./repokey.ts";
|
|
66
66
|
import { readLines } from "./logfmt.ts";
|
|
67
67
|
import { buildCapturePrompt, flattenTranscript, parseCandidates, parseModelRef } from "./capture.ts";
|
|
68
68
|
|
|
@@ -168,12 +168,13 @@ const PLUGIN_ID = "opencode-self-improvement";
|
|
|
168
168
|
// (the only members of the V2 context this plugin actually touches).
|
|
169
169
|
const impl = {
|
|
170
170
|
async setup(ctx: PluginCtx): Promise<Cleanup | void> {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
171
|
+
// Identity vs verification tree are separate concerns: the repo STAMP is
|
|
172
|
+
// resolved per session from the event stream (never from process.cwd(),
|
|
173
|
+
// which is the server launch dir and poisons rows invisibly); cwd remains
|
|
174
|
+
// only as the gate's verification tree when no session location is known.
|
|
175
|
+
const setupRoot = ctx?.location?.directory;
|
|
176
|
+
const liveRoot = setupRoot ?? process.cwd();
|
|
177
|
+
setRepoScope(setupRoot ? resolveRepo(setupRoot) : null);
|
|
177
178
|
|
|
178
179
|
// Character budget for the epoch-frozen injection block: plugin options
|
|
179
180
|
// (object form in opencode.json(c)) win, then OPENCODE_MEMORY_INJECT_BUDGET,
|
|
@@ -197,7 +198,7 @@ const impl = {
|
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
const gateContext = (): GateContext => ({
|
|
200
|
-
liveRoot,
|
|
201
|
+
liveRoot: sessionDir(getCurrent().sid) ?? liveRoot,
|
|
201
202
|
evidenceCalls: evidenceCalls(getCurrent().sid),
|
|
202
203
|
hasGateCheck: hasGateCheck(getCurrent().sid),
|
|
203
204
|
hasEngagement: hasEngagement(getCurrent().sid),
|
|
@@ -239,7 +240,7 @@ const impl = {
|
|
|
239
240
|
"Initialize repository memory and knowledge graph. Ensures the codebase-memory graph for this repo is indexed (one-shot `cli` spawn; no session MCP needed) and re-derives build/lint/test/typecheck command memories through the write gate, then stores an init stamp keyed on git HEAD. Idempotent: a no-op when the stamp matches HEAD, unless force=true (refresh after a refactor).",
|
|
240
241
|
options: direct,
|
|
241
242
|
async execute(args): Promise<{ content: string }> {
|
|
242
|
-
const summary = await runInit({ liveRoot, force: args.force, mode: args.mode });
|
|
243
|
+
const summary = await runInit({ liveRoot: gateContext().liveRoot, force: args.force, mode: args.mode });
|
|
243
244
|
return { content: formatInit(summary) };
|
|
244
245
|
},
|
|
245
246
|
}));
|
|
@@ -647,6 +648,8 @@ const impl = {
|
|
|
647
648
|
// Tiny system digest + open-thread frontier block, injected into the system prompt.
|
|
648
649
|
await ctx.session?.hook("context", (event: any) =>
|
|
649
650
|
guarded("session.context (systemDigest)", async () => {
|
|
651
|
+
// Route store reads in this hook through the right session's scope.
|
|
652
|
+
setCurrent({ sessionID: event?.sessionID });
|
|
650
653
|
const parts: string[] = [];
|
|
651
654
|
await systemDigest({ sessionID: event?.sessionID }, { system: parts });
|
|
652
655
|
for (const text of parts) event?.system?.push?.({ type: "text", text });
|
|
@@ -676,6 +679,7 @@ const impl = {
|
|
|
676
679
|
// the UI.
|
|
677
680
|
await ctx.session?.hook("prompt", (event: any) =>
|
|
678
681
|
guarded("session.prompt (inject)", async () => {
|
|
682
|
+
setCurrent({ sessionID: event?.sessionID });
|
|
679
683
|
const sid = event?.sessionID;
|
|
680
684
|
if (typeof sid === "string") {
|
|
681
685
|
markTurn(sid);
|
|
@@ -724,6 +728,7 @@ const impl = {
|
|
|
724
728
|
// the summarizer call entirely.
|
|
725
729
|
await ctx.session?.hook("compaction", (event: any) =>
|
|
726
730
|
guarded("session.compaction (onCompacting)", async () => {
|
|
731
|
+
setCurrent({ sessionID: event?.sessionID });
|
|
727
732
|
await onCompacting({ sessionID: event?.sessionID });
|
|
728
733
|
if (typeof event?.sessionID === "string") epoch.delete(event.sessionID);
|
|
729
734
|
const anchor = await compactionAnchor();
|
|
@@ -747,6 +752,22 @@ const impl = {
|
|
|
747
752
|
try {
|
|
748
753
|
for await (const raw of eventApi.subscribe({ signal: measuredController.signal })) {
|
|
749
754
|
const ev = raw as Record<string, any>;
|
|
755
|
+
// Session identity: created fires for new sessions, execution.started
|
|
756
|
+
// every turn (covers sessions resumed across a restart), moved on a
|
|
757
|
+
// workspace change. Stamps and visibility resolve through this.
|
|
758
|
+
if (
|
|
759
|
+
ev?.type === "session.created" ||
|
|
760
|
+
ev?.type === "session.execution.started" ||
|
|
761
|
+
ev?.type === "session.moved"
|
|
762
|
+
) {
|
|
763
|
+
const d = ev.data ?? ev.properties ?? {};
|
|
764
|
+
const info = d.info ?? d;
|
|
765
|
+
const sid = info.sessionID ?? d.sessionID;
|
|
766
|
+
const dir =
|
|
767
|
+
info.location?.directory ?? d.location?.directory ?? ev.location?.directory ?? info.directory;
|
|
768
|
+
if (typeof sid === "string" && typeof dir === "string") rememberSessionDir(sid, dir);
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
750
771
|
if (ev?.type !== "message.updated") continue;
|
|
751
772
|
const info = ev.properties?.info ?? ev.info;
|
|
752
773
|
if (!info || info.role !== "assistant") continue;
|
|
@@ -812,6 +833,9 @@ const impl = {
|
|
|
812
833
|
if (event?.type !== "session.idle") continue;
|
|
813
834
|
const sid = event.sessionID ?? event.properties?.sessionID;
|
|
814
835
|
if (typeof sid !== "string") continue;
|
|
836
|
+
// Bind the idle session so captures stamp (and gate against)
|
|
837
|
+
// that session's repo, not whatever tool call ran last.
|
|
838
|
+
setCurrent({ sessionID: sid });
|
|
815
839
|
const now = Date.now();
|
|
816
840
|
if (now - (lastRun.get(sid) ?? 0) < 5 * 60_000) continue; // 5-min cooldown
|
|
817
841
|
lastRun.set(sid, now);
|
package/src/memory.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
6
|
import { join, basename } from "node:path";
|
|
7
7
|
import { memoryRoot, deletionsFile, snapshotDir } from "./config.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { repoScopeFor } from "./repokey.ts";
|
|
9
|
+
import { getCurrent } from "./state.ts";
|
|
9
10
|
import { appendLog, ensureDir, formatLine, parseLine, readLines } from "./logfmt.ts";
|
|
10
11
|
import { blockedTs } from "./blocklist.ts";
|
|
11
12
|
|
|
@@ -173,11 +174,13 @@ export async function appendMemory(input: AppendMemoryInput): Promise<string> {
|
|
|
173
174
|
}
|
|
174
175
|
// Repo stamp at the single write choke point: gated writes,
|
|
175
176
|
// viewer restores and edits all pass through here. Explicit "global"
|
|
176
|
-
// survives; an unscoped process (viewer) stamps nothing = global.
|
|
177
|
+
// survives; an unscoped process (viewer) stamps nothing = global. Session
|
|
178
|
+
// identity wins over the module scope so a server hosting several workspaces
|
|
179
|
+
// stamps the writing session's repo.
|
|
177
180
|
let repo = input.repo;
|
|
178
181
|
let repoPath = input.repoPath;
|
|
179
182
|
if (repo === undefined) {
|
|
180
|
-
const cur =
|
|
183
|
+
const cur = repoScopeFor(getCurrent().sid);
|
|
181
184
|
if (cur) {
|
|
182
185
|
repo = cur.key;
|
|
183
186
|
repoPath = cur.path;
|
|
@@ -416,7 +419,7 @@ export function isActive(m: Memory): boolean {
|
|
|
416
419
|
// entry points.
|
|
417
420
|
export function isVisibleHere(m: Memory): boolean {
|
|
418
421
|
if (!m.repo || m.repo === "global") return true;
|
|
419
|
-
const cur =
|
|
422
|
+
const cur = repoScopeFor(getCurrent().sid);
|
|
420
423
|
if (!cur) return true; // unscoped process: no partitioning
|
|
421
424
|
return m.repo === cur.key || (!!m.repoPath && m.repoPath === cur.path);
|
|
422
425
|
}
|
package/src/repokey.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
// Repo identity for memory scoping.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Repo identity for memory scoping. A server process can host sessions from
|
|
2
|
+
// several workspaces, and setup() can run before the host attaches a session
|
|
3
|
+
// location — so identity is resolved PER SESSION from the event stream and
|
|
4
|
+
// falls back to the module scope (which itself may be null, never a guessed
|
|
5
|
+
// cwd). Every store write stamps it; every model-facing read filters by it.
|
|
4
6
|
//
|
|
5
7
|
// Two keys, both cheap, with opposite failure modes:
|
|
6
8
|
// key — sha1 of the normalized origin URL: survives re-clone, moves, and
|
|
@@ -64,3 +66,46 @@ export function resolveRepo(startDir: string): RepoScope {
|
|
|
64
66
|
: path;
|
|
65
67
|
return { key, path };
|
|
66
68
|
}
|
|
69
|
+
|
|
70
|
+
// --- per-session identity -----------------------------------------------------
|
|
71
|
+
// Learned from session events (created / execution.started / moved). Bounded:
|
|
72
|
+
// a long-lived server must not grow this with every session ever seen.
|
|
73
|
+
const SESSION_CAP = 64;
|
|
74
|
+
const byDir = new Map<string, RepoScope>();
|
|
75
|
+
const bySession = new Map<string, { dir: string; scope: RepoScope }>();
|
|
76
|
+
|
|
77
|
+
function scopeForDir(dir: string): RepoScope {
|
|
78
|
+
let s = byDir.get(dir);
|
|
79
|
+
if (!s) {
|
|
80
|
+
s = resolveRepo(dir);
|
|
81
|
+
byDir.set(dir, s);
|
|
82
|
+
}
|
|
83
|
+
return s;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function rememberSessionDir(sid: string, dir: string): void {
|
|
87
|
+
const prev = bySession.get(sid);
|
|
88
|
+
if (prev && prev.dir === dir) return;
|
|
89
|
+
bySession.set(sid, { dir, scope: scopeForDir(dir) });
|
|
90
|
+
while (bySession.size > SESSION_CAP) bySession.delete(bySession.keys().next().value as string);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function sessionDir(sid?: string): string | undefined {
|
|
94
|
+
if (!sid || sid === "?") return undefined;
|
|
95
|
+
return bySession.get(sid)?.dir;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// The scope a write/read belongs to: the session's when known, else the
|
|
99
|
+
// module scope (null = unscoped process = global rows).
|
|
100
|
+
export function repoScopeFor(sid?: string): RepoScope | null {
|
|
101
|
+
if (sid && sid !== "?") {
|
|
102
|
+
const e = bySession.get(sid);
|
|
103
|
+
if (e) return e.scope;
|
|
104
|
+
}
|
|
105
|
+
return current;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function resetSessionScopes(): void {
|
|
109
|
+
bySession.clear();
|
|
110
|
+
byDir.clear();
|
|
111
|
+
}
|