@gamaze/hicortex 0.17.6 → 0.18.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.
Files changed (50) hide show
  1. package/README.md +26 -25
  2. package/assets/dashboard.html +121 -5
  3. package/assets/{context.html → identity.html} +18 -18
  4. package/assets/viz.html +19 -7
  5. package/dist/claude-md.d.ts +2 -1
  6. package/dist/claude-md.js +2 -1
  7. package/dist/cli-args.d.ts +9 -0
  8. package/dist/cli-args.js +16 -0
  9. package/dist/cli.js +29 -20
  10. package/dist/consolidate.d.ts +15 -0
  11. package/dist/consolidate.js +30 -3
  12. package/dist/dashboard.d.ts +58 -1
  13. package/dist/dashboard.js +27 -1
  14. package/dist/extensions.d.ts +1 -1
  15. package/dist/extensions.js +1 -1
  16. package/dist/health.d.ts +68 -0
  17. package/dist/health.js +73 -0
  18. package/dist/identity-cli.d.ts +90 -0
  19. package/dist/{context-cli.js → identity-cli.js} +66 -48
  20. package/dist/{context-store.d.ts → identity-store.d.ts} +94 -31
  21. package/dist/{context-store.js → identity-store.js} +212 -71
  22. package/dist/index.d.ts +12 -5
  23. package/dist/index.js +57 -29
  24. package/dist/init.d.ts +44 -8
  25. package/dist/init.js +142 -37
  26. package/dist/{lessons-context.d.ts → learnings-identity.d.ts} +32 -21
  27. package/dist/{lessons-context.js → learnings-identity.js} +50 -39
  28. package/dist/mcp-server.d.ts +2 -0
  29. package/dist/mcp-server.js +168 -58
  30. package/dist/memory-instructions.d.ts +6 -6
  31. package/dist/memory-instructions.js +6 -6
  32. package/dist/nightly.js +65 -6
  33. package/dist/paths.js +1 -1
  34. package/dist/recall-hook-cli.d.ts +1 -1
  35. package/dist/recall-hook-cli.js +3 -3
  36. package/dist/recall-index.js +5 -2
  37. package/dist/status.d.ts +2 -2
  38. package/dist/status.js +11 -9
  39. package/dist/telemetry.d.ts +10 -0
  40. package/dist/type-classify.js +4 -1
  41. package/dist/type-labels.d.ts +30 -0
  42. package/dist/type-labels.js +43 -0
  43. package/dist/types.d.ts +28 -0
  44. package/dist/uninstall.d.ts +12 -0
  45. package/dist/uninstall.js +21 -3
  46. package/dist/viz.d.ts +24 -11
  47. package/dist/viz.js +97 -32
  48. package/hermes-plugin/hicortex/README.md +4 -2
  49. package/package.json +2 -2
  50. package/dist/context-cli.d.ts +0 -69
package/README.md CHANGED
@@ -67,7 +67,7 @@ The plugin connects to `http://127.0.0.1:8787` by default. For a remote server,
67
67
 
68
68
  | When | What | How |
69
69
  |------|------|-----|
70
- | Agent start | Standing context (`## Context`) + recent lessons fetched fresh and injected | CC SessionStart hook (calls `hicortex lessons-context`) / Hermes plugin `system_prompt_block` / OC `before_agent_start` hook |
70
+ | Agent start | Standing identity (`## Identity`) + recent lessons fetched fresh and injected | CC SessionStart hook (calls `hicortex learnings-identity`; `lessons-context` kept as an alias so existing installed hooks don't break) / Hermes plugin `system_prompt_block` / OC `before_agent_start` hook |
71
71
  | Every prompt (0.14) | A compact **recall index** of relevant memories is injected — one line per memory; the agent lazy-loads full content with `hicortex_get` only when needed | All three harnesses call server `POST /recall-index` per turn: CC UserPromptSubmit hook (`hicortex recall-hook`), Hermes plugin `prefetch` (0.7.0; falls back to `/search` injection against a pre-0.14 server), OC `before_agent_start` hook (fires per inbound message). Turn-based dedup per session; resets on new session/compaction. Fail-soft |
72
72
  | Nightly | Denoise sessions → POST /distill → server distills + embeds + stores → consolidate (score, reflect, link, decay) | Automatic pipeline — no manual steps |
73
73
 
@@ -132,28 +132,28 @@ The run is resumable — interrupt it any time and it continues where it stopped
132
132
 
133
133
  Explicit learnings: call `hicortex_ingest` directly (capture is otherwise automatic, nightly).
134
134
 
135
- ## Context Layer
135
+ ## Identity Layer
136
136
 
137
- Beyond auto-distilled memories and lessons, Hicortex holds a **hand-edited context layer** — standing "who you are + how to work" Markdown injected into every session at start. Unlike memories, it is **never distilled, scored, or decayed**: what you write stays verbatim until you change it.
137
+ Beyond auto-distilled memories and lessons, Hicortex holds a **hand-edited identity layer** — standing "who you are + how to work" Markdown injected into every session at start. Unlike memories, it is **never distilled, scored, or decayed**: what you write stays verbatim until you change it.
138
138
 
139
- - **Storage:** plain files on the server at `~/.hicortex/context/*.md` — one file per section (recommended starter sections `user.md` + `rules.md`, which you create — nothing is pre-populated; add more by dropping in a file). It lives outside the memories table; consolidation never touches it.
140
- - **Edit:** the web editor at `http://localhost:8787/context/ui` (one tab per section, Save), or the CLI `hicortex context show [name]` / `hicortex context edit <name>`.
141
- - **Delivery:** injected into the harnesses listed in `contextClients` (default `["cc"]`; `"all"` or any subset of `cc`/`hermes`/`oc` — all three are supported since 0.13).
139
+ - **Storage:** plain files on the server at `~/.hicortex/identity/*.md` — one file per section (recommended starter sections `user.md` + `rules.md`, which you create — nothing is pre-populated; add more by dropping in a file). It lives outside the memories table; consolidation never touches it.
140
+ - **Edit:** the web editor at `http://localhost:8787/identity/ui` (one tab per section, Save), or the CLI `hicortex identity show [name]` / `hicortex identity edit <name>`.
141
+ - **Delivery:** injected into the harnesses listed in `identityClients` (default `["cc"]`; `"all"` or any subset of `cc`/`hermes`/`oc` — all three are supported since 0.13).
142
142
  - **Deletion** is filesystem-only — remove the file on the server (as the daemon user).
143
143
 
144
- ### Per-agent context (0.13)
144
+ ### Per-agent identity (0.13)
145
145
 
146
- One server serves a fleet of distinct-persona agents. Each agent can have its **own** context, resolved server-side into one of three modes:
146
+ One server serves a fleet of distinct-persona agents. Each agent can have its **own** identity, resolved server-side into one of three modes:
147
147
 
148
148
  - **`override`** (default when an `agents/<id>/` dir exists) — the agent's sections win **per section name**, falling back to the global set for any section it doesn't define.
149
149
  - **`global`** — the shared global set (the 0.12 behavior).
150
150
  - **`off`** — inject nothing for that agent.
151
151
 
152
- - **Agent id:** Hermes and OC scope per profile/agent automatically. **CC is global by default** — it sends no `?agent=`, so all your CC machines share one global context (one user = one identity across machines). `agentName` is an explicit opt-in: set it with `init --agent-name <name>` and CC will send `?agent=<config.agentName>`; clear it with `init --agent-name ""` to return to global. The id is on the same strict allowlist as section names (it becomes a path). Shown by `hicortex status` (or `(not set — global context)` when unset).
153
- - **Storage:** per-agent sections live at `~/.hicortex/context/agents/<id>/*.md`; the global reader never descends into `agents/`.
154
- - **Config:** `contextAgents` maps agent id → mode; a dropped-in `agents/<id>/` dir alone means `override` with no config. Editing `contextAgents` needs a daemon restart; dropping in a dir takes effect immediately.
155
- - **Edit:** the web editor's scope selector (`Global` | `<agent>`; inherited sections shown dimmed), or `hicortex context show|edit --agent <id>`.
156
- - **Backward compatible:** no `?agent=` and no `contextAgents`/`agents/` dir → every agent gets the global set.
152
+ - **Agent id:** Hermes and OC scope per profile/agent automatically. **CC is global by default** — it sends no `?agent=`, so all your CC machines share one global identity (one user = one identity across machines). `agentName` is an explicit opt-in: set it with `init --agent-name <name>` and CC will send `?agent=<config.agentName>`; clear it with `init --agent-name ""` to return to global. The id is on the same strict allowlist as section names (it becomes a path). Shown by `hicortex status` (or `(not set — global identity)` when unset).
153
+ - **Storage:** per-agent sections live at `~/.hicortex/identity/agents/<id>/*.md`; the global reader never descends into `agents/`.
154
+ - **Config:** `identityAgents` maps agent id → mode; a dropped-in `agents/<id>/` dir alone means `override` with no config. Editing `identityAgents` needs a daemon restart; dropping in a dir takes effect immediately.
155
+ - **Edit:** the web editor's scope selector (`Global` | `<agent>`; inherited sections shown dimmed), or `hicortex identity show|edit --agent <id>`.
156
+ - **Backward compatible:** no `?agent=` and no `identityAgents`/`agents/` dir → every agent gets the global set.
157
157
 
158
158
  ## CLI Commands
159
159
 
@@ -168,11 +168,11 @@ npx @gamaze/hicortex classify-domains # Backfill domain tags over the c
168
168
  npx @gamaze/hicortex classify-types # Reclassify memory types (episode/fact/decision)
169
169
  npx @gamaze/hicortex dedup # Preview near-duplicate memory clusters (dry run, no changes)
170
170
  npx @gamaze/hicortex dedup --apply # Merge near-duplicate clusters (backs up the DB first)
171
- npx @gamaze/hicortex context show [name] # Print the standing context layer (see Context Layer)
172
- npx @gamaze/hicortex context edit <name> # Edit a context section in $EDITOR
173
- npx @gamaze/hicortex context show --agent <id> # Show a specific agent's resolved context (0.13)
174
- npx @gamaze/hicortex init --agent-name <name> # Opt in to a per-agent context id (default: unset — shared global context)
175
- npx @gamaze/hicortex init --agent-name "" # Clear it back to global context
171
+ npx @gamaze/hicortex identity show [name] # Print the standing identity layer (see Identity Layer)
172
+ npx @gamaze/hicortex identity edit <name> # Edit an identity section in $EDITOR
173
+ npx @gamaze/hicortex identity show --agent <id> # Show a specific agent's resolved identity (0.13)
174
+ npx @gamaze/hicortex init --agent-name <name> # Opt in to a per-agent identity id (default: unset — shared global identity)
175
+ npx @gamaze/hicortex init --agent-name "" # Clear it back to global identity
176
176
  npx @gamaze/hicortex init --repair-config # Recover from a malformed config.json (see below)
177
177
  npx @gamaze/hicortex telemetry # Show exactly what anonymous telemetry sends
178
178
  npx @gamaze/hicortex status # Show config, DB stats
@@ -215,17 +215,18 @@ Config at `~/.hicortex/config.json`. Created by `init`. Key options:
215
215
  | `ollamaFlushEvery` | Flush ollama's accumulated memory every N scoring calls. **Off by default (0)** — opt-in only for an **ollama** install whose runner RSS growth (~171 MB/call) swap-thrashes long consolidations on a RAM-constrained box; N=15 caps a cycle at ~2.5 GB. Gated on the provider being ollama (local **or** remote) — no effect for non-ollama providers. Only you can judge whether your ollama endpoint actually suffers the growth (a managed/cloud ollama host may not), so it stays off until you set it. |
216
216
  | `ollamaFlushWaitMs` | Milliseconds to wait after an ollama flush for the runner to exit + release memory (default 180000 = 3 min). |
217
217
  | `authToken` | Bearer token for endpoint auth. Generated on first `init` in server mode. Find the active token with `hicortex status` or in `~/.hicortex/config.json`. |
218
- | `corsAllowedOrigins` | Browser origins allowed to read cross-origin responses, e.g. `["https://ui.example.com"]`. **Empty by default** — the server sends no `Access-Control-Allow-Origin` and never `Allow-Credentials`, so no external web page can read its data. The bundled `/viz` and `/context/ui` pages are same-origin and need no entry. |
218
+ | `corsAllowedOrigins` | Browser origins allowed to read cross-origin responses, e.g. `["https://ui.example.com"]`. **Empty by default** — the server sends no `Access-Control-Allow-Origin` and never `Allow-Credentials`, so no external web page can read its data. The bundled `/viz` and `/identity/ui` pages are same-origin and need no entry. |
219
219
  | `licenseKey` | Commercial license key (optional; for display in `hicortex status`) |
220
220
  | `domains` | Your memory domain list (`[{name, description}]`). Scaffolded by `init`; edit freely — see [Memory Domains & Tags](#memory-domains--tags) |
221
221
  | `weakPrimaryFloor` | Minimum similarity for a no-fit memory to keep a weak domain association (default: 0.45) |
222
222
  | `moduleIndexTokenBudget` | Max tokens for domain index in lessons context (default: 500) |
223
223
  | `lessonsLimit` | Max lessons injected into an agent's session-start context (default: 10). Lessons are ranked per session by project/domain affinity + recency + strength + access, so each session sees its most-relevant slice. Lower = leaner system prompts. |
224
- | `contextClients` | Which harnesses inject the [context layer](#context-layer) at session start (default `["cc"]`; `"all"` or any subset of `cc`/`hermes`/`oc`) |
225
- | `contextAgents` | Per-agent context modes (0.13): `{ "<id>": "override" \| "global" \| "off" }`. Absent + no `agents/<id>/` dir → every agent gets the global set. Boot-time (restart to apply) — see [Per-agent context](#per-agent-context-013) |
226
- | `agentName` | This install's per-agent context id sent as `?agent=`. **Unset by default** (CC shares the global context — no `?agent=` sent). Explicit opt-in via `init --agent-name <name>`; `init --agent-name ""` clears it. An empty/whitespace value equals unset |
224
+ | `identityClients` | Which harnesses inject the [identity layer](#identity-layer) at session start (default `["cc"]`; `"all"` or any subset of `cc`/`hermes`/`oc`) |
225
+ | `identityAgents` | Per-agent identity modes (0.13): `{ "<id>": "override" \| "global" \| "off" }`. Absent + no `agents/<id>/` dir → every agent gets the global set. Boot-time (restart to apply) — see [Per-agent identity](#per-agent-identity-013) |
226
+ | `agentName` | This install's per-agent identity id sent as `?agent=`. **Unset by default** (CC shares the global identity — no `?agent=` sent). Explicit opt-in via `init --agent-name <name>`; `init --agent-name ""` clears it. An empty/whitespace value equals unset |
227
227
  | `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) |
228
228
  | `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 |
229
+ | `timerJitterSeconds` | Max random delay (seconds) added to **generated** consolidation timers (#256), so a fleet doesn't all fire on the same minute (thundering-herd → LLM-backend contention). systemd: a single `RandomizedDelaySec=<n>`; launchd has no native equivalent so a per-install randomized `Minute` offset is baked into every `StartCalendarInterval` dict (sub-60s values no-op on launchd). Default `3600` (≈±30 min spread on the 2-slot/day cadence); `0` disables. Affects timers on the next `init` (re-init rewrites the unit files; installs that don't re-init keep their existing timers) |
229
230
  | `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) |
230
231
  | `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 |
231
232
  | `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) |
@@ -268,8 +269,8 @@ Full docs: [hicortex.gamaze.com/docs/configuration.html](https://hicortex.gamaze
268
269
  | `/recall-index` | POST | Yes | Pushed recall index (0.14): `{session_id, prompt}` → compact one-line-per-memory block (or `null`); `{session_id, reset: true}` clears the session's dedup state. Appearing in the index marks memories *shown*, never *used* |
269
270
  | `/memory` | GET | Yes | Fetch one memory by `?id=` (0.14). Marks it as used (strengthens) — the lazy-load counterpart of `/recall-index`. Response includes a server-rendered `citation` (id, date, origin agent) — agents are instructed to cite memories that shape their answers, so memory influence is always visible to the user (0.14.1) |
270
271
  | `/recent` | GET | Yes | Recent memories, queryless recall (renamed from `/context` in 0.12) |
271
- | `/context` | GET / PUT | Yes | Standing [context layer](#context-layer): read all sections / partial-upsert named sections. `?agent=<id>` selects a [per-agent scope](#per-agent-context-013) (server resolves override/global/off + merge); invalid id → 400. Recall-style query params on GET → 400 (use `/recent`) |
272
- | `/context/ui` | GET | No* | Web editor for the context layer (shell served without auth, like `/viz`; data via `/context`) |
272
+ | `/identity` | GET / PUT | Yes | Standing [identity layer](#identity-layer): read all sections / partial-upsert named sections. `?agent=<id>` selects a [per-agent scope](#per-agent-identity-013) (server resolves override/global/off + merge); invalid id → 400. Recall-style query params on GET → 400 (use `/recent`). (`/context` remains as a backcompat alias.) |
273
+ | `/identity/ui` | GET | No* | Web editor for the identity layer (shell served without auth, like `/viz`; data via `/identity`) |
273
274
  | `/lessons` | GET | Yes | Lessons + memory index (used by CC SessionStart hook) |
274
275
  | `/ingest` | POST | Yes | Legacy: accept a single pre-distilled memory from older clients |
275
276
  | `/sse` | GET | Yes | MCP SSE stream for agent connections |
@@ -311,7 +312,7 @@ LLM selection is **user-controlled**: `npx @gamaze/hicortex init` detects candid
311
312
  | Custom provider | `llmBaseUrl` + `llmApiKey` | Any OpenAI-compatible endpoint |
312
313
  | Hicortex env vars | `HICORTEX_LLM_BASE_URL` + `HICORTEX_LLM_API_KEY` | Override at runtime |
313
314
 
314
- If no LLM is configured, the server starts in **recall-only mode**: search, lessons, and context work; `/distill` and consolidation are disabled. Run `npx @gamaze/hicortex init` to configure.
315
+ If no LLM is configured, the server starts in **recall-only mode**: search, lessons, and identity work; `/distill` and consolidation are disabled. Run `npx @gamaze/hicortex init` to configure.
315
316
 
316
317
  ## Database
317
318
 
@@ -122,7 +122,7 @@
122
122
  <div class="hc-nav-links">
123
123
  <a href="/dashboard" data-nav-active>Dashboard</a>
124
124
  <a href="/viz">Graph</a>
125
- <a href="/context/ui">Context</a>
125
+ <a href="/identity/ui">Identity</a>
126
126
  <span class="hc-nav-disabled" title="Coming soon — #250">Self-improvement</span>
127
127
  </div>
128
128
  </nav>
@@ -183,6 +183,10 @@
183
183
  <div id="digest-dedup"></div>
184
184
  <h2 id="digest-tokens-section" style="margin-top:16px;display:none;">Token usage breakdown by stage</h2>
185
185
  <div id="digest-tokens"></div>
186
+ <div id="digest-budget-section" style="margin-top:16px;display:none;">
187
+ <div class="sub" style="margin-bottom:6px;">Consolidation + LLM usage — two usage-vs-budget bars: per-run LLM calls spent vs the consolidation ceiling, and this billing period's tokens vs the monthly fair-use cap. The amber pill appears only when the call cap was hit. The tokens bar renders only when a monthly cap is configured (cap &gt; 0); uncapped installs (the self-hosted default) show the absolute token count instead.</div>
188
+ <div id="digest-budget"></div>
189
+ </div>
186
190
  </div>
187
191
 
188
192
  <div class="sub muted" style="margin-top:24px;">
@@ -194,7 +198,7 @@
194
198
  <script>
195
199
  "use strict";
196
200
  // ---------------------------------------------------------------------------
197
- // Token handling — mirror /viz + /context/ui: ?token= handoff (stripped on
201
+ // Token handling — mirror /viz + /identity/ui: ?token= handoff (stripped on
198
202
  // load), localStorage persistence, 401 prompt. Same-origin fetch only.
199
203
  // ---------------------------------------------------------------------------
200
204
  const TOKEN_KEY = "hicortex-dashboard-token";
@@ -234,6 +238,17 @@ let compKey = "by_type";
234
238
 
235
239
  const $ = (id) => document.getElementById(id);
236
240
 
241
+ // #264 WS2: human-term labels for the internal memory_type enum. The data
242
+ // blob carries the RAW enum keys (a stable JSON contract, also relied on by
243
+ // tests); only the rendered label changes. Unknown keys pass through.
244
+ const TYPE_LABELS = {
245
+ fact: "Knowledge",
246
+ episode: "Experience",
247
+ decision: "Decisions",
248
+ lesson: "Learnings"
249
+ };
250
+ function labelForType(t) { return TYPE_LABELS[t] || t; }
251
+
237
252
  // ---------------------------------------------------------------------------
238
253
  // Render: headline
239
254
  // ---------------------------------------------------------------------------
@@ -419,7 +434,10 @@ function renderComposition(comp, key) {
419
434
  // attacker-influenced (any authenticated /distill client sets source_agent
420
435
  // verbatim). Injected into both a title="..." attribute and text — close
421
436
  // the attribute and add an on* handler otherwise.
422
- const ek = escapeHtml(k);
437
+ // #264 WS2: when the composition key is by_type, show the human-term label
438
+ // (Knowledge/Experience/...) instead of the raw enum value.
439
+ const display = compKey === "by_type" ? labelForType(k) : k;
440
+ const ek = escapeHtml(display);
423
441
  return `<div class="bar-row"><span class="k" title="${ek}">${ek}</span><span class="track"><span class="fill" style="width:${pct}%"></span></span><span class="n">${v}</span></div>`;
424
442
  }).join("");
425
443
  }
@@ -434,7 +452,7 @@ function linkId(id) {
434
452
  // is a server hex token but escape the text anyway (defense-in-depth).
435
453
  return `<a href="/memory?id=${encodeURIComponent(id)}" title="Open memory">${escapeHtml(id.slice(0, 8))}</a>`;
436
454
  }
437
- function renderDigest(digest) {
455
+ function renderDigest(digest, tokens) {
438
456
  $("digest-date").textContent = digest.date || "—";
439
457
  const s = digest.stages;
440
458
  const stages = [
@@ -444,6 +462,8 @@ function renderDigest(digest) {
444
462
  `<span class="stage-pill">supersession: ${s.supersession}</span>`,
445
463
  s.evicted !== undefined && s.evicted > 0 ? `<span class="stage-pill warn">evicted: ${s.evicted}</span>` : "",
446
464
  s.tokens !== undefined ? `<span class="stage-pill">tokens: ${escapeHtml(formatTokens(s.tokens))}</span>` : "",
465
+ // #255 CR: budget exhaustion pill lives in the dedicated budget-usage bar
466
+ // below (alert state on top of the continuous used/max bar), not here.
447
467
  ].join(" ");
448
468
  $("digest-stages").innerHTML = stages;
449
469
  // Per-stage token breakdown (#246) — only when the day's run metered tokens
@@ -464,6 +484,102 @@ function renderDigest(digest) {
464
484
  $("digest-tokens-section").style.display = "none";
465
485
  }
466
486
 
487
+ // #255 CR + tokens-vs-cap: TWO usage-vs-budget bars in this section.
488
+ //
489
+ // Bar 1 — Consolidation calls (per-run): budget_calls_used / budget_max_calls.
490
+ // Renders whenever the day's run carried both (consolidation ran). The
491
+ // amber "budget exhausted" pill sits on top as the alert state.
492
+ //
493
+ // Bar 2 — LLM tokens (monthly): tokens.used / tokens.cap, where cap is the
494
+ // configured `llmTokensPerMonth` fair-use cap. CONDITIONAL (see the
495
+ // section doc): cap > 0 → render a used/cap bar in the SAME green/amber/
496
+ // red style; cap <= 0 (the self-hosted default, "unlimited") → render the
497
+ // absolute usage figure only (no broken "X / 0" bar). Hidden entirely
498
+ // when used is 0 (no metered run yet) — mirroring the headline stat.
499
+ //
500
+ // The section shows when EITHER bar renders. XSS-safe: only numbers flow
501
+ // in (Number + toLocaleString / formatTokens), the only string (deferred
502
+ // JSON) is escapeHtml'd.
503
+ const used = s.budget_calls_used;
504
+ const cap = s.budget_max_calls;
505
+ const haveCallsBar = typeof used === "number" && typeof cap === "number" && cap > 0;
506
+
507
+ // Tokens monthly accrual comes from headline.tokens (state.llmTokensThisPeriod,
508
+ // the same source the throttle check reads). cap <= 0 = unlimited.
509
+ const tokUsed = tokens && typeof tokens.used === "number" ? tokens.used : 0;
510
+ const tokCap = tokens && typeof tokens.cap === "number" ? tokens.cap : 0;
511
+ const haveTokensBar = tokUsed > 0 && tokCap > 0;
512
+ const haveTokensAbsolute = tokUsed > 0 && !(tokCap > 0); // uncapped: figure only
513
+
514
+ if (haveCallsBar || haveTokensBar || haveTokensAbsolute) {
515
+ const pieces = [];
516
+
517
+ // Bar 1: Consolidation calls.
518
+ if (haveCallsBar) {
519
+ const pct = Math.min(100, (used / cap) * 100);
520
+ const exhausted = s.budget_exhausted === true;
521
+ const over = used >= cap;
522
+ const near = pct >= 85 && !over;
523
+ const barColor = over ? "var(--bad, #e05555)" : near ? "var(--warn, #e0b055)" : "var(--ok, #61c98f)";
524
+ const deferred = s.budget_deferred_by_stage ?? {};
525
+ const pill = exhausted
526
+ ? ` <span class="stage-pill warn" title="Consolidation budget exhausted — LLM-bound stages deferred remaining work. Per-stage refused-request counts: ${escapeHtml(JSON.stringify(deferred))}">budget exhausted</span>`
527
+ : "";
528
+ pieces.push(`
529
+ <div style="margin-bottom:10px;">
530
+ <div class="muted" style="font-size:12px;margin-bottom:4px;text-transform:uppercase;letter-spacing:0.04em;">Consolidation calls (this run)</div>
531
+ <div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
532
+ <div style="flex:1;min-width:200px;">
533
+ <div class="muted" style="font-size:13px;margin-bottom:4px;">
534
+ ${used.toLocaleString()} / ${cap.toLocaleString()} LLM calls${exhausted ? " — exhausted" : ""}
535
+ </div>
536
+ <div style="height:6px;background:rgba(128,128,128,0.2);border-radius:3px;overflow:hidden;">
537
+ <div style="width:${pct.toFixed(1)}%;height:100%;background:${barColor};"></div>
538
+ </div>
539
+ </div>${pill}
540
+ </div>
541
+ </div>`);
542
+ }
543
+
544
+ // Bar 2: LLM tokens (monthly). Conditional on cap > 0 for the bar form;
545
+ // otherwise show the absolute usage figure (uncapped installs).
546
+ if (haveTokensBar) {
547
+ const pct = Math.min(100, (tokUsed / tokCap) * 100);
548
+ const over = tokUsed >= tokCap;
549
+ const near = pct >= 85 && !over;
550
+ const barColor = over ? "var(--bad, #e05555)" : near ? "var(--warn, #e0b055)" : "var(--ok, #61c98f)";
551
+ pieces.push(`
552
+ <div>
553
+ <div class="muted" style="font-size:12px;margin-bottom:4px;text-transform:uppercase;letter-spacing:0.04em;">LLM tokens (monthly)</div>
554
+ <div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
555
+ <div style="flex:1;min-width:200px;">
556
+ <div class="muted" style="font-size:13px;margin-bottom:4px;">
557
+ ${escapeHtml(formatTokens(tokUsed))} / ${escapeHtml(formatTokens(tokCap))} tokens${over ? " — over cap" : ""}
558
+ </div>
559
+ <div style="height:6px;background:rgba(128,128,128,0.2);border-radius:3px;overflow:hidden;">
560
+ <div style="width:${pct.toFixed(1)}%;height:100%;background:${barColor};"></div>
561
+ </div>
562
+ </div>
563
+ </div>
564
+ </div>`);
565
+ } else if (haveTokensAbsolute) {
566
+ // Unlimited case (cap <= 0, the self-hosted default): no denominator to
567
+ // draw a bar against, so show the absolute usage figure only. Never
568
+ // render a broken "X / 0" bar.
569
+ pieces.push(`
570
+ <div>
571
+ <div class="muted" style="font-size:12px;margin-bottom:4px;text-transform:uppercase;letter-spacing:0.04em;">LLM tokens (monthly) <span style="text-transform:none;letter-spacing:0;">— uncapped</span></div>
572
+ <div class="muted" style="font-size:13px;">${escapeHtml(formatTokens(tokUsed))} tokens this month</div>
573
+ </div>`);
574
+ }
575
+
576
+ $("digest-budget").innerHTML = pieces.join("");
577
+ $("digest-budget-section").style.display = "";
578
+ } else {
579
+ $("digest-budget").innerHTML = "";
580
+ $("digest-budget-section").style.display = "none";
581
+ }
582
+
467
583
  if (digest.sample.length === 0) {
468
584
  $("digest-sample").innerHTML = `<div class="empty">No new memories that day.</div>`;
469
585
  } else {
@@ -517,7 +633,7 @@ async function load() {
517
633
  renderHeadline(state.headline);
518
634
  renderGrowth(state.series, $("split").value, $("filter").value);
519
635
  renderComposition(state.composition, compKey);
520
- renderDigest(state.digest);
636
+ renderDigest(state.digest, state.headline.tokens);
521
637
 
522
638
  // Populate the growth-filter from source_agent + domain union (best-effort —
523
639
  // a v1 affordance; the series already carries per-day totals, so the filter
@@ -3,18 +3,18 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Hicortex — context</title>
6
+ <title>Hicortex — identity</title>
7
7
  <!--
8
- Hicortex standing-context editor (#165, 0.12 — spec 2026-07-12 §5).
9
- Self-contained: inline CSS/JS, ZERO external requests (CSP-safe). The PRIMARY
10
- edit surface for the context layer.
8
+ Hicortex standing-identity editor (#165, 0.12 — spec 2026-07-12 §5; renamed
9
+ from "context" in 0.18 #264). Self-contained: inline CSS/JS, ZERO external
10
+ requests (CSP-safe). The PRIMARY edit surface for the identity layer.
11
11
 
12
- One tab per section, tabs built dynamically from GET /context `sections`
12
+ One tab per section, tabs built dynamically from GET /identity `sections`
13
13
  (the server serves any allowlist-named *.md as a section — nothing hardcoded).
14
14
  Each tab is a <textarea>; Save PUTs only the active section (partial upsert).
15
15
 
16
16
  The page SHELL is public (exempted in createAuthMiddleware like /viz); the
17
- GET/PUT /context data calls are bearer-only with a localhost bypass, so on the
17
+ GET/PUT /identity data calls are bearer-only with a localhost bypass, so on the
18
18
  server box no token is needed. Remote browsers hand off a token via
19
19
  ?token=<t> (stripped from the URL on load), localStorage, or a 401 prompt.
20
20
 
@@ -192,14 +192,14 @@
192
192
  <div class="hc-nav-links">
193
193
  <a href="/dashboard">Dashboard</a>
194
194
  <a href="/viz">Graph</a>
195
- <a href="/context/ui" data-nav-active>Context</a>
195
+ <a href="/identity/ui" data-nav-active>Identity</a>
196
196
  <span class="hc-nav-disabled" title="Coming soon — #250">Self-improvement</span>
197
197
  </div>
198
198
  </nav>
199
199
  <header>
200
- <h1>Hicortex — context</h1>
200
+ <h1>Hicortex — identity</h1>
201
201
  <label class="meta" for="scope">scope</label>
202
- <select id="scope" title="Which agent's context to edit"></select>
202
+ <select id="scope" title="Which agent's identity to edit"></select>
203
203
  <input type="text" id="newagent" placeholder="new-agent-id" autocomplete="off" spellcheck="false">
204
204
  <button class="btn secondary small" id="addagent">Add agent</button>
205
205
  <span class="meta" id="clients"></span>
@@ -251,7 +251,7 @@
251
251
  "use strict";
252
252
  (function () {
253
253
  // =========================================================================
254
- // Section-name allowlist — mirrors the server contract (context-store.ts
254
+ // Section-name allowlist — mirrors the server contract (identity-store.ts
255
255
  // SECTION_NAME_RE / SECTION_NAME_MAX). Client check is UX; the server also
256
256
  // enforces it and is the authority.
257
257
  // =========================================================================
@@ -346,14 +346,14 @@
346
346
  var afterAuth = null; // action to retry after a successful token prompt
347
347
 
348
348
  // Per-agent scope (0.13). scope = null → the global set; otherwise an agent
349
- // id, and edits target PUT /context?agent=<id>. `agents` is the id -> mode
349
+ // id, and edits target PUT /identity?agent=<id>. `agents` is the id -> mode
350
350
  // map from the bare (global) GET; `origins` is the id -> "global"|"agent"
351
351
  // provenance from an agent-scope GET (used to dim inherited tabs).
352
352
  var scope = null;
353
353
  var agents = {};
354
354
  var origins = {};
355
355
 
356
- // ?agent= query for the active scope. Written so the literal fetch("/context"
356
+ // ?agent= query for the active scope. Written so the literal fetch("/identity"
357
357
  // survives the self-containment test; the server does the resolution.
358
358
  function scopeQS() {
359
359
  return scope ? "?agent=" + encodeURIComponent(scope) : "";
@@ -447,11 +447,11 @@
447
447
  }
448
448
 
449
449
  // =========================================================================
450
- // Load — GET /context, build tabs from the returned sections keys.
450
+ // Load — GET /identity, build tabs from the returned sections keys.
451
451
  // =========================================================================
452
452
  function load() {
453
453
  setMsg("Loading…", "dim");
454
- fetch("/context" + scopeQS(), { headers: authHeaders() })
454
+ fetch("/identity" + scopeQS(), { headers: authHeaders() })
455
455
  .then(function (resp) {
456
456
  if (resp.status === 401) { afterAuth = load; showTokenPrompt(); throw new Error("unauthorized"); }
457
457
  if (!resp.ok) {
@@ -497,7 +497,7 @@
497
497
  }
498
498
 
499
499
  // =========================================================================
500
- // Save — PUT /context with ONLY the active section (partial upsert).
500
+ // Save — PUT /identity with ONLY the active section (partial upsert).
501
501
  // =========================================================================
502
502
  function save() {
503
503
  if (!active) return;
@@ -515,7 +515,7 @@
515
515
 
516
516
  saveBtn.disabled = true;
517
517
  setMsg("Saving…", "dim");
518
- fetch("/context" + scopeQS(), {
518
+ fetch("/identity" + scopeQS(), {
519
519
  method: "PUT",
520
520
  headers: authHeaders({ "Content-Type": "application/json" }),
521
521
  body: JSON.stringify(payload),
@@ -576,7 +576,7 @@
576
576
  }
577
577
 
578
578
  // =========================================================================
579
- // Scope switching — reload the selected agent's (or the global) context.
579
+ // Scope switching — reload the selected agent's (or the global) identity.
580
580
  // Warns before discarding unsaved edits in the current scope.
581
581
  // =========================================================================
582
582
  function anyDirty() {
@@ -623,7 +623,7 @@
623
623
  try { localStorage.setItem("hicortexToken", v); } catch (e) { /* ignore */ }
624
624
  tokenErr.style.display = "none";
625
625
  // Verify with a cheap GET before closing; retry the pending action on success.
626
- fetch("/context", { headers: authHeaders() }).then(function (resp) {
626
+ fetch("/identity", { headers: authHeaders() }).then(function (resp) {
627
627
  if (resp.status === 401) { tokenErr.style.display = "block"; return; }
628
628
  overlay.classList.remove("open");
629
629
  var next = afterAuth || load;
package/assets/viz.html CHANGED
@@ -249,7 +249,7 @@
249
249
  <nav class="hc-nav-links">
250
250
  <a href="/dashboard">Dashboard</a>
251
251
  <a href="/viz" data-nav-active>Graph</a>
252
- <a href="/context/ui">Context</a>
252
+ <a href="/identity/ui">Identity</a>
253
253
  <span class="hc-nav-disabled" title="Coming soon — #250">Self-improvement</span>
254
254
  </nav>
255
255
  </div>
@@ -259,7 +259,7 @@
259
259
  <div class="row" data-tip="Show only memories from one knowledge domain. Node colors match domains (see legend)."><label for="f-domain">Domain</label>
260
260
  <select id="f-domain"><option value="">all</option></select>
261
261
  </div>
262
- <div class="row" data-tip="Show only one memory type (episode, lesson, fact…)."><label for="f-type">Type</label>
262
+ <div class="row" data-tip="Show only one memory type (Knowledge, Experience, Decisions, Learnings)."><label for="f-type">Type</label>
263
263
  <select id="f-type"><option value="">all</option></select>
264
264
  </div>
265
265
  <div class="row" data-tip="Hide memories weaker than this. Strength decays over time and grows with use and links — node size shows it."><label for="f-strength">Min strength</label>
@@ -388,6 +388,18 @@
388
388
  var metaEl = document.getElementById("meta");
389
389
  var selDomain = document.getElementById("f-domain");
390
390
  var selType = document.getElementById("f-type");
391
+
392
+ // #264 WS2: human-term labels for the internal memory_type enum. Node data
393
+ // carries the RAW enum (the /graph JSON contract filters on it); only the
394
+ // rendered strings change. Unknown values pass through unchanged.
395
+ var TYPE_LABELS = {
396
+ fact: "Knowledge",
397
+ episode: "Experience",
398
+ decision: "Decisions",
399
+ lesson: "Learnings"
400
+ };
401
+ function labelForType(t) { return TYPE_LABELS[t] || t; }
402
+
391
403
  var rngStrength = document.getElementById("f-strength");
392
404
  var rngStrengthVal = document.getElementById("f-strength-val");
393
405
  var inpLimit = document.getElementById("f-limit");
@@ -604,7 +616,7 @@
604
616
  dataVersion++;
605
617
 
606
618
  fillSelect(selDomain, data.domains || []);
607
- fillSelect(selType, data.types || []);
619
+ fillSelect(selType, data.types || [], labelForType);
608
620
  buildDomainLegend(data.domains || []);
609
621
  buildEdgeLegend(data.edges || []);
610
622
  computeLabeled();
@@ -697,12 +709,12 @@
697
709
  }
698
710
  }
699
711
 
700
- function fillSelect(sel, values) {
712
+ function fillSelect(sel, values, labeler) {
701
713
  var current = sel.value;
702
714
  while (sel.options.length > 1) sel.remove(1);
703
715
  values.forEach(function (v) {
704
716
  var opt = document.createElement("option");
705
- opt.value = v; opt.textContent = v;
717
+ opt.value = v; opt.textContent = labeler ? labeler(v) : v;
706
718
  sel.appendChild(opt);
707
719
  });
708
720
  sel.value = values.indexOf(current) >= 0 ? current : "";
@@ -733,7 +745,7 @@
733
745
 
734
746
  function tooltipHtml(n) {
735
747
  return "<b>" + esc(labelText(n)) + "</b><br>" +
736
- esc((n.domain || "no domain") + " · " + (n.memory_type || "?") +
748
+ esc((n.domain || "no domain") + " · " + labelForType(n.memory_type || "?") +
737
749
  " · strength " + (typeof n.strength === "number" ? n.strength.toFixed(2) : "?"));
738
750
  }
739
751
 
@@ -1070,7 +1082,7 @@
1070
1082
  ["ID", n.id],
1071
1083
  ["Domain", n.domain || "—"],
1072
1084
  ["Project", n.project || "—"],
1073
- ["Type", n.memory_type || "—"],
1085
+ ["Type", labelForType(n.memory_type || "—")],
1074
1086
  ["Strength", typeof n.strength === "number" ? n.strength.toFixed(3) : "—"],
1075
1087
  ["Links", String(n.linkCount) + (n.isHub ? " (hub)" : "")],
1076
1088
  ["Created", n.created_at || "—"]
@@ -2,7 +2,8 @@
2
2
  * CLAUDE.md block management — removeLessonsBlock only (0.9.0+).
3
3
  *
4
4
  * Lesson injection was removed in 0.9.0: lessons are now fetched at query time
5
- * via the CC SessionStart hook (`hicortex lessons-context`) and by the Hermes
5
+ * via the CC SessionStart hook (`hicortex learnings-identity`, aliased as the
6
+ * legacy `lessons-context`) and by the Hermes
6
7
  * plugin's prefetch/system_prompt_block. File-based injection caused EPERM
7
8
  * errors on macOS, stale blocks, and unwinnable multi-file bookkeeping.
8
9
  *
package/dist/claude-md.js CHANGED
@@ -3,7 +3,8 @@
3
3
  * CLAUDE.md block management — removeLessonsBlock only (0.9.0+).
4
4
  *
5
5
  * Lesson injection was removed in 0.9.0: lessons are now fetched at query time
6
- * via the CC SessionStart hook (`hicortex lessons-context`) and by the Hermes
6
+ * via the CC SessionStart hook (`hicortex learnings-identity`, aliased as the
7
+ * legacy `lessons-context`) and by the Hermes
7
8
  * plugin's prefetch/system_prompt_block. File-based injection caused EPERM
8
9
  * errors on macOS, stale blocks, and unwinnable multi-file bookkeeping.
9
10
  *
@@ -14,3 +14,12 @@ export declare class MissingFlagValueError extends Error {
14
14
  * `--server` as the agent name — a typo must be loud.
15
15
  */
16
16
  export declare function readValueFlag(argv: string[], flag: string): string | undefined;
17
+ /**
18
+ * Normalize a raw CLI command word. Hidden aliases resolve to their canonical
19
+ * name so the cli.ts dispatch switch only spells the canonical form. Aliases
20
+ * today (#264 rename): `context` → `identity` and `lessons-context` →
21
+ * `learnings-identity`; kept so old scripts, installed hooks, and muscle memory
22
+ * keep working. Pure + side-effect-free (unlike cli.ts, which dispatches on
23
+ * import) so it can be unit-tested directly.
24
+ */
25
+ export declare function resolveCommandAlias(raw: string | undefined): string | undefined;
package/dist/cli-args.js CHANGED
@@ -7,6 +7,7 @@
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.MissingFlagValueError = void 0;
9
9
  exports.readValueFlag = readValueFlag;
10
+ exports.resolveCommandAlias = resolveCommandAlias;
10
11
  /** Thrown when a value-flag is present but missing its value. */
11
12
  class MissingFlagValueError extends Error {
12
13
  }
@@ -28,3 +29,18 @@ function readValueFlag(argv, flag) {
28
29
  }
29
30
  return val;
30
31
  }
32
+ /**
33
+ * Normalize a raw CLI command word. Hidden aliases resolve to their canonical
34
+ * name so the cli.ts dispatch switch only spells the canonical form. Aliases
35
+ * today (#264 rename): `context` → `identity` and `lessons-context` →
36
+ * `learnings-identity`; kept so old scripts, installed hooks, and muscle memory
37
+ * keep working. Pure + side-effect-free (unlike cli.ts, which dispatches on
38
+ * import) so it can be unit-tested directly.
39
+ */
40
+ function resolveCommandAlias(raw) {
41
+ if (raw === "context")
42
+ return "identity";
43
+ if (raw === "lessons-context")
44
+ return "learnings-identity";
45
+ return raw;
46
+ }