@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
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ /**
3
+ * Operator capture pause (#423 phase 3, D3) — the server-side 200-skip.
4
+ *
5
+ * A pause is a row in `capture_pauses` (migration v18): machine × harness →
6
+ * paused_at. A row EXISTS = paused for that bundle; the /distill handler
7
+ * reads the table per POST, so a pause takes effect on the very next post —
8
+ * no restart — and answers 200 {skipped: true, paused: true}. The 200 is the
9
+ * whole point: capture.ts treats every 200 as confirmed and advances its
10
+ * cursor, so sessions that arrive while paused are deliberately NOT captured
11
+ * and are never re-sent or backfilled. Zero client changes.
12
+ *
13
+ * THE BUNDLE KEY. The pause's (machine, harness) must derive EXACTLY like
14
+ * the traffic it gates: machine via storage.sanitizeSourceMachine ('' when
15
+ * absent) and harness via harnessOfAgent below — the same normalization
16
+ * recordDistillActivity applies when it writes distill_activity, and the same
17
+ * "harness/profile" → "harness" split the console groups its bundles on. A
18
+ * key derived any other way would never match and the pause would silently
19
+ * not fire.
20
+ *
21
+ * LAST-SEEN (presence dots) derives ONLY from /distill activity — the one
22
+ * per-agent-identified traffic the server sees. Recall traffic (/search,
23
+ * /recall-index, /memory) carries no agent/machine identity on the wire, so
24
+ * attributing it would need new client fields — the heartbeats the spec
25
+ * forbids. Thresholds (green ≤36h — a nightly poster reads online through
26
+ * the following day; amber ≤7d — the distill_activity retention window; none
27
+ * beyond or with no rows) are page-side presentation; this module just
28
+ * reports the newest ts per bundle.
29
+ *
30
+ * Pure, unit-testable without express (the capture-health.ts layering):
31
+ * mcp-server.ts and dashboard.ts wire these functions to the live db.
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.harnessOfAgent = harnessOfAgent;
35
+ exports.capturePauseKey = capturePauseKey;
36
+ exports.isCapturePaused = isCapturePaused;
37
+ exports.setCapturePause = setCapturePause;
38
+ exports.listCapturePauses = listCapturePauses;
39
+ exports.readFleetLastSeen = readFleetLastSeen;
40
+ const storage_js_1 = require("./storage.js");
41
+ /**
42
+ * Derive the harness from a source_agent wire value — the bundle split the
43
+ * console already uses ("claude-code/main" → "claude-code"): the part before
44
+ * the first '/' when a slash is present at index > 0, else the whole trimmed
45
+ * string, capped at 128. Non-string/blank → "unknown" (mirrors how
46
+ * recordDistillActivity stores the agent when absent).
47
+ */
48
+ function harnessOfAgent(sourceAgent) {
49
+ if (typeof sourceAgent !== "string")
50
+ return "unknown";
51
+ const t = sourceAgent.trim();
52
+ if (t.length === 0)
53
+ return "unknown";
54
+ const slash = t.indexOf("/");
55
+ return (slash > 0 ? t.slice(0, slash) : t).slice(0, 128);
56
+ }
57
+ /**
58
+ * Normalize the raw /distill wire fields into the pause key. MUST match the
59
+ * recordDistillActivity normalization (machine '' when absent, agent
60
+ * 'unknown' when absent) and the console's bundle grouping
61
+ * ((machine||'')+'|'+harness) — see the module doc.
62
+ */
63
+ function capturePauseKey(machine, sourceAgent) {
64
+ return {
65
+ machine: (0, storage_js_1.sanitizeSourceMachine)(machine) ?? "",
66
+ harness: harnessOfAgent(sourceAgent),
67
+ };
68
+ }
69
+ /** True when a pause row exists for the (machine, harness) bundle. */
70
+ function isCapturePaused(db, machine, harness) {
71
+ return (db
72
+ .prepare("SELECT 1 FROM capture_pauses WHERE machine = ? AND harness = ?")
73
+ .get(machine, harness) !== undefined);
74
+ }
75
+ /**
76
+ * Pause (upsert the row, timestamp now) or resume (delete it). Returns the
77
+ * persisted paused_at when pausing, null when resuming. No pruning, ever —
78
+ * see migration v18's provenance comment.
79
+ */
80
+ function setCapturePause(db, machine, harness, paused) {
81
+ if (!paused) {
82
+ db.prepare("DELETE FROM capture_pauses WHERE machine = ? AND harness = ?").run(machine, harness);
83
+ return null;
84
+ }
85
+ const pausedAt = new Date().toISOString();
86
+ db.prepare("INSERT OR REPLACE INTO capture_pauses (machine, harness, paused_at) VALUES (?, ?, ?)").run(machine, harness, pausedAt);
87
+ return pausedAt;
88
+ }
89
+ /** Every paused bundle (newest first) — the dashboard fleet.pauses block. */
90
+ function listCapturePauses(db) {
91
+ return db
92
+ .prepare("SELECT machine, harness, paused_at FROM capture_pauses ORDER BY paused_at DESC")
93
+ .all();
94
+ }
95
+ /**
96
+ * The newest /distill activity per bundle: for each (machine, agent) take
97
+ * MAX(ts) with that latest row's outcome, derive the harness per agent, then
98
+ * merge same-bundle agents keeping the newest ts (one dot per bundle, not
99
+ * per profile). Reads only distill_activity, which the recorder prunes to
100
+ * 7 days — older-than-window bundles simply have no rows and no dot.
101
+ */
102
+ function readFleetLastSeen(db) {
103
+ const rows = db
104
+ .prepare(`SELECT a.machine, a.agent, a.ts, a.outcome
105
+ FROM distill_activity a
106
+ JOIN (
107
+ SELECT machine, agent, MAX(ts) AS max_ts
108
+ FROM distill_activity
109
+ GROUP BY machine, agent
110
+ ) latest
111
+ ON a.machine = latest.machine AND a.agent = latest.agent AND a.ts = latest.max_ts`)
112
+ .all();
113
+ const byBundle = new Map();
114
+ for (const r of rows) {
115
+ const key = `${r.machine}|${harnessOfAgent(r.agent)}`;
116
+ const prev = byBundle.get(key);
117
+ if (!prev || r.ts > prev.last_seen) {
118
+ byBundle.set(key, {
119
+ machine: r.machine,
120
+ harness: harnessOfAgent(r.agent),
121
+ last_seen: r.ts,
122
+ last_outcome: r.outcome,
123
+ });
124
+ }
125
+ }
126
+ return [...byBundle.values()].sort((a, b) => (a.last_seen < b.last_seen ? 1 : -1));
127
+ }
package/dist/capture.d.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import type { TranscriptBatch } from "./transcript-reader.js";
16
16
  import type { CursorStore } from "./capture-cursors.js";
17
+ import type { RunDeadline } from "./run-deadline.js";
17
18
  /**
18
19
  * Max denoised chars per segment. Kept below the server's 80K distill cap
19
20
  * (distiller.ts MAX_TRANSCRIPT_CHARS) with ~20K headroom so NO capture path can
@@ -56,6 +57,9 @@ export interface DistillBody {
56
57
  source_agent_id?: string | null;
57
58
  /** Client-declared topic/domain of the capturing agent. Provenance only. */
58
59
  source_domain?: string | null;
60
+ /** Machine this capture ran on (#421 machine × harness): config
61
+ * `machineName` ?? os.hostname(), stamped by the nightly. */
62
+ source_machine?: string | null;
59
63
  project: string;
60
64
  session_id: string;
61
65
  segment_id: string;
@@ -105,6 +109,21 @@ export interface CaptureOptions {
105
109
  * `source_domain` provenance. Null when undeclared.
106
110
  */
107
111
  sourceDomain?: string | null;
112
+ /**
113
+ * Machine stamp on every segment (#421 machine × harness): config
114
+ * `machineName` when set, else os.hostname() — resolved by the nightly
115
+ * caller. Null disables stamping.
116
+ */
117
+ sourceMachine?: string | null;
118
+ /**
119
+ * The run-wide pipeline deadline (#405), checked BETWEEN segment POSTs —
120
+ * a boundary the per-session cursor discipline already guarantees is safe
121
+ * (the cursor only advances past server-confirmed segments, so a deadline
122
+ * stop holds every unconfirmed segment for the next run; dup-over-loss).
123
+ * Full and consolidate-only nightlies pass it; capture-only/watchdog runs
124
+ * keep their 30-min unit backstop instead.
125
+ */
126
+ deadline?: RunDeadline;
108
127
  }
109
128
  export interface CaptureResult {
110
129
  memoriesIngested: number;
@@ -113,10 +132,12 @@ export interface CaptureResult {
113
132
  /**
114
133
  * Set when the loop stopped early on a terminal server response: "limit"
115
134
  * (token-budget 429, mcp-server.ts's `"token budget exceeded"` gate) or
116
- * "auth" (401). A rate-limit 429 never sets this — it is transient (#327).
117
- * The caller decides watermark handling.
135
+ * "auth" (401); "deadline" (#405) when the run-wide pipeline deadline fired
136
+ * between segments (transient — the watermark holds, everything unconfirmed
137
+ * retries next run). A rate-limit 429 never sets this — it is transient
138
+ * (#327). The caller decides watermark handling.
118
139
  */
119
- stopped?: "limit" | "auth";
140
+ stopped?: "limit" | "auth" | "deadline";
120
141
  /**
121
142
  * Run-global rate-429 latch (#327 CR): true when at least one session
122
143
  * SURRENDERED to a rate-limit 429 (the transient kind — postWithRateRetry
package/dist/capture.js CHANGED
@@ -204,7 +204,7 @@ async function postWithRateRetry(post, body) {
204
204
  * re-paying the Retry-After ladder (#327).
205
205
  */
206
206
  async function captureBatches(batches, opts) {
207
- const { post, cursorStore, dryRun = false, segmentMaxChars = exports.SEGMENT_MAX_CHARS, sourceAgentId, sourceDomain } = opts;
207
+ const { post, cursorStore, dryRun = false, segmentMaxChars = exports.SEGMENT_MAX_CHARS, sourceAgentId, sourceDomain, sourceMachine, deadline } = opts;
208
208
  let memoriesIngested = 0;
209
209
  let sessionsSent = 0;
210
210
  let hadTransientFailure = false;
@@ -249,6 +249,15 @@ async function captureBatches(batches, opts) {
249
249
  let sessionPosted = false;
250
250
  for (let s = 0; s < segments.length; s++) {
251
251
  const seg = segments[s];
252
+ // #405: stop BETWEEN segments — a safe boundary by construction (the
253
+ // cursor below only advances past server-confirmed segments). The whole
254
+ // session loop breaks on `stopped` at the bottom; unconfirmed segments
255
+ // hold and retry next run.
256
+ if (!dryRun && deadline?.hit("capture")) {
257
+ console.warn(`[hicortex] Run deadline reached — capture stops after the last confirmed segment`);
258
+ stopped = "deadline";
259
+ break;
260
+ }
252
261
  // A segment advances the cursor to its segEnd only when it is the LAST
253
262
  // segment ending at that boundary. Hard-split pieces (.p0,.p1,…) of one
254
263
  // entry share the same segEnd; confirming an earlier piece must NOT move
@@ -267,6 +276,7 @@ async function captureBatches(batches, opts) {
267
276
  source_agent: batch.sourceAgent ?? `claude-code/${batch.projectName}`,
268
277
  source_agent_id: sourceAgentId ?? null,
269
278
  source_domain: sourceDomain ?? null,
279
+ source_machine: sourceMachine ?? null,
270
280
  project: batch.projectName,
271
281
  session_id: batch.sessionId,
272
282
  segment_id: `${genPrefix}${seg.segStart}-${seg.segEnd}${seg.idSuffix}`,
@@ -50,6 +50,12 @@ export interface ClassifyDomainsOptions {
50
50
  llm?: LlmClient;
51
51
  /** Config override (tests). Defaults to reading stateDir/config.json. */
52
52
  config?: Record<string, unknown> | null;
53
+ /**
54
+ * Weak-primary floor (#408): release-managed default (calibration.ts via
55
+ * nofit's DEFAULT_WEAK_PRIMARY_FLOOR); this field is the eval/test seam —
56
+ * the config key is gone from the surface. Invalid → default.
57
+ */
58
+ weakPrimaryFloor?: number;
53
59
  /**
54
60
  * Embedder override (tests). Used only for domain-description prototype
55
61
  * seeds; defaults to the local ONNX embedder, loaded lazily on first need
@@ -118,7 +118,13 @@ async function runClassifyDomains(options = {}) {
118
118
  '{ "name": "Boating", "description": "..." }] } and re-run. ' +
119
119
  "No fallback bucket is needed — no-fit memories are handled automatically.");
120
120
  }
121
- const weakPrimaryFloor = (0, nofit_js_1.resolveWeakPrimaryFloor)(config);
121
+ // #408: the floor is a release-managed calibration constant; the Options
122
+ // field is the eval/test seam (invalid values keep the default, the stage
123
+ // knob-validation style — silent fallback, no warn).
124
+ const floorRaw = Number(options.weakPrimaryFloor);
125
+ const weakPrimaryFloor = Number.isFinite(floorRaw) && floorRaw > 0 && floorRaw < 1
126
+ ? floorRaw
127
+ : nofit_js_1.DEFAULT_WEAK_PRIMARY_FLOOR;
122
128
  // Resolve the LLM (one model serves all phases — #231).
123
129
  let llm;
124
130
  if (options.llm) {
package/dist/cli.js CHANGED
@@ -189,6 +189,36 @@ switch (command) {
189
189
  });
190
190
  break;
191
191
  }
192
+ case "rescore-importance": {
193
+ // #425 — one-shot LLM backfill: re-judge the corpus under the
194
+ // re-anchored importance rubric. classify-domains shape (resumable
195
+ // cursor, --batch, --reset) + dedup discipline (dry-run default,
196
+ // --apply, DB backup before any write).
197
+ const args = process.argv.slice(3);
198
+ const intFlag = (name) => {
199
+ const idx = args.indexOf(name);
200
+ if (idx === -1)
201
+ return undefined;
202
+ const val = parseInt(args[idx + 1], 10);
203
+ if (isNaN(val)) {
204
+ console.error(`[hicortex] rescore-importance: ${name} requires an integer value`);
205
+ process.exit(1);
206
+ }
207
+ return val;
208
+ };
209
+ const rescoreOptions = {
210
+ apply: args.includes("--apply"),
211
+ reset: args.includes("--reset"),
212
+ batchSize: intFlag("--batch"),
213
+ };
214
+ import("./rescore-importance.js").then(({ runRescoreImportance }) => {
215
+ runRescoreImportance(rescoreOptions).catch((err) => {
216
+ console.error(err instanceof Error ? err.message : `[hicortex] rescore-importance failed: ${err}`);
217
+ process.exit(1);
218
+ });
219
+ });
220
+ break;
221
+ }
192
222
  case "classify-types": {
193
223
  const args = process.argv.slice(3);
194
224
  const intFlag = (name) => {
@@ -421,6 +451,8 @@ Commands:
421
451
  backup Snapshot the DB + identity + state to a tar.gz (online, WAL-safe)
422
452
  classify-domains Backfill content-based domain tags over the corpus (server mode, needs config.domains)
423
453
  classify-types Backfill episode→fact/decision type tags over the corpus (server mode)
454
+ rescore-importance Re-judge all memories' importance under the current rubric
455
+ (server mode; dry run by default — --apply executes; resumable)
424
456
  learnings-identity Fetch identity + lessons and print Markdown to stdout (CC SessionStart hook)
425
457
  (alias: lessons-context — the pre-#264 name, kept for backcompat)
426
458
  recall-hook Pushed recall index for the current prompt (CC UserPromptSubmit/SessionStart hook)
@@ -445,9 +477,8 @@ Options:
445
477
  dedup --apply Execute the merge (default: dry run, report only)
446
478
  Losers are absorbed — hidden from recall, kept as
447
479
  evidence (fetchable by id; dedup_log audit) — not deleted
448
- dedup --threshold <t> Override the threshold for one run (default: config
449
- dedupAutoMergeThreshold, legacy dedupMergeThreshold
450
- still honored; else 0.92)
480
+ dedup --threshold <t> Override the threshold for one run (default: the
481
+ release-managed calibration ceiling, 0.92)
451
482
  dedup --db <path> DB path override (defaults to the configured DB)
452
483
  history --rollback <row> Roll back history row <row>: restores prior content/status, un-absorbs triggers
453
484
  history --db <path> DB path override (defaults to the configured DB)
@@ -459,6 +490,10 @@ Options:
459
490
  classify-types --all Reclassify every memory (default: only episodes)
460
491
  classify-types --batch <n> Memories per batch (default: 200)
461
492
  classify-types --reset Restart from the beginning (ignore saved cursor)
493
+ rescore-importance --apply Execute the importance backfill (default: dry run, report only)
494
+ Takes a DB backup first; resumable via a state.json cursor
495
+ rescore-importance --batch <n> Rows per invocation (default: 500; LLM calls are 10 rows each)
496
+ rescore-importance --reset Restart from the beginning (ignore saved cursor)
462
497
  identity show [name] Print all identity sections, or just <name> (raw, pipeable)
463
498
  identity edit <name> Edit a section in $EDITOR; PUT only if changed
464
499
  identity … --agent <id> Target a per-agent scope instead of the global set
@@ -27,7 +27,7 @@ export declare function readStrictBoolean(config: Record<string, unknown>, key:
27
27
  /**
28
28
  * Read a non-negative finite number (allows 0, unlike readPositiveConfig).
29
29
  * Returns `def` when absent OR invalid. Used for keys where 0 is a valid "off"
30
- * value (e.g. ollamaFlushEvery).
30
+ * value (e.g. memorySoftCap's eviction opt-out, timerJitterSeconds).
31
31
  */
32
32
  export declare function readNonNegativeConfig(config: Record<string, unknown>, key: string, def: number): number;
33
33
  /**
@@ -52,7 +52,7 @@ function readStrictBoolean(config, key) {
52
52
  /**
53
53
  * Read a non-negative finite number (allows 0, unlike readPositiveConfig).
54
54
  * Returns `def` when absent OR invalid. Used for keys where 0 is a valid "off"
55
- * value (e.g. ollamaFlushEvery).
55
+ * value (e.g. memorySoftCap's eviction opt-out, timerJitterSeconds).
56
56
  */
57
57
  function readNonNegativeConfig(config, key, def) {
58
58
  const v = config[key];
@@ -152,6 +152,63 @@ const IGNORED_CONFIG_KEYS = [
152
152
  "classifyModel", "classifyBaseUrl", "classifyApiKey", "classifyProvider",
153
153
  "distillFallback",
154
154
  ];
155
+ /**
156
+ * Config keys REMOVED by the #405 budget simplification, each mapped to its
157
+ * replacement (or "removed" when there is none). The old single 0.16.8-style
158
+ * message does not fit — every key here needs to name where its job went.
159
+ * Warned at the config boundary alongside the 0.16.8 keys above.
160
+ */
161
+ const REMOVED_CONFIG_KEYS = {
162
+ reconsolidationMaxMinutes: "removed — the ONE run deadline (nightlyTimeBudgetMinutes) bounds the stage",
163
+ reconsolidationMaxCalls: "removed — the ONE call budget (nightlyLlmCallBudget) caps all stages",
164
+ supersessionMaxCalls: "removed — the ONE call budget (nightlyLlmCallBudget) caps all stages",
165
+ dedupNightlyMaxMerges: "removed — the run deadline (nightlyTimeBudgetMinutes) bounds nightly merges",
166
+ classifyMaxTokens: "removed — maxTokens is the single output ceiling for every call",
167
+ llmBreakerThreshold: "removed — the breaker threshold is a constant (3)",
168
+ llmBreakerCooldownMs: "removed — the breaker cooldown is a constant (10 min)",
169
+ llmSingleFlightWaitMs: "removed — the wait derives from llmTimeoutMs",
170
+ preflightTimeoutMs: "removed — a constant (20 s per attempt)",
171
+ preflightAttempts: "removed — a constant (3 attempts)",
172
+ preflightRetryGapMs: "removed — a constant (60 s gap)",
173
+ moduleIndexTokenBudget: "removed — was documentation-only",
174
+ };
175
+ /**
176
+ * Config keys that became RELEASE-MANAGED CALIBRATION constants (#408): the
177
+ * ~35 tuning keys of the 0.15–0.20 era. Their values are ignored — the
178
+ * constants ship with each release (src/calibration.ts) and change only in
179
+ * releases with eval evidence linked in the changelog. Like the #405 list,
180
+ * this is deliberately WIDER than the `HicortexConfig` type ever was: several
181
+ * keys (supersessionPenalty, the rrf/bm25 knob set, recallTitleChars, ...)
182
+ * were README-documented but never typed. Don't trim the list to match the
183
+ * interface — the warn exists precisely for the untyped surface.
184
+ */
185
+ const RELEASE_MANAGED_CONFIG_KEYS = [
186
+ "decayHalfLifeDays", "searchLimit", "recentLimit", "recentWindowDays",
187
+ "coldExposureSlots", "recallMaxItems", "recallMinSimilarity",
188
+ "recallReshowTurns", "recallMinPromptChars", "recallTitleChars",
189
+ "sessionIntentWeight", "noveltyFloorSlots", "scoreSimilarityWeight",
190
+ "scoreStrengthWeight", "scoreConnectionsWeight", "scoreRecencyWeight",
191
+ "freshnessBoostDays", "freshnessBoostWeight", "supersededDemotion",
192
+ "projectAffinityWeight", "domainAffinityWeight", "rrfK",
193
+ "rrfCompositeWeight", "rrfFtsWeight", "rrfVectorWeight",
194
+ "bm25WeightBody", "bm25WeightProject", "bm25WeightDomain",
195
+ "dedupAutoMergeThreshold", "dedupMergeThreshold",
196
+ "supersessionMinSimilarity", "supersessionPenalty",
197
+ "correctionMinSimilarity", "correctionRewriteMinConfidence",
198
+ "weakPrimaryFloor",
199
+ ];
200
+ /**
201
+ * Config keys MOVED to the diagnostic env tier (#408): the ollama-operational
202
+ * family left config.json and now resolves from environment variables
203
+ * (calibration.ts resolvers; env > constant, invalid env warns + falls back).
204
+ * Each entry maps the old config key to its env replacement so the warning
205
+ * can name the exact variable.
206
+ */
207
+ const ENV_MOVED_CONFIG_KEYS = {
208
+ numCtx: "HICORTEX_NUM_CTX",
209
+ ollamaFlushEvery: "HICORTEX_OLLAMA_FLUSH_EVERY",
210
+ ollamaFlushWaitMs: "HICORTEX_OLLAMA_FLUSH_WAIT_MS",
211
+ };
155
212
  /**
156
213
  * Warn if the saved config carries keys that 0.16.8+ ignores. Call at every
157
214
  * config read (daemon boot + nightly). The warning clears once the keys are
@@ -163,12 +220,42 @@ function warnIgnoredConfigKeys(savedConfig) {
163
220
  return;
164
221
  const present = IGNORED_CONFIG_KEYS.filter((k) => savedConfig[k] !== undefined);
165
222
  const hasModelsBlock = savedConfig.models !== undefined;
166
- if (present.length === 0 && !hasModelsBlock)
167
- return;
168
- const detail = [...present, ...(hasModelsBlock ? ["models"] : [])].join(", ");
169
- console.warn(`[hicortex] config has keys IGNORED since 0.16.8 (${detail}). They have no effect now. ` +
170
- `Per-stage model keys / the \`models\` block: one model serves all phases — set ` +
171
- `llmModel/llmBaseUrl/llmProvider (+ llmApiKey) to your intended model. ` +
172
- `distillFallback: removed (strict mode is default — a failed distill retries next run). ` +
173
- `Remove these keys to clear this warning. See the 0.16.8 changelog.`);
223
+ if (present.length > 0 || hasModelsBlock) {
224
+ const detail = [...present, ...(hasModelsBlock ? ["models"] : [])].join(", ");
225
+ console.warn(`[hicortex] config has keys IGNORED since 0.16.8 (${detail}). They have no effect now. ` +
226
+ `Per-stage model keys / the \`models\` block: one model serves all phases — set ` +
227
+ `llmModel/llmBaseUrl/llmProvider (+ llmApiKey) to your intended model. ` +
228
+ `distillFallback: removed (strict mode is default — a failed distill retries next run). ` +
229
+ `Remove these keys to clear this warning. See the 0.16.8 changelog.`);
230
+ }
231
+ // #405: keys removed by the budget simplification — one line naming each
232
+ // present key and its replacement (or "removed").
233
+ const removed = Object.entries(REMOVED_CONFIG_KEYS)
234
+ .filter(([k]) => savedConfig[k] !== undefined);
235
+ if (removed.length > 0) {
236
+ const detail = removed.map(([k, v]) => `${k} (${v})`).join(", ");
237
+ console.warn(`[hicortex] config has keys REMOVED by the budget simplification (#405): ${detail}. ` +
238
+ `They have no effect. Remove them to clear this warning.`);
239
+ }
240
+ // #408: tuning keys that became release-managed calibration constants —
241
+ // one line naming every present key (their VALUES are ignored; the
242
+ // constants ship with each release and change only with published eval
243
+ // evidence linked in the changelog).
244
+ const releaseManaged = RELEASE_MANAGED_CONFIG_KEYS
245
+ .filter((k) => savedConfig[k] !== undefined);
246
+ if (releaseManaged.length > 0) {
247
+ console.warn(`[hicortex] config has keys that are now RELEASE-MANAGED CALIBRATION ` +
248
+ `(${releaseManaged.join(", ")}): their values are ignored — calibration ` +
249
+ `ships with each release and changes only with published eval evidence. ` +
250
+ `Remove them to clear this warning.`);
251
+ }
252
+ // #408: the diagnostic-tier keys that moved to environment variables —
253
+ // one line naming each old key AND its env replacement.
254
+ const envMoved = Object.entries(ENV_MOVED_CONFIG_KEYS)
255
+ .filter(([k]) => savedConfig[k] !== undefined);
256
+ if (envMoved.length > 0) {
257
+ const detail = envMoved.map(([k, env]) => `${k} → set ${env} instead`).join(", ");
258
+ console.warn(`[hicortex] config has keys that MOVED to environment variables (#408): ${detail}. ` +
259
+ `The config keys are ignored. Remove them to clear this warning.`);
260
+ }
174
261
  }