@gamaze/hicortex 0.16.6 → 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.
@@ -76,8 +76,8 @@ const init_js_1 = require("./init.js");
76
76
  // ---------------------------------------------------------------------------
77
77
  let db = null;
78
78
  let llm = null;
79
- // llmConfig is module-level so the /distill handler can call resolveDistillFallback
80
- // without having to read config on every request. null when no LLM is configured.
79
+ // llmConfig is module-level so the /distill handler can read numCtx without
80
+ // re-resolving on every request. null when no LLM is configured.
81
81
  let llmConfig = null;
82
82
  // One-time-per-process deprecation warning for the `?privacy=` query param
83
83
  // (0.16.x: the column is vestigial, never filtered). Old clients/plugins still
@@ -94,9 +94,6 @@ function warnDeprecatedPrivacyParamIfPresent(query, route) {
94
94
  `privacy is no longer filtered server-side (the column is vestigial). ` +
95
95
  `Use a separate Hicortex server for isolation. (This warning fires once per process.)`);
96
96
  }
97
- // distillFallbackMode controls whether a failed remote distill endpoint causes an
98
- // immediate abort ("strict", default) or a fallback to the base model ("local").
99
- let distillFallbackMode = "strict";
100
97
  let stateDir = "";
101
98
  // Resolved contextClients list (spec §2) — the harness names allowed to inject
102
99
  // the standing context layer. Echoed by GET /context so each hook self-gates.
@@ -374,7 +371,8 @@ async function startServer(options = {}) {
374
371
  // Named backends (claude-cli, ollama) → immediate config; everything else
375
372
  // goes through resolveExplicitLlmConfig which requires a user-chosen provider.
376
373
  // If nothing is configured: start recall-only with an unmissable warning.
377
- const savedConfig = (0, llm_js_1.applyModelsBlock)(readConfigFile(stateDir));
374
+ // One model serves all phases (#231) — no per-tier overlay here.
375
+ const savedConfig = readConfigFile(stateDir);
378
376
  // 0.16.2 activation gap: self-heal the agentId provenance field for
379
377
  // pre-0.16.2 server installs on first boot after upgrade. The server's own
380
378
  // nightly captures its sessions to localhost:8787/distill and needs this id;
@@ -402,7 +400,6 @@ async function startServer(options = {}) {
402
400
  baseUrl: savedConfig.llmBaseUrl ?? "http://localhost:11434",
403
401
  apiKey: "",
404
402
  model: savedConfig.llmModel ?? "qwen3.5:4b",
405
- reflectModel: savedConfig.reflectModel ?? savedConfig.llmModel ?? "qwen3.5:4b",
406
403
  provider: "ollama",
407
404
  };
408
405
  }
@@ -411,41 +408,15 @@ async function startServer(options = {}) {
411
408
  llmBaseUrl: savedConfig?.llmBaseUrl,
412
409
  llmApiKey: savedConfig?.llmApiKey,
413
410
  llmModel: savedConfig?.llmModel,
414
- reflectModel: savedConfig?.reflectModel,
415
411
  });
416
412
  }
417
413
  if (llmConfig) {
418
- // Apply optional distill endpoint (e.g. remote Ollama with faster model)
419
- if (savedConfig?.distillModel) {
420
- llmConfig.distillModel = savedConfig.distillModel;
421
- }
422
- if (savedConfig?.distillBaseUrl) {
423
- llmConfig.distillBaseUrl = savedConfig.distillBaseUrl;
424
- llmConfig.distillApiKey = savedConfig.distillApiKey ?? llmConfig.apiKey;
425
- llmConfig.distillProvider = savedConfig.distillProvider ?? llmConfig.provider;
426
- }
427
- // Heavy-phase tuning (#220): maxTokens + enableThinking, validated + copied
428
- // via the shared overlay (also applied in resolveSavedLlmConfig for the
429
- // nightly's reflect + classify). Wrong-typed values warn + drop.
414
+ // Tuning (#220: maxTokens + enableThinking + numCtx + flush), validated +
415
+ // copied via the shared overlay (also applied in resolveSavedLlmConfig for
416
+ // the nightly). Wrong-typed values warn + drop.
430
417
  (0, llm_js_1.applyTierTuningOverlay)(llmConfig, savedConfig);
431
- // Apply separate reflect endpoint if configured (e.g. remote Ollama with larger model)
432
- if (savedConfig?.reflectBaseUrl) {
433
- llmConfig.reflectBaseUrl = savedConfig.reflectBaseUrl;
434
- llmConfig.reflectApiKey = savedConfig.reflectApiKey ?? llmConfig.apiKey;
435
- llmConfig.reflectProvider = savedConfig.reflectProvider ?? llmConfig.provider;
436
- }
437
- // distillFallback: "strict" (default) aborts on remote failure so the session
438
- // is retried next run. "local" restores 0.9.0 fallback-to-base-model behavior.
439
- const df = savedConfig?.distillFallback;
440
- distillFallbackMode = df === "local" ? "local" : "strict";
441
418
  llm = new llm_js_1.LlmClient(llmConfig);
442
- const distillInfo = llmConfig.distillBaseUrl
443
- ? `${llmConfig.distillProvider}/${llmConfig.distillModel}@${llmConfig.distillBaseUrl}`
444
- : llmConfig.distillModel ? llmConfig.distillModel : "";
445
- const reflectInfo = llmConfig.reflectBaseUrl
446
- ? `${llmConfig.reflectProvider}/${llmConfig.reflectModel}@${llmConfig.reflectBaseUrl}`
447
- : llmConfig.reflectModel;
448
- console.log(`[hicortex] LLM fast: ${llmConfig.provider}/${llmConfig.model}${distillInfo ? `, distill: ${distillInfo}` : ""}, reflect: ${reflectInfo}`);
419
+ console.log(`[hicortex] LLM (one model, all phases): ${llmConfig.provider}/${llmConfig.model}`);
449
420
  }
450
421
  else {
451
422
  llm = null;
@@ -884,23 +855,12 @@ async function startServer(options = {}) {
884
855
  return;
885
856
  }
886
857
  }
887
- // Pre-flight the distill endpoint. In strict mode (default) a failed remote
888
- // probe returns "abort" immediately without mutating llmConfig — the nightly
889
- // watermark stays put so the session is re-shipped next run. In "local" mode
890
- // the config is mutated to fall back to the base model.
891
- const cfg = llmConfig;
892
- const distillFallbackStatus = await (0, llm_js_1.resolveDistillFallback)(cfg, distillFallbackMode);
893
- if (distillFallbackStatus === "abort") {
894
- res.status(503).json({ error: "Distill endpoint unavailable — session will be retried next run" });
895
- return;
896
- }
897
858
  // Cache detectChunkSize per endpoint so we probe at most once per server boot.
898
- const effectiveProvider = cfg.distillProvider ?? cfg.provider;
899
- const effectiveModel = cfg.distillModel ?? cfg.model;
900
- const effectiveBaseUrl = cfg.distillBaseUrl ?? cfg.baseUrl;
901
- const cacheKey = `${effectiveProvider}/${effectiveModel}@${effectiveBaseUrl}`;
859
+ // numCtx is passed so chunk size derives from the request's ACTUAL context
860
+ // window (#231, #228) the chunker and the request agree by construction.
861
+ const cacheKey = `${llmConfig.provider}/${llmConfig.model}@${llmConfig.baseUrl}`;
902
862
  if (!chunkSizeCache.has(cacheKey)) {
903
- chunkSizeCache.set(cacheKey, await (0, distiller_js_1.detectChunkSize)(effectiveProvider, effectiveModel, effectiveBaseUrl));
863
+ chunkSizeCache.set(cacheKey, await (0, distiller_js_1.detectChunkSize)(llmConfig.provider, llmConfig.model, llmConfig.baseUrl, llmConfig.numCtx));
904
864
  }
905
865
  const chunkSize = chunkSizeCache.get(cacheKey);
906
866
  const date = typeof session_date === "string" && session_date ? session_date : new Date().toISOString().slice(0, 10);
@@ -18,7 +18,6 @@ const node_os_1 = require("node:os");
18
18
  const node_child_process_1 = require("node:child_process");
19
19
  const db_js_1 = require("./db.js");
20
20
  const state_js_1 = require("./state.js");
21
- const llm_js_1 = require("./llm.js");
22
21
  const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
23
22
  const CONFIG_PATH = (0, node_path_1.join)(HICORTEX_HOME, "config.json");
24
23
  async function showNightlyStatus() {
@@ -39,7 +38,7 @@ async function showNightlyStatus() {
39
38
  try {
40
39
  // No `?? {}` coercion: a null/invalid parse must fall through to the catch
41
40
  // below (as it did pre-0.13.1) rather than print a fabricated-healthy status.
42
- const config = (0, llm_js_1.applyModelsBlock)(JSON.parse((0, node_fs_1.readFileSync)(CONFIG_PATH, "utf-8")));
41
+ const config = JSON.parse((0, node_fs_1.readFileSync)(CONFIG_PATH, "utf-8"));
43
42
  const backend = config.llmBackend ?? "auto-detect";
44
43
  const model = config.llmModel ?? "default";
45
44
  const mode = config.mode === "client" ? "client → " + (config.serverUrl ?? "?") : "server (local)";
package/dist/nightly.js CHANGED
@@ -371,57 +371,18 @@ async function runNightly(options = {}) {
371
371
  console.error("[hicortex] consolidation skipped: no LLM configured — run npx @gamaze/hicortex init");
372
372
  }
373
373
  else {
374
- // Pre-flight health check for the reflect endpoint.
375
- // If reflectBaseUrl points to a remote Ollama and it's down (MBP offline),
376
- // skip reflection entirely instead of waiting through 3 retries (~3.5 min).
377
- // Scoring + linking + decay still run.
378
- let skipReflection = false;
379
- if (llmConfig.reflectBaseUrl && (llmConfig.reflectProvider ?? llmConfig.provider) === "ollama") {
380
- const reflectModel = llmConfig.reflectModel ?? llmConfig.model;
381
- const health = await (0, llm_js_1.probeOllamaModel)(llmConfig.reflectBaseUrl, reflectModel);
382
- if (!health.ok) {
383
- const reason = health.reason === "unreachable"
384
- ? `reflect endpoint unreachable (${llmConfig.reflectBaseUrl})`
385
- : `reflect model not loaded (${reflectModel} missing on ${llmConfig.reflectBaseUrl})`;
386
- console.warn(`[hicortex] ${reason} — skipping reflection, scoring + linking will still run`);
387
- skipReflection = true;
388
- }
389
- }
390
- // Content-based domain classification (config-owned `domains`) uses
391
- // the classify tier (classifyModel/classifyBaseUrl) when configured,
392
- // else the reflect tier. Pre-flight the endpoint classification will
393
- // ACTUALLY use (resolveClassifyProbeTarget is the shared source of
394
- // truth with `hicortex classify-domains`). If it is down, content
395
- // classification is NOT ready this run (strict — skip, don't fall
396
- // back). When no `domains` list is configured, this is inert and the
397
- // legacy project-grouping path runs.
374
+ // One model serves all phases (#231) — there is no separate endpoint to
375
+ // pre-flight. If the model doesn't answer, `complete()` already retries at
376
+ // 30s/60s/120s (~3.5 min); anything still failing after that is an outage,
377
+ // not a blip. A failed phase costs latency, not data: capture cursors hold
378
+ // on failure (dup-over-loss), and consolidation has resumable cursors
379
+ // (domainCursor, supersessionCursor). The nightly runs 2-4×/day, so the
380
+ // wait is hours — no polling, no new config. (Issue #231.)
398
381
  const cfgDomains = (0, domain_classify_js_1.parseConfigDomains)(savedConfig);
399
- let contentDomainsReady = true;
400
- if (cfgDomains) {
401
- const classifyTarget = (0, llm_js_1.resolveClassifyProbeTarget)(llmConfig);
402
- if (classifyTarget?.tier === "reflect") {
403
- // Classification rides the reflect endpoint — reuse the probe above.
404
- contentDomainsReady = !skipReflection;
405
- }
406
- else if (classifyTarget) {
407
- const health = await (0, llm_js_1.probeOllamaModel)(classifyTarget.baseUrl, classifyTarget.model);
408
- if (!health.ok) {
409
- const reason = health.reason === "unreachable"
410
- ? `classify endpoint unreachable (${classifyTarget.baseUrl})`
411
- : `classify model not loaded (${classifyTarget.model} missing on ${classifyTarget.baseUrl})`;
412
- console.warn(`[hicortex] ${reason}`);
413
- contentDomainsReady = false;
414
- }
415
- }
416
- // classifyTarget === null → base endpoint or API provider, no probe.
417
- if (!contentDomainsReady) {
418
- console.warn("[hicortex] content-domain classification skipped — classification endpoint offline (strict)");
419
- }
420
- }
421
382
  console.log(`[hicortex] Running consolidation...`);
422
- const report = await (0, consolidate_js_1.runConsolidation)(db, llm, embedder_js_1.embed, dryRun, skipReflection, undefined, {
383
+ const report = await (0, consolidate_js_1.runConsolidation)(db, llm, embedder_js_1.embed, dryRun, false, undefined, {
423
384
  domains: cfgDomains,
424
- contentDomainsReady,
385
+ contentDomainsReady: true,
425
386
  weakPrimaryFloor: (0, nofit_js_1.resolveWeakPrimaryFloor)(savedConfig),
426
387
  }, {
427
388
  minSimilarity: savedConfig?.supersessionMinSimilarity,
package/dist/types.d.ts CHANGED
@@ -154,16 +154,6 @@ export interface ConsolidationReport {
154
154
  calls_by_stage: Record<string, number>;
155
155
  };
156
156
  }
157
- /**
158
- * A single per-stage override inside the nested `models` server-config block.
159
- * Consumed by `applyModelsBlock` (llm.ts), which re-exports this type.
160
- */
161
- export interface ModelTierOverride {
162
- model?: string;
163
- baseUrl?: string;
164
- apiKey?: string;
165
- provider?: string;
166
- }
167
157
  /** Plugin configuration from openclaw.plugin.json configSchema. */
168
158
  export interface HicortexConfig {
169
159
  licenseKey?: string;
@@ -186,33 +176,6 @@ export interface HicortexConfig {
186
176
  llmApiKey?: string;
187
177
  /** @deprecated Use the Hicortex server for distillation and consolidation. */
188
178
  llmModel?: string;
189
- /** @deprecated Use the Hicortex server for distillation and consolidation. */
190
- reflectModel?: string;
191
- /**
192
- * Optional dedicated model for memory tag classification (server-side
193
- * nightly + `hicortex classify-domains`). When unset, classification uses
194
- * the reflect tier exactly as before.
195
- */
196
- classifyModel?: string;
197
- /**
198
- * Optional dedicated endpoint for classification. When only classifyModel
199
- * is set, it runs on the reflect endpoint (else the base endpoint).
200
- */
201
- classifyBaseUrl?: string;
202
- /** Optional API key for the classify endpoint (defaults to the base apiKey). */
203
- classifyApiKey?: string;
204
- /** Optional provider for the classify endpoint (defaults to the base provider). */
205
- classifyProvider?: string;
206
- /**
207
- * Server config (NOT an OC-plugin key): nested per-stage model overrides.
208
- * `{ score|distill|reflect|classify: { model?, baseUrl?, apiKey?, provider? } }`.
209
- * Normalized onto the flat `llm*` / `distill*` / `reflect*` / `classify*`
210
- * keys at read time (see applyModelsBlock in llm.ts); nested wins, and the
211
- * flat keys remain supported at lower precedence. Happy path is a single model via
212
- * `llmModel`; use this block only for per-stage routing. `score.provider` is
213
- * ignored (base provider comes from llmBackend).
214
- */
215
- models?: Record<string, ModelTierOverride>;
216
179
  /** @deprecated Consolidation is owned by the server nightly. */
217
180
  consolidateHour?: number;
218
181
  /** @deprecated The OC plugin no longer opens its own database. */
@@ -283,44 +246,51 @@ export interface HicortexConfig {
283
246
  */
284
247
  preflightRetryGapMs?: number;
285
248
  /**
286
- * Max output tokens for the LLM phases — distillation, reflection, classification,
287
- * and scoring (completeDistill / completeReflect / completeClassify / completeFast).
288
- * Default 8192 for the heavy phases, 2048 for scoring (the local fast tier); an
289
- * explicit value overrides all of them. A ceiling, not a target: generation stops
290
- * at the model's natural end (finish_reason stop), so a higher cap costs no latency
249
+ * Max output tokens for the ONE LLM model used by all phases — distillation,
250
+ * reflection, classification, and scoring. Default 8192. An explicit value
251
+ * overrides the default. A ceiling, not a target: generation stops at the
252
+ * model's natural end (finish_reason stop), so a higher cap costs no latency
291
253
  * when it finishes early. Read in llm.ts; see #220.
292
254
  */
293
255
  maxTokens?: number;
294
256
  /**
295
257
  * Toggle the model's internal reasoning ("thinking") stream on the openai-compat
296
- * path — applies to the heavy phases (distill / reflect / classify) that share an
297
- * OpenAI-compatible endpoint (e.g. a reasoning model served via a local gateway).
298
- * Default false. A thinking model with thinking ON can burn the entire token
299
- * budget on an unclosed <think> block and emit nothing (probed 2026-08-04). When
300
- * set (true or false), completeOpenAiCompat sends
301
- * chat_template_kwargs:{enable_thinking}. Threaded structurally from each heavy
302
- * phase, so scoring (ollama) never sends it. No effect on the anthropic path.
303
- * See #220.
258
+ * path — applies to ALL phases (distill / reflect / classify / scoring) since one
259
+ * model serves all of them. Default false. A thinking model with thinking ON can
260
+ * burn the entire token budget on an unclosed <think> block and emit nothing
261
+ * (probed 2026-08-04). When set (true or false), completeOpenAiCompat sends
262
+ * chat_template_kwargs:{enable_thinking}. LOCAL-ENDPOINT ONLY: this is meaningful
263
+ * only for a chat-template-aware server (ollama, mlx-lm). If the one model is a
264
+ * cloud OpenAI-compatible endpoint (OpenAI / OpenRouter / Groq / z.ai), LEAVE THIS
265
+ * UNSET — the non-standard chat_template_kwargs field rides every call and can 400
266
+ * the whole pipeline (provider cannot distinguish MLX-gateway-as-openai from real
267
+ * cloud openai, so the gate must be operator-set, not detected). No effect on the
268
+ * anthropic or claude-cli paths. See #220, #231.
304
269
  */
305
270
  enableThinking?: boolean;
306
271
  /**
307
- * Context window for the ollama FAST tier (importance scoring via `completeFast`).
308
- * Default 2048. Scoring prompts are ~850 tokens, so 2048 fits with headroom. The
309
- * prior hardcoded 32768 still the default for the heavy distill/reflect/classify
310
- * tiers (which need it for `detectChunkSize`'s chunk-sizing) — made the KV-cache +
311
- * prompt-cache accumulate past available RAM on memory-constrained boxes during long
312
- * consolidations and swap-thrash the nightly. Applies to scoring only; raise if a
313
- * scoring prompt actually needs more.
272
+ * Context window for ollama (the one model, all phases). Default 8192 — the point
273
+ * where context stops being the binding constraint for a sub-8B model on ollama
274
+ * (above it the SMALL_MODEL_MAX_CHUNK_CHARS speed cap binds instead, so extra
275
+ * context buys nothing). Also drives `detectChunkSize`'s chunk sizing
276
+ * (chunkChars numCtx × 0.6 × 4 chars), so numCtx is the single dial and the
277
+ * chunker/request agreement is enforced by construction (#228). For an ≥8B model
278
+ * on ollama the speed cap is 60,000 chars, needing numCtx ≈ 25000 to reach — raise
279
+ * it if running 8B+ locally. No effect for non-ollama providers.
314
280
  */
315
281
  numCtx?: number;
316
282
  /**
317
- * Flush ollama's accumulated memory every N ollama calls — workaround for
283
+ * Flush ollama's accumulated memory every N scoring calls — workaround for
318
284
  * ollama's per-request memory growth (the runner's RSS climbs ~171 MB/call and
319
285
  * isn't freed between requests), which swap-thrashes RAM-constrained boxes
320
- * during long consolidations. Default 0 (off). When >0, every Nth ollama call
321
- * triggers a `keep_alive:0` unload + an `ollamaFlushWaitMs` pause for the
322
- * runner to exit + release, then the next call reloads fresh. N=15 caps a
323
- * cycle at ~2.5 GB. Opt-in set e.g. 15 on constrained boxes.
286
+ * during long consolidations. Default 0 (off). When >0, every Nth scoring call
287
+ * (`completeFast`) triggers a `keep_alive:0` unload + an `ollamaFlushWaitMs`
288
+ * pause for the runner to exit + release, then the next call reloads fresh.
289
+ * N=15 caps a cycle at ~2.5 GB. Scoped to the fast tier (scoring) only. Note:
290
+ * N counts **logical** scoring calls, not raw HTTP requests — `complete()`
291
+ * retries up to 4× on timeout, so under retry pressure the actual accumulation
292
+ * may be up to 4×N calls' worth. In practice the flush prevents the thrash that
293
+ * causes retries, keeping the count accurate.
324
294
  */
325
295
  ollamaFlushEvery?: number;
326
296
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamaze/hicortex",
3
- "version": "0.16.6",
3
+ "version": "0.16.8",
4
4
  "description": "Self-learning memory for AI agents — experience captured automatically, distilled into lessons overnight, shared across your whole fleet. Works with Hermes, OpenClaw, Claude Code, and Pi.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {