@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 +7 -28
- package/dist/classify-domains.d.ts +12 -13
- package/dist/classify-domains.js +12 -40
- package/dist/consolidate.d.ts +5 -5
- package/dist/consolidate.js +3 -4
- package/dist/distiller.d.ts +12 -4
- package/dist/distiller.js +20 -15
- package/dist/domain-classify.d.ts +7 -7
- package/dist/domain-classify.js +7 -7
- package/dist/init.d.ts +9 -10
- package/dist/init.js +11 -14
- package/dist/llm.d.ts +35 -147
- package/dist/llm.js +63 -379
- package/dist/mcp-server.js +12 -52
- package/dist/nightly-status.js +1 -2
- package/dist/nightly.js +9 -48
- package/dist/types.d.ts +23 -57
- package/package.json +1 -1
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
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
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`,
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
96
|
-
*
|
|
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>;
|
package/dist/classify-domains.js
CHANGED
|
@@ -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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
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
|
-
*
|
|
122
|
-
*
|
|
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 (
|
|
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);
|
package/dist/consolidate.d.ts
CHANGED
|
@@ -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
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
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
|
*/
|
package/dist/consolidate.js
CHANGED
|
@@ -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 ("
|
|
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
|
|
1152
|
-
//
|
|
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) {
|
package/dist/distiller.d.ts
CHANGED
|
@@ -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:
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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:
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
`
|
|
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
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* The classifier makes ONE constrained LLM call per memory (via the one model
|
|
37
|
+
* #231 — completeClassify, 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
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
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,
|
package/dist/domain-classify.js
CHANGED
|
@@ -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
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* The classifier makes ONE constrained LLM call per memory (via the one model
|
|
38
|
+
* #231 — completeClassify, 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
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
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
|
|
35
|
-
* `models
|
|
36
|
-
*
|
|
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
|
-
* `
|
|
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` / `
|
|
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
|
-
* `
|
|
106
|
-
*
|
|
107
|
-
*
|
|
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
|
|
479
|
-
* `models
|
|
480
|
-
*
|
|
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
|
-
|
|
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
|
-
* `
|
|
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` / `
|
|
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
|
-
* `
|
|
804
|
-
*
|
|
805
|
-
*
|
|
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 /
|
|
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).`);
|