@gamaze/hicortex 0.20.7 → 0.20.10

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 (86) hide show
  1. package/README.md +18 -41
  2. package/assets/dashboard.html +3989 -836
  3. package/dist/calibration.d.ts +293 -0
  4. package/dist/calibration.js +379 -0
  5. package/dist/capture-health.d.ts +87 -0
  6. package/dist/capture-health.js +106 -0
  7. package/dist/capture-pause.d.ts +86 -0
  8. package/dist/capture-pause.js +127 -0
  9. package/dist/capture.d.ts +24 -3
  10. package/dist/capture.js +11 -1
  11. package/dist/classify-domains.d.ts +6 -0
  12. package/dist/classify-domains.js +7 -1
  13. package/dist/cli.js +38 -3
  14. package/dist/config-read.d.ts +1 -1
  15. package/dist/config-read.js +96 -9
  16. package/dist/consolidate.d.ts +114 -68
  17. package/dist/consolidate.js +302 -182
  18. package/dist/dashboard.d.ts +326 -6
  19. package/dist/dashboard.js +592 -7
  20. package/dist/db.js +105 -0
  21. package/dist/dedup.d.ts +34 -26
  22. package/dist/dedup.js +91 -57
  23. package/dist/distiller.js +1 -1
  24. package/dist/domain-classify.d.ts +7 -6
  25. package/dist/domain-classify.js +12 -10
  26. package/dist/eval/decay-eval.d.ts +3 -3
  27. package/dist/eval/decay-eval.js +4 -4
  28. package/dist/eval/importance-eval.d.ts +85 -0
  29. package/dist/eval/importance-eval.js +286 -0
  30. package/dist/eval/planted-eval.d.ts +26 -0
  31. package/dist/eval/planted-eval.js +97 -0
  32. package/dist/eval/planted-fixtures.d.ts +107 -0
  33. package/dist/eval/planted-fixtures.js +283 -0
  34. package/dist/eval/planted-harness.d.ts +176 -0
  35. package/dist/eval/planted-harness.js +649 -0
  36. package/dist/eval/ranking-battery.d.ts +78 -0
  37. package/dist/eval/ranking-battery.js +181 -0
  38. package/dist/eval/ranking-eval.d.ts +41 -0
  39. package/dist/eval/ranking-eval.js +391 -0
  40. package/dist/eval/ranking-fixtures.d.ts +77 -0
  41. package/dist/eval/ranking-fixtures.js +226 -0
  42. package/dist/identity-store.d.ts +21 -0
  43. package/dist/identity-store.js +49 -0
  44. package/dist/index.js +4 -3
  45. package/dist/init.d.ts +23 -3
  46. package/dist/init.js +84 -9
  47. package/dist/llm.d.ts +43 -58
  48. package/dist/llm.js +87 -101
  49. package/dist/mcp-server.d.ts +12 -0
  50. package/dist/mcp-server.js +213 -32
  51. package/dist/nightly.d.ts +9 -1
  52. package/dist/nightly.js +164 -110
  53. package/dist/nofit.d.ts +4 -11
  54. package/dist/nofit.js +6 -23
  55. package/dist/prompts.d.ts +10 -0
  56. package/dist/prompts.js +28 -5
  57. package/dist/recall-index.d.ts +30 -28
  58. package/dist/recall-index.js +21 -18
  59. package/dist/recall-registry.d.ts +2 -1
  60. package/dist/recall-registry.js +35 -1
  61. package/dist/reconsolidation.d.ts +168 -87
  62. package/dist/reconsolidation.js +818 -377
  63. package/dist/relink.js +3 -4
  64. package/dist/rescore-importance.d.ts +80 -0
  65. package/dist/rescore-importance.js +236 -0
  66. package/dist/retrieval.d.ts +80 -35
  67. package/dist/retrieval.js +322 -105
  68. package/dist/run-deadline.d.ts +62 -0
  69. package/dist/run-deadline.js +73 -0
  70. package/dist/schema-prototypes.d.ts +3 -3
  71. package/dist/schema-prototypes.js +3 -3
  72. package/dist/stages.d.ts +37 -0
  73. package/dist/stages.js +51 -0
  74. package/dist/state.d.ts +34 -9
  75. package/dist/storage.d.ts +50 -18
  76. package/dist/storage.js +125 -30
  77. package/dist/telemetry.d.ts +8 -7
  78. package/dist/token-budget.js +3 -4
  79. package/dist/type-classify.js +4 -4
  80. package/dist/types.d.ts +143 -155
  81. package/domains.example.json +4 -5
  82. package/hermes-plugin/hicortex/README.md +2 -2
  83. package/openclaw.plugin.json +1 -1
  84. package/package.json +4 -1
  85. package/pi-extension/hicortex/README.md +1 -1
  86. package/server.json +3 -3
@@ -51,6 +51,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
51
51
  exports.resolveDistillProbeGate = resolveDistillProbeGate;
52
52
  exports.createMcpServer = createMcpServer;
53
53
  exports.resolveBodyLimitMb = resolveBodyLimitMb;
54
+ exports.resolveSearchSimilarityFloor = resolveSearchSimilarityFloor;
54
55
  exports.makeBodyLimitErrorHandler = makeBodyLimitErrorHandler;
55
56
  exports.makeContentLengthGate = makeContentLengthGate;
56
57
  exports.startServer = startServer;
@@ -76,6 +77,7 @@ const viz_js_1 = require("./viz.js");
76
77
  const dashboard_js_1 = require("./dashboard.js");
77
78
  const identity_store_js_1 = require("./identity-store.js");
78
79
  const retrieval = __importStar(require("./retrieval.js"));
80
+ const CALIBRATION = __importStar(require("./calibration.js"));
79
81
  const recall_registry_js_1 = require("./recall-registry.js");
80
82
  const memory_instructions_js_1 = require("./memory-instructions.js");
81
83
  const recall_index_js_1 = require("./recall-index.js");
@@ -87,6 +89,8 @@ const distiller_js_1 = require("./distiller.js");
87
89
  const dedup_js_1 = require("./dedup.js");
88
90
  const reconsolidation_js_1 = require("./reconsolidation.js");
89
91
  const redact_js_1 = require("./redact.js");
92
+ const capture_health_js_1 = require("./capture-health.js");
93
+ const capture_pause_js_1 = require("./capture-pause.js");
90
94
  const init_js_1 = require("./init.js");
91
95
  // ---------------------------------------------------------------------------
92
96
  // Server state
@@ -180,7 +184,7 @@ function createMcpServer() {
180
184
  // -- hicortex_search --
181
185
  server.tool("hicortex_search", "Search shared long-term memory (all agents, all sessions). CALL THIS BEFORE assuming, guessing, or asking the user about anything that may have come up before: prior decisions, preferences, project facts, people, hardware, past incidents. If you are about to write 'I don't have information about…', search first.", {
182
186
  query: zod_1.z.string().describe("Search query text"),
183
- limit: zod_1.z.coerce.number().optional().describe("Max results (default: server config searchLimit)"),
187
+ limit: zod_1.z.coerce.number().optional().describe("Max results (default: the server default)"),
184
188
  project: zod_1.z.string().optional().describe("Filter by project name"),
185
189
  }, async ({ query, limit, project }) => {
186
190
  if (!db)
@@ -214,7 +218,7 @@ function createMcpServer() {
214
218
  // -- hicortex_recent --
215
219
  server.tool("hicortex_recent", "Get recent memories, optionally filtered by project. CALL THIS AT THE START of substantive work on a project to catch up on its latest state — cheaper than asking the user what happened.", {
216
220
  project: zod_1.z.string().optional().describe("Filter by project name"),
217
- limit: zod_1.z.coerce.number().optional().describe("Max results (default: server config recentLimit)"),
221
+ limit: zod_1.z.coerce.number().optional().describe("Max results (default: the server default)"),
218
222
  }, async ({ project, limit }) => {
219
223
  if (!db)
220
224
  return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
@@ -497,6 +501,25 @@ function resolveBodyLimitMb(configVal, hostedMode) {
497
501
  return cfg;
498
502
  return hostedMode ? 5 : 25;
499
503
  }
504
+ /**
505
+ * Resolve the OPTIONAL /search relevance floor (`minSimilarity` query param,
506
+ * #409 console polish). Pure — exported for tests. Absent/blank/invalid →
507
+ * undefined = NO gate (byte-identical to every pre-existing caller: agents,
508
+ * plugins, MCP tools never send the param). A finite number in [0, 1] → that
509
+ * floor, clamped into range so a hostile `?minSimilarity=42` cannot widen or
510
+ * invert the gate. Applied AFTER retrieve() with the exported recall gate
511
+ * (passesRelevanceGate: FTS/`both` hits pass regardless — a token match is
512
+ * real evidence; vector-only hits must clear the floor) — the same post-hoc
513
+ * shape /recall-index uses, so the two recall surfaces gate identically.
514
+ */
515
+ function resolveSearchSimilarityFloor(raw) {
516
+ if (typeof raw !== "string" || raw.trim() === "")
517
+ return undefined;
518
+ const v = Number(raw);
519
+ if (!Number.isFinite(v))
520
+ return undefined;
521
+ return Math.min(1, Math.max(0, v));
522
+ }
500
523
  /**
501
524
  * Express error middleware (#7): translate express.json's default HTML 413
502
525
  * (entity.too.large) into a consistent JSON response. Catches body-parser
@@ -674,9 +697,10 @@ async function startServer(options = {}) {
674
697
  });
675
698
  }
676
699
  if (llmConfig) {
677
- // Tuning (#220: maxTokens + enableThinking + numCtx + flush), validated +
678
- // copied via the shared overlay (also applied in resolveSavedLlmConfig for
679
- // the nightly). Wrong-typed values warn + drop.
700
+ // Tuning (#220: maxTokens + enableThinking; #408: the diagnostic env tier
701
+ // for numCtx + ollama flush), validated + resolved via the shared overlay
702
+ // (also applied in resolveSavedLlmConfig for the nightly — the ONE
703
+ // resolution point per process). Wrong-typed values warn + drop.
680
704
  (0, llm_js_1.applyTierTuningOverlay)(llmConfig, savedConfig);
681
705
  llm = new llm_js_1.LlmClient(llmConfig);
682
706
  console.log(`[hicortex] LLM (one model, all phases): ${llmConfig.provider}/${llmConfig.model}`);
@@ -749,34 +773,29 @@ async function startServer(options = {}) {
749
773
  console.warn("[hicortex] Config uses the legacy 'contextAgents' key — renamed to 'identityAgents' in 0.18 (#264). " +
750
774
  "The legacy key still works; update your config to silence this warning.");
751
775
  }
752
- // #192 recall/decay alignment: decay speed + recall breadth + pushed-recall
753
- // knobs, ALL from config (see retrieval.ts configureRecall for the key list)
754
- // so calibration is a config edit + restart, never a release.
755
- retrieval.configureDecay({ halfLifeDays: savedConfig?.decayHalfLifeDays });
756
- const recallCfg = retrieval.configureRecall(savedConfig);
757
- const scoringCfg = retrieval.configureScoring(savedConfig);
758
- const sessionIntentCfg = retrieval.configureSessionIntent(savedConfig);
759
- console.log(`[hicortex] Recall: k=${recallCfg.searchLimit}/recent=${recallCfg.recentLimit}` +
776
+ // #408: recall/decay calibration is RELEASE-MANAGED (calibration.ts) — no
777
+ // config keys are read here anymore. The plain configure*() calls pin this
778
+ // process to the shipped constants (they remain the eval/test seam; see
779
+ // retrieval.ts). The boot log still prints the active values — now sourced
780
+ // from the calibration constants via the configure*() returns.
781
+ retrieval.configureDecay();
782
+ const recallCfg = retrieval.configureRecall();
783
+ const scoringCfg = retrieval.configureScoring();
784
+ const sessionIntentCfg = retrieval.configureSessionIntent();
785
+ console.log(`[hicortex] Recall (release-managed calibration): k=${recallCfg.searchLimit}/recent=${recallCfg.recentLimit}` +
760
786
  `/window=${recallCfg.recentWindowDays}d/cold=${recallCfg.coldExposureSlots}` +
761
- `/novelty=${(0, recall_index_js_1.resolveNoveltyFloorSlots)(savedConfig?.noveltyFloorSlots, savedConfig?.recallMaxItems)}` +
787
+ `/novelty=${CALIBRATION.NOVELTY_FLOOR_SLOTS}` +
762
788
  ` · ` +
763
789
  `score sim=${scoringCfg.similarity}/str=${scoringCfg.strength}/conn=${scoringCfg.connections}` +
764
790
  `/rec=${scoringCfg.recency}, fresh=${scoringCfg.freshnessBoostWeight}@${scoringCfg.freshnessBoostDays}d, ` +
765
791
  `superseded×${scoringCfg.supersededDemotion}` +
766
792
  `, intent w=${sessionIntentCfg.weight}` +
767
793
  (sessionIntentCfg.weight === 0 ? " (disabled)" : ""));
768
- recallRegistry = new recall_registry_js_1.SessionRecallRegistry({
769
- reshowTurns: savedConfig?.recallReshowTurns,
770
- });
771
- recallIndexOptions = {
772
- minSimilarity: savedConfig?.recallMinSimilarity,
773
- maxItems: savedConfig?.recallMaxItems,
774
- minPromptLength: savedConfig?.recallMinPromptChars,
775
- titleChars: savedConfig?.recallTitleChars,
776
- // #324 novelty floor: slots of recallMaxItems guaranteed to the
777
- // pure-prompt (unblended) search's top passing hit(s). 0 disables.
778
- noveltyFloorSlots: savedConfig?.noveltyFloorSlots,
779
- };
794
+ // Defaults resolve inside recall-index.ts / recall-registry.ts from the
795
+ // calibration constants (#408) — the options objects stay as the seams the
796
+ // eval injects through.
797
+ recallRegistry = new recall_registry_js_1.SessionRecallRegistry();
798
+ recallIndexOptions = {};
780
799
  memoryInstructionsEnabled = savedConfig?.memoryInstructions !== false;
781
800
  if (resolvedAgents.dropped.length > 0) {
782
801
  console.warn(`[hicortex] Ignoring invalid identityAgents entries: ${resolvedAgents.dropped.join(", ")} ` +
@@ -918,7 +937,7 @@ async function startServer(options = {}) {
918
937
  res.status(503).json({ error: "Server not initialized" });
919
938
  return;
920
939
  }
921
- const { content, source_agent, source_agent_id, source_domain, project, memory_type, privacy, source_session, session_date, corrects, supersedes } = req.body ?? {};
940
+ const { content, source_agent, source_agent_id, source_domain, source_machine, project, memory_type, privacy, source_session, session_date, corrects, supersedes } = req.body ?? {};
922
941
  if (!content || typeof content !== "string") {
923
942
  res.status(400).json({ error: "Missing or invalid 'content' field" });
924
943
  return;
@@ -969,6 +988,7 @@ async function startServer(options = {}) {
969
988
  // Attribution + provenance passthrough (0.16.x); null when absent.
970
989
  sourceAgentId: typeof source_agent_id === "string" ? source_agent_id : null,
971
990
  sourceDomain: typeof source_domain === "string" ? source_domain : null,
991
+ sourceMachine: storage.sanitizeSourceMachine(source_machine),
972
992
  sourceSession: source_session ?? undefined,
973
993
  project: project ?? undefined,
974
994
  memoryType: normalizedType ?? "experience",
@@ -1002,15 +1022,24 @@ async function startServer(options = {}) {
1002
1022
  res.status(400).json({ error: "Missing 'query'" });
1003
1023
  return;
1004
1024
  }
1005
- // No hardcoded default: absent limit → config-driven (searchLimit).
1025
+ // No hardcoded default: absent limit → the release-managed calibration
1026
+ // default (#408 — was the searchLimit config key).
1006
1027
  const limit = req.query.limit ? Number(req.query.limit) : undefined;
1007
1028
  const project = typeof req.query.project === "string" && req.query.project ? req.query.project : undefined;
1008
1029
  // 0.16.x: `privacy` query param is ACCEPTED for backward compat (old
1009
1030
  // clients/plugins still send it) but no longer read — retrieval ignores
1010
1031
  // privacy entirely (the column is vestigial, never filtered).
1011
1032
  warnDeprecatedPrivacyParamIfPresent(req.query, "search");
1033
+ // #409 console polish: OPTIONAL relevance floor. Absent (every legacy
1034
+ // caller) = ungated, byte-identical behavior. The console sends the
1035
+ // recall floor (RECALL_MIN_SIMILARITY, via the /dashboard/field echo) so
1036
+ // vector nearest-neighbor junk below it never renders as "results".
1037
+ const minSimilarity = resolveSearchSimilarityFloor(req.query.minSimilarity);
1012
1038
  try {
1013
- const results = await retrieval.retrieve(db, embedder_js_1.embed, query, { limit, project });
1039
+ let results = await retrieval.retrieve(db, embedder_js_1.embed, query, { limit, project });
1040
+ if (minSimilarity !== undefined) {
1041
+ results = results.filter((r) => (0, recall_index_js_1.passesRelevanceGate)(r, minSimilarity));
1042
+ }
1014
1043
  res.json({ results });
1015
1044
  }
1016
1045
  catch (err) {
@@ -1074,7 +1103,8 @@ async function startServer(options = {}) {
1074
1103
  return;
1075
1104
  }
1076
1105
  const project = typeof req.query.project === "string" && req.query.project ? req.query.project : undefined;
1077
- // No hardcoded default: absent limit → config-driven (recentLimit).
1106
+ // No hardcoded default: absent limit → the release-managed calibration
1107
+ // default (#408 — was the recentLimit config key).
1078
1108
  // 0.16.x: `privacy` query param accepted but ignored (vestigial column).
1079
1109
  const limit = req.query.limit ? Number(req.query.limit) : undefined;
1080
1110
  warnDeprecatedPrivacyParamIfPresent(req.query, "recent");
@@ -1172,6 +1202,35 @@ async function startServer(options = {}) {
1172
1202
  (0, health_js_1.logAndSendInternalError)(res, "context", err);
1173
1203
  }
1174
1204
  });
1205
+ // PUT /identity/mode — switch ONE agent's identity scope (#423 phase 3).
1206
+ // No /context/mode alias: this is a NEW endpoint with no legacy callers.
1207
+ //
1208
+ // Single-writer discipline: the daemon's boot-time identityAgents map is
1209
+ // normally read-once-at-boot — THIS route is the one live writer. The
1210
+ // adapter reads the FRESH config (not the boot snapshot) so two switches in
1211
+ // a row can never drop each other's writes, then on success (1) persists
1212
+ // the merged map as config `identityAgents` (survives restarts) and (2)
1213
+ // sets the module-level map to the SAME map — the switch is live on the
1214
+ // very next GET /identity?agent= (applies: "immediate"). Externally
1215
+ // hand-edited config still needs a restart (pre-existing posture). Note
1216
+ // the interplay: PUT /identity's black-hole guard 409s section writes while
1217
+ // config forces off/global — switching to 'override' here first unblocks
1218
+ // section editing. Bearer-only (standard auth middleware, no exemption).
1219
+ app.put("/identity/mode", (req, res) => {
1220
+ try {
1221
+ const fresh = readConfigFile(stateDir) ?? {};
1222
+ const r = (0, identity_store_js_1.handleIdentityModePut)(identityDir, req.body ?? null, req.query, (0, identity_store_js_1.resolveIdentityAgentsConfig)(fresh).agents);
1223
+ if (r.status === 200 && r.agents) {
1224
+ const merged = r.agents;
1225
+ (0, init_js_1.persistConfigUpdates)((0, node_path_1.join)(stateDir, "config.json"), { identityAgents: merged });
1226
+ identityAgents = merged;
1227
+ }
1228
+ res.status(r.status).json(r.body);
1229
+ }
1230
+ catch (err) {
1231
+ (0, health_js_1.logAndSendInternalError)(res, "identity/mode", err);
1232
+ }
1233
+ });
1175
1234
  // REST /distill — canonical capture endpoint (0.9.0+).
1176
1235
  // Every machine (including the server itself) POSTs denoised session text here.
1177
1236
  // The server distills, embeds, stores. Body limit: see `distillBodyLimitMb`
@@ -1185,11 +1244,42 @@ async function startServer(options = {}) {
1185
1244
  res.status(503).json({ error: "Server not initialized" });
1186
1245
  return;
1187
1246
  }
1247
+ // #422: destructured ABOVE the no-LLM guard (it used to sit below) so the
1248
+ // attribution fields exist at EVERY exit incl. the 503 — capture-health
1249
+ // accounting records held posts there too. Nothing else reads them before
1250
+ // the guard, so semantics are unchanged.
1251
+ const { text, messages, source_agent, source_agent_id, source_domain, source_machine, project, session_id, segment_id, session_date, privacy } = req.body ?? {};
1252
+ // #422 capture health: normalize the wire fields ONCE so every exit below
1253
+ // records with a one-liner (recordDistillActivity re-sanitizes machine/
1254
+ // agent — the builder only forwards + picks the byte count). bytes = the
1255
+ // resolved (post-redaction) conversationText length, 0 while unresolved.
1256
+ const capEntry = (bytes, outcome) => ({
1257
+ machine: source_machine,
1258
+ agent: source_agent,
1259
+ sessionId: session_id,
1260
+ segmentId: segment_id,
1261
+ bytes,
1262
+ outcome,
1263
+ });
1264
+ // #423 phase 3 (D3): operator capture pause — server-side 200-skip. The
1265
+ // pause table is read per POST, so a pause takes effect on the very next
1266
+ // /distill (no restart). Deliberately ABOVE the no-LLM guard: a paused
1267
+ // bundle must skip regardless of LLM state — a 503 there would hold the
1268
+ // client's cursor on a box that is deliberately not capturing. The 200 is
1269
+ // the point: capture.ts treats every 200 as confirmed and advances its
1270
+ // cursor, so paused sessions are deliberately NOT captured and never
1271
+ // re-sent/backfilled. Zero client changes.
1272
+ const pause = (0, capture_pause_js_1.capturePauseKey)(source_machine, source_agent);
1273
+ if ((0, capture_pause_js_1.isCapturePaused)(db, pause.machine, pause.harness)) {
1274
+ (0, capture_health_js_1.recordDistillActivity)(db, capEntry(0, "paused"));
1275
+ res.status(200).json({ skipped: true, paused: true, machine: pause.machine, harness: pause.harness });
1276
+ return;
1277
+ }
1188
1278
  if (!llm || !llmConfig) {
1279
+ (0, capture_health_js_1.recordDistillActivity)(db, capEntry(0, "held"));
1189
1280
  res.status(503).json({ error: "No LLM configured — run npx @gamaze/hicortex init. Session will be retried." });
1190
1281
  return;
1191
1282
  }
1192
- const { text, messages, source_agent, source_agent_id, source_domain, project, session_id, segment_id, session_date, privacy } = req.body ?? {};
1193
1283
  // Resolve the conversation text from either the pre-denoised string or raw messages array.
1194
1284
  // `fromTextBranch` is captured once so the redaction gate below uses the SAME
1195
1285
  // discriminator as the resolution (avoids re-redacting the messages-derived
@@ -1239,6 +1329,7 @@ async function startServer(options = {}) {
1239
1329
  if (session_id && segment_id) {
1240
1330
  const existingCount = (0, dedup_js_1.countExistingSegment)(db, session_id, segment_id);
1241
1331
  if (existingCount > 0) {
1332
+ (0, capture_health_js_1.recordDistillActivity)(db, capEntry(conversationText.length, "skipped"));
1242
1333
  res.status(200).json({ skipped: true, existing_count: existingCount });
1243
1334
  return;
1244
1335
  }
@@ -1250,6 +1341,7 @@ async function startServer(options = {}) {
1250
1341
  if (session_id && !segment_id) {
1251
1342
  const existingCount = (0, dedup_js_1.countExistingSession)(db, session_id);
1252
1343
  if (existingCount > 0) {
1344
+ (0, capture_health_js_1.recordDistillActivity)(db, capEntry(conversationText.length, "skipped"));
1253
1345
  res.status(200).json({ skipped: true, existing_count: existingCount });
1254
1346
  return;
1255
1347
  }
@@ -1261,6 +1353,7 @@ async function startServer(options = {}) {
1261
1353
  // diagnosis — the capture client treats non-201/200 as transient and holds
1262
1354
  // its cursor (capture.ts), so the segment is retried next run, never lost.
1263
1355
  if (!(await resolveDistillProbeGate(llm, llmConfig))) {
1356
+ (0, capture_health_js_1.recordDistillActivity)(db, capEntry(conversationText.length, "held"));
1264
1357
  res.status(503).json({ error: "LLM endpoint not generating — session will be retried" });
1265
1358
  return;
1266
1359
  }
@@ -1286,6 +1379,7 @@ async function startServer(options = {}) {
1286
1379
  // a skipped duplicate neither trips the gate nor consumes budget. The client
1287
1380
  // capture loop holds its cursor on 429 (dup-over-loss, capture.ts:303).
1288
1381
  if ((0, token_budget_js_1.isTokenBudgetExceeded)(stateDir)) {
1382
+ (0, capture_health_js_1.recordDistillActivity)(db, capEntry(conversationText.length, "held"));
1289
1383
  res.status(429).json({ error: "token budget exceeded", retry: "next billing period" });
1290
1384
  return;
1291
1385
  }
@@ -1334,6 +1428,7 @@ async function startServer(options = {}) {
1334
1428
  // filtered. Default null for older clients that don't send them.
1335
1429
  sourceAgentId: typeof source_agent_id === "string" ? source_agent_id : null,
1336
1430
  sourceDomain: typeof source_domain === "string" ? source_domain : null,
1431
+ sourceMachine: storage.sanitizeSourceMachine(source_machine),
1337
1432
  // Per-chunk key: "<session_id>[#<segment_id>]#<i>". The prefix
1338
1433
  // matches the dedup checks above, so a re-run is idempotent.
1339
1434
  sourceSession: sourcePrefix ? `${sourcePrefix}#${i}` : undefined,
@@ -1352,6 +1447,7 @@ async function startServer(options = {}) {
1352
1447
  return out;
1353
1448
  });
1354
1449
  const ids = insertAll();
1450
+ (0, capture_health_js_1.recordDistillActivity)(db, capEntry(conversationText.length, "ok"));
1355
1451
  res.status(201).json({
1356
1452
  ids,
1357
1453
  distilled: ids.length,
@@ -1365,6 +1461,7 @@ async function startServer(options = {}) {
1365
1461
  });
1366
1462
  }
1367
1463
  catch (err) {
1464
+ (0, capture_health_js_1.recordDistillActivity)(db, capEntry(conversationText.length, "held"));
1368
1465
  res.status(500).json({ error: "Distillation failed" });
1369
1466
  console.error(`[hicortex] /distill: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
1370
1467
  }
@@ -1447,6 +1544,52 @@ async function startServer(options = {}) {
1447
1544
  }
1448
1545
  });
1449
1546
  // -------------------------------------------------------------------------
1547
+ // REST /enrich — owner corroboration (#423 phase 3).
1548
+ //
1549
+ // An enrich is EVIDENCE ABOUT IMPORTANCE: it bumps corroboration_count and
1550
+ // base_strength (+the calibration delta, capped at 1.0) — the same anchor
1551
+ // the nightly's LLM scoring and hub-boost write. It must NOT touch
1552
+ // access_count (reserved for real recall use) or shown_count (index
1553
+ // exposure) — faking either corrupts the uses-per-showing adoption metric.
1554
+ // Never a stage write: stages are derived presentation (the E-reframe).
1555
+ // An absorbed memory is invisible evidence and cannot be corroborated
1556
+ // (409, same posture as /update).
1557
+ // -------------------------------------------------------------------------
1558
+ app.post("/enrich", (req, res) => {
1559
+ if (!db) {
1560
+ res.status(503).json({ error: "Server not initialized" });
1561
+ return;
1562
+ }
1563
+ const { id } = req.body ?? {};
1564
+ if (!id || typeof id !== "string") {
1565
+ res.status(400).json({ error: "Missing or invalid 'id' field" });
1566
+ return;
1567
+ }
1568
+ const fullId = resolveMemoryId(db, id);
1569
+ if (!fullId) {
1570
+ res.status(404).json({ error: "Memory not found" });
1571
+ return;
1572
+ }
1573
+ // Same absorbed guard as /update: corroborating an invisible row would
1574
+ // strengthen evidence the store deliberately folded into another memory.
1575
+ const target = storage.getMemory(db, fullId);
1576
+ if (target?.status === "absorbed") {
1577
+ res.status(409).json({ error: "Memory is absorbed — invisible evidence cannot be corroborated" });
1578
+ return;
1579
+ }
1580
+ try {
1581
+ const r = storage.enrichMemory(db, fullId, new Date().toISOString());
1582
+ if (!r) {
1583
+ res.status(404).json({ error: "Memory not found" });
1584
+ return;
1585
+ }
1586
+ res.status(200).json({ id: fullId, corroboration_count: r.corroborationCount, base_strength: r.baseStrength });
1587
+ }
1588
+ catch (err) {
1589
+ (0, health_js_1.logAndSendInternalError)(res, "enrich", err);
1590
+ }
1591
+ });
1592
+ // -------------------------------------------------------------------------
1450
1593
  // REST /delete — permanently delete a memory and its links.
1451
1594
  //
1452
1595
  // NOTE for #124: returns {deleted: true, id} — clean JSON for future /viz.
@@ -1663,6 +1806,43 @@ async function startServer(options = {}) {
1663
1806
  // express adapter that injects the live db + config. STRICTLY view-only —
1664
1807
  // no mutation endpoints on the dashboard surface.
1665
1808
  app.get("/dashboard/data", (0, dashboard_js_1.dashboardDataHandler)(() => db, () => readConfigFile(stateDir)));
1809
+ // GET /dashboard/field — the console flight-field payload (#409/#421
1810
+ // Phase 1): the whole live store as minimal fields (titles ≤100 via the
1811
+ // production memoryTitle, derived stage + effective strength, no content
1812
+ // bodies) plus every link edge {a, b, rel}. Bearer-only (auth middleware,
1813
+ // no shell exemption — it carries data); localhost bypass applies. Handler
1814
+ // + gzip adapter live in src/dashboard.ts next to its /data sibling; the
1815
+ // field is one row per memory, so Accept-Encoding: gzip clients get the
1816
+ // compressed wire form.
1817
+ app.get("/dashboard/field", (0, dashboard_js_1.dashboardFieldHandler)(() => db));
1818
+ // GET /dashboard/events?days=N — the console replay ledger (#409/#421
1819
+ // Phase 1): night-resolution synthesis over existing tables (created_at /
1820
+ // memory_history / dedup_log / memory_links; no schema change, no
1821
+ // event-sourcing store). ids only. Bearer-only like /dashboard/field;
1822
+ // localhost bypass applies.
1823
+ app.get("/dashboard/events", (0, dashboard_js_1.dashboardEventsHandler)(() => db));
1824
+ // GET/PUT /dashboard/model — the console's model-settings surface (#422
1825
+ // Phase 2): the ONE scoped writer on the dashboard (view-only everywhere
1826
+ // else). GET echoes the config's model knobs raw (null = unset) + the
1827
+ // boot-resolved provider from the daemon's in-memory llmConfig; api_key_set
1828
+ // carries ONLY the key's presence — no key material on the wire, ever. PUT
1829
+ // validates an allowlisted subset (null clears a config key) and persists
1830
+ // via init.ts persistConfigUpdates — strict load, so a malformed config.json
1831
+ // throws → 500 with the file untouched. Bearer-only (auth middleware, no
1832
+ // shell exemption — it carries install config); localhost bypass applies.
1833
+ // Applies on restart: the daemon resolves config at boot (llmConfig is the
1834
+ // boot snapshot; the card footnotes this).
1835
+ app.get("/dashboard/model", (0, dashboard_js_1.dashboardModelGetHandler)(() => readConfigFile(stateDir), () => llmConfig));
1836
+ app.put("/dashboard/model", (0, dashboard_js_1.dashboardModelPutHandler)((updates) => (0, init_js_1.persistConfigUpdates)((0, node_path_1.join)(stateDir, "config.json"), updates), () => llmConfig));
1837
+ // PUT /dashboard/capture-pause — the console's pause/resume toggle (#423
1838
+ // phase 3, D3). Body {machine?, harness, paused}: a pause makes /distill
1839
+ // 200-skip the bundle's posts — deliberate NON-capture, the sessions are
1840
+ // not backfilled (the client cursor advances on the 200, by design). The
1841
+ // effect is IMMEDIATE — no restart — because the /distill handler reads the
1842
+ // capture_pauses table on every post. Bearer-only like /dashboard/model
1843
+ // (auth middleware, no shell exemption — it mutates operator state);
1844
+ // localhost bypass applies. Handler + adapter live in src/dashboard.ts.
1845
+ app.put("/dashboard/capture-pause", (0, dashboard_js_1.dashboardCapturePausePutHandler)(() => db));
1666
1846
  // GET /account — account identity for the console nav (name/org/plan from
1667
1847
  // config). The LIGHTWEIGHT twin of the account block inside /dashboard/data:
1668
1848
  // the /viz and /identity/ui pages need only this, not the metric payload;
@@ -1789,7 +1969,8 @@ function readConfigFile(stateDir) {
1789
1969
  }
1790
1970
  catch (e) {
1791
1971
  console.warn(`[hicortex] ${configPath} exists but could not be parsed — server booting degraded ` +
1792
- `(config-driven LLM/decay/recall knobs and agentId self-heal will not apply). ` +
1972
+ `(config-driven LLM knobs and agentId self-heal will not apply; decay/recall ` +
1973
+ `calibration is release-managed and unaffected). ` +
1793
1974
  `Fix the JSON and restart. Cause: ${e instanceof Error ? e.message : String(e)}`);
1794
1975
  return null;
1795
1976
  }
package/dist/nightly.d.ts CHANGED
@@ -20,6 +20,14 @@
20
20
  * (#189 review, fix 3). Per-session cursors keep the wide re-scan cheap: an
21
21
  * already-captured session yields an empty delta.
22
22
  *
23
+ * First run (#436): with NO watermark yet, since = now − firstRunLookbackDays
24
+ * (default 7 — owner ruling 2026-09-14: full-history first-run ingestion is
25
+ * not feasible; a long-term AI user's entire session store must not be
26
+ * discovered night one). The widening-only invariant composes unchanged:
27
+ * min(now−7d, now−N) — a recapture window wider than the default widens, a
28
+ * narrower one leaves the floor. Installing more history is the deliberate
29
+ * `--recapture-window` act, not a default.
30
+ *
23
31
  * Clock-jump clamp (#327): a FUTURE-dated lastNightly (client clock error —
24
32
  * NTP not yet synced at write time) would, once the clock corrects, sit ahead
25
33
  * of every session mtime and permanently skip quiet sessions (their mtimes
@@ -27,7 +35,7 @@
27
35
  * fires once per affected run (this function runs once per nightly).
28
36
  * `now` is injectable for tests.
29
37
  */
30
- export declare function computeSince(stateDir: string, recaptureWindowDays?: number, now?: Date): Date;
38
+ export declare function computeSince(stateDir: string, recaptureWindowDays?: number, now?: Date, firstRunLookbackDays?: number): Date;
31
39
  /**
32
40
  * Parse a `Retry-After` header into ms (#327). Handles both RFC forms —
33
41
  * delay-seconds (`"30"`) and HTTP-date — and returns undefined for anything