@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.
- package/README.md +26 -25
- package/assets/dashboard.html +121 -5
- package/assets/{context.html → identity.html} +18 -18
- package/assets/viz.html +19 -7
- package/dist/claude-md.d.ts +2 -1
- package/dist/claude-md.js +2 -1
- package/dist/cli-args.d.ts +9 -0
- package/dist/cli-args.js +16 -0
- package/dist/cli.js +29 -20
- package/dist/consolidate.d.ts +15 -0
- package/dist/consolidate.js +30 -3
- package/dist/dashboard.d.ts +58 -1
- package/dist/dashboard.js +27 -1
- package/dist/extensions.d.ts +1 -1
- package/dist/extensions.js +1 -1
- package/dist/health.d.ts +68 -0
- package/dist/health.js +73 -0
- package/dist/identity-cli.d.ts +90 -0
- package/dist/{context-cli.js → identity-cli.js} +66 -48
- package/dist/{context-store.d.ts → identity-store.d.ts} +94 -31
- package/dist/{context-store.js → identity-store.js} +212 -71
- package/dist/index.d.ts +12 -5
- package/dist/index.js +57 -29
- package/dist/init.d.ts +44 -8
- package/dist/init.js +142 -37
- package/dist/{lessons-context.d.ts → learnings-identity.d.ts} +32 -21
- package/dist/{lessons-context.js → learnings-identity.js} +50 -39
- package/dist/mcp-server.d.ts +2 -0
- package/dist/mcp-server.js +168 -58
- package/dist/memory-instructions.d.ts +6 -6
- package/dist/memory-instructions.js +6 -6
- package/dist/nightly.js +65 -6
- package/dist/paths.js +1 -1
- package/dist/recall-hook-cli.d.ts +1 -1
- package/dist/recall-hook-cli.js +3 -3
- package/dist/recall-index.js +5 -2
- package/dist/status.d.ts +2 -2
- package/dist/status.js +11 -9
- package/dist/telemetry.d.ts +10 -0
- package/dist/type-classify.d.ts +13 -9
- package/dist/type-classify.js +26 -12
- package/dist/type-labels.d.ts +30 -0
- package/dist/type-labels.js +43 -0
- package/dist/types.d.ts +28 -0
- package/dist/uninstall.d.ts +12 -0
- package/dist/uninstall.js +21 -3
- package/dist/viz.d.ts +24 -11
- package/dist/viz.js +97 -32
- package/hermes-plugin/hicortex/README.md +4 -2
- package/package.json +2 -2
- package/dist/context-cli.d.ts +0 -69
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
|
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
|
|
101
|
-
// the standing
|
|
102
|
-
|
|
103
|
-
//
|
|
104
|
-
|
|
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
|
|
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
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
const
|
|
471
|
-
|
|
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
|
|
487
|
+
console.warn(`[hicortex] Ignoring unknown identityClients: ${resolvedClients.dropped.join(", ")} ` +
|
|
474
488
|
`(known: cc, hermes, oc)`);
|
|
475
489
|
}
|
|
476
|
-
// Per-agent
|
|
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
|
|
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,
|
|
481
|
-
|
|
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
|
|
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
|
-
|
|
566
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
798
|
+
(0, health_js_1.logAndSendInternalError)(res, "recent", err);
|
|
758
799
|
}
|
|
759
800
|
});
|
|
760
801
|
// -------------------------------------------------------------------------
|
|
761
|
-
// REST /
|
|
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>/
|
|
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
|
|
767
|
-
// now /recent (§Naming). Stale-client tripwire: old recall callers
|
|
768
|
-
// send project/limit/privacy query params;
|
|
769
|
-
// so those params on GET /
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
838
|
+
(0, health_js_1.logAndSendInternalError)(res, "identity", err);
|
|
792
839
|
}
|
|
793
840
|
});
|
|
794
|
-
app.put("/
|
|
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,
|
|
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
|
-
|
|
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 (
|
|
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"
|
|
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"
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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 /
|
|
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
|
|
1187
|
-
// CSS/JS, zero external requests) served from assets/
|
|
1188
|
-
// tab per section from GET /
|
|
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 /
|
|
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
|
-
|
|
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 /
|
|
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
|
|
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 `##
|
|
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 /
|
|
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 /
|
|
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
|
|
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 /
|
|
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
|
|
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 `##
|
|
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 /
|
|
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 /
|
|
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
|
|
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
|
|
136
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
}
|