@gamaze/hicortex 0.20.9 → 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 (48) hide show
  1. package/README.md +8 -0
  2. package/assets/dashboard.html +3989 -836
  3. package/dist/calibration.d.ts +119 -0
  4. package/dist/calibration.js +149 -1
  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 +9 -0
  10. package/dist/capture.js +2 -1
  11. package/dist/cli.js +36 -0
  12. package/dist/consolidate.d.ts +35 -0
  13. package/dist/consolidate.js +85 -9
  14. package/dist/dashboard.d.ts +322 -3
  15. package/dist/dashboard.js +592 -7
  16. package/dist/db.js +105 -0
  17. package/dist/eval/importance-eval.d.ts +85 -0
  18. package/dist/eval/importance-eval.js +286 -0
  19. package/dist/eval/planted-fixtures.d.ts +1 -1
  20. package/dist/eval/ranking-battery.d.ts +78 -0
  21. package/dist/eval/ranking-battery.js +181 -0
  22. package/dist/eval/ranking-eval.d.ts +41 -0
  23. package/dist/eval/ranking-eval.js +391 -0
  24. package/dist/eval/ranking-fixtures.d.ts +77 -0
  25. package/dist/eval/ranking-fixtures.js +226 -0
  26. package/dist/identity-store.d.ts +21 -0
  27. package/dist/identity-store.js +49 -0
  28. package/dist/init.d.ts +14 -0
  29. package/dist/init.js +32 -0
  30. package/dist/mcp-server.d.ts +12 -0
  31. package/dist/mcp-server.js +184 -3
  32. package/dist/nightly.d.ts +9 -1
  33. package/dist/nightly.js +59 -7
  34. package/dist/prompts.d.ts +10 -0
  35. package/dist/prompts.js +28 -5
  36. package/dist/reconsolidation.d.ts +59 -30
  37. package/dist/reconsolidation.js +526 -296
  38. package/dist/rescore-importance.d.ts +80 -0
  39. package/dist/rescore-importance.js +236 -0
  40. package/dist/retrieval.d.ts +12 -0
  41. package/dist/retrieval.js +30 -1
  42. package/dist/stages.d.ts +37 -0
  43. package/dist/stages.js +51 -0
  44. package/dist/state.d.ts +32 -6
  45. package/dist/storage.d.ts +34 -2
  46. package/dist/storage.js +63 -6
  47. package/dist/types.d.ts +48 -0
  48. package/package.json +3 -1
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Planted ranking fixtures for the search-trust calibration gate (#425).
3
+ *
4
+ * Two fixture cases, both shaped like the field failure that opened the
5
+ * issue (the owner's live console test, 2026-09-13): a user searches a
6
+ * DISTINCTIVE PROPER NOUN they know exists; the one memory carrying that
7
+ * token has the best similarity of all candidates AND the literal FTS hit,
8
+ * yet ranks behind hardened but less-relevant memories that win on
9
+ * effective strength + graph connections.
10
+ *
11
+ * (a) sirnas_exact_match — the AC1 fixture: a low-strength, unlinked,
12
+ * never-accessed memory carrying an invented proper noun against
13
+ * base-≥0.9 rivals with links among themselves, access history, and
14
+ * older creation dates. The rivals share the query's DOMAIN
15
+ * vocabulary (harbours, docks, boats) but never the proper noun.
16
+ * Declared expectation: the proper-noun row ranks #1.
17
+ * (b) no_match_control — the AC2 fixture: a "mamma"-class query with no
18
+ * relevant match and NO FTS hits anywhere in the corpus. Whatever
19
+ * nearest-neighbor junk vector search returns must be byte-stable
20
+ * under the ranking work (no both-channel candidates exist, so the
21
+ * boost can never fire — the case pins that the no-regression
22
+ * guarantee is structural, not lucky).
23
+ *
24
+ * Honesty rules (the planted-pairs #393 A discipline):
25
+ * - The texts are synthetic and generic (this package publishes to npm —
26
+ * no real infrastructure, people, or fleet detail), but SHAPED like the
27
+ * field failure: the proper noun is invented (it must appear nowhere
28
+ * else — verified by an invariant test), the rivals are hardened with
29
+ * metadata the production pipeline itself writes (base strength, access
30
+ * hardening, link counts).
31
+ * - Similarities are MEASURED at run time with the real bge-small-en-v1.5
32
+ * embedder (ranking-eval.ts) — never asserted into existence. The
33
+ * declared expectation is about ORDER under the shipped ranking, and
34
+ * the report prints the measured similarity/score of every top row.
35
+ * - Strength metadata is written through the production helpers
36
+ * (insertMemory + updateMemory + addLink), not hand-rolled SQL, so the
37
+ * fixture exercises the same rows retrieval() will see.
38
+ */
39
+ export interface RankingRow {
40
+ /** Stable identifier used by expectations, links, and the report. */
41
+ key: string;
42
+ content: string;
43
+ /** ISO timestamp. */
44
+ createdAt: string;
45
+ /** knowledge | experience | decision. */
46
+ memoryType: string;
47
+ /** Birth base_strength — the value stageImportance would have written. */
48
+ baseStrength: number;
49
+ /** Access history (hardening + the decay clock). */
50
+ accessCount: number;
51
+ /** Days before the run's `now` of the last access (0 = touched today). */
52
+ lastAccessedDaysAgo: number;
53
+ /** Keys this row links to (links are created after all inserts). */
54
+ linksTo: string[];
55
+ }
56
+ export interface RankingFixture {
57
+ /** Case identifier (report + photo keys). */
58
+ key: string;
59
+ description: string;
60
+ rows: RankingRow[];
61
+ query: string;
62
+ /**
63
+ * The token that makes the query distinctive — invariant-tested to appear
64
+ * in exactly one row (the target) and in no rival/filler content.
65
+ */
66
+ distinctiveToken: string;
67
+ /** Declared expectation: this row's key must be returned at position 1. */
68
+ expectedTop1: string;
69
+ }
70
+ /** All planted rows share project + source_agent (metadata never refuses). */
71
+ export declare const RANKING_PROJECT = "ranking-eval";
72
+ export declare const RANKING_SOURCE_AGENT = "ranking-eval";
73
+ export declare const SIRNAS_FIXTURE: RankingFixture;
74
+ export declare const NO_MATCH_FIXTURE: RankingFixture;
75
+ /** The battery runs against a real snapshot copy (ranking-eval.ts case 3). */
76
+ export declare const REAL_SIRNAS_MEMORY_ID = "12980c9d-0eba-4963-95eb-0548bc2aa93e";
77
+ export declare function checkFixtureInvariants(f: RankingFixture): string[];
@@ -0,0 +1,226 @@
1
+ "use strict";
2
+ /**
3
+ * Planted ranking fixtures for the search-trust calibration gate (#425).
4
+ *
5
+ * Two fixture cases, both shaped like the field failure that opened the
6
+ * issue (the owner's live console test, 2026-09-13): a user searches a
7
+ * DISTINCTIVE PROPER NOUN they know exists; the one memory carrying that
8
+ * token has the best similarity of all candidates AND the literal FTS hit,
9
+ * yet ranks behind hardened but less-relevant memories that win on
10
+ * effective strength + graph connections.
11
+ *
12
+ * (a) sirnas_exact_match — the AC1 fixture: a low-strength, unlinked,
13
+ * never-accessed memory carrying an invented proper noun against
14
+ * base-≥0.9 rivals with links among themselves, access history, and
15
+ * older creation dates. The rivals share the query's DOMAIN
16
+ * vocabulary (harbours, docks, boats) but never the proper noun.
17
+ * Declared expectation: the proper-noun row ranks #1.
18
+ * (b) no_match_control — the AC2 fixture: a "mamma"-class query with no
19
+ * relevant match and NO FTS hits anywhere in the corpus. Whatever
20
+ * nearest-neighbor junk vector search returns must be byte-stable
21
+ * under the ranking work (no both-channel candidates exist, so the
22
+ * boost can never fire — the case pins that the no-regression
23
+ * guarantee is structural, not lucky).
24
+ *
25
+ * Honesty rules (the planted-pairs #393 A discipline):
26
+ * - The texts are synthetic and generic (this package publishes to npm —
27
+ * no real infrastructure, people, or fleet detail), but SHAPED like the
28
+ * field failure: the proper noun is invented (it must appear nowhere
29
+ * else — verified by an invariant test), the rivals are hardened with
30
+ * metadata the production pipeline itself writes (base strength, access
31
+ * hardening, link counts).
32
+ * - Similarities are MEASURED at run time with the real bge-small-en-v1.5
33
+ * embedder (ranking-eval.ts) — never asserted into existence. The
34
+ * declared expectation is about ORDER under the shipped ranking, and
35
+ * the report prints the measured similarity/score of every top row.
36
+ * - Strength metadata is written through the production helpers
37
+ * (insertMemory + updateMemory + addLink), not hand-rolled SQL, so the
38
+ * fixture exercises the same rows retrieval() will see.
39
+ */
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.REAL_SIRNAS_MEMORY_ID = exports.NO_MATCH_FIXTURE = exports.SIRNAS_FIXTURE = exports.RANKING_SOURCE_AGENT = exports.RANKING_PROJECT = void 0;
42
+ exports.checkFixtureInvariants = checkFixtureInvariants;
43
+ /** All planted rows share project + source_agent (metadata never refuses). */
44
+ exports.RANKING_PROJECT = "ranking-eval";
45
+ exports.RANKING_SOURCE_AGENT = "ranking-eval";
46
+ // ---------------------------------------------------------------------------
47
+ // Case 1 — sirnas_exact_match (AC1)
48
+ // ---------------------------------------------------------------------------
49
+ //
50
+ // The target mirrors the field case's SHAPE with invented names: a home
51
+ // harbour memory carrying one distinctive proper noun, born weak (base 0.35,
52
+ // the "routine-but-curated" band), never accessed, unlinked, a few months
53
+ // old. The rivals are what the decay model produces for early-hardened
54
+ // technical rows: base 0.90-0.95, mutual links, access history, created
55
+ // BEFORE the target. They share the harbour/boating domain vocabulary — so
56
+ // a bare proper-noun query lands in their neighborhood — but none carries
57
+ // the proper noun.
58
+ const SIRNAS_ROWS = [
59
+ // --- rivals: hardened, connected, older, domain-adjacent ---
60
+ {
61
+ key: "rival_fees",
62
+ content: "Harbour fees and guest dock rules: pay at the machine by the fuel quay, electricity on the guest dock is metered, " +
63
+ "and visiting boats must clear their berth by ten in the morning during the summer season.",
64
+ createdAt: "2026-03-02T09:00:00.000Z",
65
+ memoryType: "knowledge",
66
+ baseStrength: 0.9,
67
+ accessCount: 4,
68
+ lastAccessedDaysAgo: 6,
69
+ linksTo: ["rival_channel", "rival_maintenance"],
70
+ },
71
+ {
72
+ key: "rival_channel",
73
+ content: "Sailing notes for the home bay: the channel is marked with cardinal buoys, the shallow spit near the outer dock " +
74
+ "dries at low water, and the ferry has right of way inside the harbour basin.",
75
+ createdAt: "2026-03-16T09:00:00.000Z",
76
+ memoryType: "knowledge",
77
+ baseStrength: 0.95,
78
+ accessCount: 6,
79
+ lastAccessedDaysAgo: 2,
80
+ linksTo: ["rival_fees", "rival_maintenance"],
81
+ },
82
+ {
83
+ key: "rival_maintenance",
84
+ content: "Boat maintenance log: scrub the hull and check the anodes before launch, service the winch every spring, and " +
85
+ "book the boatyard crane early — the lifting slot list fills up in May.",
86
+ createdAt: "2026-03-30T09:00:00.000Z",
87
+ memoryType: "experience",
88
+ baseStrength: 0.9,
89
+ accessCount: 3,
90
+ lastAccessedDaysAgo: 11,
91
+ linksTo: ["rival_fees"],
92
+ },
93
+ {
94
+ key: "rival_passage",
95
+ content: "Passage plan habits: file the float plan before leaving the dock, plot the tidal gate times, and always take the " +
96
+ "safe water mark side of the reef when the light is obscured.",
97
+ createdAt: "2026-04-13T09:00:00.000Z",
98
+ memoryType: "knowledge",
99
+ baseStrength: 0.9,
100
+ accessCount: 2,
101
+ lastAccessedDaysAgo: 20,
102
+ linksTo: ["rival_channel"],
103
+ },
104
+ // --- the target: the exact proper-noun match, born humble ---
105
+ {
106
+ key: "target_harbour",
107
+ content: "Home harbour: Vindarsvik (near Stensundet). The guest berth is east of the ferry pier; hold slack when the " +
108
+ "fishing boats unload.",
109
+ createdAt: "2026-05-25T09:00:00.000Z",
110
+ memoryType: "knowledge",
111
+ baseStrength: 0.35,
112
+ accessCount: 0,
113
+ lastAccessedDaysAgo: 118,
114
+ linksTo: [],
115
+ },
116
+ // --- neutral filler: keeps neighborhoods non-trivial, off-domain ---
117
+ {
118
+ key: "fill_recipe",
119
+ content: "Batch-cooked a tomato and butter sauce from the Rome cookbook: San Marzano tomatoes, one whole onion halved, and far more butter than seems reasonable.",
120
+ createdAt: "2026-04-01T09:00:00.000Z",
121
+ memoryType: "experience",
122
+ baseStrength: 0.5,
123
+ accessCount: 0,
124
+ lastAccessedDaysAgo: 160,
125
+ linksTo: [],
126
+ },
127
+ {
128
+ key: "fill_books",
129
+ content: "Finished the third novel in the frontier trilogy. The middle book dragged through the mining-town chapters, but the finale pays off when the surveyor reads her grandmother's letters.",
130
+ createdAt: "2026-04-20T09:00:00.000Z",
131
+ memoryType: "experience",
132
+ baseStrength: 0.5,
133
+ accessCount: 0,
134
+ lastAccessedDaysAgo: 140,
135
+ linksTo: [],
136
+ },
137
+ {
138
+ key: "fill_lang",
139
+ content: "Language study notes: the dative prepositions finally clicked after drilling them as a sung list; irregular verbs still need spaced repetition.",
140
+ createdAt: "2026-05-10T09:00:00.000Z",
141
+ memoryType: "experience",
142
+ baseStrength: 0.5,
143
+ accessCount: 0,
144
+ lastAccessedDaysAgo: 120,
145
+ linksTo: [],
146
+ },
147
+ {
148
+ key: "fill_garden",
149
+ content: "The raised beds need refreshing before spring: compost the spent tomato vines, rotate the legume row, and prune the apple espalier before the buds swell.",
150
+ createdAt: "2026-06-02T09:00:00.000Z",
151
+ memoryType: "experience",
152
+ baseStrength: 0.5,
153
+ accessCount: 0,
154
+ lastAccessedDaysAgo: 100,
155
+ linksTo: [],
156
+ },
157
+ ];
158
+ exports.SIRNAS_FIXTURE = {
159
+ key: "sirnas_exact_match",
160
+ description: "AC1: a distinctive proper-noun query must rank the exact-match (both-channel) memory first, " +
161
+ "ahead of hardened domain-adjacent rivals that win on strength + connections.",
162
+ rows: SIRNAS_ROWS,
163
+ query: "Vindarsvik",
164
+ distinctiveToken: "vindarsvik",
165
+ expectedTop1: "target_harbour",
166
+ };
167
+ // ---------------------------------------------------------------------------
168
+ // Case 2 — no_match_control (AC2)
169
+ // ---------------------------------------------------------------------------
170
+ //
171
+ // A "mamma"-class query: a common household word in the user's language that
172
+ // appears in NO corpus row — no FTS token, no semantic neighbor. Vector
173
+ // search still returns nearest-neighbor junk (the field's 0.53-similarity
174
+ // technical rows); the case pins the FULL returned list so any drift in
175
+ // how unrelated junk is ordered is caught, and asserts structurally that
176
+ // the ranking work cannot touch it (no both-channel candidates exist).
177
+ exports.NO_MATCH_FIXTURE = {
178
+ key: "no_match_control",
179
+ description: "AC2: a no-match query (no FTS hits anywhere) returns a byte-stable list — no both-channel " +
180
+ "candidates exist, so the boost is structurally inert.",
181
+ // Same corpus as case 1 (minus nothing): the query matches none of it.
182
+ rows: SIRNAS_ROWS,
183
+ query: "mamma",
184
+ distinctiveToken: "",
185
+ // No declared top-1 — the assertion is stability, not order (the photo
186
+ // records the list; the gate compares runs).
187
+ expectedTop1: "",
188
+ };
189
+ /** The battery runs against a real snapshot copy (ranking-eval.ts case 3). */
190
+ exports.REAL_SIRNAS_MEMORY_ID = "12980c9d-0eba-4963-95eb-0548bc2aa93e";
191
+ // ---------------------------------------------------------------------------
192
+ // Fixture invariants (checked by ranking-eval at run time AND pinned in
193
+ // tests/ranking-eval-tooling.test.ts — a corpus that silently drifts from
194
+ // its declared shape would fake a pass/fail)
195
+ // ---------------------------------------------------------------------------
196
+ function checkFixtureInvariants(f) {
197
+ const problems = [];
198
+ const keys = new Set(f.rows.map((r) => r.key));
199
+ if (keys.size !== f.rows.length)
200
+ problems.push("duplicate row keys");
201
+ for (const r of f.rows) {
202
+ for (const to of r.linksTo) {
203
+ if (!keys.has(to))
204
+ problems.push(`${r.key} links to unknown key ${to}`);
205
+ if (to === r.key)
206
+ problems.push(`${r.key} links to itself`);
207
+ }
208
+ }
209
+ if (f.expectedTop1 && !keys.has(f.expectedTop1)) {
210
+ problems.push(`expectedTop1 ${f.expectedTop1} is not a row key`);
211
+ }
212
+ if (f.distinctiveToken) {
213
+ const token = f.distinctiveToken.toLowerCase();
214
+ const carriers = f.rows.filter((r) => r.content.toLowerCase().includes(token));
215
+ if (carriers.length !== 1) {
216
+ problems.push(`distinctive token "${f.distinctiveToken}" carried by ${carriers.length} rows (expected exactly 1)`);
217
+ }
218
+ else if (f.expectedTop1 && carriers[0].key !== f.expectedTop1) {
219
+ problems.push(`distinctive token carried by ${carriers[0].key}, not expectedTop1 ${f.expectedTop1}`);
220
+ }
221
+ if (!f.query.toLowerCase().includes(token)) {
222
+ problems.push(`query does not contain the distinctive token`);
223
+ }
224
+ }
225
+ return problems;
226
+ }
@@ -351,3 +351,24 @@ export declare function serveIdentityBody(handlerResult: HandlerResult, memoryEn
351
351
  * symlink safety). Throws are left to the adapter to turn into a 500.
352
352
  */
353
353
  export declare function handleIdentityPut(identityDir: string, body: unknown, query?: Record<string, unknown>, identityAgents?: Record<string, AgentMode>): HandlerResult;
354
+ /**
355
+ * PUT /identity/mode (#423 phase 3): switch ONE agent's scope to
356
+ * override/global/off. Pure validation + merge — the ADAPTER (mcp-server.ts)
357
+ * does three things with the result: (1) persists `agents` as the config's
358
+ * `identityAgents` (survives restarts), (2) sets the daemon's live boot-time
359
+ * map to the same merged map (single-writer: the map changes at boot or
360
+ * here — never diverges), so the switch is live on the very next
361
+ * GET /identity?agent= (applies: "immediate"), and (3) responds with this
362
+ * body. Externally hand-edited config still needs a restart (pre-existing
363
+ * posture — only this endpoint updates the live map).
364
+ *
365
+ * Interplay with PUT /identity: its black-hole guard 409s section writes
366
+ * while config forces off/global for the agent (sections written under
367
+ * agents/<id>/ would never be served); switching to 'override' here FIRST
368
+ * unblocks section editing. `dir_present` tells the caller whether an agent
369
+ * section dir already exists ('real' — agent content to serve) or not
370
+ * (override on a fresh dir = global content until sections are written).
371
+ */
372
+ export declare function handleIdentityModePut(identityDir: string, body: unknown, query: Record<string, unknown>, currentAgents: Record<string, AgentMode>): HandlerResult & {
373
+ agents?: Record<string, AgentMode>;
374
+ };
@@ -56,6 +56,7 @@ exports.readResolvedSections = readResolvedSections;
56
56
  exports.handleIdentityGet = handleIdentityGet;
57
57
  exports.serveIdentityBody = serveIdentityBody;
58
58
  exports.handleIdentityPut = handleIdentityPut;
59
+ exports.handleIdentityModePut = handleIdentityModePut;
59
60
  const node_fs_1 = require("node:fs");
60
61
  const node_path_1 = require("node:path");
61
62
  const node_crypto_1 = require("node:crypto");
@@ -875,3 +876,51 @@ function handleIdentityPut(identityDir, body, query = {}, identityAgents = {}) {
875
876
  }
876
877
  return { status: 200, body: respBody, warn };
877
878
  }
879
+ /**
880
+ * PUT /identity/mode (#423 phase 3): switch ONE agent's scope to
881
+ * override/global/off. Pure validation + merge — the ADAPTER (mcp-server.ts)
882
+ * does three things with the result: (1) persists `agents` as the config's
883
+ * `identityAgents` (survives restarts), (2) sets the daemon's live boot-time
884
+ * map to the same merged map (single-writer: the map changes at boot or
885
+ * here — never diverges), so the switch is live on the very next
886
+ * GET /identity?agent= (applies: "immediate"), and (3) responds with this
887
+ * body. Externally hand-edited config still needs a restart (pre-existing
888
+ * posture — only this endpoint updates the live map).
889
+ *
890
+ * Interplay with PUT /identity: its black-hole guard 409s section writes
891
+ * while config forces off/global for the agent (sections written under
892
+ * agents/<id>/ would never be served); switching to 'override' here FIRST
893
+ * unblocks section editing. `dir_present` tells the caller whether an agent
894
+ * section dir already exists ('real' — agent content to serve) or not
895
+ * (override on a fresh dir = global content until sections are written).
896
+ */
897
+ function handleIdentityModePut(identityDir, body, query, currentAgents) {
898
+ const { agentId, error } = extractAgentParam(query);
899
+ if (error)
900
+ return { status: 400, body: { error } };
901
+ if (agentId === null) {
902
+ return {
903
+ status: 400,
904
+ body: { error: "The ?agent= query parameter is required (PUT /identity/mode sets ONE agent's scope)" },
905
+ };
906
+ }
907
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
908
+ return { status: 400, body: { error: "Body must be a JSON object {mode}" } };
909
+ }
910
+ const { mode } = body;
911
+ if (mode !== "override" && mode !== "global" && mode !== "off") {
912
+ return { status: 400, body: { error: "Invalid 'mode' — expected one of \"override\", \"global\", \"off\"" } };
913
+ }
914
+ return {
915
+ status: 200,
916
+ body: {
917
+ agent: agentId,
918
+ mode,
919
+ dir_present: agentDirState(identityDir, agentId) === "real",
920
+ applies: "immediate",
921
+ },
922
+ // The merged map the adapter persists AND applies to the live boot-time
923
+ // map — other agents' entries are preserved untouched.
924
+ agents: { ...currentAgents, [agentId]: mode },
925
+ };
926
+ }
package/dist/init.d.ts CHANGED
@@ -94,6 +94,20 @@ export declare function loadConfigStrict(configPath: string): {
94
94
  config: Record<string, unknown>;
95
95
  hadFile: boolean;
96
96
  };
97
+ /**
98
+ * Apply a sparse set of config updates atomically-ish (strict load → merge →
99
+ * save): the HTTP-side sibling of persistLlmConfig (which drives the
100
+ * interactive init flow). `null` values DELETE the key — the console's
101
+ * "clear field" semantic. Values are assumed VALIDATED by the caller (the
102
+ * /dashboard/model PUT validates its allowlisted subset BEFORE calling); this
103
+ * function only owns load-strictly + merge + save, so a malformed existing
104
+ * config throws here and the file is left UNTOUCHED (the 0.16.x contract —
105
+ * see loadConfigStrict). Returns the freshly persisted config so the caller
106
+ * can answer with the post-write truth (re-read, not echo).
107
+ *
108
+ * Exported for dashboard.ts's PUT handler + tests.
109
+ */
110
+ export declare function persistConfigUpdates(configPath: string, updates: Record<string, unknown>): Record<string, unknown>;
97
111
  /**
98
112
  * `init --repair-config` escape hatch: move a malformed config.json aside so
99
113
  * init can rebuild, instead of dead-ending on loadConfigStrict's throw.
package/dist/init.js CHANGED
@@ -30,6 +30,7 @@ exports.parseEnvFile = parseEnvFile;
30
30
  exports.isLlmConfigured = isLlmConfigured;
31
31
  exports.persistLlmConfig = persistLlmConfig;
32
32
  exports.loadConfigStrict = loadConfigStrict;
33
+ exports.persistConfigUpdates = persistConfigUpdates;
33
34
  exports.quarantineMalformedConfig = quarantineMalformedConfig;
34
35
  exports.generateAuthToken = generateAuthToken;
35
36
  exports.persistAuthToken = persistAuthToken;
@@ -65,6 +66,7 @@ const node_crypto_1 = require("node:crypto");
65
66
  const claude_md_js_1 = require("./claude-md.js");
66
67
  const claude_desktop_js_1 = require("./claude-desktop.js");
67
68
  const config_read_js_1 = require("./config-read.js");
69
+ const calibration_js_1 = require("./calibration.js");
68
70
  const run_deadline_js_1 = require("./run-deadline.js");
69
71
  const identity_store_js_1 = require("./identity-store.js");
70
72
  const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
@@ -1052,6 +1054,30 @@ function loadConfigStrict(configPath) {
1052
1054
  }
1053
1055
  return { config: parsed, hadFile: true };
1054
1056
  }
1057
+ /**
1058
+ * Apply a sparse set of config updates atomically-ish (strict load → merge →
1059
+ * save): the HTTP-side sibling of persistLlmConfig (which drives the
1060
+ * interactive init flow). `null` values DELETE the key — the console's
1061
+ * "clear field" semantic. Values are assumed VALIDATED by the caller (the
1062
+ * /dashboard/model PUT validates its allowlisted subset BEFORE calling); this
1063
+ * function only owns load-strictly + merge + save, so a malformed existing
1064
+ * config throws here and the file is left UNTOUCHED (the 0.16.x contract —
1065
+ * see loadConfigStrict). Returns the freshly persisted config so the caller
1066
+ * can answer with the post-write truth (re-read, not echo).
1067
+ *
1068
+ * Exported for dashboard.ts's PUT handler + tests.
1069
+ */
1070
+ function persistConfigUpdates(configPath, updates) {
1071
+ const { config } = loadConfigStrict(configPath);
1072
+ for (const [k, v] of Object.entries(updates)) {
1073
+ if (v === null)
1074
+ delete config[k];
1075
+ else
1076
+ config[k] = v;
1077
+ }
1078
+ saveConfig(configPath, config);
1079
+ return config;
1080
+ }
1055
1081
  /**
1056
1082
  * `init --repair-config` escape hatch: move a malformed config.json aside so
1057
1083
  * init can rebuild, instead of dead-ending on loadConfigStrict's throw.
@@ -1943,6 +1969,9 @@ async function runInit(options = {}) {
1943
1969
  // the install → first-nightly → retained funnel is measurable. Never blocks:
1944
1970
  // failures are swallowed inside sendLifecycleEvent.
1945
1971
  await (0, telemetry_js_1.sendLifecycleEvent)("install", HICORTEX_HOME, readHomeConfig(HICORTEX_HOME), pkgVersion());
1972
+ // #436: the first-run lookback cap is invisible until it bites — tell the
1973
+ // long-history user where the rest went and how to get it, once, at install.
1974
+ console.log(`History import: the first nightly captures the last ${calibration_js_1.DEFAULT_FIRST_RUN_LOOKBACK_DAYS} days of your agents' sessions by default — run \`hicortex nightly --recapture-window <days>\` once to import more.`);
1946
1975
  console.log("Next steps:");
1947
1976
  // Counter-based so the list stays contiguous (1,2,3,4) whether or not Hermes
1948
1977
  // was detected — a conditional middle step used to leave a "1, 3, 4" gap.
@@ -2128,6 +2157,9 @@ async function runClientInit(serverUrl, agentName) {
2128
2157
  // the install → first-nightly → retained funnel is measurable. Never blocks:
2129
2158
  // failures are swallowed inside sendLifecycleEvent.
2130
2159
  await (0, telemetry_js_1.sendLifecycleEvent)("install", HICORTEX_HOME, readHomeConfig(HICORTEX_HOME), pkgVersion());
2160
+ // #436: the first-run lookback cap is invisible until it bites — tell the
2161
+ // long-history user where the rest went and how to get it, once, at install.
2162
+ console.log(`History import: the first nightly captures the last ${calibration_js_1.DEFAULT_FIRST_RUN_LOOKBACK_DAYS} days of your agents' sessions on this machine by default — run \`hicortex nightly --recapture-window <days>\` once to import more.`);
2131
2163
  console.log("How it works:");
2132
2164
  console.log(" • MCP tools (search, identity, ingest) talk to the remote server");
2133
2165
  console.log(" • Nightly pipeline denoises CC transcripts, POSTs to server for distillation");
@@ -42,6 +42,18 @@ export declare function createMcpServer(): McpServer;
42
42
  * at each step; invalid/absent falls through.
43
43
  */
44
44
  export declare function resolveBodyLimitMb(configVal: unknown, hostedMode: boolean): number;
45
+ /**
46
+ * Resolve the OPTIONAL /search relevance floor (`minSimilarity` query param,
47
+ * #409 console polish). Pure — exported for tests. Absent/blank/invalid →
48
+ * undefined = NO gate (byte-identical to every pre-existing caller: agents,
49
+ * plugins, MCP tools never send the param). A finite number in [0, 1] → that
50
+ * floor, clamped into range so a hostile `?minSimilarity=42` cannot widen or
51
+ * invert the gate. Applied AFTER retrieve() with the exported recall gate
52
+ * (passesRelevanceGate: FTS/`both` hits pass regardless — a token match is
53
+ * real evidence; vector-only hits must clear the floor) — the same post-hoc
54
+ * shape /recall-index uses, so the two recall surfaces gate identically.
55
+ */
56
+ export declare function resolveSearchSimilarityFloor(raw: unknown): number | undefined;
45
57
  /**
46
58
  * Express error middleware (#7): translate express.json's default HTML 413
47
59
  * (entity.too.large) into a consistent JSON response. Catches body-parser