@gamaze/hicortex 0.17.1 → 0.17.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/README.md CHANGED
@@ -218,9 +218,10 @@ Config at `~/.hicortex/config.json`. Created by `init`. Key options:
218
218
  | `captureCooldownHours` | Success-cooldown (hours) for the **capture watchdog** (0.17). The capture timer polls every ~20 min; the watchdog captures only if more than this has elapsed since the last *successful* capture (`state.lastNightly`). Default `6` (≈4 captures/day). A failed preflight retries on the next poll (~20 min) — so a transient fire-instant network miss costs minutes, not a day (#239) |
219
219
  | `consolidationHours` | Hours (0–23, local) for the **consolidation** timer — the full nightly (capture + distill + score + reflect + link). Installed for **server/co-located only** (clients have no local DB). Default `[10, 22]`: the 22:00 evening slot runs after the day's capture waves (same-day results); the 10:00 morning slot runs *after* the morning capture so wake-up pushes are caught. Omitted on clients |
220
220
  | `consolidateMaxLlmCalls` | Ceiling on total LLM calls across all classify-tier consolidation stages (content-domain, link discovery, supersession) per run. A runaway **backstop**, not a throughput throttle — on a free local model the binding constraint is the nightly unit's wall-clock timeout, not call count. Default `5000` (was a hard-coded 200 that starved link/supersession during a classification backlog) |
221
+ | `memorySoftCap` | Soft cap on the memory corpus (default 10000). When the corpus exceeds this, the nightly's capacity-eviction stage removes the lowest-`effectiveStrength` memories (ties broken by oldest access) until under the cap — the active forgetting mechanism that bounds DB size, vector-index RAM, and consolidation workload. `0` disables eviction (indefinite growth — the pre-#245 behaviour). The evicted tail is cold by construction (effectiveStrength is the same decay-weighted score the recall ranker uses, so these were not surfacing in the top-k anyway). At 10K memories the load + JS sort is <100 ms |
221
222
  | `updateChannel` | Release channel pinned into the generated daemon/timer ExecStart for **npx-thin** installs (global-binary installs use the absolute binary and are unaffected). A dist-tag (`"rc"`, `"next"`) or an exact version (`"0.17.1"`). E.g. `"rc"` → the timer runs `npx -y @gamaze/hicortex@rc nightly`, so the host tracks the rc dist-tag (an internal fleet can ride rc through a pre-promotion soak). Validated as `[\w.\-]+` (rejects anything that'd break the unit/plist templates). Absent → auto-detect (bare on `latest`, else `@next`). (0.17.1) |
222
223
  | `nightlyHour` | **Deprecated (0.17) single-slot fallback.** Local hour (0–23) honoured only when `consolidationHours` is absent — yields one daily consolidation slot at that hour (preserves the pre-0.17 "one daily job" intent). New installs should use `consolidationHours` |
223
- | `preflightTimeoutMs` | **Client mode only.** Per-attempt timeout for the nightly's server-reachability check before it starts capturing (default: 20000 ms, bumped from 15000 in 0.17 to absorb a cold Tailscale handshake on a roaming laptop) |
224
+ | `preflightTimeoutMs` | **Client mode only.** Per-attempt timeout for the nightly's server-reachability check before it starts capturing (default: 20000 ms, bumped from 15000 in 0.17 to absorb a slow link re-establishing after the client wakes) |
224
225
  | `preflightAttempts` | **Client mode only.** Reachability-check retries before the nightly aborts (default: 3; floored at 1). `1` = single try, no retry |
225
226
  | `preflightRetryGapMs` | **Client mode only.** Delay between reachability retries (default: 60000 ms). Note: timers don't advance while the machine is asleep, so on a sleeping laptop this gap counts awake-time, not wall-clock |
226
227
  | `scoreSimilarityWeight` | Weight of semantic similarity in the ranking score (default: 0.50) |
@@ -34,6 +34,7 @@
34
34
  --danger: #c0392b;
35
35
  --ok: #2f8f4e;
36
36
  --tab-active: #ffffff;
37
+ --nav-bg: #f6f7f9;
37
38
  }
38
39
  @media (prefers-color-scheme: dark) {
39
40
  :root {
@@ -47,6 +48,7 @@
47
48
  --danger: #e05555;
48
49
  --ok: #5fbf77;
49
50
  --tab-active: #0f1117;
51
+ --nav-bg: #010409;
50
52
  }
51
53
  }
52
54
  /* Explicit theme override wins over the media default in BOTH directions. */
@@ -54,11 +56,13 @@
54
56
  --bg: #ffffff; --panel: #f6f7f9; --panel-border: #d9dce2;
55
57
  --text: #1c1f26; --text-dim: #656d7a; --accent: #2f6fb0;
56
58
  --accent-text: #ffffff; --danger: #c0392b; --ok: #2f8f4e; --tab-active: #ffffff;
59
+ --nav-bg: #f6f7f9;
57
60
  }
58
61
  :root[data-theme="dark"] {
59
62
  --bg: #0f1117; --panel: #161a23; --panel-border: #262c3a;
60
63
  --text: #d5dae4; --text-dim: #8b93a5; --accent: #5b9dd9;
61
64
  --accent-text: #0b0d12; --danger: #e05555; --ok: #5fbf77; --tab-active: #0f1117;
65
+ --nav-bg: #010409;
62
66
  }
63
67
 
64
68
  * { box-sizing: border-box; margin: 0; padding: 0; }
@@ -165,9 +169,33 @@
165
169
  padding: 8px; font: inherit; margin-bottom: 10px;
166
170
  }
167
171
  #token-err { color: var(--danger); font-size: 13px; margin-top: 8px; display: none; }
172
+
173
+ /* ---- shared console nav (#249) ---- */
174
+ .hc-nav {
175
+ display: flex; align-items: center; gap: 16px;
176
+ height: 48px; padding: 0 18px; flex: none;
177
+ background: var(--nav-bg);
178
+ border-bottom: 1px solid var(--panel-border);
179
+ font-size: 14px;
180
+ }
181
+ .hc-nav-logo { color: var(--text); font-weight: 600; text-decoration: none; letter-spacing: 0.02em; }
182
+ .hc-nav-links { margin-left: auto; display: flex; align-items: center; gap: 16px; }
183
+ .hc-nav-links a { color: var(--text-dim); text-decoration: none; transition: color 0.12s; }
184
+ .hc-nav-links a:hover { color: var(--accent); }
185
+ .hc-nav-links a[data-nav-active] { color: var(--text); }
186
+ .hc-nav-disabled { color: var(--text-dim); opacity: 0.4; cursor: not-allowed; user-select: none; }
168
187
  </style>
169
188
  </head>
170
189
  <body>
190
+ <nav class="hc-nav">
191
+ <a class="hc-nav-logo" href="/dashboard">Hicortex</a>
192
+ <div class="hc-nav-links">
193
+ <a href="/dashboard">Dashboard</a>
194
+ <a href="/viz">Graph</a>
195
+ <a href="/context/ui" data-nav-active>Context</a>
196
+ <span class="hc-nav-disabled" title="Coming soon — #250">Self-improvement</span>
197
+ </div>
198
+ </nav>
171
199
  <header>
172
200
  <h1>Hicortex — context</h1>
173
201
  <label class="meta" for="scope">scope</label>
@@ -249,6 +277,22 @@
249
277
  }
250
278
  })();
251
279
 
280
+ // Shared console nav (#249): append ?token= to internal nav links on click
281
+ // so navigation between pages doesn't re-authenticate. Reads token from any
282
+ // page-scoped localStorage key (each page historically used its own).
283
+ document.querySelectorAll(".hc-nav-links a, .hc-nav-logo").forEach(function (a) {
284
+ a.addEventListener("click", function (e) {
285
+ var t = token
286
+ || localStorage.getItem("hicortexToken")
287
+ || localStorage.getItem("hicortex-dashboard-token");
288
+ if (!t) return;
289
+ e.preventDefault();
290
+ var u = new URL(a.getAttribute("href"), location.origin);
291
+ u.searchParams.set("token", t);
292
+ location.href = u.toString();
293
+ });
294
+ });
295
+
252
296
  function authHeaders(extra) {
253
297
  var h = extra ? Object.assign({}, extra) : {};
254
298
  if (token) h["Authorization"] = "Bearer " + token;
@@ -41,6 +41,7 @@
41
41
  --accent-2: #2f8f4e;
42
42
  --warn: #b08030;
43
43
  --danger: #c0392b;
44
+ --nav-bg: #f6f7f9;
44
45
  }
45
46
  }
46
47
  * { box-sizing: border-box; margin: 0; padding: 0; }
@@ -89,9 +90,42 @@
89
90
  table.digest td.k { color: var(--text-dim); width: 180px; }
90
91
  .stage-pill { display: inline-block; padding: 1px 6px; border-radius: 10px; background: rgba(128,128,128,0.18); font-size: 11px; color: var(--text-dim); margin-right: 4px; }
91
92
  .empty { color: var(--text-dim); font-style: italic; padding: 12px 0; }
93
+
94
+ /* ---- shared console nav (#249) ---- */
95
+ .hc-nav {
96
+ position: sticky; top: 0; z-index: 50;
97
+ display: flex; align-items: center; gap: 16px;
98
+ height: 48px; padding: 0 20px;
99
+ margin: -20px -20px 20px;
100
+ background: var(--nav-bg, #010409);
101
+ border-bottom: 1px solid var(--panel-border);
102
+ font-size: 14px;
103
+ }
104
+ .hc-nav-logo {
105
+ color: var(--text); font-weight: 600; text-decoration: none;
106
+ letter-spacing: 0.02em;
107
+ }
108
+ .hc-nav-links { margin-left: auto; display: flex; align-items: center; gap: 16px; }
109
+ .hc-nav-links a {
110
+ color: var(--text-dim); text-decoration: none; transition: color 0.12s;
111
+ }
112
+ .hc-nav-links a:hover { color: var(--accent); }
113
+ .hc-nav-links a[data-nav-active] { color: var(--text); }
114
+ .hc-nav-disabled {
115
+ color: var(--text-dim); opacity: 0.4; cursor: not-allowed; user-select: none;
116
+ }
92
117
  </style>
93
118
  </head>
94
119
  <body>
120
+ <nav class="hc-nav">
121
+ <a class="hc-nav-logo" href="/dashboard">Hicortex</a>
122
+ <div class="hc-nav-links">
123
+ <a href="/dashboard" data-nav-active>Dashboard</a>
124
+ <a href="/viz">Graph</a>
125
+ <a href="/context/ui">Context</a>
126
+ <span class="hc-nav-disabled" title="Coming soon — #250">Self-improvement</span>
127
+ </div>
128
+ </nav>
95
129
  <h1>Hicortex — memory dashboard</h1>
96
130
  <div class="sub">View-only analytics. All metrics derived from the live corpus + nightly snapshots.</div>
97
131
 
@@ -147,6 +181,8 @@
147
181
  <div id="digest-lessons"></div>
148
182
  <h2 style="margin-top:16px;">Dedup merges</h2>
149
183
  <div id="digest-dedup"></div>
184
+ <h2 id="digest-tokens-section" style="margin-top:16px;display:none;">Token usage breakdown by stage</h2>
185
+ <div id="digest-tokens"></div>
150
186
  </div>
151
187
 
152
188
  <div class="sub muted" style="margin-top:24px;">
@@ -205,13 +241,71 @@ function renderHeadline(h) {
205
241
  const ups = h.uses_per_showing === null || h.uses_per_showing === undefined
206
242
  ? "—"
207
243
  : (h.uses_per_showing * 100).toFixed(1) + "%";
244
+ // Capacity gauge (#245): X / cap memories. Amber at ≥85% of cap, red at ≥100%.
245
+ // Hidden when the cap is 0 (disabled — indefinite growth, the pre-#245 default).
246
+ let capStat = "";
247
+ if (h.memory_soft_cap && h.memory_soft_cap > 0) {
248
+ const cap = h.memory_soft_cap;
249
+ const n = h.total_memories;
250
+ const pct = Math.min(100, (n / cap) * 100);
251
+ const over = n >= cap;
252
+ const near = pct >= 85 && !over;
253
+ const barColor = over ? "var(--bad, #e05555)" : near ? "var(--warn, #e0b055)" : "var(--ok, #61c98f)";
254
+ capStat = `
255
+ <div class="stat" style="flex-basis:240px;">
256
+ <div class="v" style="font-size:18px;">${n.toLocaleString()} <span class="muted" style="font-size:13px;">/ ${cap.toLocaleString()}</span></div>
257
+ <div class="l">Memories vs soft cap ${over ? "(over cap)" : ""}</div>
258
+ <div style="margin-top:6px;height:6px;background:rgba(128,128,128,0.2);border-radius:3px;overflow:hidden;">
259
+ <div style="width:${pct.toFixed(1)}%;height:100%;background:${barColor};"></div>
260
+ </div>
261
+ </div>`;
262
+ }
263
+ // LLM tokens this period (#246): hidden when `used` is 0 (no metered run yet
264
+ // — nothing to show). When capped (cap > 0), render a usage bar with the
265
+ // percentage; amber at ≥85%, red at ≥100%. When uncapped (self-hosted
266
+ // default), show just the count. Both shapes include "this month" so the
267
+ // billing-period framing is uniform.
268
+ let tokenStat = "";
269
+ const tok = h.tokens;
270
+ if (tok && tok.used > 0) {
271
+ const usedStr = formatTokens(tok.used);
272
+ if (tok.cap > 0) {
273
+ const pct = Math.min(100, (tok.used / tok.cap) * 100);
274
+ const over = tok.used >= tok.cap;
275
+ const near = pct >= 85 && !over;
276
+ const barColor = over ? "var(--bad, #e05555)" : near ? "var(--warn, #e0b055)" : "var(--ok, #61c98f)";
277
+ tokenStat = `
278
+ <div class="stat" style="flex-basis:240px;">
279
+ <div class="v" style="font-size:18px;">${escapeHtml(usedStr)} <span class="muted" style="font-size:13px;">/ ${escapeHtml(formatTokens(tok.cap))}</span></div>
280
+ <div class="l">LLM tokens this month ${over ? "(over cap)" : ""}</div>
281
+ <div style="margin-top:6px;height:6px;background:rgba(128,128,128,0.2);border-radius:3px;overflow:hidden;">
282
+ <div style="width:${pct.toFixed(1)}%;height:100%;background:${barColor};"></div>
283
+ </div>
284
+ </div>`;
285
+ } else {
286
+ tokenStat = `
287
+ <div class="stat">
288
+ <div class="v">${escapeHtml(usedStr)}</div>
289
+ <div class="l">LLM tokens this month</div>
290
+ </div>`;
291
+ }
292
+ }
208
293
  $("headline").innerHTML = `
209
294
  <div class="stat"><div class="v">${h.total_memories}</div><div class="l">Total memories</div></div>
210
295
  <div class="stat"><div class="v">${ups}</div><div class="l">Uses per showing (headline)</div></div>
211
296
  <div class="stat"><div class="v warn">${h.cold_count}</div><div class="l">Cold long-tail (never shown/used)</div></div>
297
+ ${capStat}
298
+ ${tokenStat}
212
299
  `;
213
300
  }
214
301
 
302
+ /** Compact human-readable token count: 1234 → "1.2K", 1_500_000 → "1.5M". */
303
+ function formatTokens(n) {
304
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
305
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
306
+ return String(n);
307
+ }
308
+
215
309
  // ---------------------------------------------------------------------------
216
310
  // Render: growth chart (hand-rolled inline SVG line chart)
217
311
  //
@@ -348,8 +442,27 @@ function renderDigest(digest) {
348
442
  s.lessonsGenerated !== undefined ? `<span class="stage-pill">lessons: ${s.lessonsGenerated}</span>` : "",
349
443
  `<span class="stage-pill">dedup: ${s.dedup}</span>`,
350
444
  `<span class="stage-pill">supersession: ${s.supersession}</span>`,
445
+ s.evicted !== undefined && s.evicted > 0 ? `<span class="stage-pill warn">evicted: ${s.evicted}</span>` : "",
446
+ s.tokens !== undefined ? `<span class="stage-pill">tokens: ${escapeHtml(formatTokens(s.tokens))}</span>` : "",
351
447
  ].join(" ");
352
448
  $("digest-stages").innerHTML = stages;
449
+ // Per-stage token breakdown (#246) — only when the day's run metered tokens
450
+ // and the breakdown was carried (always present together with `tokens`).
451
+ const tbs = s.tokens_by_stage;
452
+ if (tbs && Object.keys(tbs).length > 0) {
453
+ const rows = Object.entries(tbs)
454
+ .sort((a, b) => (b[1].total - a[1].total))
455
+ .map(([k, v]) =>
456
+ `<tr><td class="k">${escapeHtml(k)}</td>` +
457
+ `<td>${escapeHtml(formatTokens(v.total))}</td>` +
458
+ `<td class="muted">${escapeHtml(formatTokens(v.prompt))} in / ${escapeHtml(formatTokens(v.completion))} out</td></tr>`
459
+ ).join("");
460
+ $("digest-tokens").innerHTML = `<table class="digest"><tbody>${rows}</tbody></table>`;
461
+ $("digest-tokens-section").style.display = "";
462
+ } else {
463
+ $("digest-tokens").innerHTML = `<div class="empty">No token usage that day (no metered LLM run).</div>`;
464
+ $("digest-tokens-section").style.display = "none";
465
+ }
353
466
 
354
467
  if (digest.sample.length === 0) {
355
468
  $("digest-sample").innerHTML = `<div class="empty">No new memories that day.</div>`;
@@ -427,6 +540,22 @@ async function load() {
427
540
  }
428
541
 
429
542
  document.addEventListener("DOMContentLoaded", () => {
543
+ // Shared console nav (#249): append ?token= to internal nav links on click
544
+ // so navigation between pages doesn't re-authenticate. Reads the token from
545
+ // any of the page-scoped localStorage keys (each page historically used its
546
+ // own key) — keeps the snippet identical across the three served pages.
547
+ document.querySelectorAll(".hc-nav-links a, .hc-nav-logo").forEach(function (a) {
548
+ a.addEventListener("click", function (e) {
549
+ var t = token
550
+ || localStorage.getItem("hicortexToken")
551
+ || localStorage.getItem("hicortex-dashboard-token");
552
+ if (!t) return;
553
+ e.preventDefault();
554
+ var u = new URL(a.getAttribute("href"), location.origin);
555
+ u.searchParams.set("token", t);
556
+ location.href = u.toString();
557
+ });
558
+ });
430
559
  // Seed the date picker to the digest date from the first load (today).
431
560
  $("reload").addEventListener("click", load);
432
561
  $("range").addEventListener("change", load);
package/assets/viz.html CHANGED
@@ -55,17 +55,26 @@
55
55
  #topbar {
56
56
  position: fixed; top: 0; left: 0; right: 0;
57
57
  display: flex; align-items: center; gap: 10px;
58
- padding: 8px 12px;
58
+ height: 48px;
59
+ padding: 0 12px;
59
60
  background: rgba(22, 26, 35, 0.94);
60
61
  border-bottom: 1px solid var(--panel-border);
61
62
  z-index: 10;
62
63
  }
63
64
  #topbar h1 { font-size: 14px; font-weight: 600; white-space: nowrap; }
64
- #meta { color: var(--text-dim); margin-left: auto; white-space: nowrap; }
65
+ #meta { color: var(--text-dim); white-space: nowrap; margin-left: auto; }
66
+
67
+ /* ---- shared console nav links (#249), integrated into the viz topbar ---- */
68
+ .hc-nav-logo { color: var(--text); text-decoration: none; letter-spacing: 0.02em; }
69
+ .hc-nav-links { margin-left: 16px; display: flex; align-items: center; gap: 16px; font-size: 14px; }
70
+ .hc-nav-links a { color: var(--text-dim); text-decoration: none; transition: color 0.12s; }
71
+ .hc-nav-links a:hover { color: var(--accent); }
72
+ .hc-nav-links a[data-nav-active] { color: var(--text); }
73
+ .hc-nav-disabled { color: var(--text-dim); opacity: 0.4; cursor: not-allowed; user-select: none; }
65
74
 
66
75
  /* ---- left control panel ---- */
67
76
  #panel {
68
- position: fixed; top: 46px; left: 12px; width: 236px;
77
+ position: fixed; top: 48px; left: 12px; width: 236px;
69
78
  background: rgba(22, 26, 35, 0.94);
70
79
  border: 1px solid var(--panel-border); border-radius: 8px;
71
80
  padding: 12px; z-index: 10;
@@ -235,8 +244,14 @@
235
244
  <div id="graph-2d"></div>
236
245
 
237
246
  <div id="topbar">
238
- <h1>Hicortex knowledge graph</h1>
247
+ <h1 class="hc-nav-logo"><a href="/dashboard" style="color:inherit;text-decoration:none">Hicortex</a></h1>
239
248
  <span id="meta"></span>
249
+ <nav class="hc-nav-links">
250
+ <a href="/dashboard">Dashboard</a>
251
+ <a href="/viz" data-nav-active>Graph</a>
252
+ <a href="/context/ui">Context</a>
253
+ <span class="hc-nav-disabled" title="Coming soon — #250">Self-improvement</span>
254
+ </nav>
240
255
  </div>
241
256
 
242
257
  <div id="panel">
@@ -343,6 +358,22 @@
343
358
  }
344
359
  })();
345
360
 
361
+ // Shared console nav (#249): append ?token= to internal nav links on click
362
+ // so navigation between pages doesn't re-authenticate. Reads token from any
363
+ // page-scoped localStorage key (each page historically used its own).
364
+ document.querySelectorAll(".hc-nav-links a, .hc-nav-logo").forEach(function (a) {
365
+ a.addEventListener("click", function (e) {
366
+ var t = token
367
+ || localStorage.getItem("hicortexToken")
368
+ || localStorage.getItem("hicortex-dashboard-token");
369
+ if (!t) return;
370
+ e.preventDefault();
371
+ var u = new URL(a.getAttribute("href"), location.origin);
372
+ u.searchParams.set("token", t);
373
+ location.href = u.toString();
374
+ });
375
+ });
376
+
346
377
  // =========================================================================
347
378
  // DOM references
348
379
  // =========================================================================
package/dist/cli.d.ts CHANGED
@@ -6,8 +6,9 @@
6
6
  * server Start the MCP HTTP/SSE server (persistent daemon)
7
7
  * init Detect existing setup and configure for CC/OC
8
8
  * nightly Run capture + consolidate (manual trigger)
9
- * nightly --capture-only Capture only, skip consolidation
10
- * nightly --status Show nightly pipeline health check
9
+ * nightly --capture-only Capture only, skip consolidation
10
+ * nightly --consolidate-only Consolidate only, skip capture (hosted service)
11
+ * nightly --status Show nightly pipeline health check
11
12
  * relink Resumable link-discovery pass over the entire corpus (issue #143)
12
13
  * dedup Cluster + merge near-duplicate memories (issue #100)
13
14
  * dedup --apply Execute the merge (default: dry run)
package/dist/cli.js CHANGED
@@ -7,8 +7,9 @@
7
7
  * server Start the MCP HTTP/SSE server (persistent daemon)
8
8
  * init Detect existing setup and configure for CC/OC
9
9
  * nightly Run capture + consolidate (manual trigger)
10
- * nightly --capture-only Capture only, skip consolidation
11
- * nightly --status Show nightly pipeline health check
10
+ * nightly --capture-only Capture only, skip consolidation
11
+ * nightly --consolidate-only Consolidate only, skip capture (hosted service)
12
+ * nightly --status Show nightly pipeline health check
12
13
  * relink Resumable link-discovery pass over the entire corpus (issue #143)
13
14
  * dedup Cluster + merge near-duplicate memories (issue #100)
14
15
  * dedup --apply Execute the merge (default: dry run)
@@ -69,7 +70,12 @@ switch (command) {
69
70
  else {
70
71
  const dryRun = args.includes("--dry-run");
71
72
  const captureOnly = args.includes("--capture-only");
73
+ const consolidateOnly = args.includes("--consolidate-only");
72
74
  const watchdog = args.includes("--watchdog");
75
+ if (captureOnly && consolidateOnly) {
76
+ console.error("[hicortex] nightly: --capture-only and --consolidate-only are mutually exclusive");
77
+ process.exit(1);
78
+ }
73
79
  // Timestamp every log line. The nightly writes to a file (launchd /
74
80
  // systemd StandardOutput append) with NO per-line timestamp, which made
75
81
  // diagnosing capture gaps impossible (the #239 investigation couldn't
@@ -92,7 +98,7 @@ switch (command) {
92
98
  }
93
99
  }
94
100
  import("./nightly.js").then(({ runNightly }) => {
95
- runNightly({ dryRun, captureOnly, watchdog, recaptureWindowDays }).catch((err) => {
101
+ runNightly({ dryRun, captureOnly, consolidateOnly, watchdog, recaptureWindowDays }).catch((err) => {
96
102
  console.error("[hicortex] Nightly pipeline failed:", err);
97
103
  process.exit(1);
98
104
  });
@@ -285,6 +291,7 @@ Options:
285
291
  server --host <h> Host (default: 127.0.0.1)
286
292
  nightly --dry-run Preview without changes
287
293
  nightly --capture-only Capture only, skip consolidation (safe to run multiple times/day)
294
+ nightly --consolidate-only Consolidate only, skip capture (hosted-service per-tenant runs)
288
295
  nightly --recapture-window <days> Re-discover sessions quiet since <days> ago (one-shot #189 recovery)
289
296
  nightly --status Show nightly pipeline health
290
297
  relink --dry-run Discovery + counts only, zero writes, cursor untouched
@@ -63,12 +63,63 @@ export declare class BudgetTracker {
63
63
  maxCalls: number;
64
64
  callsUsed: number;
65
65
  callsByStage: Record<string, number>;
66
+ /**
67
+ * Token usage per stage (#246). Keys are the same stage labels passed to
68
+ * `use()`. A stage that made no metered calls (no usage returned — never the
69
+ * path on a healthy openai/ollama endpoint) is absent, NOT zero, so the
70
+ * dashboard can distinguish "nothing spent" from "no signal".
71
+ */
72
+ tokensByStage: Record<string, {
73
+ prompt: number;
74
+ completion: number;
75
+ total: number;
76
+ }>;
77
+ /** Run-wide totals — the sum of every recordUsage() call this run. */
78
+ totalTokens: {
79
+ prompt: number;
80
+ completion: number;
81
+ total: number;
82
+ };
66
83
  constructor(maxCalls: number);
67
84
  get exhausted(): boolean;
68
85
  get remaining(): number;
69
86
  use(stage: string, count?: number): boolean;
87
+ /**
88
+ * Record token usage from one LLM call (#246). Called by the consolidation
89
+ * stages after each metered completion. `undefined` usage (claude-cli path,
90
+ * or a non-conforming endpoint that returned no usage object) is a no-op —
91
+ * never recorded as zero, which would silently undercount real spend.
92
+ */
93
+ recordUsage(stage: string, usage: {
94
+ prompt_tokens: number;
95
+ completion_tokens: number;
96
+ total_tokens: number;
97
+ } | undefined): void;
70
98
  summary(): NonNullable<ConsolidationReport["budget"]>;
71
99
  }
100
+ /**
101
+ * Decide whether consolidation should be throttled this run based on the
102
+ * `llmTokensPerMonth` fair-use cap. Pure (no I/O) so it can be unit-tested
103
+ * independently of the nightly wiring.
104
+ *
105
+ * Returns `{ throttle: true, used, cap }` when the projected post-run total
106
+ * would exceed the cap; `{ throttle: false }` otherwise. The estimate is the
107
+ * previous run's actual usage (`llmTokensLastRun`, 0/absent on the first
108
+ * metered run = never throttle the first run — no baseline yet).
109
+ *
110
+ * `cap = 0` (the self-hosted default) → never throttle (unlimited).
111
+ * `periodStart` in a previous calendar month → period resets to 0 first
112
+ * (mirrors the reset logic in nightly.ts; both sides agree because both read
113
+ * the same state + clock).
114
+ */
115
+ export declare function shouldThrottleTokens(cap: number, period: {
116
+ total: number;
117
+ periodStart: string;
118
+ } | undefined, lastRunTokens: number, now?: Date): {
119
+ throttle: boolean;
120
+ used?: number;
121
+ cap?: number;
122
+ };
72
123
  /**
73
124
  * Parse JSON from LLM output, tolerating markdown fences and indexed formats.
74
125
  */
@@ -203,6 +254,17 @@ export declare function stageDecayPrune(db: Database.Database, dryRun: boolean):
203
254
  pruned: number;
204
255
  failed: number;
205
256
  };
257
+ /**
258
+ * Default soft cap on the memory corpus (#245). Above this the lowest-value
259
+ * memories are evicted each nightly. 10000 balances headroom for a busy
260
+ * self-hosted install against the noise cost of a bloated vector index
261
+ * (recall top-k competes against the long tail). Override via `memorySoftCap`.
262
+ */
263
+ export declare const DEFAULT_MEMORY_SOFT_CAP = 10000;
264
+ export declare function stageMemoryCapEviction(db: Database.Database, dryRun: boolean, cap: number): {
265
+ cap: number;
266
+ evicted: number;
267
+ };
206
268
  /**
207
269
  * Run the full consolidation pipeline. Returns a structured report.
208
270
  */
@@ -232,7 +294,11 @@ export declare function runConsolidation(db: Database.Database, llm: LlmClient,
232
294
  /** Total LLM-call ceiling across classify-tier stages (#241). The caller
233
295
  * reads `consolidateMaxLlmCalls` from config and passes it; unset → the
234
296
  * exported `CONSOLIDATE_MAX_LLM_CALLS` default (5000). */
235
- budgetMaxCalls?: number): Promise<ConsolidationReport>;
297
+ budgetMaxCalls?: number,
298
+ /** Soft cap on the corpus (#245). Nightly.ts reads `memorySoftCap` from
299
+ * config and passes it; unset → `DEFAULT_MEMORY_SOFT_CAP` (10000). `0`
300
+ * disables eviction (indefinite growth). */
301
+ memorySoftCap?: number): Promise<ConsolidationReport>;
236
302
  /**
237
303
  * Calculate milliseconds until the next occurrence of a given hour (local time).
238
304
  */