@gamaze/hicortex 0.17.5 → 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 (51) 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.d.ts +13 -9
  41. package/dist/type-classify.js +26 -12
  42. package/dist/type-labels.d.ts +30 -0
  43. package/dist/type-labels.js +43 -0
  44. package/dist/types.d.ts +28 -0
  45. package/dist/uninstall.d.ts +12 -0
  46. package/dist/uninstall.js +21 -3
  47. package/dist/viz.d.ts +24 -11
  48. package/dist/viz.js +97 -32
  49. package/hermes-plugin/hicortex/README.md +4 -2
  50. package/package.json +2 -2
  51. package/dist/context-cli.d.ts +0 -69
@@ -49,6 +49,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
49
49
  };
50
50
  Object.defineProperty(exports, "__esModule", { value: true });
51
51
  exports.startServer = startServer;
52
+ exports.formatResults = formatResults;
52
53
  const express_1 = __importDefault(require("express"));
53
54
  const node_path_1 = require("node:path");
54
55
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
@@ -64,14 +65,17 @@ const storage = __importStar(require("./storage.js"));
64
65
  const graph_js_1 = require("./graph.js");
65
66
  const viz_js_1 = require("./viz.js");
66
67
  const dashboard_js_1 = require("./dashboard.js");
67
- const context_store_js_1 = require("./context-store.js");
68
+ const identity_store_js_1 = require("./identity-store.js");
68
69
  const retrieval = __importStar(require("./retrieval.js"));
69
70
  const recall_registry_js_1 = require("./recall-registry.js");
70
71
  const memory_instructions_js_1 = require("./memory-instructions.js");
71
72
  const recall_index_js_1 = require("./recall-index.js");
73
+ const type_labels_js_1 = require("./type-labels.js");
74
+ const health_js_1 = require("./health.js");
72
75
  const seed_lesson_js_1 = require("./seed-lesson.js");
73
76
  const distiller_js_1 = require("./distiller.js");
74
77
  const dedup_js_1 = require("./dedup.js");
78
+ const redact_js_1 = require("./redact.js");
75
79
  const init_js_1 = require("./init.js");
76
80
  // ---------------------------------------------------------------------------
77
81
  // Server state
@@ -97,13 +101,15 @@ function warnDeprecatedPrivacyParamIfPresent(query, route) {
97
101
  `Use a separate Hicortex server for isolation. (This warning fires once per process.)`);
98
102
  }
99
103
  let stateDir = "";
100
- // Resolved contextClients list (spec §2) — the harness names allowed to inject
101
- // the standing context layer. Echoed by GET /context so each hook self-gates.
102
- let contextClients = ["cc"];
103
- // Resolved contextAgents map (0.13) — agent id → mode (override/global/off).
104
- // Read once at boot (like contextClients); the drop-in-a-dir presence path is
104
+ // Resolved identityClients list (spec §2) — the harness names allowed to inject
105
+ // the standing identity layer. Echoed by GET /identity so each hook self-gates.
106
+ // (#264: was contextClients; the legacy key is still read with a one-time
107
+ // deprecation warning via resolveIdentityClientsConfig.)
108
+ let identityClients = ["cc"];
109
+ // Resolved identityAgents map (0.13) — agent id → mode (override/global/off).
110
+ // Read once at boot (like identityClients); the drop-in-a-dir presence path is
105
111
  // per-request, so only explicit config entries need a daemon restart to apply.
106
- let contextAgents = {};
112
+ let identityAgents = {};
107
113
  // Pushed-recall dedup registry (#192) + options; configured at boot.
108
114
  let recallRegistry = new recall_registry_js_1.SessionRecallRegistry();
109
115
  let recallIndexOptions = {};
@@ -464,21 +470,33 @@ async function startServer(options = {}) {
464
470
  console.warn("[hicortex] WARNING: no authToken configured — remote connections will be rejected " +
465
471
  "(localhost still works). Run `npx @gamaze/hicortex init` to generate a token.");
466
472
  }
467
- // Context layer (0.12): resolve which harnesses may inject the standing
468
- // context. Warn once per boot on unknown names so typos (e.g. "herms")
469
- // surface instead of silently dropping.
470
- const resolvedClients = (0, context_store_js_1.resolveContextClients)(savedConfig?.contextClients);
471
- contextClients = resolvedClients.clients;
473
+ // Optional rotation-grace token (#254): config-only (no env var rotation is
474
+ // an explicit, deliberate op). When set, both tokens are accepted so client
475
+ // reconfiguration never causes failed requests.
476
+ const authTokenPrevious = savedConfig?.authTokenPrevious;
477
+ // Identity layer (0.12; renamed from context layer in 0.18 #264): resolve
478
+ // which harnesses may inject the standing identity. Warn once per boot on
479
+ // unknown names so typos (e.g. "herms") surface instead of silently dropping.
480
+ const resolvedClients = (0, identity_store_js_1.resolveIdentityClientsConfig)(savedConfig);
481
+ identityClients = resolvedClients.clients;
482
+ if (resolvedClients.legacy) {
483
+ console.warn("[hicortex] Config uses the legacy 'contextClients' key — renamed to 'identityClients' in 0.18 (#264). " +
484
+ "The legacy key still works; update your config to silence this warning.");
485
+ }
472
486
  if (resolvedClients.dropped.length > 0) {
473
- console.warn(`[hicortex] Ignoring unknown contextClients: ${resolvedClients.dropped.join(", ")} ` +
487
+ console.warn(`[hicortex] Ignoring unknown identityClients: ${resolvedClients.dropped.join(", ")} ` +
474
488
  `(known: cc, hermes, oc)`);
475
489
  }
476
- // Per-agent context (0.13): resolve the config-declared modes. Warn once per
490
+ // Per-agent identity (0.13): resolve the config-declared modes. Warn once per
477
491
  // boot on dropped entries (bad key or bad mode) so typos surface. NOTE: this
478
- // map is boot-time; editing contextAgents needs a daemon restart. Dropping an
492
+ // map is boot-time; editing identityAgents needs a daemon restart. Dropping an
479
493
  // agents/<id> dir onto disk takes effect immediately (per-request presence).
480
- const resolvedAgents = (0, context_store_js_1.resolveContextAgents)(savedConfig?.contextAgents);
481
- contextAgents = resolvedAgents.agents;
494
+ const resolvedAgents = (0, identity_store_js_1.resolveIdentityAgentsConfig)(savedConfig);
495
+ identityAgents = resolvedAgents.agents;
496
+ if (resolvedAgents.legacy) {
497
+ console.warn("[hicortex] Config uses the legacy 'contextAgents' key — renamed to 'identityAgents' in 0.18 (#264). " +
498
+ "The legacy key still works; update your config to silence this warning.");
499
+ }
482
500
  // #192 recall/decay alignment: decay speed + recall breadth + pushed-recall
483
501
  // knobs, ALL from config (see retrieval.ts configureRecall for the key list)
484
502
  // so calibration is a config edit + restart, never a release.
@@ -504,9 +522,19 @@ async function startServer(options = {}) {
504
522
  };
505
523
  memoryInstructionsEnabled = savedConfig?.memoryInstructions !== false;
506
524
  if (resolvedAgents.dropped.length > 0) {
507
- console.warn(`[hicortex] Ignoring invalid contextAgents entries: ${resolvedAgents.dropped.join(", ")} ` +
525
+ console.warn(`[hicortex] Ignoring invalid identityAgents entries: ${resolvedAgents.dropped.join(", ")} ` +
508
526
  `(keys must match ^[a-z0-9][a-z0-9_-]*$; modes must be override|global|off)`);
509
527
  }
528
+ // #264 dir migration: rename <hicortex-home>/context/ → identity/ on boot
529
+ // when only the legacy dir exists. The fallback read in identity-store.ts
530
+ // (readSectionsWithFallback) is the safety net for a partial/no migration.
531
+ const idMig = (0, identity_store_js_1.migrateIdentityDir)(stateDir);
532
+ if (idMig.renamed) {
533
+ console.log(`[hicortex] Migrated identity dir: ${idMig.from} → ${idMig.to}`);
534
+ }
535
+ else if (idMig.reason && idMig.reason !== "no legacy context/ dir" && !idMig.reason.startsWith("identity/ already exists")) {
536
+ console.warn(`[hicortex] Identity dir migration skipped: ${idMig.reason}`);
537
+ }
510
538
  // Express app
511
539
  const app = (0, express_1.default)();
512
540
  // Raise the body limit — whole-session denoised transcripts exceed the 100 kB default.
@@ -551,20 +579,32 @@ async function startServer(options = {}) {
551
579
  // /dashboard has its own shell-exemption pattern. Gives the console one entry
552
580
  // point: http://<host>:8787/ → /dashboard.
553
581
  app.get("/", (_req, res) => res.redirect("/dashboard"));
554
- app.use((0, viz_js_1.createAuthMiddleware)(authToken));
582
+ app.use((0, viz_js_1.createAuthMiddleware)(authToken, authTokenPrevious));
555
583
  // SSE transport management — each connection gets its own McpServer instance
556
584
  const transports = new Map();
557
- // Health endpoint
585
+ // Health endpoint — PUBLIC minimal probe. Unauthenticated (the auth
586
+ // middleware exempts /health) and carries NO data: just liveness for load
587
+ // balancers, watchdogs, and anonymous probers. Tenant/install BI (memory
588
+ // count, link count, DB size, version, the full LLM backend string) lives
589
+ // on /health/detail, which is auth-gated (localhost bypasses auth so
590
+ // co-located tooling — `hicortex status`, nightly preflight, `init` detect
591
+ // — sees it without a token). #253 — spec 2026-07-27-hosted-service §6.
558
592
  app.get("/health", (_req, res) => {
593
+ res.json((0, health_js_1.publicHealthResponse)());
594
+ });
595
+ // Operator-only diagnostics. Goes through the standard auth middleware
596
+ // (not in the public-path exemption list in viz.ts); localhost bypasses
597
+ // auth, remote needs the bearer token. Keeps the public LB/watchdog path
598
+ // cheap (no COUNT(*)) and the diagnostics off the public surface.
599
+ app.get("/health/detail", (_req, res) => {
559
600
  const s = db ? (0, db_js_1.getStats)(db, dbPath) : { memories: 0, links: 0, db_size_bytes: 0, by_type: {} };
560
- res.json({
561
- status: "ok",
562
- version: VERSION,
601
+ res.json((0, health_js_1.detailedHealthResponse)({
563
602
  memories: s.memories,
564
603
  links: s.links,
565
- db_size_kb: Math.round(s.db_size_bytes / 1024),
566
- llm: llmConfig ? `${llmConfig.provider}/${llmConfig.model}` : "not configured",
567
- });
604
+ dbSizeBytes: s.db_size_bytes,
605
+ version: VERSION,
606
+ llmLabel: llmConfig ? `${llmConfig.provider}/${llmConfig.model}` : "not configured",
607
+ }));
568
608
  });
569
609
  // REST /lessons — return lessons + memory index for client CLAUDE.md injection
570
610
  app.get("/lessons", (_req, res) => {
@@ -599,7 +639,7 @@ async function startServer(options = {}) {
599
639
  });
600
640
  }
601
641
  catch (err) {
602
- res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
642
+ (0, health_js_1.logAndSendInternalError)(res, "lessons", err);
603
643
  }
604
644
  });
605
645
  // REST /ingest — accept pre-distilled memories from remote clients
@@ -644,7 +684,8 @@ async function startServer(options = {}) {
644
684
  res.status(201).json({ id, message: "Memory ingested" });
645
685
  }
646
686
  catch (err) {
647
- res.status(500).json({ error: "Ingestion failed", message: err instanceof Error ? err.message : String(err) });
687
+ res.status(500).json({ error: "Ingestion failed" });
688
+ console.error(`[hicortex] /ingest: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
648
689
  }
649
690
  });
650
691
  // REST /search — semantic search over the memory store.
@@ -671,7 +712,7 @@ async function startServer(options = {}) {
671
712
  res.json({ results });
672
713
  }
673
714
  catch (err) {
674
- res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
715
+ (0, health_js_1.logAndSendInternalError)(res, "search", err);
675
716
  }
676
717
  });
677
718
  // REST /recall-index — pushed recall index (#192). One recall logic for all
@@ -735,7 +776,7 @@ async function startServer(options = {}) {
735
776
  res.status(r.status).json(r.body);
736
777
  }
737
778
  catch (err) {
738
- res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
779
+ (0, health_js_1.logAndSendInternalError)(res, "memory", err);
739
780
  }
740
781
  });
741
782
  // REST /recent — recent memories, optionally filtered by project.
@@ -754,30 +795,36 @@ async function startServer(options = {}) {
754
795
  res.json({ results });
755
796
  }
756
797
  catch (err) {
757
- res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
798
+ (0, health_js_1.logAndSendInternalError)(res, "recent", err);
758
799
  }
759
800
  });
760
801
  // -------------------------------------------------------------------------
761
- // REST /context — standing context layer (0.12, spec 2026-07-12).
802
+ // REST /identity — standing identity layer (0.12, spec 2026-07-12; renamed
803
+ // from /context in 0.18 #264).
762
804
  //
763
- // GET → { sections, updated_at, clients } read from <hicortex-home>/context/.
805
+ // GET → { sections, updated_at, clients } read from <hicortex-home>/identity/.
764
806
  // PUT → partial upsert of named sections (allowlisted names, atomic).
765
807
  //
766
- // This is NOT recall. The recall endpoint that previously held this name is
767
- // now /recent (§Naming). Stale-client tripwire: old recall callers always
768
- // send project/limit/privacy query params; context-layer callers never do —
769
- // so those params on GET /context return a loud, self-explaining 400 instead
770
- // of silently degrading recall to an empty {sections} response.
808
+ // This is NOT recall. The recall endpoint that previously held the /context
809
+ // name is now /recent (§Naming). Stale-client tripwire: old recall callers
810
+ // always send project/limit/privacy query params; identity-layer callers
811
+ // never do — so those params on GET /identity return a loud, self-explaining
812
+ // 400 instead of silently degrading recall to an empty {sections} response.
771
813
  //
772
814
  // Auth is the standard model (bearer; localhost bypass) via the shared
773
815
  // middleware — no special-casing here.
774
816
  // -------------------------------------------------------------------------
775
817
  // Thin adapters: all logic (tripwire, validation, allowlist, atomicity,
776
- // symlink safety, size warn) lives in the pure handlers in context-store.ts,
818
+ // symlink safety, size warn) lives in the pure handlers in identity-store.ts,
777
819
  // which the tests exercise directly — no mirror-app drift.
778
- app.get("/context", (req, res) => {
820
+ //
821
+ // #264 backcompat: GET/PUT /context remain mounted BELOW as aliases that
822
+ // route to the SAME handlers (Hermes/OC plugins and pre-0.18 clients keep
823
+ // working unchanged). Both endpoints read/write the SAME identity dir.
824
+ const identityDir = (0, node_path_1.join)(stateDir, "identity");
825
+ app.get("/identity", (req, res) => {
779
826
  try {
780
- const r = (0, context_store_js_1.handleContextGet)((0, node_path_1.join)(stateDir, "context"), contextClients, req.query, contextAgents);
827
+ const r = (0, identity_store_js_1.handleIdentityGet)(identityDir, identityClients, req.query, identityAgents);
781
828
  // #192: product-owned memory instructions ride as a synthetic read-only
782
829
  // `memory` section (config memoryInstructions !== false; agent mode
783
830
  // "off" respected inside the helper). Every harness renders it via the
@@ -788,10 +835,10 @@ async function startServer(options = {}) {
788
835
  res.status(r.status).json(r.body);
789
836
  }
790
837
  catch (err) {
791
- res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
838
+ (0, health_js_1.logAndSendInternalError)(res, "identity", err);
792
839
  }
793
840
  });
794
- app.put("/context", (req, res) => {
841
+ app.put("/identity", (req, res) => {
795
842
  try {
796
843
  // Reserved product section: never writable, loud error (no silent skip).
797
844
  const putSections = req.body?.sections;
@@ -799,13 +846,46 @@ async function startServer(options = {}) {
799
846
  res.status(400).json({ error: `Section name '${memory_instructions_js_1.MEMORY_SECTION_NAME}' is reserved for the product-owned memory instructions (config memoryInstructions to disable them)` });
800
847
  return;
801
848
  }
802
- const r = (0, context_store_js_1.handleContextPut)((0, node_path_1.join)(stateDir, "context"), req.body, req.query, contextAgents);
849
+ const r = (0, identity_store_js_1.handleIdentityPut)(identityDir, req.body, req.query, identityAgents);
803
850
  if (r.warn)
804
851
  console.warn(`[hicortex] ${r.warn}`);
805
852
  res.status(r.status).json(r.body);
806
853
  }
807
854
  catch (err) {
808
- res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
855
+ (0, health_js_1.logAndSendInternalError)(res, "identity", err);
856
+ }
857
+ });
858
+ // #264 backcompat aliases: /context → /identity handlers (same dir, same
859
+ // clients/agents). Kept indefinitely so external callers (the Hermes plugin,
860
+ // pre-0.18 OC clients, operator scripts) never break. The dir is "identity"
861
+ // in BOTH aliases — the rename + migration is server-side; clients see no
862
+ // difference in behaviour, only the URL.
863
+ app.get("/context", (req, res) => {
864
+ try {
865
+ const r = (0, identity_store_js_1.handleIdentityGet)(identityDir, identityClients, req.query, identityAgents);
866
+ if (r.status === 200) {
867
+ (0, memory_instructions_js_1.injectMemorySection)(r.body, memoryInstructionsEnabled);
868
+ }
869
+ res.status(r.status).json(r.body);
870
+ }
871
+ catch (err) {
872
+ (0, health_js_1.logAndSendInternalError)(res, "context", err);
873
+ }
874
+ });
875
+ app.put("/context", (req, res) => {
876
+ try {
877
+ const putSections = req.body?.sections;
878
+ if (putSections && Object.keys(putSections).some((n) => (0, memory_instructions_js_1.isReservedSectionName)(n))) {
879
+ res.status(400).json({ error: `Section name '${memory_instructions_js_1.MEMORY_SECTION_NAME}' is reserved for the product-owned memory instructions (config memoryInstructions to disable them)` });
880
+ return;
881
+ }
882
+ const r = (0, identity_store_js_1.handleIdentityPut)(identityDir, req.body, req.query, identityAgents);
883
+ if (r.warn)
884
+ console.warn(`[hicortex] ${r.warn}`);
885
+ res.status(r.status).json(r.body);
886
+ }
887
+ catch (err) {
888
+ (0, health_js_1.logAndSendInternalError)(res, "context", err);
809
889
  }
810
890
  });
811
891
  // REST /distill — canonical capture endpoint (0.9.0+).
@@ -826,17 +906,41 @@ async function startServer(options = {}) {
826
906
  }
827
907
  const { text, messages, source_agent, source_agent_id, source_domain, project, session_id, segment_id, session_date, privacy } = req.body ?? {};
828
908
  // Resolve the conversation text from either the pre-denoised string or raw messages array.
909
+ // `fromTextBranch` is captured once so the redaction gate below uses the SAME
910
+ // discriminator as the resolution (avoids re-redacting the messages-derived
911
+ // text in the `{text: "", messages: [...]}` edge case — harmless only because
912
+ // redaction is idempotent, but the comment/code must agree).
913
+ const fromTextBranch = typeof text === "string" && text.length > 0;
829
914
  let conversationText;
830
- if (typeof text === "string" && text.length > 0) {
915
+ if (fromTextBranch) {
831
916
  conversationText = text;
832
917
  }
833
918
  else if (Array.isArray(messages) && messages.length > 0) {
919
+ // The messages branch already redacts via extractConversationText
920
+ // (distiller.ts), which calls redact() as its final step.
834
921
  conversationText = (0, distiller_js_1.extractConversationText)(messages);
835
922
  }
836
923
  else {
837
924
  res.status(400).json({ error: "Provide either 'text' (string) or 'messages' (array)" });
838
925
  return;
839
926
  }
927
+ // SERVER-SIDE REDACTION (#252): scrub secrets/PII from the text branch
928
+ // BEFORE it reaches the distillation LLM or storage. Client-side redaction
929
+ // (capture.ts) is customer-disableable; a processor cannot base a privacy
930
+ // claim on scrubbing the caller can switch off, and unredacted secrets
931
+ // would reach the LLM subprocessor. Unconditional + idempotent — safe for
932
+ // self-hosted too (a second pass over already-redacted text is a no-op;
933
+ // the [REDACTED] marker is excluded by the generic_secret pattern's
934
+ // negative lookahead, and format-specific patterns don't match it). The
935
+ // messages branch is already covered above. Disable-resistance
936
+ // (hostedMode) is Phase 0b.
937
+ if (fromTextBranch) {
938
+ const { text: redacted, count } = (0, redact_js_1.redact)(conversationText);
939
+ if (count > 0) {
940
+ console.log(`[hicortex] Redacted ${count} secret(s) from /distill text`);
941
+ }
942
+ conversationText = redacted;
943
+ }
840
944
  // Segment-exact dedup (#189): an incremental capture POST carries
841
945
  // segment_id "<start>-<end>[.pN]". Skip iff THIS exact segment's chunks are
842
946
  // already stored (keys "<sid>#<segment_id>#<i>"). This is what lets a failed
@@ -938,7 +1042,8 @@ async function startServer(options = {}) {
938
1042
  });
939
1043
  }
940
1044
  catch (err) {
941
- res.status(500).json({ error: "Distillation failed", message: err instanceof Error ? err.message : String(err) });
1045
+ res.status(500).json({ error: "Distillation failed" });
1046
+ console.error(`[hicortex] /distill: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
942
1047
  }
943
1048
  });
944
1049
  // -------------------------------------------------------------------------
@@ -991,7 +1096,8 @@ async function startServer(options = {}) {
991
1096
  res.json({ updated: true, id: fullId });
992
1097
  }
993
1098
  catch (err) {
994
- res.status(500).json({ error: "Update failed", message: err instanceof Error ? err.message : String(err) });
1099
+ res.status(500).json({ error: "Update failed" });
1100
+ console.error(`[hicortex] /update: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
995
1101
  }
996
1102
  });
997
1103
  // -------------------------------------------------------------------------
@@ -1019,7 +1125,8 @@ async function startServer(options = {}) {
1019
1125
  res.json({ deleted: true, id: fullId });
1020
1126
  }
1021
1127
  catch (err) {
1022
- res.status(500).json({ error: "Delete failed", message: err instanceof Error ? err.message : String(err) });
1128
+ res.status(500).json({ error: "Delete failed" });
1129
+ console.error(`[hicortex] /delete: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
1023
1130
  }
1024
1131
  });
1025
1132
  // -------------------------------------------------------------------------
@@ -1057,7 +1164,7 @@ async function startServer(options = {}) {
1057
1164
  res.json({ projects: rows.map((r) => ({ name: r.project, count: r.cnt })) });
1058
1165
  }
1059
1166
  catch (err) {
1060
- res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
1167
+ (0, health_js_1.logAndSendInternalError)(res, "index", err);
1061
1168
  }
1062
1169
  });
1063
1170
  // -------------------------------------------------------------------------
@@ -1160,7 +1267,7 @@ async function startServer(options = {}) {
1160
1267
  }
1161
1268
  }
1162
1269
  catch (err) {
1163
- res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
1270
+ (0, health_js_1.logAndSendInternalError)(res, "graph", err);
1164
1271
  }
1165
1272
  });
1166
1273
  // -------------------------------------------------------------------------
@@ -1181,16 +1288,19 @@ async function startServer(options = {}) {
1181
1288
  // (static third-party code from the npm tarball, no data) — the exemption
1182
1289
  // lives in createAuthMiddleware next to the /viz one.
1183
1290
  app.get("/viz/vendor/:file", (0, viz_js_1.vizVendorHandler)());
1184
- // GET /context/ui — standing-context editor page (0.12, spec 2026-07-12 §5).
1291
+ // GET /identity/ui — standing-identity editor page (0.12, spec 2026-07-12 §5;
1292
+ // renamed from /context/ui in 0.18 #264).
1185
1293
  //
1186
- // The PRIMARY edit surface for the context layer. Self-contained HTML (inline
1187
- // CSS/JS, zero external requests) served from assets/context.html; builds one
1188
- // tab per section from GET /context and saves via PUT /context. The page
1294
+ // The PRIMARY edit surface for the identity layer. Self-contained HTML (inline
1295
+ // CSS/JS, zero external requests) served from assets/identity.html; builds one
1296
+ // tab per section from GET /identity and saves via PUT /identity. The page
1189
1297
  // SHELL is public (exempted in createAuthMiddleware, like /viz — it carries
1190
- // no data); the GET/PUT /context data calls stay bearer-only (localhost
1298
+ // no data); the GET/PUT /identity data calls stay bearer-only (localhost
1191
1299
  // bypass). The page collects the token client-side: ?token= URL param
1192
1300
  // (stripped on load) or an in-page prompt on 401, persisted in localStorage.
1193
- app.get("/context/ui", (0, viz_js_1.contextUiHandler)());
1301
+ // #264 backcompat: /context/ui remains mounted below as an alias.
1302
+ app.get("/identity/ui", (0, viz_js_1.identityUiHandler)());
1303
+ app.get("/context/ui", (0, viz_js_1.identityUiHandler)());
1194
1304
  // GET /dashboard — view-only memory analytics page (#224).
1195
1305
  //
1196
1306
  // Self-contained HTML (inline CSS/JS, hand-rolled inline SVG charts, zero
@@ -1352,6 +1462,6 @@ function formatResults(results) {
1352
1462
  // (id + date), hicortex_get lazy-load of truncated content, hicortex_graph
1353
1463
  // entry points, and hicortex_update/delete self-correction (#192).
1354
1464
  return results
1355
- .map((r) => `[${r.id}] [${r.memory_type}] (${(r.created_at ?? "").slice(0, 10)}, score: ${r.score.toFixed(3)}, strength: ${r.effective_strength.toFixed(3)}) ${r.content.slice(0, 500)}`)
1465
+ .map((r) => `[${r.id}] [${(0, type_labels_js_1.labelForType)(r.memory_type)}] (${(r.created_at ?? "").slice(0, 10)}, score: ${r.score.toFixed(3)}, strength: ${r.effective_strength.toFixed(3)}) ${r.content.slice(0, 500)}`)
1356
1466
  .join("\n\n");
1357
1467
  }
@@ -3,19 +3,19 @@
3
3
  *
4
4
  * The instructions for HOW agents use Hicortex are shipped BY the product,
5
5
  * versioned with the server, and injected as a synthetic read-only `memory`
6
- * section in the GET /context response. Rationale ("enforced, built-in"):
6
+ * section in the GET /identity response. Rationale ("enforced, built-in"):
7
7
  * - Harness personas (SOUL.md etc.) carry ZERO hicortex content — mechanics
8
8
  * described there rot silently when the product changes (field evidence:
9
9
  * stale "captured via hooks" sentences; an agent shell-spelunking its own
10
10
  * plugin infrastructure when told "the plugin was updated").
11
- * - User context files (user.md / rules.md) stay purely personal — norms the
11
+ * - User identity files (user.md / rules.md) stay purely personal — norms the
12
12
  * product depends on must not live in user-editable files (same principle
13
13
  * as the built-in citation norm, 0.14.1).
14
- * - Because every harness already renders `## Context` sections through the
14
+ * - Because every harness already renders `## Identity` sections through the
15
15
  * shared gate/render path, a synthetic section ships fleet-wide with zero
16
16
  * client changes — including plugins that predate this feature.
17
17
  *
18
- * The section name is RESERVED: PUT /context rejects it, and the synthetic
18
+ * The section name is RESERVED: PUT /identity rejects it, and the synthetic
19
19
  * text overrides any user file of the same name (enforced means enforced).
20
20
  * Off-switch: config `memoryInstructions: false`.
21
21
  */
@@ -27,9 +27,9 @@ export declare function renderMemoryInstructions(): string;
27
27
  * section names are lowercase by allowlist, but be safe). */
28
28
  export declare function isReservedSectionName(name: unknown): boolean;
29
29
  /**
30
- * Inject the synthetic section into a successful GET /context body.
30
+ * Inject the synthetic section into a successful GET /identity body.
31
31
  * Pure: returns the same body object with sections.memory set. Skips agent
32
- * mode "off" (operator explicitly silenced context for that agent) and
32
+ * mode "off" (operator explicitly silenced identity for that agent) and
33
33
  * non-object bodies (error shapes). Overrides a user file named memory.md.
34
34
  */
35
35
  export declare function injectMemorySection<T extends {
@@ -4,19 +4,19 @@
4
4
  *
5
5
  * The instructions for HOW agents use Hicortex are shipped BY the product,
6
6
  * versioned with the server, and injected as a synthetic read-only `memory`
7
- * section in the GET /context response. Rationale ("enforced, built-in"):
7
+ * section in the GET /identity response. Rationale ("enforced, built-in"):
8
8
  * - Harness personas (SOUL.md etc.) carry ZERO hicortex content — mechanics
9
9
  * described there rot silently when the product changes (field evidence:
10
10
  * stale "captured via hooks" sentences; an agent shell-spelunking its own
11
11
  * plugin infrastructure when told "the plugin was updated").
12
- * - User context files (user.md / rules.md) stay purely personal — norms the
12
+ * - User identity files (user.md / rules.md) stay purely personal — norms the
13
13
  * product depends on must not live in user-editable files (same principle
14
14
  * as the built-in citation norm, 0.14.1).
15
- * - Because every harness already renders `## Context` sections through the
15
+ * - Because every harness already renders `## Identity` sections through the
16
16
  * shared gate/render path, a synthetic section ships fleet-wide with zero
17
17
  * client changes — including plugins that predate this feature.
18
18
  *
19
- * The section name is RESERVED: PUT /context rejects it, and the synthetic
19
+ * The section name is RESERVED: PUT /identity rejects it, and the synthetic
20
20
  * text overrides any user file of the same name (enforced means enforced).
21
21
  * Off-switch: config `memoryInstructions: false`.
22
22
  */
@@ -44,9 +44,9 @@ function isReservedSectionName(name) {
44
44
  return typeof name === "string" && name.trim().toLowerCase() === exports.MEMORY_SECTION_NAME;
45
45
  }
46
46
  /**
47
- * Inject the synthetic section into a successful GET /context body.
47
+ * Inject the synthetic section into a successful GET /identity body.
48
48
  * Pure: returns the same body object with sections.memory set. Skips agent
49
- * mode "off" (operator explicitly silenced context for that agent) and
49
+ * mode "off" (operator explicitly silenced identity for that agent) and
50
50
  * non-object bodies (error shapes). Overrides a user file named memory.md.
51
51
  */
52
52
  function injectMemorySection(body, enabled) {
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
  }