@gamaze/hicortex 0.17.6 → 0.18.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.
Files changed (51) hide show
  1. package/README.md +30 -28
  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/learnings-identity.d.ts +149 -0
  27. package/dist/{lessons-context.js → learnings-identity.js} +96 -52
  28. package/dist/mcp-server.d.ts +2 -0
  29. package/dist/mcp-server.js +217 -68
  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
  51. package/dist/lessons-context.d.ts +0 -102
package/dist/nightly.js CHANGED
@@ -69,6 +69,7 @@ const oc_transcript_reader_js_1 = require("./oc-transcript-reader.js");
69
69
  const features_js_1 = require("./features.js");
70
70
  const retrieval_js_1 = require("./retrieval.js");
71
71
  const state_js_1 = require("./state.js");
72
+ const identity_store_js_1 = require("./identity-store.js");
72
73
  const capture_cursors_js_1 = require("./capture-cursors.js");
73
74
  const capture_js_1 = require("./capture.js");
74
75
  const dashboard_js_1 = require("./dashboard.js");
@@ -132,12 +133,16 @@ function computeSince(stateDir, recaptureWindowDays) {
132
133
  }
133
134
  return lastRun;
134
135
  }
135
- /** POST /distill transport for server mode — localhost, no auth (localhost bypasses). */
136
- function makeLocalPost(port) {
136
+ /** POST /distill transport for server mode — localhost. Sends authToken so
137
+ * self-capture works regardless of the localhost-bypass marker (#271 root-cause fix). */
138
+ function makeLocalPost(port, authToken) {
137
139
  return async (body) => {
138
140
  const resp = await fetch(`http://127.0.0.1:${port}/distill`, {
139
141
  method: "POST",
140
- headers: { "Content-Type": "application/json" },
142
+ headers: {
143
+ "Content-Type": "application/json",
144
+ ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
145
+ },
141
146
  body: JSON.stringify(body),
142
147
  // Synchronous 35B distillation of a large segment can take minutes.
143
148
  signal: AbortSignal.timeout(20 * 60 * 1000),
@@ -223,6 +228,16 @@ async function runNightly(options = {}) {
223
228
  rotateNightlyLog(stateDir);
224
229
  // One-time migration of legacy state files (no-op if state.json exists)
225
230
  (0, state_js_1.migrateLegacyState)(stateDir);
231
+ // #264: rename <home>/context/ → identity/ on the next nightly run when only
232
+ // the legacy dir exists. The identity-store fallback read is the safety net
233
+ // for a partial/no migration. No-op when neither dir exists (fresh install).
234
+ const idMig = (0, identity_store_js_1.migrateIdentityDir)(stateDir);
235
+ if (idMig.renamed) {
236
+ console.log(`[hicortex] Migrated identity dir: ${idMig.from} → ${idMig.to}`);
237
+ }
238
+ else if (idMig.reason && idMig.reason !== "no legacy context/ dir" && !idMig.reason.startsWith("identity/ already exists")) {
239
+ console.warn(`[hicortex] Identity dir migration skipped: ${idMig.reason}`);
240
+ }
226
241
  // Check mode: client or server
227
242
  const savedConfig = readNightlyConfig(stateDir);
228
243
  // 0.16.8 upgrade guard: warn if ignored per-stage keys are still present.
@@ -401,7 +416,7 @@ async function runNightly(options = {}) {
401
416
  // source_agent_id / source_domain are per-client provenance from
402
417
  // config.json (agentId / sourceDomain) — attribution only, no filtering.
403
418
  const result = await (0, capture_js_1.captureBatches)(batches, {
404
- post: makeLocalPost(port),
419
+ post: makeLocalPost(port, savedConfig?.authToken),
405
420
  cursorStore,
406
421
  dryRun,
407
422
  sourceAgentId: savedConfig?.agentId,
@@ -450,6 +465,18 @@ async function runNightly(options = {}) {
450
465
  let tokensThisRun;
451
466
  // #246: per-stage token breakdown (hoisted for the dashboard snapshot).
452
467
  let tokensByStage;
468
+ // #255: budget-exhaustion flag + per-stage deferred counts (hoisted for
469
+ // telemetry + the dashboard snapshot). Undefined when consolidation didn't
470
+ // run at all (capture-only / no_llm / throttled) — the optional fields are
471
+ // omitted so the aggregate treats absent as "not measurable".
472
+ let budgetExhausted;
473
+ let budgetDeferredByStage;
474
+ // #255 CR: always-on usage metric — hoisted for the dashboard snapshot so
475
+ // the digest renders a continuous used/max bar (consolidation
476
+ // completeness as a health metric), not just an amber pill at exhaustion.
477
+ // Undefined when consolidation didn't run; presence = a run happened.
478
+ let budgetCallsUsed;
479
+ let budgetMaxCalls;
453
480
  if (!dryRun && !captureOnly) {
454
481
  if (!llm || !llmConfig) {
455
482
  console.error("[hicortex] consolidation skipped: no LLM configured — run npx @gamaze/hicortex init");
@@ -540,6 +567,19 @@ async function runNightly(options = {}) {
540
567
  tokensThisRun = tokensTotal.total;
541
568
  tokensByStage = report.budget?.tokens_by_stage;
542
569
  }
570
+ // #255: budget exhaustion — always populated when consolidation ran
571
+ // (report.budget.exhausted is a boolean). The dashboard + telemetry
572
+ // treat true as a quality-degradation health signal. The
573
+ // ran-vs-didn't-run distinction is carried by `budgetCallsUsed`/
574
+ // `budgetMaxCalls` (forwarded whenever consolidation ran), NOT by a
575
+ // false `budget_exhausted` flag — the snapshot forwards
576
+ // `budget_exhausted` only on exhaustion (alert state), so the
577
+ // aggregate reads: calls_used present + budget_exhausted undefined
578
+ // = "ran and didn't exhaust"; calls_used undefined = "didn't run".
579
+ budgetExhausted = report.budget?.exhausted;
580
+ budgetDeferredByStage = report.budget?.deferred_by_stage;
581
+ budgetCallsUsed = report.budget?.calls_used;
582
+ budgetMaxCalls = report.budget?.max_calls;
543
583
  // #246: accrue to state.json (monthly reset + last-run estimate for
544
584
  // the next throttle check). Written even on a failed run — a partial
545
585
  // run that made metered calls before the failure still spent tokens,
@@ -633,6 +673,15 @@ async function runNightly(options = {}) {
633
673
  // when consolidation didn't run or made no metered calls).
634
674
  tokensThisRun,
635
675
  tokensByStage,
676
+ // #255 CR: always-on budget usage — undefined when consolidation
677
+ // didn't run (capture-only / no_llm / throttled). Forwarded whenever
678
+ // consolidation ran so the digest renders a continuous used/max bar.
679
+ budgetCallsUsed,
680
+ budgetMaxCalls,
681
+ // #255: budget exhaustion — undefined when consolidation didn't run
682
+ // (capture-only / no_llm / throttled) or didn't exhaust.
683
+ budgetExhausted,
684
+ budgetDeferredByStage,
636
685
  }, memorySoftCapResolved);
637
686
  }
638
687
  catch (snapErr) {
@@ -675,6 +724,10 @@ async function runNightly(options = {}) {
675
724
  // #246: total tokens consumed by this run's consolidation (absent on
676
725
  // capture-only / throttled / no_llm / skipped — no metered calls).
677
726
  tokens_this_run: tokensThisRun,
727
+ // #255: budget exhaustion — forwarded only when consolidation ran AND
728
+ // exhausted (false is omitted to keep the ping minimal; the aggregate
729
+ // treats absent as "not exhausted / not measurable").
730
+ ...(budgetExhausted ? { budget_exhausted: true } : {}),
678
731
  sessions: batches.length,
679
732
  ok: !hadTransientFailure,
680
733
  shown: adoption.shown,
@@ -720,11 +773,17 @@ async function runClientNightly(config, dryRun, stateDir = HICORTEX_HOME, recapt
720
773
  let reachable = false;
721
774
  for (let attempt = 1; attempt <= PREFLIGHT_ATTEMPTS; attempt++) {
722
775
  try {
776
+ // PUBLIC /health probe — liveness only, no auth required. Client-mode
777
+ // preflight runs against a REMOTE server over Tailscale, and the client
778
+ // has NO bearer token to hand on this path (the auth token is the
779
+ // server's, not the client's; /distill uses the configured authToken
780
+ // but the liveness check must work even before that resolves). The
781
+ // public /health returns only {status:"ok"} (#253), so we log
782
+ // reachability without a version/memory count.
723
783
  const resp = await fetch(`${serverUrl}/health`, { signal: AbortSignal.timeout(PREFLIGHT_TIMEOUT_MS) });
724
784
  if (!resp.ok)
725
785
  throw new Error(`HTTP ${resp.status}`);
726
- const data = await resp.json();
727
- console.log(`[hicortex] Server OK: v${data.version}, ${data.memories} memories`);
786
+ console.log(`[hicortex] Server reachable at ${serverUrl}`);
728
787
  reachable = true;
729
788
  break;
730
789
  }
package/dist/paths.js CHANGED
@@ -8,7 +8,7 @@ exports.hicortexHome = hicortexHome;
8
8
  * HICORTEX_DB_PATH convention in db.ts); otherwise defaults to ~/.hicortex.
9
9
  * Every module that needs the home dir routes through here, so the override
10
10
  * behaves consistently across all commands instead of being honored by some
11
- * (context-cli, lessons-context) and hardcoded away by others.
11
+ * (identity-cli, learnings-identity) and hardcoded away by others.
12
12
  */
13
13
  const node_os_1 = require("node:os");
14
14
  const node_path_1 = require("node:path");
@@ -8,7 +8,7 @@
8
8
  * - SessionStart (startup/resume/clear/compact): POST a reset so the
9
9
  * server's per-session shown-set matches the fresh context window.
10
10
  *
11
- * Fail-soft like lessons-context: ANY failure (no config, timeout, non-2xx,
11
+ * Fail-soft like learnings-identity: ANY failure (no config, timeout, non-2xx,
12
12
  * parse error) prints nothing and exits 0 — a broken hook must never block or
13
13
  * slow a CC session beyond the fetch timeout (1000 ms, owner-set).
14
14
  */
@@ -9,14 +9,14 @@
9
9
  * - SessionStart (startup/resume/clear/compact): POST a reset so the
10
10
  * server's per-session shown-set matches the fresh context window.
11
11
  *
12
- * Fail-soft like lessons-context: ANY failure (no config, timeout, non-2xx,
12
+ * Fail-soft like learnings-identity: ANY failure (no config, timeout, non-2xx,
13
13
  * parse error) prints nothing and exits 0 — a broken hook must never block or
14
14
  * slow a CC session beyond the fetch timeout (1000 ms, owner-set).
15
15
  */
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.buildHookRequest = buildHookRequest;
18
18
  exports.runRecallHook = runRecallHook;
19
- const lessons_context_js_1 = require("./lessons-context.js");
19
+ const learnings_identity_js_1 = require("./learnings-identity.js");
20
20
  const node_path_1 = require("node:path");
21
21
  const FETCH_TIMEOUT_MS = 1000;
22
22
  /** Read all of stdin (CC pipes the hook payload JSON). */
@@ -51,7 +51,7 @@ function buildHookRequest(payload, cwd = process.cwd()) {
51
51
  return { session_id: sessionId, prompt, project: (0, node_path_1.basename)(cwd) };
52
52
  }
53
53
  async function runRecallHook() {
54
- const cfg = (0, lessons_context_js_1.resolveConfig)();
54
+ const cfg = (0, learnings_identity_js_1.resolveConfig)();
55
55
  if (!cfg)
56
56
  return;
57
57
  let payload;
@@ -62,6 +62,7 @@ exports.handleRecallIndex = handleRecallIndex;
62
62
  exports.handleMemoryGet = handleMemoryGet;
63
63
  exports.formatMemoryGetText = formatMemoryGetText;
64
64
  const storage = __importStar(require("./storage.js"));
65
+ const type_labels_js_1 = require("./type-labels.js");
65
66
  /** Relevance-gate floor for vector-only candidates (config `recallMinSimilarity`).
66
67
  * 0.62 (was 0.55; raised 2026-08-03 on the fine-grain floor sweep — see the
67
68
  * minSimilarity doc above). */
@@ -116,7 +117,7 @@ function formatIndexLine(r, maxLen = DEFAULT_TITLE_CHARS) {
116
117
  formatDate(r.created_at),
117
118
  r.domain ?? r.project ?? undefined,
118
119
  r.source_agent ?? undefined,
119
- r.memory_type,
120
+ (0, type_labels_js_1.labelForType)(r.memory_type),
120
121
  ]
121
122
  .filter(Boolean)
122
123
  .join(", ");
@@ -281,7 +282,9 @@ function formatMemoryGetText(db, query) {
281
282
  const mem = r.body.memory;
282
283
  const citation = r.body.citation; // carries FETCHED (#204)
283
284
  const date = (mem.created_at ?? "").slice(0, 10);
284
- const header = `[memory ${mem.id} | ${mem.memory_type ?? "episode"} | ${mem.project ?? "-"} | from ${mem.source_agent ?? "unknown"} | ${date}]\n` +
285
+ // #264 WS2: render the human-term label (Knowledge/Experience/...), not the
286
+ // internal enum, in the citation header shown to the agent/user.
287
+ const header = `[memory ${mem.id} | ${(0, type_labels_js_1.labelForType)(mem.memory_type ?? "episode")} | ${mem.project ?? "-"} | from ${mem.source_agent ?? "unknown"} | ${date}]\n` +
285
288
  `Cite as ${citation} where this shapes your answer; it may be stale — newer memories supersede older.`;
286
289
  return { status: 200, text: `${header}\n\n${mem.content ?? ""}` };
287
290
  }
package/dist/status.d.ts CHANGED
@@ -4,9 +4,9 @@
4
4
  /**
5
5
  * The value shown after "Agent name:" in `hicortex status` (#179, A3). Reports
6
6
  * EXACTLY what the CC hook resolves (shared `resolveAgentIdentity`), so the
7
- * operator never keys `contextAgents`/`agents/<id>/` on an id the install does
7
+ * operator never keys `identityAgents`/`agents/<id>/` on an id the install does
8
8
  * not actually send. Unset → the install sends no `?agent=` and shares the
9
- * global context (CC default). A configured-but-unsanitizable value is called
9
+ * global identity (CC default). A configured-but-unsanitizable value is called
10
10
  * out as invalid (the hook sends none) rather than silently accepted.
11
11
  */
12
12
  export declare function statusAgentLine(config: Record<string, unknown>): string;
package/dist/status.js CHANGED
@@ -13,27 +13,27 @@ const node_child_process_1 = require("node:child_process");
13
13
  const db_js_1 = require("./db.js");
14
14
  const features_js_1 = require("./features.js");
15
15
  const state_js_1 = require("./state.js");
16
- const context_store_js_1 = require("./context-store.js");
16
+ const identity_store_js_1 = require("./identity-store.js");
17
17
  const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
18
18
  const CC_SETTINGS = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "settings.json");
19
19
  const OC_CONFIG = (0, node_path_1.join)((0, node_os_1.homedir)(), ".openclaw", "openclaw.json");
20
20
  /**
21
21
  * The value shown after "Agent name:" in `hicortex status` (#179, A3). Reports
22
22
  * EXACTLY what the CC hook resolves (shared `resolveAgentIdentity`), so the
23
- * operator never keys `contextAgents`/`agents/<id>/` on an id the install does
23
+ * operator never keys `identityAgents`/`agents/<id>/` on an id the install does
24
24
  * not actually send. Unset → the install sends no `?agent=` and shares the
25
- * global context (CC default). A configured-but-unsanitizable value is called
25
+ * global identity (CC default). A configured-but-unsanitizable value is called
26
26
  * out as invalid (the hook sends none) rather than silently accepted.
27
27
  */
28
28
  function statusAgentLine(config) {
29
- const id = (0, context_store_js_1.resolveAgentIdentity)(config);
29
+ const id = (0, identity_store_js_1.resolveAgentIdentity)(config);
30
30
  switch (id.source) {
31
31
  case "configured":
32
32
  return id.agentId;
33
33
  case "invalid-config":
34
34
  return `(invalid configured value "${id.rawConfigured}" — fix config.agentName; hook sends none)`;
35
35
  default: // unset
36
- return "(not set — global context)";
36
+ return "(not set — global identity)";
37
37
  }
38
38
  }
39
39
  async function runStatus() {
@@ -87,8 +87,8 @@ async function runStatus() {
87
87
  else if (!isClientMode && !savedAuthToken) {
88
88
  console.log(`Auth token: not configured (run: npx @gamaze/hicortex init)`);
89
89
  }
90
- // Per-agent context id (#179) — the id this install sends as ?agent= and the
91
- // key operators use for contextAgents / agents/<id>/ dirs.
90
+ // Per-agent identity id (#179) — the id this install sends as ?agent= and the
91
+ // key operators use for identityAgents / agents/<id>/ dirs.
92
92
  console.log(`Agent name: ${statusAgentLine(parsedConfig)}`);
93
93
  console.log();
94
94
  // Adapters
@@ -119,11 +119,13 @@ async function runStatus() {
119
119
  catch { /* no CC settings */ }
120
120
  console.log(` CC MCP: ${ccRegistered ? `registered → ${ccUrl}` : "not registered"}`);
121
121
  console.log();
122
- // Server status
122
+ // Server status. /health/detail carries the diagnostics (version, memories,
123
+ // llm) — /health itself is the public minimal {status:"ok"} probe (#253).
124
+ // localhost bypasses auth, so on-box `hicortex status` gets the fields.
123
125
  console.log("Server:");
124
126
  let serverRunning = false;
125
127
  try {
126
- const resp = await fetch("http://127.0.0.1:8787/health", {
128
+ const resp = await fetch("http://127.0.0.1:8787/health/detail", {
127
129
  signal: AbortSignal.timeout(2000),
128
130
  });
129
131
  if (resp.ok) {
@@ -106,6 +106,16 @@ export interface TelemetryPayload {
106
106
  * wire (that lives in the dashboard snapshot, not the telemetry ping).
107
107
  */
108
108
  tokens_this_run?: number;
109
+ /**
110
+ * True when the per-tenant consolidation budget (`consolidateMaxLlmCalls`)
111
+ * was exhausted this run (#255) — LLM-bound stages deferred remaining work.
112
+ * A quality-degradation signal concentrated on heavy users; absent on a
113
+ * pre-#255 ping, a capture-only/no-LLM/throttled/skipped run, or when the
114
+ * budget was NOT exhausted (additive optional — the aggregate treats absent
115
+ * as "no exhaustion / not measurable"). Per-stage deferred counts live in
116
+ * the dashboard snapshot, not on the wire (mirrors `tokens_this_run`).
117
+ */
118
+ budget_exhausted?: boolean;
109
119
  }
110
120
  /**
111
121
  * Check if telemetry is enabled. Disabled by:
@@ -45,6 +45,7 @@ const node_path_1 = require("node:path");
45
45
  const db_js_1 = require("./db.js");
46
46
  const state_js_1 = require("./state.js");
47
47
  const llm_js_1 = require("./llm.js");
48
+ const type_labels_js_1 = require("./type-labels.js");
48
49
  const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
49
50
  /** Max chars of memory content fed to the classify prompt. */
50
51
  const CLASSIFY_CONTENT_MAX_CHARS = 1500;
@@ -293,7 +294,9 @@ async function runClassifyTypes(options = {}) {
293
294
  .all();
294
295
  for (const c of counts)
295
296
  report.byType[c.memory_type] = c.cnt;
296
- const breakdown = counts.map((c) => `${c.memory_type}=${c.cnt}`).join(", ") || "none";
297
+ // #264 WS2: display the human-term label (Knowledge/Experience/...), not
298
+ // the internal enum — the operator reading the log sees human terms.
299
+ const breakdown = counts.map((c) => `${(0, type_labels_js_1.labelForType)(c.memory_type)}=${c.cnt}`).join(", ") || "none";
297
300
  console.log(`[hicortex] classify-types ${report.aborted ? "ABORTED" : "complete"}: ` +
298
301
  `${report.scanned} classified, ${report.reclassified} reclassified, ` +
299
302
  `${report.unchanged} unchanged, ${report.failed} infra-skipped`);
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Human-term labels for the internal `memory_type` enum (#264 WS2).
3
+ *
4
+ * The data model stores four types under unintuitive internal names
5
+ * (`fact` / `episode` / `decision` / `lesson`). Product-facing surfaces show
6
+ * the human terms instead — Knowledge / Experience / Decisions / Learnings —
7
+ * via this single label map. The enum values themselves are NEVER changed:
8
+ * no DB column, query, distiller tag, or storage path is altered. This is a
9
+ * DISPLAY rename only; every user-facing rendering routes through
10
+ * `labelForType`.
11
+ *
12
+ * Mapping (decided 2026-08-11, research/2026-08-11-identity-reframe-brainstorm.md):
13
+ * fact → Knowledge
14
+ * episode → Experience
15
+ * decision → Decisions
16
+ * lesson → Learnings
17
+ *
18
+ * Unknown / future types fall back to the raw value (never silently remapped).
19
+ */
20
+ /**
21
+ * The four documented memory types mapped to their human-term labels.
22
+ * Kept as a plain record so it can be iterated for coverage assertions.
23
+ */
24
+ export declare const MEMORY_TYPE_LABELS: Record<string, string>;
25
+ /**
26
+ * Return the human-term label for a `memory_type` enum value. Unknown or
27
+ * future types (including null/undefined) fall back to the raw input so new
28
+ * types are visible rather than silently mislabeled.
29
+ */
30
+ export declare function labelForType(t: string | null | undefined): string;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /**
3
+ * Human-term labels for the internal `memory_type` enum (#264 WS2).
4
+ *
5
+ * The data model stores four types under unintuitive internal names
6
+ * (`fact` / `episode` / `decision` / `lesson`). Product-facing surfaces show
7
+ * the human terms instead — Knowledge / Experience / Decisions / Learnings —
8
+ * via this single label map. The enum values themselves are NEVER changed:
9
+ * no DB column, query, distiller tag, or storage path is altered. This is a
10
+ * DISPLAY rename only; every user-facing rendering routes through
11
+ * `labelForType`.
12
+ *
13
+ * Mapping (decided 2026-08-11, research/2026-08-11-identity-reframe-brainstorm.md):
14
+ * fact → Knowledge
15
+ * episode → Experience
16
+ * decision → Decisions
17
+ * lesson → Learnings
18
+ *
19
+ * Unknown / future types fall back to the raw value (never silently remapped).
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.MEMORY_TYPE_LABELS = void 0;
23
+ exports.labelForType = labelForType;
24
+ /**
25
+ * The four documented memory types mapped to their human-term labels.
26
+ * Kept as a plain record so it can be iterated for coverage assertions.
27
+ */
28
+ exports.MEMORY_TYPE_LABELS = {
29
+ fact: "Knowledge",
30
+ episode: "Experience",
31
+ decision: "Decisions",
32
+ lesson: "Learnings",
33
+ };
34
+ /**
35
+ * Return the human-term label for a `memory_type` enum value. Unknown or
36
+ * future types (including null/undefined) fall back to the raw input so new
37
+ * types are visible rather than silently mislabeled.
38
+ */
39
+ function labelForType(t) {
40
+ if (!t)
41
+ return "—";
42
+ return exports.MEMORY_TYPE_LABELS[t] ?? t;
43
+ }
package/dist/types.d.ts CHANGED
@@ -161,6 +161,26 @@ export interface ConsolidationReport {
161
161
  calls_used: number;
162
162
  calls_remaining: number;
163
163
  calls_by_stage: Record<string, number>;
164
+ /**
165
+ * True when `calls_used >= max_calls` at run end (#255). The run continued
166
+ * to completion (no abort) but LLM-bound stages past the boundary deferred
167
+ * their remaining work — a quality-degradation signal, not a failure.
168
+ * Absent on pre-#255 reports; treat as false.
169
+ */
170
+ exhausted?: boolean;
171
+ /**
172
+ * Per-stage count of LLM-call REQUESTS refused because the budget was
173
+ * exhausted (#255). Keys are the same stage labels passed to
174
+ * `BudgetTracker.use()`. The value is the SUM of the `count` args passed
175
+ * to each refused `use()` call in that stage — in production every `use()`
176
+ * call passes count=1, so a stage present here with count N hit the
177
+ * boundary and had N further single-call requests denied (stages break on
178
+ * first refusal, so N is small per stage). For item-level "how many
179
+ * memories/pairs were skipped" see the per-stage reports (e.g.
180
+ * stages.importance.skipped_budget), which count MEMORIES not call
181
+ * requests. Absent on pre-#255 reports.
182
+ */
183
+ deferred_by_stage?: Record<string, number>;
164
184
  /**
165
185
  * Token usage per stage (#246). Each value sums prompt + completion +
166
186
  * total across every metered LLM call in that stage this run. A stage with
@@ -187,6 +207,14 @@ export interface HicortexConfig {
187
207
  serverUrl?: string;
188
208
  /** Bearer token for the Hicortex server. Localhost bypasses auth by default. */
189
209
  authToken?: string;
210
+ /**
211
+ * Optional PRIOR bearer token kept around during rotation (#254). When set,
212
+ * BOTH `authToken` and `authTokenPrevious` are accepted (constant-time,
213
+ * zero-downtime rotation). Absent/empty → single-token behaviour. Rotate by
214
+ * writing the new value to `authToken` and the old value here, then later
215
+ * clearing this key once all clients have switched.
216
+ */
217
+ authTokenPrevious?: string;
190
218
  /**
191
219
  * Stable per-install UUID generated by `init` (see ensureAgentId in init.ts;
192
220
  * never rotated). Attribution identity of the capturing client — stored on
@@ -2,4 +2,16 @@
2
2
  * Hicortex uninstall — clean removal of CC integration.
3
3
  * Preserves the database (user data).
4
4
  */
5
+ /**
6
+ * Matches a CC SessionStart hook `command` that runs the Hicortex
7
+ * identity/learnings hook — the canonical `learnings-identity` (#264) OR the
8
+ * legacy `lessons-context` alias. Exported so the uninstall behavior (which
9
+ * name variants get cleaned up) is unit-testable without spinning up CC.
10
+ * Word-boundary guard so "learnings-identity" never matches a hypothetical
11
+ * "learnings-identity-foo", the two names stay distinct from each other, and
12
+ * neither collides with the sibling `recall-hook` SessionStart hook.
13
+ */
14
+ export declare const SESSION_START_HOOK_COMMAND_RE: RegExp;
15
+ /** True when a CC hook `command` string runs the Hicortex SessionStart hook. */
16
+ export declare function isHicortexSessionStartHook(command: string): boolean;
5
17
  export declare function runUninstall(): Promise<void>;
package/dist/uninstall.js CHANGED
@@ -4,6 +4,8 @@
4
4
  * Preserves the database (user data).
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.SESSION_START_HOOK_COMMAND_RE = void 0;
8
+ exports.isHicortexSessionStartHook = isHicortexSessionStartHook;
7
9
  exports.runUninstall = runUninstall;
8
10
  const paths_js_1 = require("./paths.js");
9
11
  const node_fs_1 = require("node:fs");
@@ -17,6 +19,20 @@ const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
17
19
  const CC_SETTINGS = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "settings.json");
18
20
  const CC_COMMANDS_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "commands");
19
21
  const CLAUDE_MD = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "CLAUDE.md");
22
+ /**
23
+ * Matches a CC SessionStart hook `command` that runs the Hicortex
24
+ * identity/learnings hook — the canonical `learnings-identity` (#264) OR the
25
+ * legacy `lessons-context` alias. Exported so the uninstall behavior (which
26
+ * name variants get cleaned up) is unit-testable without spinning up CC.
27
+ * Word-boundary guard so "learnings-identity" never matches a hypothetical
28
+ * "learnings-identity-foo", the two names stay distinct from each other, and
29
+ * neither collides with the sibling `recall-hook` SessionStart hook.
30
+ */
31
+ exports.SESSION_START_HOOK_COMMAND_RE = /(^|\s)(?:learnings-identity|lessons-context)(\s|$)/;
32
+ /** True when a CC hook `command` string runs the Hicortex SessionStart hook. */
33
+ function isHicortexSessionStartHook(command) {
34
+ return typeof command === "string" && exports.SESSION_START_HOOK_COMMAND_RE.test(command);
35
+ }
20
36
  async function ask(question) {
21
37
  const rl = (0, node_readline_1.createInterface)({ input: process.stdin, output: process.stdout });
22
38
  return new Promise((resolve) => {
@@ -130,7 +146,9 @@ async function runUninstall() {
130
146
  }
131
147
  if (removedCmds > 0)
132
148
  console.log(` ✓ Removed ${removedCmds} legacy CC command${removedCmds > 1 ? "s" : ""} (/learn, /hicortex-activate)`);
133
- // 4. Remove SessionStart hook (JSON merge — filter out entries containing "lessons-context")
149
+ // 4. Remove SessionStart hook (JSON merge — filter out entries containing
150
+ // EITHER the canonical "learnings-identity" OR the legacy "lessons-context"
151
+ // alias, #264 backcompat: an install may have written either name.)
134
152
  try {
135
153
  const raw = (0, node_fs_1.readFileSync)(CC_SETTINGS, "utf-8");
136
154
  const settings = JSON.parse(raw);
@@ -147,7 +165,7 @@ async function runUninstall() {
147
165
  if (typeof h !== "object" || h === null)
148
166
  return false;
149
167
  const hook = h;
150
- return typeof hook.command === "string" && hook.command.includes("lessons-context");
168
+ return typeof hook.command === "string" && isHicortexSessionStartHook(hook.command);
151
169
  });
152
170
  }
153
171
  return true;
@@ -155,7 +173,7 @@ async function runUninstall() {
155
173
  if (filtered.length < before) {
156
174
  hooks.SessionStart = filtered;
157
175
  (0, node_fs_1.writeFileSync)(CC_SETTINGS, JSON.stringify(settings, null, 2));
158
- console.log(" ✓ Removed SessionStart lessons-context hook");
176
+ console.log(" ✓ Removed SessionStart learnings-identity hook");
159
177
  }
160
178
  }
161
179
  }
package/dist/viz.d.ts CHANGED
@@ -37,8 +37,16 @@ export declare const VIZ_VENDOR_FILES: ReadonlySet<string>;
37
37
  * middleware before this runs). With no token configured, remote requests are
38
38
  * REJECTED (not open): the default bind is 0.0.0.0, so "no token = no auth"
39
39
  * would expose the whole memory store to the network.
40
+ *
41
+ * `authTokenPrevious` (optional, #254) is the prior token kept around during
42
+ * rotation. Both tokens are accepted; this gives a zero-downtime rotation
43
+ * window — in-flight clients configured with the old token keep working until
44
+ * they pick up the new one. BOTH comparisons are constant-time and both are
45
+ * always evaluated (no short-circuit), so a caller cannot learn WHICH token
46
+ * matched from the response timing. Absent/empty `authTokenPrevious` behaves
47
+ * exactly as the single-token middleware always has.
40
48
  */
41
- export declare function createAuthMiddleware(authToken: string | undefined): express.RequestHandler;
49
+ export declare function createAuthMiddleware(authToken: string | undefined, authTokenPrevious?: string): express.RequestHandler;
42
50
  /**
43
51
  * Resolve the on-disk path of the viz page. Throws (fail explicitly) when the
44
52
  * asset is missing — a broken install should surface, not degrade silently.
@@ -52,24 +60,29 @@ export declare function readVizHtml(): string;
52
60
  */
53
61
  export declare function vizHandler(): express.RequestHandler;
54
62
  /**
55
- * Resolve the on-disk path of the context-layer editor page. Throws (fail
63
+ * Resolve the on-disk path of the identity-layer editor page. Throws (fail
56
64
  * explicitly) when the asset is missing — same contract as resolveVizHtmlPath.
57
65
  * assets/ sits next to both dist/ (dist/viz.js → ../assets/) and src/
58
66
  * (src/viz.ts → ../assets/ under tsx), so one sibling candidate covers both.
59
67
  */
60
- export declare function resolveContextHtmlPath(): string;
61
- /** Read the context editor page. Read at request time so a reinstall is live. */
62
- export declare function readContextHtml(): string;
68
+ export declare function resolveIdentityHtmlPath(): string;
69
+ /** Read the identity editor page. Read at request time so a reinstall is live. */
70
+ export declare function readIdentityHtml(): string;
63
71
  /**
64
- * Express handler for GET /context/ui — the PRIMARY edit surface for the
65
- * standing context layer. 503 with the usual {error} shape when the asset
66
- * cannot be read, exactly like vizHandler.
72
+ * Express handler for GET /identity/ui — the PRIMARY edit surface for the
73
+ * standing identity layer. 503 with the usual {error} shape when the asset
74
+ * cannot be read, exactly like vizHandler. Also serves the legacy
75
+ * /context/ui URL (#264 backcompat).
67
76
  */
68
- export declare function contextUiHandler(): express.RequestHandler;
77
+ export declare function identityUiHandler(): express.RequestHandler;
78
+ /** Backcompat aliases (#264). */
79
+ export declare const resolveContextHtmlPath: typeof resolveIdentityHtmlPath;
80
+ export declare const readContextHtml: typeof readIdentityHtml;
81
+ export declare const contextUiHandler: typeof identityUiHandler;
69
82
  /**
70
83
  * Resolve the on-disk path of the dashboard page. Throws (fail explicitly)
71
84
  * when the asset is missing — same contract as resolveVizHtmlPath and
72
- * resolveContextHtmlPath. assets/ sits next to both dist/ and src/ (the
85
+ * resolveIdentityHtmlPath. assets/ sits next to both dist/ and src/ (the
73
86
  * sibling layout the other resolvers rely on).
74
87
  */
75
88
  export declare function resolveDashboardHtmlPath(): string;
@@ -78,7 +91,7 @@ export declare function readDashboardHtml(): string;
78
91
  /**
79
92
  * Express handler for GET /dashboard — the view-only analytics page (#224).
80
93
  * 503 with the usual {error} shape when the asset cannot be read, exactly like
81
- * vizHandler and contextUiHandler. The page SHELL is public (exempted in
94
+ * vizHandler and identityUiHandler. The page SHELL is public (exempted in
82
95
  * createAuthMiddleware); all data comes from GET /dashboard/data (bearer-only).
83
96
  */
84
97
  export declare function dashboardHandler(): express.RequestHandler;