opencode-usage-coach 0.4.0 → 0.5.0

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
@@ -153,6 +153,8 @@ Place in the **work directory**. Each role runs on its model, so per-model quota
153
153
  | `UC_TTL_MS` | 60000 | quota cache TTL (ms) |
154
154
  | `UC_DEBUG` | 0 | set to `1` for a diagnostic log at `~/.cache/opencode-usage-coach/coach.log` |
155
155
  | `UC_HARNESS_AGENT` | `Usage-Coach-Harness` | comma-separated agent modes allowed to use harness tools + receive quota coaching (case-insensitive; must match the agent id, e.g. `usage-coach-harness` from `agents/usage-coach-harness.md`) |
156
+ | `UC_WORM_MAX_AGE_DAYS` | 180 | domain DB worm (GC): drop nodes not accessed in N days (~6 months) |
157
+ | `UC_WORM_MAX_NODES` | 100000 | domain DB worm (GC): cap node count, evict oldest-accessed beyond this |
156
158
 
157
159
  ## Agent-mode scoping
158
160
 
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import { mkdirSync as mkdirSync2, writeFileSync, appendFileSync as appendFileSync2, readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
2
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
3
3
  import { spawn } from "child_process";
4
4
  import { createHash } from "crypto";
5
5
  import { homedir } from "os";
@@ -7,7 +7,7 @@ import { join as join2, resolve, dirname } from "path";
7
7
  import { tool } from "@opencode-ai/plugin";
8
8
 
9
9
  // src/domain.ts
10
- import { mkdirSync, appendFileSync, readFileSync, existsSync } from "fs";
10
+ import { mkdirSync, appendFileSync, readFileSync, existsSync, writeFileSync } from "fs";
11
11
  import { join } from "path";
12
12
  var BASE_DIR = "";
13
13
  function initDomain(stateDir) {
@@ -41,6 +41,14 @@ function addDomainNode(node) {
41
41
  }
42
42
  return full.id;
43
43
  }
44
+ function writeNodes(nodes) {
45
+ try {
46
+ mkdirSync(BASE_DIR, { recursive: true });
47
+ const lines = nodes.map((n) => JSON.stringify(n));
48
+ writeFileSync(nodesFile(), lines.length ? lines.join("\n") + "\n" : "");
49
+ } catch {
50
+ }
51
+ }
44
52
  function queryDomain(keywords) {
45
53
  const lc = keywords.map((k) => k.toLowerCase());
46
54
  const nodes = readNodes();
@@ -48,10 +56,47 @@ function queryDomain(keywords) {
48
56
  const hay = (n.name + " " + JSON.stringify(n.props)).toLowerCase();
49
57
  return lc.some((k) => k && hay.includes(k));
50
58
  });
59
+ if (matched.length) touchNodes(new Set(matched.map((n) => n.id)));
51
60
  const ids = new Set(matched.map((n) => n.id));
52
61
  const edges = readEdges().filter((e) => ids.has(e.from) || ids.has(e.to));
53
62
  return { nodes: matched, edges };
54
63
  }
64
+ function touchNodes(ids) {
65
+ if (ids.size === 0) return;
66
+ try {
67
+ const nodes = readNodes();
68
+ let changed = false;
69
+ const now = (/* @__PURE__ */ new Date()).toISOString();
70
+ for (const n of nodes) {
71
+ if (ids.has(n.id)) {
72
+ n.lastAccessed = now;
73
+ n.accessCount = (n.accessCount ?? 0) + 1;
74
+ changed = true;
75
+ }
76
+ }
77
+ if (changed) writeNodes(nodes);
78
+ } catch {
79
+ }
80
+ }
81
+ function evictStale(maxAgeDays = 30, maxNodes = 1e3) {
82
+ try {
83
+ const nodes = readNodes();
84
+ if (nodes.length === 0) return { removed: 0, kept: 0 };
85
+ const now = Date.now();
86
+ const ageMs = maxAgeDays * 864e5;
87
+ const lastTs = (n) => new Date(n.lastAccessed ?? n.ts).getTime();
88
+ let kept = nodes.filter((n) => now - lastTs(n) < ageMs);
89
+ if (kept.length > maxNodes) {
90
+ kept.sort((a, b) => lastTs(b) - lastTs(a));
91
+ kept = kept.slice(0, maxNodes);
92
+ }
93
+ const removed = nodes.length - kept.length;
94
+ if (removed > 0) writeNodes(kept);
95
+ return { removed, kept: kept.length };
96
+ } catch {
97
+ return { removed: 0, kept: 0 };
98
+ }
99
+ }
55
100
  function saveInvestigationResult(keywords, result, source) {
56
101
  try {
57
102
  return addDomainNode({
@@ -93,7 +138,7 @@ function log(msg) {
93
138
  function writeState(c) {
94
139
  try {
95
140
  mkdirSync2(STATE_DIR, { recursive: true });
96
- writeFileSync(STATE_FILE, JSON.stringify({ ...c, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
141
+ writeFileSync2(STATE_FILE, JSON.stringify({ ...c, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
97
142
  } catch {
98
143
  }
99
144
  }
@@ -146,7 +191,7 @@ function writeHarness(sessionID, h) {
146
191
  const f = harnessFile(sessionID);
147
192
  mkdirSync2(dirname(f), { recursive: true });
148
193
  h.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
149
- writeFileSync(f, JSON.stringify(h, null, 2));
194
+ writeFileSync2(f, JSON.stringify(h, null, 2));
150
195
  } catch {
151
196
  }
152
197
  }
@@ -211,6 +256,8 @@ var THR_5H = num("UC_THROTTLE_5H", 70);
211
256
  var STOP_WK = num("UC_STOP_WEEKLY", 95);
212
257
  var THR_WK = num("UC_THROTTLE_WEEKLY", 85);
213
258
  var STOP_MO = num("UC_STOP_MONTHLY", 98);
259
+ var WORM_MAX_AGE_DAYS = num("UC_WORM_MAX_AGE_DAYS", 180);
260
+ var WORM_MAX_NODES = num("UC_WORM_MAX_NODES", 1e5);
214
261
  function humanRemaining(iso) {
215
262
  try {
216
263
  if (!iso) return "";
@@ -397,6 +444,14 @@ async function UsageCoachPlugin(input) {
397
444
  event: async ({ event }) => {
398
445
  try {
399
446
  if (event.type === "session.created" || event.type === "session.idle") refreshBackground();
447
+ if (event.type === "session.idle") {
448
+ try {
449
+ const r = evictStale(WORM_MAX_AGE_DAYS, WORM_MAX_NODES);
450
+ if (r.removed) log(`evictStale: removed ${r.removed}, kept ${r.kept} (maxAge=${WORM_MAX_AGE_DAYS}d, maxNodes=${WORM_MAX_NODES})`);
451
+ } catch (e) {
452
+ log(`evictStale err: ${String(e)}`);
453
+ }
454
+ }
400
455
  } catch (e) {
401
456
  log(`event err: ${String(e)}`);
402
457
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-usage-coach",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "opencode closed-loop usage coach — quota SENSE -> coaching DECIDE -> loop ACT + TUI integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",