@gamaze/hicortex 0.16.7 → 0.16.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -199,9 +199,12 @@ Config at `~/.hicortex/config.json`. Created by `init`. Key options:
199
199
  |-------|-------------|
200
200
  | `mode` | `"server"` (default) or `"client"` |
201
201
  | `serverUrl` | Remote server URL (client mode) |
202
- | `llmModel` | The one model the whole pipeline uses (server mode). One model is the happy path; route individual stages with `models` — see [Advanced: per-stage models](#advanced-per-stage-models) |
203
- | `models` | Optional nested per-stage model overrides (`score`/`distill`/`reflect`/`classify`) see [Advanced: per-stage models](#advanced-per-stage-models) |
204
- | `distillFallback` | `"strict"` (default) abort on remote distill failure, retry next run; `"local"` fall back to base model (lower quality, 0.9.0 behaviour) |
202
+ | `llmModel` | The one model used by all phases (distill, score, classify, reflect). Set via `init`. |
203
+ | `numCtx` | Context window for ollama (default 8192, one value for all phases). Scoring uses ~850 tokens, so 2048 is ample; distill/reflect/classify need more for `detectChunkSize`'s chunk sizing. |
204
+ | `enableThinking` | Toggle the model's internal reasoning ("thinking") stream for OpenAI-compatible endpoints (default false). Only meaningful for local chat-template-aware servers (ollama, mlx-lm); leave unset for cloud OpenAI/OpenRouter/Groq endpoints (they 400 on the unknown `chat_template_kwargs` field). |
205
+ | `maxTokens` | Max output tokens for all phases (default 8192). A ceiling, not a target — the model stops early when done. |
206
+ | `ollamaFlushEvery` | Flush ollama's accumulated memory every N scoring calls (default 0 = off). Workaround for ollama's memory-growth bug (~171 MB/call). N=15 caps a cycle at ~2.5 GB. Scoped to the fast tier (scoring) only. |
207
+ | `ollamaFlushWaitMs` | Milliseconds to wait after an ollama flush for the runner to exit + release memory (default 180000 = 3 min). |
205
208
  | `authToken` | Bearer token for endpoint auth. Generated on first `init` in server mode. Find the active token with `hicortex status` or in `~/.hicortex/config.json`. |
206
209
  | `corsAllowedOrigins` | Browser origins allowed to read cross-origin responses, e.g. `["https://ui.example.com"]`. **Empty by default** — the server sends no `Access-Control-Allow-Origin` and never `Allow-Credentials`, so no external web page can read its data. The bundled `/viz` and `/context/ui` pages are same-origin and need no entry. |
207
210
  | `licenseKey` | Commercial license key (optional; for display in `hicortex status`) |
@@ -295,30 +298,6 @@ LLM selection is **user-controlled**: `npx @gamaze/hicortex init` detects candid
295
298
 
296
299
  If no LLM is configured, the server starts in **recall-only mode**: search, lessons, and context work; `/distill` and consolidation are disabled. Run `npx @gamaze/hicortex init` to configure.
297
300
 
298
- ### Advanced: per-stage models
299
-
300
- The happy path is **one model** (`llmModel`) for the whole pipeline. If you want to route the four pipeline stages to different models or endpoints, add a nested `models` block to `~/.hicortex/config.json`:
301
-
302
- ```json
303
- {
304
- "llmBackend": "ollama",
305
- "llmModel": "qwen3.5:4b",
306
- "models": {
307
- "score": { "model": "qwen3.5:4b" },
308
- "distill": { "model": "qwen3.5:35b-a3b", "baseUrl": "http://gpu-box:11434" },
309
- "reflect": { "model": "qwen3.5:35b-a3b", "baseUrl": "http://gpu-box:11434" },
310
- "classify": { "model": "gemma4-31b", "baseUrl": "http://gpu-box:11434" }
311
- }
312
- }
313
- ```
314
-
315
- The four tiers: `score` (importance scoring — this **is** the base model), `distill` (session distillation, 9b+ recommended), `reflect` (nightly reflection, largest available), `classify` (memory-domain tagging). Each accepts `model`, `baseUrl`, `apiKey`, and `provider`. Omitting a tier inherits: `distill` and `reflect` fall back to the **base** (`score`) model; **`classify` falls back to the `reflect` tier** (not the base — `classify` delegates to the reflect path when unset).
316
-
317
- - **Flat keys still work.** `distillModel`/`distillBaseUrl`/`reflectModel`/`reflectBaseUrl`/`classifyModel`/`classifyBaseUrl` remain supported at **lower precedence** — a `models` entry wins over the flat key of the same name.
318
- - **A tier's `apiKey`/`provider` require the tier's own `baseUrl`.** They only take effect when the tier sets `baseUrl`; set on a tier without a `baseUrl` they are **ignored with a warning** (a bare `models.reflect: { model, apiKey }` would otherwise silently bill to the base key). Set `provider` when a tier's `baseUrl` points at a different provider type than the base (e.g. an OpenAI-compatible API while the base is Ollama); set `apiKey` for an API-provider tier over an Ollama base, whose base key is empty (`""`).
319
- - **`init` writes a flat `llmModel`.** The already-configured guard now recognizes a nested-only config (`models.score`), so re-running `init` on one is a no-op. But if a config has **both** a flat `llmModel` and a `models.score.model`, the nested value **shadows** the flat one (nested > flat) — keep the model in one place.
320
- - **`score.provider` (and `score.apiKey` on an Ollama base) are ignored** (a warning is logged): the base provider comes from `llmBackend` (or is auto-detected from the base endpoint), and the Ollama base path sends no api key.
321
-
322
301
  ## Database
323
302
 
324
303
  Canonical location: `~/.hicortex/hicortex.db`. The OC plugin no longer owns its own database — it is a thin client to the server. Previously, OC installations at `~/.openclaw/data/hicortex.db` were migrated automatically on upgrade; this migration path remains in the server's `resolveDbPath` for any pre-0.10.0 installations.
@@ -334,7 +313,7 @@ npm test
334
313
 
335
314
  ## Troubleshooting
336
315
 
337
- **`init` fails with "Refusing to write ~/.hicortex/config.json":** the file exists but is not valid JSON — usually a hand-edit slip (a trailing comma, a truncated write). `init` refuses rather than overwriting it, because overwriting would lose `authToken`, `licenseKey`, `distillApiKey`, and your `domains` list. Two ways out:
316
+ **`init` fails with "Refusing to write ~/.hicortex/config.json":** the file exists but is not valid JSON — usually a hand-edit slip (a trailing comma, a truncated write). `init` refuses rather than overwriting it, because overwriting would lose `authToken`, `licenseKey`, and your `domains` list. Two ways out:
338
317
 
339
318
  1. **Preferred — fix the JSON.** The error names the parse failure and its position. Correct it and re-run `init`. Nothing is lost.
340
319
  2. **`npx @gamaze/hicortex init --repair-config`.** Moves the broken file to `config.json.corrupt-<timestamp>` and rebuilds from scratch. Nothing is deleted, and it prints the top-level key names it found (names only — never secret values) so you know what to copy back. **This mints a new `authToken`**, so every thin client pointing at this server must be updated or its recall will silently 401 (recall is fail-soft — you will see no error, just no memories).
@@ -12,11 +12,10 @@
12
12
  * state.json after each batch. Interruption never loses more than the
13
13
  * current batch. `--reset` restarts from rowid 0.
14
14
  * - Classification: one constrained MULTI-TAG LLM call per memory via the
15
- * CLASSIFY tier (classifyBaseUrl/classifyModel when configured, else the
16
- * reflect tier), same as the nightly. The LLM emits ONLY the ordered tag
17
- * set; per-tag weights come from the domain prototypes (computed once at
18
- * run start) and the PRIMARY (memories.domain) is derived (argmax weight,
19
- * LLM order breaking ties) inside
15
+ * one model (#231 same as the nightly's reflect/classify/scoring). The
16
+ * LLM emits ONLY the ordered tag set; per-tag weights come from the domain
17
+ * prototypes (computed once at run start) and the PRIMARY (memories.domain)
18
+ * is derived (argmax weight, LLM order breaking ties) inside
20
19
  * storage.setMemoryTags. After a completed (non-aborted) run the
21
20
  * prototypes, all weights, and all primaries are recomputed from the
22
21
  * final tag sets — same reconsolidation pass as the nightly.
@@ -25,11 +24,8 @@
25
24
  * a WEAK primary (argmax prototype cosine, when >= weakPrimaryFloor) or,
26
25
  * below the floor, accelerated decay (base_strength halved, domain left
27
26
  * NULL so later runs re-attempt it). See nofit.ts.
28
- * - LLM pre-flight: the endpoint classification will actually use (when a
29
- * separate Ollama) is probed before any work. If unreachable, abort
30
- * cleanly (nothing written, cursor untouched) — strict, like distill.
31
27
  * - Infra-error abort (issue #150): if the classifier returns null mid-run
32
- * (endpoint died AFTER preflight), the run aborts after committing
28
+ * (endpoint died mid-run), the run aborts after committing
33
29
  * the last full batch. The failing memory is left completely untouched; the
34
30
  * cursor sits at the last committed batch so a re-run resumes cleanly.
35
31
  * - Server-mode only: needs the local DB.
@@ -50,7 +46,7 @@ export interface ClassifyDomainsOptions {
50
46
  dbPath?: string;
51
47
  /** State dir override (tests). Defaults to ~/.hicortex. */
52
48
  stateDir?: string;
53
- /** LLM override (tests). Bypasses config resolution + preflight. */
49
+ /** LLM override (tests). Bypasses config resolution. */
54
50
  llm?: LlmClient;
55
51
  /** Config override (tests). Defaults to reading stateDir/config.json. */
56
52
  config?: Record<string, unknown> | null;
@@ -91,8 +87,11 @@ export interface ClassifyDomainsReport {
91
87
  }
92
88
  /**
93
89
  * Run the classify-domains pass. Returns a structured report.
94
- * Throws on unrecoverable setup errors (client mode, no domains, no LLM,
95
- * classification endpoint down) — the cursor always reflects the last
96
- * committed batch.
90
+ * Throws on unrecoverable setup errors (client mode, no domains, no LLM) —
91
+ * the cursor always reflects the last committed batch. Per-memory
92
+ * classification failures are handled gracefully inside the classifier
93
+ * (returns null → memory left unclassified, retried next run via the cursor);
94
+ * a totally-down model costs latency, not data (#231 removed the pre-flight
95
+ * probe in favor of complete()'s internal 30s/60s/120s retries).
97
96
  */
98
97
  export declare function runClassifyDomains(options?: ClassifyDomainsOptions): Promise<ClassifyDomainsReport>;
@@ -13,11 +13,10 @@
13
13
  * state.json after each batch. Interruption never loses more than the
14
14
  * current batch. `--reset` restarts from rowid 0.
15
15
  * - Classification: one constrained MULTI-TAG LLM call per memory via the
16
- * CLASSIFY tier (classifyBaseUrl/classifyModel when configured, else the
17
- * reflect tier), same as the nightly. The LLM emits ONLY the ordered tag
18
- * set; per-tag weights come from the domain prototypes (computed once at
19
- * run start) and the PRIMARY (memories.domain) is derived (argmax weight,
20
- * LLM order breaking ties) inside
16
+ * one model (#231 same as the nightly's reflect/classify/scoring). The
17
+ * LLM emits ONLY the ordered tag set; per-tag weights come from the domain
18
+ * prototypes (computed once at run start) and the PRIMARY (memories.domain)
19
+ * is derived (argmax weight, LLM order breaking ties) inside
21
20
  * storage.setMemoryTags. After a completed (non-aborted) run the
22
21
  * prototypes, all weights, and all primaries are recomputed from the
23
22
  * final tag sets — same reconsolidation pass as the nightly.
@@ -26,11 +25,8 @@
26
25
  * a WEAK primary (argmax prototype cosine, when >= weakPrimaryFloor) or,
27
26
  * below the floor, accelerated decay (base_strength halved, domain left
28
27
  * NULL so later runs re-attempt it). See nofit.ts.
29
- * - LLM pre-flight: the endpoint classification will actually use (when a
30
- * separate Ollama) is probed before any work. If unreachable, abort
31
- * cleanly (nothing written, cursor untouched) — strict, like distill.
32
28
  * - Infra-error abort (issue #150): if the classifier returns null mid-run
33
- * (endpoint died AFTER preflight), the run aborts after committing
29
+ * (endpoint died mid-run), the run aborts after committing
34
30
  * the last full batch. The failing memory is left completely untouched; the
35
31
  * cursor sits at the last committed batch so a re-run resumes cleanly.
36
32
  * - Server-mode only: needs the local DB.
@@ -93,33 +89,14 @@ function readConfig(stateDir) {
93
89
  return null;
94
90
  }
95
91
  }
96
- /**
97
- * Pre-flight the endpoint classification will ACTUALLY use — the classify
98
- * tier (classifyModel/classifyBaseUrl) when configured, else the reflect tier.
99
- * Target resolution is the pure resolveClassifyProbeTarget (llm.ts), the same
100
- * source of truth as the nightly's contentDomainsReady gate.
101
- *
102
- * Returns null when ready, or a reason string when it is unreachable (caller
103
- * aborts clean). Only a separate Ollama endpoint can go unreachable mid-run;
104
- * API providers are cloud-reachable.
105
- */
106
- async function preflightClassify(config) {
107
- const target = (0, llm_js_1.resolveClassifyProbeTarget)(config);
108
- if (!target)
109
- return null;
110
- const health = await (0, llm_js_1.probeOllamaModel)(target.baseUrl, target.model);
111
- if (!health.ok) {
112
- return health.reason === "unreachable"
113
- ? `${target.tier} endpoint unreachable (${target.baseUrl})`
114
- : `${target.tier} model not loaded (${target.model} missing on ${target.baseUrl})`;
115
- }
116
- return null;
117
- }
118
92
  /**
119
93
  * Run the classify-domains pass. Returns a structured report.
120
- * Throws on unrecoverable setup errors (client mode, no domains, no LLM,
121
- * classification endpoint down) — the cursor always reflects the last
122
- * committed batch.
94
+ * Throws on unrecoverable setup errors (client mode, no domains, no LLM) —
95
+ * the cursor always reflects the last committed batch. Per-memory
96
+ * classification failures are handled gracefully inside the classifier
97
+ * (returns null → memory left unclassified, retried next run via the cursor);
98
+ * a totally-down model costs latency, not data (#231 removed the pre-flight
99
+ * probe in favor of complete()'s internal 30s/60s/120s retries).
123
100
  */
124
101
  async function runClassifyDomains(options = {}) {
125
102
  const batchSize = options.batchSize ?? 200;
@@ -142,7 +119,7 @@ async function runClassifyDomains(options = {}) {
142
119
  "No fallback bucket is needed — no-fit memories are handled automatically.");
143
120
  }
144
121
  const weakPrimaryFloor = (0, nofit_js_1.resolveWeakPrimaryFloor)(config);
145
- // Resolve the LLM (classify tier does the classifying; falls back to reflect).
122
+ // Resolve the LLM (one model serves all phases #231).
146
123
  let llm;
147
124
  if (options.llm) {
148
125
  llm = options.llm;
@@ -152,11 +129,6 @@ async function runClassifyDomains(options = {}) {
152
129
  if (!resolved.config) {
153
130
  throw new Error("[hicortex] classify-domains: no LLM configured — run `npx @gamaze/hicortex init`.");
154
131
  }
155
- const classifyDown = await preflightClassify(resolved.config);
156
- if (classifyDown) {
157
- throw new Error(`[hicortex] classify-domains aborted: ${classifyDown}. ` +
158
- "Nothing written, cursor untouched — retry when the endpoint is up.");
159
- }
160
132
  llm = new llm_js_1.LlmClient(resolved.config);
161
133
  }
162
134
  const dbPath = (0, db_js_1.resolveDbPath)(options.dbPath);
@@ -198,11 +198,11 @@ export declare function stageDecayPrune(db: Database.Database, dryRun: boolean):
198
198
  * Options controlling how the domain-assignment stage runs.
199
199
  *
200
200
  * When `domains` is a non-empty list, the pipeline uses content-based
201
- * classification (config-owned) INSTEAD of project grouping provided the
202
- * classification endpoint (classify tier when configured, else reflect) passed
203
- * pre-flight (`contentDomainsReady`). If that endpoint is unreachable, the
204
- * caller sets `contentDomainsReady: false` and the stage is SKIPPED entirely
205
- * (strict no fall-back to a weak model or to project grouping). When
201
+ * classification (config-owned) INSTEAD of project grouping. The single
202
+ * model serves all phases; if it's unavailable, `complete()` retries
203
+ * internally (30s/60s/120s) and the phase fails soft on persistence —
204
+ * the nightly retries on the next run. No pre-flight health checks; the
205
+ * phase either answers or is skipped until the next scheduled run. When
206
206
  * `domains` is absent/empty, the legacy project-grouping curation runs
207
207
  * unchanged.
208
208
  */
@@ -862,7 +862,7 @@ const SUPERSESSION_BATCH_SIZE = 500;
862
862
  * A memory whose content/type marks it as a SUPERSEDABLE claim — one a newer
863
863
  * memory about the same subject can replace. Decisions and corrections were the
864
864
  * original scope; plain facts and project-state updates were added because an
865
- * updated fact ("distillModel is X" → later "is Y") otherwise never gets a
865
+ * updated fact ("scoring model is X" → later "is Y") otherwise never gets a
866
866
  * superseded_by link and both versions compete in recall forever. Ordinary
867
867
  * episodic chatter and problem/solution history stay excluded: they record
868
868
  * events, not mutable state, so there is nothing to supersede.
@@ -1148,9 +1148,8 @@ async function runConsolidation(db, llm, embedFn, dryRun = false, skipReflection
1148
1148
  }
1149
1149
  // Stage 2.7: Domain assignment.
1150
1150
  // Content-based (config-owned domains) REPLACES project grouping when a
1151
- // domain list is configured AND the reflect endpoint passed pre-flight.
1152
- // If the list is configured but the reflect endpoint is down, SKIP the
1153
- // stage (strict) — do not fall back to project grouping.
1151
+ // domain list is configured. The single model serves all phases; if it's
1152
+ // down, the phase skips and retries on the next nightly run (no fallback).
1154
1153
  const cfgDomains = domainOptions?.domains;
1155
1154
  if (cfgDomains && cfgDomains.length > 0) {
1156
1155
  if (domainOptions?.contentDomainsReady === false) {
@@ -8,12 +8,20 @@ import { type RedactionConfig } from "./redact.js";
8
8
  /**
9
9
  * Estimate a safe chunk size in chars based on the LLM provider and model.
10
10
  * - API providers (Anthropic, OpenAI, claude-cli): no chunking needed (large context windows)
11
- * - Ollama: query /api/show for context_length AND parameter_count, cap based on both
12
- * - Small models (<8B params): max 20K chars (~5K tokens) — keeps CPU inference under ~60s
13
- * - Larger models: up to 60K chars (~15K tokens)
11
+ * - Ollama: chunk size derives from the resolved `numCtx` (the request's actual
12
+ * context window), capped by parameter-count-derived speed limits:
13
+ * - Small models (<8B params): max 20K chars (~5K tokens) — keeps CPU inference under ~60s
14
+ * - Larger models: up to 60K chars (~15K tokens)
14
15
  * - Fallback: 20K chars
16
+ *
17
+ * `numCtx` is the single source of truth for the context constraint (#231): it is
18
+ * the value completeOllama will actually send as `num_ctx`, so chunking against it
19
+ * keeps the chunker and the request in agreement (kills the silent-truncation bug
20
+ * #228, where chunks were sized from the model's ADVERTISED context while the
21
+ * request used a smaller `numCtx`). The `/api/show` query is KEPT — but only for
22
+ * the parameter-count speed cap (`maxBySpeed`, SMALL vs LARGE), NOT for context.
15
23
  */
16
- export declare function detectChunkSize(provider: string, model: string, baseUrl?: string): Promise<number>;
24
+ export declare function detectChunkSize(provider: string, model: string, baseUrl?: string, numCtx?: number): Promise<number>;
17
25
  /**
18
26
  * Convert session messages to a filtered transcript string.
19
27
  * Handles OC hook format, CC JSONL, and Pi JSONL.
package/dist/distiller.js CHANGED
@@ -21,17 +21,25 @@ const LARGE_MODEL_MAX_CHUNK_CHARS = 60_000; // ~15K tokens — ok for 8B+ on GPU
21
21
  /**
22
22
  * Estimate a safe chunk size in chars based on the LLM provider and model.
23
23
  * - API providers (Anthropic, OpenAI, claude-cli): no chunking needed (large context windows)
24
- * - Ollama: query /api/show for context_length AND parameter_count, cap based on both
25
- * - Small models (<8B params): max 20K chars (~5K tokens) — keeps CPU inference under ~60s
26
- * - Larger models: up to 60K chars (~15K tokens)
24
+ * - Ollama: chunk size derives from the resolved `numCtx` (the request's actual
25
+ * context window), capped by parameter-count-derived speed limits:
26
+ * - Small models (<8B params): max 20K chars (~5K tokens) — keeps CPU inference under ~60s
27
+ * - Larger models: up to 60K chars (~15K tokens)
27
28
  * - Fallback: 20K chars
29
+ *
30
+ * `numCtx` is the single source of truth for the context constraint (#231): it is
31
+ * the value completeOllama will actually send as `num_ctx`, so chunking against it
32
+ * keeps the chunker and the request in agreement (kills the silent-truncation bug
33
+ * #228, where chunks were sized from the model's ADVERTISED context while the
34
+ * request used a smaller `numCtx`). The `/api/show` query is KEPT — but only for
35
+ * the parameter-count speed cap (`maxBySpeed`, SMALL vs LARGE), NOT for context.
28
36
  */
29
- async function detectChunkSize(provider, model, baseUrl) {
37
+ async function detectChunkSize(provider, model, baseUrl, numCtx) {
30
38
  // API-based providers handle large contexts natively — no chunking needed
31
39
  if (provider !== "ollama") {
32
40
  return MAX_TRANSCRIPT_CHARS;
33
41
  }
34
- // Query Ollama for model metadata
42
+ // Query Ollama for model metadata (parameter count → speed cap)
35
43
  if (baseUrl) {
36
44
  try {
37
45
  const resp = await fetch(`${baseUrl}/api/show`, {
@@ -49,21 +57,18 @@ async function detectChunkSize(provider, model, baseUrl) {
49
57
  ? info[paramKey]
50
58
  : 0;
51
59
  const isSmallModel = paramCount > 0 && paramCount < SMALL_MODEL_PARAMS;
52
- // Extract context length for context-aware capping
53
- const ctxKey = Object.keys(info).find((k) => k.endsWith("context_length") || k.endsWith("context_window"));
54
- const contextTokens = ctxKey && typeof info[ctxKey] === "number"
55
- ? info[ctxKey]
56
- : 0;
57
60
  // Determine max chunk size based on model size (speed constraint)
58
61
  // Unknown param count defaults to conservative (small model) — safe for any hardware
59
62
  const maxBySpeed = !isSmallModel && paramCount > 0 ? LARGE_MODEL_MAX_CHUNK_CHARS : SMALL_MODEL_MAX_CHUNK_CHARS;
60
- // Determine max chunk size based on context window (fits-in-context constraint)
61
- const maxByContext = contextTokens > 0
62
- ? Math.floor(contextTokens * 0.6 * 4) // 60% of context, ~4 chars/token
63
- : MAX_TRANSCRIPT_CHARS;
63
+ // Determine max chunk size from the resolved numCtx (the request's ACTUAL
64
+ // context window), NOT the model's advertised context_length. 60% of context,
65
+ // ~4 chars/token. Falls back to MAX_TRANSCRIPT_CHARS when numCtx is unknown
66
+ // (caller didn't pass it) — but the mcp-server call site always passes it.
67
+ const resolvedCtx = numCtx ?? 8192;
68
+ const maxByContext = Math.floor(resolvedCtx * 0.6 * 4);
64
69
  const chunkChars = Math.min(maxBySpeed, maxByContext);
65
70
  console.log(`[hicortex] Model: ${paramCount > 0 ? `${(paramCount / 1e9).toFixed(1)}B params` : "unknown size"}, ` +
66
- `context: ${contextTokens > 0 ? `${contextTokens} tokens` : "unknown"}, ` +
71
+ `numCtx: ${resolvedCtx}, ` +
67
72
  `chunk size: ${chunkChars} chars${isSmallModel ? " (small model cap)" : ""}`);
68
73
  return chunkChars;
69
74
  }
@@ -33,9 +33,9 @@
33
33
  * project only breaks ties). This rescues terse technical memories from
34
34
  * projects whose content alone reads as ambiguous.
35
35
  *
36
- * The classifier makes ONE constrained LLM call per memory (via the classify
37
- * tierclassifyModel/classifyBaseUrl when configured, else the reflect
38
- * tier), validates every returned name against the configured vocabulary
36
+ * The classifier makes ONE constrained LLM call per memory (via the one model
37
+ * #231completeClassify, a thin wrapper over the shared complete()),
38
+ * validates every returned name against the configured vocabulary
39
39
  * (case-insensitive), and retries once on an invalid/unparseable reply.
40
40
  *
41
41
  * ROBUSTNESS (folds in issue #150):
@@ -141,10 +141,10 @@ export declare function parseTagReply(reply: string, domains: DomainDef[]): TagR
141
141
  /**
142
142
  * Multi-tag classify one memory's content against the configured vocabulary.
143
143
  *
144
- * Uses the classify tier (`completeClassify` classifyBaseUrl/classifyModel
145
- * when configured, else the reflect tier) the caller is responsible for
146
- * having pre-flighted that endpoint via resolveClassifyProbeTarget (strict:
147
- * skip classification entirely if it is unreachable).
144
+ * Uses the one model (`completeClassify` a thin wrapper over the shared
145
+ * complete(), #231). Per-memory classification failures return null (issue
146
+ * #150): the caller leaves the memory unclassified and the cursor advances,
147
+ * so a re-run retries it.
148
148
  *
149
149
  * Behaviour:
150
150
  * - Valid JSON reply with ≥1 vocabulary tag → {tags} (ordered,
@@ -34,9 +34,9 @@
34
34
  * project only breaks ties). This rescues terse technical memories from
35
35
  * projects whose content alone reads as ambiguous.
36
36
  *
37
- * The classifier makes ONE constrained LLM call per memory (via the classify
38
- * tierclassifyModel/classifyBaseUrl when configured, else the reflect
39
- * tier), validates every returned name against the configured vocabulary
37
+ * The classifier makes ONE constrained LLM call per memory (via the one model
38
+ * #231completeClassify, a thin wrapper over the shared complete()),
39
+ * validates every returned name against the configured vocabulary
40
40
  * (case-insensitive), and retries once on an invalid/unparseable reply.
41
41
  *
42
42
  * ROBUSTNESS (folds in issue #150):
@@ -238,10 +238,10 @@ function parseTagReply(reply, domains) {
238
238
  /**
239
239
  * Multi-tag classify one memory's content against the configured vocabulary.
240
240
  *
241
- * Uses the classify tier (`completeClassify` classifyBaseUrl/classifyModel
242
- * when configured, else the reflect tier) the caller is responsible for
243
- * having pre-flighted that endpoint via resolveClassifyProbeTarget (strict:
244
- * skip classification entirely if it is unreachable).
241
+ * Uses the one model (`completeClassify` a thin wrapper over the shared
242
+ * complete(), #231). Per-memory classification failures return null (issue
243
+ * #150): the caller leaves the memory unclassified and the cursor advances,
244
+ * so a re-run retries it.
245
245
  *
246
246
  * Behaviour:
247
247
  * - Valid JSON reply with ≥1 vocabulary tag → {tags} (ordered,
package/dist/init.d.ts CHANGED
@@ -31,10 +31,9 @@ export declare function parseMcpListStatus(mcpListOutput: string): "connected" |
31
31
  export declare function parseEnvFile(content: string): Record<string, string>;
32
32
  /**
33
33
  * True when an LLM is already persisted and `init` must NOT re-run provider
34
- * selection: a named/flat backend, a flat baseUrl+apiKey pair, OR a nested-only
35
- * `models.score` (model or baseUrl). The last clause (0.13.1) stops init from
36
- * walking a nested-only config back through selection and writing flat keys that
37
- * a `models.score` would then silently shadow (nested > flat).
34
+ * selection: a named backend (llmBackend) or a flat baseUrl+apiKey pair.
35
+ * (The nested `models` per-tier block was removed in #231 one model serves
36
+ * all phases, configured via the flat `llm*` keys only.)
38
37
  */
39
38
  export declare function isLlmConfigured(config: Record<string, unknown>): boolean;
40
39
  /**
@@ -58,7 +57,7 @@ export declare function persistLlmConfig(configPath?: string): Promise<void>;
58
57
  * and the writer then OVERWROTE the file — `persistAuthToken` minted a fresh
59
58
  * token (fleet-wide 401), `scaffoldDefaultDomains` re-seeded the generic
60
59
  * vocabulary over the owner list, etc. `authToken` / `licenseKey` /
61
- * `distillApiKey` / `domains` / `weakPrimaryFloor` / `contextClients` all gone.
60
+ * `llmApiKey` / `domains` / `weakPrimaryFloor` / `contextClients` all gone.
62
61
  * The early-return guards (existing-key checks) did NOT save them: those only
63
62
  * fire on a VALID parse that reads the key, not on a corrupted file.
64
63
  *
@@ -89,7 +88,7 @@ export declare function loadConfigStrict(configPath: string): {
89
88
  * Why this exists: refusing to overwrite a corrupt config is right (it closed
90
89
  * the 0.16.x wipe BLOCKER), but it leaves the operator stuck — `init` is the
91
90
  * natural repair action and it now refuses to run. Deleting the file by hand
92
- * works but silently loses `licenseKey` / `authToken` / `distillApiKey`.
91
+ * works but silently loses `licenseKey` / `authToken` / `llmApiKey`.
93
92
  *
94
93
  * Why it is OPT-IN and never automatic: rebuilding mints a fresh `authToken`,
95
94
  * which 401s every thin client on the fleet until they are re-pointed. That is
@@ -101,10 +100,10 @@ export declare function loadConfigStrict(configPath: string): {
101
100
  * Windows), and report the TOP-LEVEL KEY NAMES recovered from the raw text
102
101
  * so the operator knows what to restore.
103
102
  *
104
- * SECURITY: key NAMES only, never values. `authToken`, `licenseKey`,
105
- * `distillApiKey` and `reflectApiKey` are secrets — printing them would leak
106
- * into terminal scrollback, CI logs, and screen shares. The operator reads the
107
- * values out of the backup file themselves.
103
+ * SECURITY: key NAMES only, never values. `authToken`, `licenseKey`, and
104
+ * `llmApiKey` are secrets — printing them would leak into terminal scrollback,
105
+ * CI logs, and screen shares. The operator reads the values out of the backup
106
+ * file themselves.
108
107
  *
109
108
  * Exported for testability.
110
109
  */
package/dist/init.js CHANGED
@@ -475,15 +475,12 @@ function mergeByKey(candidates) {
475
475
  }
476
476
  /**
477
477
  * True when an LLM is already persisted and `init` must NOT re-run provider
478
- * selection: a named/flat backend, a flat baseUrl+apiKey pair, OR a nested-only
479
- * `models.score` (model or baseUrl). The last clause (0.13.1) stops init from
480
- * walking a nested-only config back through selection and writing flat keys that
481
- * a `models.score` would then silently shadow (nested > flat).
478
+ * selection: a named backend (llmBackend) or a flat baseUrl+apiKey pair.
479
+ * (The nested `models` per-tier block was removed in #231 one model serves
480
+ * all phases, configured via the flat `llm*` keys only.)
482
481
  */
483
482
  function isLlmConfigured(config) {
484
- const modelsScore = config.models?.score;
485
- const hasModelsScore = Boolean(modelsScore?.model || modelsScore?.baseUrl);
486
- return Boolean(config.llmBackend || (config.llmApiKey && config.llmBaseUrl) || hasModelsScore);
483
+ return Boolean(config.llmBackend || (config.llmApiKey && config.llmBaseUrl));
487
484
  }
488
485
  /**
489
486
  * Detect or ask for LLM config and persist to ~/.hicortex/config.json.
@@ -725,7 +722,7 @@ function saveConfig(configPath, config) {
725
722
  * and the writer then OVERWROTE the file — `persistAuthToken` minted a fresh
726
723
  * token (fleet-wide 401), `scaffoldDefaultDomains` re-seeded the generic
727
724
  * vocabulary over the owner list, etc. `authToken` / `licenseKey` /
728
- * `distillApiKey` / `domains` / `weakPrimaryFloor` / `contextClients` all gone.
725
+ * `llmApiKey` / `domains` / `weakPrimaryFloor` / `contextClients` all gone.
729
726
  * The early-return guards (existing-key checks) did NOT save them: those only
730
727
  * fire on a VALID parse that reads the key, not on a corrupted file.
731
728
  *
@@ -787,7 +784,7 @@ function loadConfigStrict(configPath) {
787
784
  * Why this exists: refusing to overwrite a corrupt config is right (it closed
788
785
  * the 0.16.x wipe BLOCKER), but it leaves the operator stuck — `init` is the
789
786
  * natural repair action and it now refuses to run. Deleting the file by hand
790
- * works but silently loses `licenseKey` / `authToken` / `distillApiKey`.
787
+ * works but silently loses `licenseKey` / `authToken` / `llmApiKey`.
791
788
  *
792
789
  * Why it is OPT-IN and never automatic: rebuilding mints a fresh `authToken`,
793
790
  * which 401s every thin client on the fleet until they are re-pointed. That is
@@ -799,10 +796,10 @@ function loadConfigStrict(configPath) {
799
796
  * Windows), and report the TOP-LEVEL KEY NAMES recovered from the raw text
800
797
  * so the operator knows what to restore.
801
798
  *
802
- * SECURITY: key NAMES only, never values. `authToken`, `licenseKey`,
803
- * `distillApiKey` and `reflectApiKey` are secrets — printing them would leak
804
- * into terminal scrollback, CI logs, and screen shares. The operator reads the
805
- * values out of the backup file themselves.
799
+ * SECURITY: key NAMES only, never values. `authToken`, `licenseKey`, and
800
+ * `llmApiKey` are secrets — printing them would leak into terminal scrollback,
801
+ * CI logs, and screen shares. The operator reads the values out of the backup
802
+ * file themselves.
806
803
  *
807
804
  * Exported for testability.
808
805
  */
@@ -828,7 +825,7 @@ function quarantineMalformedConfig(configPath) {
828
825
  if (keys.length > 0) {
829
826
  console.log(` Keys found in the old file: ${keys.join(", ")}`);
830
827
  }
831
- console.log(` ACTION REQUIRED: copy any of licenseKey / distillApiKey / reflectApiKey /`);
828
+ console.log(` ACTION REQUIRED: copy any of licenseKey / llmApiKey /`);
832
829
  console.log(` domains / weakPrimaryFloor back from the backup by hand.`);
833
830
  console.log(` A NEW authToken will be generated — every thin client pointing at this`);
834
831
  console.log(` server must be updated, or their recall will 401 (silently, fail-soft).`);