@gamaze/hicortex 0.20.7 → 0.20.9

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 (61) hide show
  1. package/README.md +10 -41
  2. package/dist/calibration.d.ts +174 -0
  3. package/dist/calibration.js +231 -0
  4. package/dist/capture.d.ts +15 -3
  5. package/dist/capture.js +10 -1
  6. package/dist/classify-domains.d.ts +6 -0
  7. package/dist/classify-domains.js +7 -1
  8. package/dist/cli.js +2 -3
  9. package/dist/config-read.d.ts +1 -1
  10. package/dist/config-read.js +96 -9
  11. package/dist/consolidate.d.ts +79 -68
  12. package/dist/consolidate.js +218 -174
  13. package/dist/dashboard.d.ts +4 -3
  14. package/dist/dedup.d.ts +34 -26
  15. package/dist/dedup.js +91 -57
  16. package/dist/distiller.js +1 -1
  17. package/dist/domain-classify.d.ts +7 -6
  18. package/dist/domain-classify.js +12 -10
  19. package/dist/eval/decay-eval.d.ts +3 -3
  20. package/dist/eval/decay-eval.js +4 -4
  21. package/dist/eval/planted-eval.d.ts +26 -0
  22. package/dist/eval/planted-eval.js +97 -0
  23. package/dist/eval/planted-fixtures.d.ts +107 -0
  24. package/dist/eval/planted-fixtures.js +283 -0
  25. package/dist/eval/planted-harness.d.ts +176 -0
  26. package/dist/eval/planted-harness.js +649 -0
  27. package/dist/index.js +4 -3
  28. package/dist/init.d.ts +9 -3
  29. package/dist/init.js +52 -9
  30. package/dist/llm.d.ts +43 -58
  31. package/dist/llm.js +87 -101
  32. package/dist/mcp-server.js +29 -29
  33. package/dist/nightly.js +105 -103
  34. package/dist/nofit.d.ts +4 -11
  35. package/dist/nofit.js +6 -23
  36. package/dist/recall-index.d.ts +30 -28
  37. package/dist/recall-index.js +21 -18
  38. package/dist/recall-registry.d.ts +2 -1
  39. package/dist/recall-registry.js +35 -1
  40. package/dist/reconsolidation.d.ts +124 -72
  41. package/dist/reconsolidation.js +359 -148
  42. package/dist/relink.js +3 -4
  43. package/dist/retrieval.d.ts +68 -35
  44. package/dist/retrieval.js +292 -104
  45. package/dist/run-deadline.d.ts +62 -0
  46. package/dist/run-deadline.js +73 -0
  47. package/dist/schema-prototypes.d.ts +3 -3
  48. package/dist/schema-prototypes.js +3 -3
  49. package/dist/state.d.ts +2 -3
  50. package/dist/storage.d.ts +16 -16
  51. package/dist/storage.js +62 -24
  52. package/dist/telemetry.d.ts +8 -7
  53. package/dist/token-budget.js +3 -4
  54. package/dist/type-classify.js +4 -4
  55. package/dist/types.d.ts +95 -155
  56. package/domains.example.json +4 -5
  57. package/hermes-plugin/hicortex/README.md +2 -2
  58. package/openclaw.plugin.json +1 -1
  59. package/package.json +2 -1
  60. package/pi-extension/hicortex/README.md +1 -1
  61. package/server.json +3 -3
@@ -76,6 +76,7 @@ const viz_js_1 = require("./viz.js");
76
76
  const dashboard_js_1 = require("./dashboard.js");
77
77
  const identity_store_js_1 = require("./identity-store.js");
78
78
  const retrieval = __importStar(require("./retrieval.js"));
79
+ const CALIBRATION = __importStar(require("./calibration.js"));
79
80
  const recall_registry_js_1 = require("./recall-registry.js");
80
81
  const memory_instructions_js_1 = require("./memory-instructions.js");
81
82
  const recall_index_js_1 = require("./recall-index.js");
@@ -180,7 +181,7 @@ function createMcpServer() {
180
181
  // -- hicortex_search --
181
182
  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
183
  query: zod_1.z.string().describe("Search query text"),
183
- limit: zod_1.z.coerce.number().optional().describe("Max results (default: server config searchLimit)"),
184
+ limit: zod_1.z.coerce.number().optional().describe("Max results (default: the server default)"),
184
185
  project: zod_1.z.string().optional().describe("Filter by project name"),
185
186
  }, async ({ query, limit, project }) => {
186
187
  if (!db)
@@ -214,7 +215,7 @@ function createMcpServer() {
214
215
  // -- hicortex_recent --
215
216
  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
217
  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)"),
218
+ limit: zod_1.z.coerce.number().optional().describe("Max results (default: the server default)"),
218
219
  }, async ({ project, limit }) => {
219
220
  if (!db)
220
221
  return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
@@ -674,9 +675,10 @@ async function startServer(options = {}) {
674
675
  });
675
676
  }
676
677
  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.
678
+ // Tuning (#220: maxTokens + enableThinking; #408: the diagnostic env tier
679
+ // for numCtx + ollama flush), validated + resolved via the shared overlay
680
+ // (also applied in resolveSavedLlmConfig for the nightly — the ONE
681
+ // resolution point per process). Wrong-typed values warn + drop.
680
682
  (0, llm_js_1.applyTierTuningOverlay)(llmConfig, savedConfig);
681
683
  llm = new llm_js_1.LlmClient(llmConfig);
682
684
  console.log(`[hicortex] LLM (one model, all phases): ${llmConfig.provider}/${llmConfig.model}`);
@@ -749,34 +751,29 @@ async function startServer(options = {}) {
749
751
  console.warn("[hicortex] Config uses the legacy 'contextAgents' key — renamed to 'identityAgents' in 0.18 (#264). " +
750
752
  "The legacy key still works; update your config to silence this warning.");
751
753
  }
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}` +
754
+ // #408: recall/decay calibration is RELEASE-MANAGED (calibration.ts) — no
755
+ // config keys are read here anymore. The plain configure*() calls pin this
756
+ // process to the shipped constants (they remain the eval/test seam; see
757
+ // retrieval.ts). The boot log still prints the active values — now sourced
758
+ // from the calibration constants via the configure*() returns.
759
+ retrieval.configureDecay();
760
+ const recallCfg = retrieval.configureRecall();
761
+ const scoringCfg = retrieval.configureScoring();
762
+ const sessionIntentCfg = retrieval.configureSessionIntent();
763
+ console.log(`[hicortex] Recall (release-managed calibration): k=${recallCfg.searchLimit}/recent=${recallCfg.recentLimit}` +
760
764
  `/window=${recallCfg.recentWindowDays}d/cold=${recallCfg.coldExposureSlots}` +
761
- `/novelty=${(0, recall_index_js_1.resolveNoveltyFloorSlots)(savedConfig?.noveltyFloorSlots, savedConfig?.recallMaxItems)}` +
765
+ `/novelty=${CALIBRATION.NOVELTY_FLOOR_SLOTS}` +
762
766
  ` · ` +
763
767
  `score sim=${scoringCfg.similarity}/str=${scoringCfg.strength}/conn=${scoringCfg.connections}` +
764
768
  `/rec=${scoringCfg.recency}, fresh=${scoringCfg.freshnessBoostWeight}@${scoringCfg.freshnessBoostDays}d, ` +
765
769
  `superseded×${scoringCfg.supersededDemotion}` +
766
770
  `, intent w=${sessionIntentCfg.weight}` +
767
771
  (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
- };
772
+ // Defaults resolve inside recall-index.ts / recall-registry.ts from the
773
+ // calibration constants (#408) — the options objects stay as the seams the
774
+ // eval injects through.
775
+ recallRegistry = new recall_registry_js_1.SessionRecallRegistry();
776
+ recallIndexOptions = {};
780
777
  memoryInstructionsEnabled = savedConfig?.memoryInstructions !== false;
781
778
  if (resolvedAgents.dropped.length > 0) {
782
779
  console.warn(`[hicortex] Ignoring invalid identityAgents entries: ${resolvedAgents.dropped.join(", ")} ` +
@@ -1002,7 +999,8 @@ async function startServer(options = {}) {
1002
999
  res.status(400).json({ error: "Missing 'query'" });
1003
1000
  return;
1004
1001
  }
1005
- // No hardcoded default: absent limit → config-driven (searchLimit).
1002
+ // No hardcoded default: absent limit → the release-managed calibration
1003
+ // default (#408 — was the searchLimit config key).
1006
1004
  const limit = req.query.limit ? Number(req.query.limit) : undefined;
1007
1005
  const project = typeof req.query.project === "string" && req.query.project ? req.query.project : undefined;
1008
1006
  // 0.16.x: `privacy` query param is ACCEPTED for backward compat (old
@@ -1074,7 +1072,8 @@ async function startServer(options = {}) {
1074
1072
  return;
1075
1073
  }
1076
1074
  const project = typeof req.query.project === "string" && req.query.project ? req.query.project : undefined;
1077
- // No hardcoded default: absent limit → config-driven (recentLimit).
1075
+ // No hardcoded default: absent limit → the release-managed calibration
1076
+ // default (#408 — was the recentLimit config key).
1078
1077
  // 0.16.x: `privacy` query param accepted but ignored (vestigial column).
1079
1078
  const limit = req.query.limit ? Number(req.query.limit) : undefined;
1080
1079
  warnDeprecatedPrivacyParamIfPresent(req.query, "recent");
@@ -1789,7 +1788,8 @@ function readConfigFile(stateDir) {
1789
1788
  }
1790
1789
  catch (e) {
1791
1790
  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). ` +
1791
+ `(config-driven LLM knobs and agentId self-heal will not apply; decay/recall ` +
1792
+ `calibration is release-managed and unaffected). ` +
1793
1793
  `Fix the JSON and restart. Cause: ${e instanceof Error ? e.message : String(e)}`);
1794
1794
  return null;
1795
1795
  }
package/dist/nightly.js CHANGED
@@ -64,7 +64,6 @@ const embedder_js_1 = require("./embedder.js");
64
64
  const storage = __importStar(require("./storage.js"));
65
65
  const consolidate_js_1 = require("./consolidate.js");
66
66
  const domain_classify_js_1 = require("./domain-classify.js");
67
- const nofit_js_1 = require("./nofit.js");
68
67
  const transcript_reader_js_1 = require("./transcript-reader.js");
69
68
  const hermes_transcript_reader_js_1 = require("./hermes-transcript-reader.js");
70
69
  const pi_transcript_reader_js_1 = require("./pi-transcript-reader.js");
@@ -76,6 +75,7 @@ const state_js_1 = require("./state.js");
76
75
  const identity_store_js_1 = require("./identity-store.js");
77
76
  const capture_cursors_js_1 = require("./capture-cursors.js");
78
77
  const capture_js_1 = require("./capture.js");
78
+ const run_deadline_js_1 = require("./run-deadline.js");
79
79
  const dashboard_js_1 = require("./dashboard.js");
80
80
  const telemetry_js_1 = require("./telemetry.js");
81
81
  const init_js_1 = require("./init.js");
@@ -167,7 +167,7 @@ function computeSince(stateDir, recaptureWindowDays, now = new Date()) {
167
167
  }
168
168
  /** POST /distill transport for server mode — localhost. Sends authToken so
169
169
  * self-capture works regardless of the localhost-bypass marker (#271 root-cause fix). */
170
- function makeLocalPost(port, authToken) {
170
+ function makeLocalPost(port, authToken, deadline) {
171
171
  return async (body) => {
172
172
  const resp = await fetch(`http://127.0.0.1:${port}/distill`, {
173
173
  method: "POST",
@@ -177,13 +177,16 @@ function makeLocalPost(port, authToken) {
177
177
  },
178
178
  body: JSON.stringify(body),
179
179
  // Synchronous 35B distillation of a large segment can take minutes.
180
- signal: AbortSignal.timeout(20 * 60 * 1000),
180
+ // #405: never wait past the run deadline — the capture loop's
181
+ // between-segments check is the clean stop; this is the hard bound
182
+ // for the POST in flight when the deadline expires mid-wait.
183
+ signal: AbortSignal.timeout(postTimeoutMs(deadline)),
181
184
  });
182
185
  return normalizePostResult(resp);
183
186
  };
184
187
  }
185
188
  /** POST /distill transport for client mode — remote URL + optional bearer token. */
186
- function makeRemotePost(serverUrl, authToken) {
189
+ function makeRemotePost(serverUrl, authToken, deadline) {
187
190
  return async (body) => {
188
191
  const resp = await fetch(`${serverUrl}/distill`, {
189
192
  method: "POST",
@@ -192,11 +195,20 @@ function makeRemotePost(serverUrl, authToken) {
192
195
  ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
193
196
  },
194
197
  body: JSON.stringify(body),
195
- signal: AbortSignal.timeout(20 * 60 * 1000),
198
+ signal: AbortSignal.timeout(postTimeoutMs(deadline)),
196
199
  });
197
200
  return normalizePostResult(resp);
198
201
  };
199
202
  }
203
+ /**
204
+ * Per-POST timeout: 20 min (synchronous distillation of a large segment can
205
+ * take minutes), clamped to the run deadline's remaining time when one is in
206
+ * force (#405) — a POST launched near expiry never outlives the deadline.
207
+ */
208
+ function postTimeoutMs(deadline) {
209
+ const CAP_MS = 20 * 60 * 1000;
210
+ return Math.max(1, Math.min(CAP_MS, deadline ? deadline.remainingMs() : Infinity));
211
+ }
200
212
  async function normalizePostResult(resp) {
201
213
  if (resp.status === 201) {
202
214
  const data = (await resp.json().catch(() => ({})));
@@ -272,9 +284,6 @@ function captureLockWaitMs() {
272
284
  const env = Number(process.env.HICORTEX_CAPTURE_LOCK_WAIT_MS);
273
285
  return Number.isFinite(env) && env >= 0 ? env : CAPTURE_LOCK_WAIT_MS;
274
286
  }
275
- // readPositiveConfig moved to ./config-read.ts (shared with the distill-tier
276
- // overlay in llm.ts / mcp-server.ts). Validates positive-number config knobs
277
- // at the disk→runtime boundary with a warn-on-rejected-value.
278
287
  const NIGHTLY_LOG_MAX_BYTES = 1024 * 1024; // 1 MB — years of normal runs
279
288
  /**
280
289
  * Keep ~/.hicortex/nightly.log bounded. The launchd plist and systemd unit
@@ -320,10 +329,11 @@ function runEvictionOnly(options = {}) {
320
329
  const stateDir = options.stateDir ?? HICORTEX_HOME;
321
330
  const savedConfig = readNightlyConfig(stateDir);
322
331
  // Same clock as the full nightly (see runNightly's identical trio) — the
323
- // eviction ranker reads these module knobs.
324
- (0, retrieval_js_1.configureDecay)({ halfLifeDays: savedConfig?.decayHalfLifeDays });
325
- (0, retrieval_js_1.configureRecall)(savedConfig);
326
- (0, retrieval_js_1.configureScoring)(savedConfig);
332
+ // eviction ranker reads these module knobs. #408: calibration constants,
333
+ // never user config — the calls just pin the process to the shipped values.
334
+ (0, retrieval_js_1.configureDecay)();
335
+ (0, retrieval_js_1.configureRecall)();
336
+ (0, retrieval_js_1.configureScoring)();
327
337
  const dbPath = (0, db_js_1.resolveDbPath)(options.dbPath);
328
338
  console.log(`[hicortex] evict-only run${options.dryRun ? " (dry run)" : ""} — DB: ${dbPath}`);
329
339
  const db = (0, db_js_1.initDb)(dbPath);
@@ -460,16 +470,31 @@ async function runNightly(options = {}) {
460
470
  }
461
471
  const dbPath = (0, db_js_1.resolveDbPath)(options.dbPath);
462
472
  // #192: consolidation's decay/prune stage must score with the same clock as
463
- // the server's retrieval path (config decayHalfLifeDays, default 365).
464
- (0, retrieval_js_1.configureDecay)({ halfLifeDays: savedConfig?.decayHalfLifeDays });
465
- (0, retrieval_js_1.configureRecall)(savedConfig);
466
- (0, retrieval_js_1.configureScoring)(savedConfig);
473
+ // the server's retrieval path. #408: that clock is the calibration constant
474
+ // set (calibration.ts) — both processes resolve identically by construction.
475
+ (0, retrieval_js_1.configureDecay)();
476
+ (0, retrieval_js_1.configureRecall)();
477
+ (0, retrieval_js_1.configureScoring)();
467
478
  const modeLabel = consolidateOnly ? " (consolidate-only)" : captureOnly ? " (capture-only)" : dryRun ? " (dry run)" : "";
468
479
  console.log(`[hicortex] Nightly pipeline starting${modeLabel}`);
469
480
  if (captureOnly) {
470
481
  console.log(`[hicortex] capture-only run — consolidation skipped`);
471
482
  }
472
483
  console.log(`[hicortex] DB: ${dbPath}`);
484
+ // #405: the ONE pipeline deadline. Full and consolidate-only runs create
485
+ // it at start and share it across capture + every consolidation stage
486
+ // (checked at safe boundaries; a hit defers cleanly — see run-deadline.ts).
487
+ // Capture-only/watchdog runs do NOT: they keep their 30-min systemd
488
+ // backstop (capture is no-LLM and short). Dry-run is exempt (writes
489
+ // nothing, spends nothing). 0/invalid config → the 240 default — a
490
+ // deadline always exists (the old reconsolidationMaxMinutes 0=off is gone).
491
+ const deadline = !dryRun && !captureOnly
492
+ ? (0, run_deadline_js_1.createRunDeadline)((0, run_deadline_js_1.resolveNightlyTimeBudgetMinutes)(savedConfig))
493
+ : undefined;
494
+ if (deadline) {
495
+ console.log(`[hicortex] Nightly time budget: ${Math.round((deadline.deadlineAt - Date.now()) / 60_000)} min ` +
496
+ `(deadline at ${new Date(deadline.deadlineAt).toISOString()})`);
497
+ }
473
498
  // Init DB — consolidation reads the DB directly; capture goes via HTTP.
474
499
  const db = (0, db_js_1.initDb)(dbPath);
475
500
  try {
@@ -515,8 +540,11 @@ async function runNightly(options = {}) {
515
540
  else {
516
541
  // Full nightly waits out a transient --capture-only overlap (each segment
517
542
  // POST can block up to 20 min); capture-only fails fast. dry-run writes
518
- // nothing so it needs no lock.
519
- const lockWaitMs = captureLockWaitMs();
543
+ // nothing so it needs no lock. #405: the wait never outlives the run
544
+ // deadline — waiting the full 30 min with 5 min left would burn the
545
+ // whole pipeline on a lock queue. captureLockWaitMs() keeps its env
546
+ // override (tests); min() clamps to whatever remains.
547
+ const lockWaitMs = Math.max(0, Math.min(captureLockWaitMs(), deadline ? deadline.remainingMs() : Infinity));
520
548
  const releaseLock = dryRun
521
549
  ? (() => { })
522
550
  : await (0, capture_js_1.acquireCaptureLock)(stateDir, captureOnly ? 0 : lockWaitMs);
@@ -581,11 +609,12 @@ async function runNightly(options = {}) {
581
609
  // source_agent_id / source_domain are per-client provenance from
582
610
  // config.json (agentId / sourceDomain) — attribution only, no filtering.
583
611
  const result = await (0, capture_js_1.captureBatches)(batches, {
584
- post: makeLocalPost(port, savedConfig?.authToken),
612
+ post: makeLocalPost(port, savedConfig?.authToken, deadline),
585
613
  cursorStore,
586
614
  dryRun,
587
615
  sourceAgentId: savedConfig?.agentId,
588
616
  sourceDomain: savedConfig?.sourceDomain,
617
+ deadline,
589
618
  });
590
619
  memoriesIngested = result.memoriesIngested;
591
620
  distillUsage = result.distillUsage;
@@ -633,10 +662,12 @@ async function runNightly(options = {}) {
633
662
  // nothing-to-do short-circuit (zero LLM calls), NOT a failure.
634
663
  // "throttled" (#246) = the llmTokensPerMonth fair-use cap was projected to
635
664
  // be exceeded, so consolidation was skipped before any LLM call.
636
- // "endpoint_down" (#337) = the pre-consolidation readiness probe failed, or
637
- // the LLM circuit breaker was open after the run — transient (retried next
638
- // run), and NEVER "completed": the stages fail soft, so without this
639
- // override a dead-endpoint run would report clean.
665
+ // "endpoint_down" (#337) = the LLM circuit breaker was open after the run —
666
+ // transient (retried next run), and NEVER "completed": the stages fail
667
+ // soft, so without this override a dead-endpoint run would report clean.
668
+ // "deferred" (#405) = the run-wide wall-clock deadline fired — at least one
669
+ // stage stopped at a safe boundary and drains next run (lastConsolidated
670
+ // held, exactly like endpoint_down).
640
671
  let consolidationStatus;
641
672
  // #246: total consolidation tokens consumed this run (hoisted for telemetry
642
673
  // + the dashboard snapshot). Undefined when consolidation didn't run at all
@@ -688,12 +719,11 @@ async function runNightly(options = {}) {
688
719
  // estimate so the next month starts clean.
689
720
  if (!dryRun) {
690
721
  (0, state_js_1.updateState)((s) => {
691
- const now = new Date();
692
- const cur = s.llmTokensThisPeriod;
693
- const startD = cur?.periodStart ? new Date(cur.periodStart) : now;
694
- if (startD.getUTCFullYear() !== now.getUTCFullYear() ||
695
- startD.getUTCMonth() !== now.getUTCMonth()) {
696
- s.llmTokensThisPeriod = { prompt: 0, completion: 0, total: 0, periodStart: now.toISOString() };
722
+ // #405: the ONE monthly-reset staleness helper — shared by
723
+ // shouldThrottleTokens, this file's post-consolidation
724
+ // accrual write, and token-budget.ts recordDistillUsage.
725
+ if ((0, consolidate_js_1.isStaleTokenPeriod)(s.llmTokensThisPeriod?.periodStart)) {
726
+ s.llmTokensThisPeriod = { prompt: 0, completion: 0, total: 0, periodStart: new Date().toISOString() };
697
727
  s.llmTokensLastRun = 0;
698
728
  console.log("[hicortex] Token fair-use period reset (new month) — throttle cleared.");
699
729
  }
@@ -702,59 +732,41 @@ async function runNightly(options = {}) {
702
732
  }
703
733
  }
704
734
  if (consolidationStatus !== "throttled") {
705
- // #337: readiness probe — ONE minimal generation request before any
706
- // consolidation phase. This REPLACES the #231 no-preflight decision
707
- // (its premise "a failed phase costs latency, not data" was falsified
708
- // by the 2026-08-23/24 incident: the gateway answered /v1/models —
709
- // liveness — while generation was dead, and the nightly retried into
710
- // it for ~5 h, making the wedge monotonically worse). A failed probe
711
- // skips consolidation entirely with the diagnosis "LLM endpoint not
712
- // generating" (not-generating, not slow) and status endpoint_down —
713
- // a transient outcome: consolidation has resumable cursors, the
714
- // nightly re-runs 2-4×/day, and capture is unaffected (the daemon's
715
- // /distill path has its own probe). Zero LLM phases run when the
716
- // probe fails — one fast failure is the whole cost.
717
- const probeOk = await llm.probe();
718
- if (!probeOk) {
719
- console.error("[hicortex] LLM endpoint not generating — consolidation skipped " +
720
- "(endpoint_down, will retry next run). See the ops runbook's " +
721
- "known failure signatures; llmProbeTimeoutMs tunes the probe's patience.");
722
- consolidationStatus = "endpoint_down";
723
- }
724
- else {
735
+ // #405: the pre-consolidation readiness probe gate is DELETED. The
736
+ // breaker's first-fast-failure semantics took over its job: a
737
+ // wedged endpoint costs at most 3 ladder-exhausted logical calls
738
+ // (~31 min at the 900 s ceiling, once per dead night, bounded by
739
+ // the run deadline) before the post-run breakerOpen override below
740
+ // reports endpoint_down. The probe SURVIVES in the daemon's
741
+ // /distill gate (mcp-server.ts resolveDistillProbeGate) — a
742
+ // different process with a different job.
743
+ {
725
744
  const cfgDomains = (0, domain_classify_js_1.parseConfigDomains)(savedConfig);
726
745
  console.log(`[hicortex] Running consolidation...`);
727
746
  const report = await (0, consolidate_js_1.runConsolidation)(db, llm, embedder_js_1.embed, dryRun, false, undefined, {
728
747
  domains: cfgDomains,
729
748
  contentDomainsReady: true,
730
- weakPrimaryFloor: (0, nofit_js_1.resolveWeakPrimaryFloor)(savedConfig),
749
+ // #408: weakPrimaryFloor is a release-managed calibration
750
+ // constant now — no config threading; the Options field stays
751
+ // as the eval/test seam.
731
752
  }, {
732
- minSimilarity: savedConfig?.supersessionMinSimilarity,
733
- maxCalls: savedConfig?.supersessionMaxCalls,
753
+ // #405: no supersessionMaxCalls — the ONE run budget is the
754
+ // only call cap. #408: minSimilarity defaults to the
755
+ // calibration constant (seam only).
734
756
  },
735
- // #241: config-driven total LLM-call ceiling (default 5000, was 200).
736
- (0, config_read_js_1.readPositiveConfig)(savedConfig ?? {}, "consolidateMaxLlmCalls", consolidate_js_1.CONSOLIDATE_MAX_LLM_CALLS),
757
+ // #405: the ONE per-run LLM-call ceiling (default 5000;
758
+ // consolidateMaxLlmCalls honored as a deprecated alias).
759
+ (0, consolidate_js_1.resolveNightlyLlmCallBudget)(savedConfig),
737
760
  // #245: soft cap on the corpus (default 10000; 0 disables eviction).
738
761
  memorySoftCapResolved, {
739
- // #384 reconsolidation knobs — threaded exactly like the
740
- // supersession pair above; the stage validates and falls back
741
- // to its defaults (0.75 / 0.80) on invalid/absent values.
742
- minSimilarity: savedConfig?.correctionMinSimilarity,
743
- rewriteMinConfidence: savedConfig?.correctionRewriteMinConfidence,
744
- // #392 unified-resolution knobs: the deterministic-merge
745
- // ceiling (legacy dedupMergeThreshold honored when the new
746
- // key is absent) and the pacing cap. Same validation posture
747
- // — the stage defaults to 0.92 / 250.
748
- autoMergeThreshold: (savedConfig?.dedupAutoMergeThreshold ??
749
- savedConfig?.dedupMergeThreshold),
750
- maxMerges: savedConfig?.dedupNightlyMaxMerges,
751
- // #401 runtime bounds: the wall-clock deadline (default 120
752
- // min, 0 disables) and the per-run classify-call ceiling
753
- // (default 600, 0 disables). Same posture — the stage
754
- // validates and falls back on invalid/absent values.
755
- maxMinutes: savedConfig?.reconsolidationMaxMinutes,
756
- maxCalls: savedConfig?.reconsolidationMaxCalls,
757
- });
762
+ // #384/#392 reconsolidation knobs — eval/test seams since
763
+ // #408 (release-managed calibration constants; nothing is
764
+ // threaded from config). The stage validates and falls back
765
+ // to its calibration defaults on invalid/absent values.
766
+ },
767
+ // #405: the ONE run-wide deadline — capture and every
768
+ // consolidation stage check this same handle.
769
+ deadline);
758
770
  console.log(`[hicortex] Consolidation ${report.status} in ${report.elapsed_seconds}s` +
759
771
  (report.stages.reflection ? ` (${report.stages.reflection.lessons_generated} lessons)` : ""));
760
772
  consolidationStatus = report.status;
@@ -812,27 +824,18 @@ async function runNightly(options = {}) {
812
824
  (0, state_js_1.updateState)((s) => {
813
825
  const now = new Date();
814
826
  const cur = s.llmTokensThisPeriod;
815
- let periodStart = cur?.periodStart ?? now.toISOString();
816
- let prompt = cur?.prompt ?? 0;
817
- let completion = cur?.completion ?? 0;
818
- let total = cur?.total ?? 0;
819
- // Monthly reset: if periodStart is in a previous calendar month,
820
- // zero the accrual before adding this run's contribution.
821
- const startD = new Date(periodStart);
822
- if (startD.getUTCFullYear() !== now.getUTCFullYear() ||
823
- startD.getUTCMonth() !== now.getUTCMonth()) {
824
- periodStart = now.toISOString();
825
- prompt = 0;
826
- completion = 0;
827
- total = 0;
828
- }
829
- if (tokensTotal) {
830
- prompt += tokensTotal.prompt;
831
- completion += tokensTotal.completion;
832
- total += tokensTotal.total;
833
- }
827
+ // Monthly reset — the ONE staleness helper (#405): zero the
828
+ // accrual before adding this run's contribution when
829
+ // periodStart sits in a previous UTC calendar month. Shared
830
+ // with shouldThrottleTokens, this file's throttle-branch
831
+ // reset, and token-budget.ts recordDistillUsage.
832
+ const stale = (0, consolidate_js_1.isStaleTokenPeriod)(cur?.periodStart, now);
833
+ const prompt = (stale ? 0 : cur?.prompt ?? 0) + (tokensTotal?.prompt ?? 0);
834
+ const completion = (stale ? 0 : cur?.completion ?? 0) + (tokensTotal?.completion ?? 0);
835
+ const total = (stale ? 0 : cur?.total ?? 0) + (tokensTotal?.total ?? 0);
834
836
  s.llmTokensThisPeriod = {
835
- prompt, completion, total, periodStart,
837
+ prompt, completion, total,
838
+ periodStart: stale ? now.toISOString() : cur.periodStart,
836
839
  };
837
840
  s.llmTokensLastRun = tokensTotal?.total ?? 0;
838
841
  }, stateDir);
@@ -1089,13 +1092,12 @@ async function runClientNightly(config, dryRun, stateDir = HICORTEX_HOME, recapt
1089
1092
  // the whole run — the pre-flight only needs the link back, which can take
1090
1093
  // ~1 min after wake.
1091
1094
  //
1092
- // Config-overridable (#163): a wired/well-connected client vs one whose link
1093
- // is slow to re-establish after wake want different values. Defaults: 20s
1094
- // per-attempt timeout, 3 attempts, 60s gap. The 20s per-attempt (bumped from
1095
- // 15s in 0.17) absorbs a slow link coming back after the client wakes — a
1096
- // remote server reached over a mesh/VPN link can take several seconds to
1097
- // answer on the first request. For a genuinely DOWN link (`fetch failed`) no
1098
- // timeout length helps — the capture watchdog's frequent retry handles that (#239).
1095
+ // #163/#405: constants (the preflightTimeoutMs/preflightAttempts/
1096
+ // preflightRetryGapMs config keys are removed — no incident in the #403
1097
+ // inventory ever required tuning them). 20s per attempt absorbs a slow link
1098
+ // re-establishing after the client wakes (bumped from 15s in 0.17); for a
1099
+ // genuinely DOWN link no timeout length helps — the capture watchdog's
1100
+ // frequent retry handles that (#239).
1099
1101
  //
1100
1102
  // WALL-CLOCK NOTE: setTimeout and AbortSignal.timeout do NOT advance while
1101
1103
  // macOS is asleep, so the ~3m worst case (3×20s + 2×60s) is wall-clock-
@@ -1103,9 +1105,9 @@ async function runClientNightly(config, dryRun, stateDir = HICORTEX_HOME, recapt
1103
1105
  // elapsed time can exceed it. Not a defect: the capture lock isn't held
1104
1106
  // during the retry and the cursor design is dup-over-loss, so a late success
1105
1107
  // is harmless. Just don't treat 3m as a hard wall-clock bound.
1106
- const PREFLIGHT_TIMEOUT_MS = (0, config_read_js_1.readPositiveConfig)(config, "preflightTimeoutMs", 20_000);
1107
- const PREFLIGHT_ATTEMPTS = Math.max(1, Math.floor((0, config_read_js_1.readPositiveConfig)(config, "preflightAttempts", 3)));
1108
- const PREFLIGHT_RETRY_GAP_MS = (0, config_read_js_1.readPositiveConfig)(config, "preflightRetryGapMs", 60_000);
1108
+ const PREFLIGHT_TIMEOUT_MS = 20_000;
1109
+ const PREFLIGHT_ATTEMPTS = 3;
1110
+ const PREFLIGHT_RETRY_GAP_MS = 60_000;
1109
1111
  let reachable = false;
1110
1112
  for (let attempt = 1; attempt <= PREFLIGHT_ATTEMPTS; attempt++) {
1111
1113
  try {
package/dist/nofit.d.ts CHANGED
@@ -54,21 +54,14 @@ import type { DomainDef } from "./types.js";
54
54
  * Default weak-primary floor: minimum cosine(memory embedding, best domain
55
55
  * prototype) for a no-fit memory to earn a weak primary.
56
56
  *
57
- * TUNING: 0.45 is a starting point for bge-small-en-v1.5 embeddings — it
58
- * should be tuned from the actual corpus weight distribution (e.g. inspect
59
- * the memory_tags.weight histogram of LLM-tagged rows and set the floor
60
- * near its lower tail). Override per install via `weakPrimaryFloor` in
61
- * ~/.hicortex/config.json.
57
+ * #408: RELEASE-MANAGED — the constant (and its provenance: a starting point
58
+ * for bge-small-en-v1.5 embeddings) lives in calibration.ts. There is no
59
+ * config override anymore; the DomainStageOptions.weakPrimaryFloor field
60
+ * stays as the eval/test seam.
62
61
  */
63
62
  export declare const DEFAULT_WEAK_PRIMARY_FLOOR = 0.45;
64
63
  /** Halving never takes base_strength below this (survivable, not zeroed). */
65
64
  export declare const NO_ASSOCIATION_MIN_STRENGTH = 0.05;
66
- /**
67
- * Resolve the weak-primary floor from a raw config object. Accepts a finite
68
- * number in (0, 1); anything else (absent, wrong type, out of range) falls
69
- * back to DEFAULT_WEAK_PRIMARY_FLOOR with a warning for invalid values.
70
- */
71
- export declare function resolveWeakPrimaryFloor(config: Record<string, unknown> | null | undefined): number;
72
65
  /** Outcome of resolving a no-fit memory against the domain prototypes. */
73
66
  export type NoFitResolution = {
74
67
  kind: "weak_primary";
package/dist/nofit.js CHANGED
@@ -84,41 +84,24 @@ var __importStar = (this && this.__importStar) || (function () {
84
84
  })();
85
85
  Object.defineProperty(exports, "__esModule", { value: true });
86
86
  exports.NO_ASSOCIATION_MIN_STRENGTH = exports.DEFAULT_WEAK_PRIMARY_FLOOR = void 0;
87
- exports.resolveWeakPrimaryFloor = resolveWeakPrimaryFloor;
88
87
  exports.resolveNoFit = resolveNoFit;
89
88
  exports.applyWeakPrimary = applyWeakPrimary;
90
89
  exports.applyNoAssociationDecay = applyNoAssociationDecay;
91
90
  const storage = __importStar(require("./storage.js"));
92
91
  const schema_prototypes_js_1 = require("./schema-prototypes.js");
92
+ const CALIBRATION = __importStar(require("./calibration.js"));
93
93
  /**
94
94
  * Default weak-primary floor: minimum cosine(memory embedding, best domain
95
95
  * prototype) for a no-fit memory to earn a weak primary.
96
96
  *
97
- * TUNING: 0.45 is a starting point for bge-small-en-v1.5 embeddings — it
98
- * should be tuned from the actual corpus weight distribution (e.g. inspect
99
- * the memory_tags.weight histogram of LLM-tagged rows and set the floor
100
- * near its lower tail). Override per install via `weakPrimaryFloor` in
101
- * ~/.hicortex/config.json.
97
+ * #408: RELEASE-MANAGED — the constant (and its provenance: a starting point
98
+ * for bge-small-en-v1.5 embeddings) lives in calibration.ts. There is no
99
+ * config override anymore; the DomainStageOptions.weakPrimaryFloor field
100
+ * stays as the eval/test seam.
102
101
  */
103
- exports.DEFAULT_WEAK_PRIMARY_FLOOR = 0.45;
102
+ exports.DEFAULT_WEAK_PRIMARY_FLOOR = CALIBRATION.WEAK_PRIMARY_FLOOR;
104
103
  /** Halving never takes base_strength below this (survivable, not zeroed). */
105
104
  exports.NO_ASSOCIATION_MIN_STRENGTH = 0.05;
106
- /**
107
- * Resolve the weak-primary floor from a raw config object. Accepts a finite
108
- * number in (0, 1); anything else (absent, wrong type, out of range) falls
109
- * back to DEFAULT_WEAK_PRIMARY_FLOOR with a warning for invalid values.
110
- */
111
- function resolveWeakPrimaryFloor(config) {
112
- const raw = config?.weakPrimaryFloor;
113
- if (raw === undefined || raw === null)
114
- return exports.DEFAULT_WEAK_PRIMARY_FLOOR;
115
- if (typeof raw === "number" && Number.isFinite(raw) && raw > 0 && raw < 1) {
116
- return raw;
117
- }
118
- console.warn(`[hicortex] invalid weakPrimaryFloor in config (${JSON.stringify(raw)}) — ` +
119
- `must be a number in (0, 1); using default ${exports.DEFAULT_WEAK_PRIMARY_FLOOR}`);
120
- return exports.DEFAULT_WEAK_PRIMARY_FLOOR;
121
- }
122
105
  /**
123
106
  * Decide what happens to a no-fit memory (READ-ONLY — no writes, so callers
124
107
  * batching writes into a transaction can decide during the scan phase):