opencode-self-improvement 0.8.11 → 0.8.12

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 CHANGED
@@ -225,6 +225,10 @@ Opencode installs the package (and its dependencies) automatically with Bun at
225
225
  startup. Quit and restart opencode; the plugin loads and `/repo-init` installs
226
226
  itself. Nothing else to do.
227
227
 
228
+ The plugin runs on the Bun embedded in your opencode build; if that Bun is
229
+ older than 1.3.0 (this repo's tested floor) the plugin says so once at startup
230
+ and memory features may misbehave.
231
+
228
232
  An unpinned entry resolves `@latest` exactly once and is never re-resolved
229
233
  afterwards, so updates are deliberate: `opencode plugin update
230
234
  opencode-self-improvement`. To install a specific release, append
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-self-improvement",
3
- "version": "0.8.11",
3
+ "version": "0.8.12",
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": {
@@ -10,6 +10,9 @@
10
10
  "module": "index.ts",
11
11
  "type": "module",
12
12
  "license": "MIT",
13
+ "engines": {
14
+ "bun": ">=1.3.0"
15
+ },
13
16
  "author": "Alex Bogartz",
14
17
  "repository": {
15
18
  "type": "git",
package/src/index.ts CHANGED
@@ -68,6 +68,7 @@ import { readLines } from "./logfmt.ts";
68
68
  import { buildCapturePrompt, flattenTranscript, parseCandidates, parseModelRef } from "./capture.ts";
69
69
  import { redactSecrets } from "./secrets.ts";
70
70
  import { flattenMemoryText } from "./sanitize.ts";
71
+ import { bunFloorWarning } from "./runtime.ts";
71
72
 
72
73
  // Tool/input boundaries are zod schemas (src/schema.ts): the JSON Schema the
73
74
  // host sees is GENERATED from them, and raw args are re-parsed at execution.
@@ -130,6 +131,7 @@ interface PluginCtx {
130
131
  delivery?: "steer" | "queue";
131
132
  }) => Promise<unknown>;
132
133
  context?: (input: { sessionID: string }) => Promise<unknown>;
134
+ get?: (input: { sessionID: string }) => Promise<unknown>;
133
135
  };
134
136
  event?: {
135
137
  subscribe: (o?: { signal?: AbortSignal }) => AsyncIterable<unknown>;
@@ -167,6 +169,10 @@ function resultMeta(result: unknown): Record<string, unknown> {
167
169
 
168
170
  const PLUGIN_ID = "opencode-self-improvement";
169
171
 
172
+ // The setup-time Bun-floor warning fires once per process (setup can run for
173
+ // more than one location).
174
+ let bunFloorChecked = false;
175
+
170
176
  // The real setup implementation, written against the loose local PluginCtx
171
177
  // (the only members of the V2 context this plugin actually touches).
172
178
  const impl = {
@@ -186,6 +192,15 @@ const impl = {
186
192
  const budget = Number(ctx.options?.injectBudget) || injectBudget();
187
193
  const openBudget = Number(ctx.options?.frontierBudget) || frontierBudget(budget);
188
194
 
195
+ // Warn-once Bun floor check (same setup-time warning style as the
196
+ // captureModel warnings below): opencode embeds Bun, and the store code's
197
+ // fs failure modes are Bun-version dependent, so flag an old host.
198
+ if (!bunFloorChecked) {
199
+ bunFloorChecked = true;
200
+ const floorWarning = bunFloorWarning(typeof Bun === "undefined" ? undefined : Bun.version);
201
+ if (floorWarning) console.warn(floorWarning);
202
+ }
203
+
189
204
  // Mirror the /repo-init slash command into opencode's global commands dir
190
205
  // (idempotent, write-if-missing). OpenCode 2.x loads `*.md` from
191
206
  // ~/.config/opencode/commands, so this is the canonical V2 install path.
@@ -212,8 +227,10 @@ const impl = {
212
227
  // Identity-event race backstop: a resumed session's FIRST turn can write
213
228
  // before the subscription has delivered its execution.started, and the
214
229
  // module-scope fallback is the last setup instance — possibly another
215
- // workspace. The session context API is synchronous and its messages
216
- // carry location.directory, so bind once from them for unmapped sids.
230
+ // workspace. Bind once from the session context API for unmapped sids —
231
+ // but note host 2.1.4 sends message payloads WITHOUT any location field,
232
+ // so this path is a legacy-shape best-effort; the session-row fallback
233
+ // below is what actually resolves serve-host sessions.
217
234
  // One-shot: never overwrites an event-bound entry.
218
235
  const bindingSids = new Set<string>();
219
236
  async function bindSessionFromMessages(sid: string | undefined): Promise<void> {
@@ -239,6 +256,37 @@ const impl = {
239
256
  }
240
257
  }
241
258
 
259
+ // Serve-host reality (recorded on 2.1.4 `serve --service`): the plugin
260
+ // instance loads AFTER the session was created, so `session.created` — the
261
+ // only event carrying a location — is never delivered; `execution.started`
262
+ // arrives with just {sessionID}. The session ROW is the remaining source
263
+ // of truth: the host's session get returns `location.directory`. One
264
+ // lookup per session; a miss is remembered so a host without the API is
265
+ // not re-asked on every turn (bounded LRU, same cap as the scope cache).
266
+ const apiBindTried = new Set<string>();
267
+ const apiBindOrder = new SessionLru(SESSION_CAP, (evicted) => apiBindTried.delete(evicted));
268
+ async function bindSessionFromApi(sid: string | undefined): Promise<void> {
269
+ if (!sid || sid === "?" || sessionDir(sid) || apiBindTried.has(sid)) return;
270
+ apiBindTried.add(sid);
271
+ apiBindOrder.touch(sid);
272
+ try {
273
+ const got = (await ctx.session?.get?.({ sessionID: sid })) as Record<string, any> | undefined;
274
+ const row = (got?.data ?? got) as Record<string, any> | undefined;
275
+ const dir = row?.location?.directory ?? row?.directory;
276
+ if (typeof dir === "string" && dir) rememberSessionDir(sid, dir);
277
+ } catch {
278
+ // no session.get on this host (or unknown session): stay unbound
279
+ }
280
+ }
281
+
282
+ // Identity must resolve BEFORE a row is stamped or a read is partitioned:
283
+ // try the (legacy) message payloads, then the session row.
284
+ async function bindSessionIdentity(sid: string | undefined): Promise<void> {
285
+ if (!sid || sid === "?" || sessionDir(sid)) return;
286
+ await bindSessionFromMessages(sid);
287
+ if (!sessionDir(sid)) await bindSessionFromApi(sid);
288
+ }
289
+
242
290
  // ---- tools --------------------------------------------------------------
243
291
  await ctx.tool?.transform((editor) => {
244
292
  const direct = { codemode: false } as const;
@@ -258,7 +306,7 @@ const impl = {
258
306
  status: args.status,
259
307
  repo: args.global ? "global" : undefined,
260
308
  };
261
- await bindSessionFromMessages(getCurrent().sid);
309
+ await bindSessionIdentity(getCurrent().sid);
262
310
  const outcome = await runWriteGate(candidate, gateContext());
263
311
  if (outcome.action === "promoted") return { content: `Remembered: ${args.type} in ${args.scope}` };
264
312
  if (outcome.action === "collapsed") return { content: `Duplicate ignored: ${args.type}/${args.scope}` };
@@ -276,7 +324,7 @@ const impl = {
276
324
  "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).",
277
325
  options: direct,
278
326
  async execute(args): Promise<{ content: string }> {
279
- await bindSessionFromMessages(getCurrent().sid);
327
+ await bindSessionIdentity(getCurrent().sid);
280
328
  const summary = await runInit({ liveRoot: gateContext().liveRoot, force: args.force, mode: args.mode });
281
329
  return { content: formatInit(summary) };
282
330
  },
@@ -323,7 +371,7 @@ const impl = {
323
371
  "Update an existing memory (gated like remember; the prior entry is superseded, not erased). `query` selects which row to update: its exact id (ts), a clock fragment (e.g. HH:MM:SS.mmm), or a unique substring of its content. Omitting `query` targets the most recent active row of scope+type. An unmatched or ambiguous `query` fails closed and writes nothing.",
324
372
  options: direct,
325
373
  async execute(args): Promise<{ content: string }> {
326
- await bindSessionFromMessages(getCurrent().sid);
374
+ await bindSessionIdentity(getCurrent().sid);
327
375
  const prior = (await loadAll()).filter(
328
376
  (m) => m.scope === args.scope && m.type === args.type && isActive(m) && isVisibleHere(m),
329
377
  );
@@ -794,7 +842,7 @@ const impl = {
794
842
  sessions.touch(sid);
795
843
  markTurn(sid);
796
844
  await checkNewSession(sid);
797
- await bindSessionFromMessages(sid);
845
+ await bindSessionIdentity(sid);
798
846
  latestPrompt.set(sid, event?.prompt?.text ?? "");
799
847
  await refreshEpoch(sid);
800
848
  }
@@ -850,6 +898,9 @@ const impl = {
850
898
  const dir =
851
899
  info.location?.directory ?? d.location?.directory ?? ev.location?.directory ?? info.directory;
852
900
  if (typeof sid === "string" && typeof dir === "string") rememberSessionDir(sid, dir);
901
+ // 2.1.4 serve sends execution.started with no location at all:
902
+ // resolve the session row instead of leaving the session unbound.
903
+ else if (typeof sid === "string") void guarded("session bind (api)", () => bindSessionFromApi(sid));
853
904
  continue;
854
905
  }
855
906
  if (ev?.type !== "message.updated") continue;
package/src/runtime.ts ADDED
@@ -0,0 +1,40 @@
1
+ // Bun version floor. The plugin runs inside opencode, whose v2 compiled
2
+ // binaries embed Bun (upstream builds with BUN_COMPILE_RELEASE=bun-v1.4.4;
3
+ // older v2 builds embed older bun). The store code's filesystem calls have
4
+ // version-divergent failure modes — e.g. DirHandle.close() behaves differently
5
+ // on some Bun+Windows combos — so an old host shows up as odd memory errors,
6
+ // not as a clean "unsupported" signal. MIN_BUN is the lower bound of the Bun
7
+ // versions this repo has actually been run against: 1.3.10 and 1.4.4. Pure
8
+ // module: nothing here reads the environment; the caller (src/index.ts)
9
+ // injects the version string, so these helpers are unit-testable anywhere.
10
+
11
+ export const MIN_BUN: readonly [number, number, number] = [1, 3, 0];
12
+
13
+ // Pulls the first x.y.z triple out of a version string: "1.4.4",
14
+ // "v1.4.4-canary" and "bun-v1.4.4" all parse; junk or a partial version
15
+ // (no patch component) yields null. Never throws.
16
+ export function parseBunVersion(raw: string): [number, number, number] | null {
17
+ const m = /v?(\d+)\.(\d+)\.(\d+)/.exec(raw);
18
+ if (!m) return null;
19
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
20
+ }
21
+
22
+ // Numeric (not lexical) triple compare: 1.10.0 > 1.3.0.
23
+ function cmpTriples(a: readonly number[], b: readonly number[]): number {
24
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
25
+ }
26
+
27
+ // One-line startup warning when the host's Bun is below MIN_BUN or its version
28
+ // could not be parsed; null when the version looks fine. Warn-only by design:
29
+ // an old embedded bun should be visible, but must not stop memory from
30
+ // loading. The caller decides how often to emit it (src/index.ts: once).
31
+ export function bunFloorWarning(versionString: string | undefined): string | null {
32
+ const floor = MIN_BUN.join(".");
33
+ const seen = typeof versionString === "string" ? parseBunVersion(versionString) : null;
34
+ if (seen) {
35
+ if (cmpTriples(seen, MIN_BUN) >= 0) return null;
36
+ return `[opencode-self-improvement] Bun ${seen.join(".")} is below the supported minimum ${floor}; memory features may misbehave.`;
37
+ }
38
+ const shown = versionString === undefined ? "unknown" : JSON.stringify(versionString);
39
+ return `[opencode-self-improvement] Bun version ${shown} is unrecognized (supported minimum ${floor}); memory features may misbehave.`;
40
+ }