opencode-self-improvement 0.9.0 → 0.9.1

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.9.0",
3
+ "version": "0.9.1",
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
@@ -36,6 +36,7 @@ import {
36
36
  compactionAnchor,
37
37
  contextualPull,
38
38
  formatMemory,
39
+ injectedRows,
39
40
  selectRelevant,
40
41
  storeVersion,
41
42
  systemDigest,
@@ -741,9 +742,10 @@ const impl = {
741
742
  if (cached?.version === version) return;
742
743
  const sel = await selectRelevant(latestPrompt.get(sid) ?? "", 5, mems); // widen candidate pool
743
744
  const block = sel.block ? capToBudget(sel.block, budget) : "";
744
- // Refs the injected rows assert (paths + commands), the join key for
745
- // attribution. Derived once per epoch — the block is frozen, so is this.
746
- const refs = deriveMemoryRefs(sel.selected);
745
+ // Refs the ACTUALLY-injected rows assert (paths + commands), the join key
746
+ // for attribution. Derived from the budget-surviving rows, not the wider
747
+ // candidate pool — crediting paths the model never saw would overclaim.
748
+ const refs = deriveMemoryRefs(injectedRows(block, sel.selected));
747
749
  const at = new Date().toISOString();
748
750
  const reason: "first" | "store_changed" = cached ? "store_changed" : "first";
749
751
  epoch.set(sid, { version, block, at, reason, refs });
@@ -877,15 +879,16 @@ const impl = {
877
879
  }),
878
880
  );
879
881
 
880
- // ---- measured prompt-cache health (message.updated usage) --------------
881
- // Zero-model by design: assistant messages carry tokens {input, output,
882
- // reasoning, cache.read/write}; the session-wide read/(read+input+write)
883
- // share is the honest receipt behind the byte-stability proxy. Streaming
884
- // updates for one message arrive repeatedly — overwriting with the latest
885
- // is correct because the final update carries the complete usage. Flushes
886
- // the per-session status file so the TUI badge refreshes once per turn
887
- // instead of per step. Degrades to "not measured" when the host exposes
888
- // no event stream.
882
+ // ---- measured prompt-cache health (usage events) -----------------------
883
+ // Zero-model by design: usage arrives on `session.usage.updated` (v2 hosts)
884
+ // or `message.updated` assistant info (older hosts), carrying tokens
885
+ // {input, output, reasoning, cache.read/write}; the session-wide
886
+ // read/(read+input+write) share is the honest receipt behind the
887
+ // byte-stability proxy. Streaming updates arrive repeatedly — overwriting
888
+ // with the latest is correct because the final update carries the complete
889
+ // usage. Flushes the per-session status file so the TUI badge refreshes
890
+ // once per turn instead of per step. Degrades to "not measured" when the
891
+ // host exposes no event stream.
889
892
  const measuredController = new AbortController();
890
893
  if (ctx.event?.subscribe) {
891
894
  const eventApi = ctx.event;
@@ -912,20 +915,33 @@ const impl = {
912
915
  else if (typeof sid === "string") void guarded("session bind (api)", () => bindSessionFromApi(sid));
913
916
  continue;
914
917
  }
915
- if (ev?.type !== "message.updated") continue;
916
- const info = ev.properties?.info ?? ev.info ?? ev.data?.info;
917
- if (!info || info.role !== "assistant") continue;
918
- const sid = info.sessionID;
919
- const t = info.tokens;
920
- if (typeof sid !== "string" || !t) continue;
921
- sessions.touch(sid);
922
- measuredTurns.set(sid, {
918
+ // Token usage: v2 hosts (>=2.0.x) emit `session.usage.updated` with
919
+ // data.{sessionID, tokens}; older hosts carried the same TokenUsage
920
+ // on message.updated's assistant info. Accept both so `measured`
921
+ // populates regardless of host version.
922
+ let usid: unknown;
923
+ let ut: { input?: number; cache?: { read?: number; write?: number } } | undefined;
924
+ if (ev?.type === "session.usage.updated") {
925
+ usid = ev.data?.sessionID;
926
+ ut = ev.data?.tokens;
927
+ } else if (ev?.type === "message.updated") {
928
+ const info = ev.properties?.info ?? ev.info ?? ev.data?.info;
929
+ if (info && info.role === "assistant") {
930
+ usid = info.sessionID;
931
+ ut = info.tokens;
932
+ }
933
+ } else {
934
+ continue;
935
+ }
936
+ if (typeof usid !== "string" || !ut) continue;
937
+ sessions.touch(usid);
938
+ measuredTurns.set(usid, {
923
939
  at: new Date().toISOString(),
924
- input: Number(t.input) || 0,
925
- read: Number(t.cache?.read) || 0,
926
- write: Number(t.cache?.write) || 0,
940
+ input: Number(ut.input) || 0,
941
+ read: Number(ut.cache?.read) || 0,
942
+ write: Number(ut.cache?.write) || 0,
927
943
  });
928
- await guarded("status flush (measured)", () => flushStatus(sid));
944
+ await guarded("status flush (measured)", () => flushStatus(usid));
929
945
  }
930
946
  } catch {
931
947
  // the subscription ended (abort or host error); nothing to do
package/src/inject.ts CHANGED
@@ -264,6 +264,16 @@ export async function relevantKnowledge(message: string, limit = 3, rows?: Memor
264
264
  return (await selectRelevant(message, limit, rows)).block;
265
265
  }
266
266
 
267
+ // The candidate rows that ACTUALLY survived capToBudget into `block`, in order.
268
+ // capToBudget drops whole lines, so attribution must key off these — not the
269
+ // pre-budget candidates — or it would credit the model with paths and commands
270
+ // it was never shown. capToBudget keeps whole lines verbatim, so a candidate is
271
+ // present iff its rendered line is a substring of the block.
272
+ export function injectedRows(block: string, candidates: Memory[]): Memory[] {
273
+ if (!block) return [];
274
+ return candidates.filter((m) => block.includes(formatMemory(m)));
275
+ }
276
+
267
277
  // Deterministic fingerprint of the rows that can affect injection. Hash the
268
278
  // exact rendered row plus its full id, in load order: content edits, blocklist
269
279
  // changes and supersession all invalidate an epoch, while evidence, hidden
package/src/status.ts CHANGED
@@ -55,10 +55,10 @@ export function emptyStats(): SessionStats {
55
55
  }
56
56
 
57
57
  /**
58
- * Measured prompt-cache usage from the last completed assistant message
59
- * (message.updated -> tokens {input, cache.read, cache.write}). This is the
60
- * SESSION-WIDE prompt cache — the honest receipt for "are we busting cache",
61
- * never billed as our block's own rate.
58
+ * Measured prompt-cache usage from the last usage event — `session.usage.updated`
59
+ * (v2 hosts) or `message.updated` assistant info (older hosts) — carrying tokens
60
+ * {input, cache.read, cache.write}. This is the SESSION-WIDE prompt cache — the
61
+ * honest receipt for "are we busting cache", never billed as our block's own rate.
62
62
  */
63
63
  export interface Measured {
64
64
  at: string;