@gamaze/hicortex 0.16.4 → 0.16.6

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/dist/llm.d.ts CHANGED
@@ -29,6 +29,12 @@ export interface LlmConfig {
29
29
  maxTokens?: number;
30
30
  /** Toggle thinking on the openai-compat path for the heavy phases. Absent = no kwarg sent. */
31
31
  enableThinking?: boolean;
32
+ /** Context window for the ollama fast tier (completeOllama). Falls back to 2048. */
33
+ numCtx?: number;
34
+ /** Flush ollama memory every N ollama calls (0 = off). See HicortexConfig.ollamaFlushEvery. */
35
+ ollamaFlushEvery?: number;
36
+ /** Ms to wait after an ollama flush for the runner to release. */
37
+ ollamaFlushWaitMs?: number;
32
38
  /** Optional separate endpoint for reflect-tier LLM (e.g. remote Ollama with larger model). */
33
39
  reflectBaseUrl?: string;
34
40
  reflectApiKey?: string;
@@ -210,6 +216,7 @@ export declare class RateLimitError extends Error {
210
216
  }
211
217
  export declare class LlmClient {
212
218
  private config;
219
+ private ollamaCallCount;
213
220
  constructor(config: LlmConfig);
214
221
  /** Endpoint identity for shared rate-limit state (provider + base URL). */
215
222
  private get endpointKey();
@@ -259,6 +266,14 @@ export declare class LlmClient {
259
266
  * Ollama: use /api/generate with think:false (important for qwen3.5 models).
260
267
  */
261
268
  private completeOllama;
269
+ /**
270
+ * Flush ollama's accumulated memory: unload the model (keep_alive:0) so the
271
+ * runner exits + releases its per-request RSS growth, then wait for the release
272
+ * before the next call reloads fresh. The runner takes >90 s to exit after
273
+ * keep_alive:0 (measured), so the wait is generous (ollamaFlushWaitMs, default
274
+ * 180 s). Best-effort — a flush failure just means no release this cycle.
275
+ */
276
+ private flushOllama;
262
277
  /**
263
278
  * Anthropic Messages API (/v1/messages).
264
279
  * Auth via x-api-key header.
package/dist/llm.js CHANGED
@@ -208,6 +208,15 @@ function applyTierTuningOverlay(llmConfig, savedConfig) {
208
208
  if (thinking !== undefined) {
209
209
  llmConfig.enableThinking = thinking;
210
210
  }
211
+ if (savedConfig.numCtx !== undefined) {
212
+ llmConfig.numCtx = (0, config_read_js_1.readPositiveConfig)(savedConfig, "numCtx", 2048);
213
+ }
214
+ if (savedConfig.ollamaFlushEvery !== undefined) {
215
+ llmConfig.ollamaFlushEvery = (0, config_read_js_1.readPositiveConfig)(savedConfig, "ollamaFlushEvery", 0);
216
+ }
217
+ if (savedConfig.ollamaFlushWaitMs !== undefined) {
218
+ llmConfig.ollamaFlushWaitMs = (0, config_read_js_1.readPositiveConfig)(savedConfig, "ollamaFlushWaitMs", 180000);
219
+ }
211
220
  }
212
221
  /**
213
222
  * Resolve an LlmConfig from a saved ~/.hicortex/config.json object.
@@ -508,6 +517,7 @@ exports.RateLimitError = RateLimitError;
508
517
  const rateLimitedUntilByEndpoint = new Map();
509
518
  class LlmClient {
510
519
  config;
520
+ ollamaCallCount = 0;
511
521
  constructor(config) {
512
522
  this.config = config;
513
523
  }
@@ -539,10 +549,11 @@ class LlmClient {
539
549
  */
540
550
  async completeFast(prompt, maxTokens) {
541
551
  const tokens = maxTokens ?? this.config.maxTokens ?? 2048;
542
- // No `thinking` arg: scoring is excluded from the thinking toggle (it's the
543
- // local fast tier; on ollama it's think:false regardless). completeFast never
544
- // threads thinking, so chat_template_kwargs is never sent for scoring.
545
- return this.complete(this.config.model, prompt, tokens, 600_000);
552
+ // Scoring is the fast tier: numCtx (default 2048, ~850-token prompts) is threaded
553
+ // so ONLY this tier gets the smaller context window the heavy tiers keep 32768
554
+ // (preserves detectChunkSize's chunk-sizing). thinking is not threaded (scoring is
555
+ // excluded from the thinking toggle; ollama is think:false regardless).
556
+ return this.complete(this.config.model, prompt, tokens, 600_000, undefined, this.config.numCtx ?? 2048);
546
557
  }
547
558
  /**
548
559
  * Reflect-tier completion (nightly reflection, needs reasoning).
@@ -608,9 +619,14 @@ class LlmClient {
608
619
  apiKey,
609
620
  provider,
610
621
  });
622
+ // numCtx intentionally NOT forwarded here: only the heavy tiers route through
623
+ // completeWithOverride (there is no fastBaseUrl/fast-tier override), and they
624
+ // must land on completeOllama's 32768 default to preserve detectChunkSize. If a
625
+ // fast-tier override is ever added, thread numCtx here too — else the fast tier
626
+ // would silently revert to 32768 (the original bug, reintroduced).
611
627
  return tempClient.complete(model, prompt, maxTokens, timeoutMs, thinking);
612
628
  }
613
- async complete(model, prompt, maxTokens, timeoutMs, thinking) {
629
+ async complete(model, prompt, maxTokens, timeoutMs, thinking, numCtx) {
614
630
  if (this.isRateLimited) {
615
631
  throw new RateLimitError(this.rateLimitedUntil - Date.now());
616
632
  }
@@ -618,7 +634,7 @@ class LlmClient {
618
634
  let lastErr;
619
635
  for (let attempt = 0; attempt <= retryDelays.length; attempt++) {
620
636
  try {
621
- return await this.completeOnce(model, prompt, maxTokens, timeoutMs, thinking);
637
+ return await this.completeOnce(model, prompt, maxTokens, timeoutMs, thinking, numCtx);
622
638
  }
623
639
  catch (err) {
624
640
  lastErr = err instanceof Error ? err : new Error(String(err));
@@ -635,12 +651,12 @@ class LlmClient {
635
651
  }
636
652
  throw lastErr;
637
653
  }
638
- async completeOnce(model, prompt, maxTokens, timeoutMs, thinking) {
654
+ async completeOnce(model, prompt, maxTokens, timeoutMs, thinking, numCtx) {
639
655
  if (this.config.provider === "claude-cli") {
640
656
  return this.completeClaude(model, prompt, timeoutMs);
641
657
  }
642
658
  if (this.config.provider === "ollama") {
643
- return this.completeOllama(model, prompt, maxTokens, timeoutMs);
659
+ return this.completeOllama(model, prompt, maxTokens, timeoutMs, numCtx);
644
660
  }
645
661
  if (this.config.provider === "anthropic") {
646
662
  return this.completeAnthropic(model, prompt, maxTokens, timeoutMs);
@@ -676,7 +692,7 @@ class LlmClient {
676
692
  /**
677
693
  * Ollama: use /api/generate with think:false (important for qwen3.5 models).
678
694
  */
679
- async completeOllama(model, prompt, maxTokens, timeoutMs) {
695
+ async completeOllama(model, prompt, maxTokens, timeoutMs, numCtx) {
680
696
  const url = `${this.config.baseUrl.replace(/\/$/, "")}/api/generate`;
681
697
  // Ollama can take minutes to process large contexts — use streaming to avoid
682
698
  // Node.js fetch headers timeout (default ~300s kills long Ollama inferences)
@@ -688,7 +704,14 @@ class LlmClient {
688
704
  prompt,
689
705
  stream: true,
690
706
  think: false,
691
- options: { num_predict: maxTokens, num_ctx: 32768 },
707
+ // num_ctx: threaded from completeFast (the fast/scoring tier) via the numCtx
708
+ // param. The fast tier passes the numCtx config key (default 2048; scoring
709
+ // prompts are ~850 tokens) to minimize the KV/prompt-cache footprint that, at
710
+ // 32768, accumulated past available RAM on memory-constrained boxes during long
711
+ // consolidations and swap-thrashed the nightly. The heavy tiers
712
+ // (completeDistill/Reflect/Classify) do NOT pass numCtx → default 32768, which
713
+ // preserves detectChunkSize's chunk-sizing (it packs ~60% of context per chunk).
714
+ options: { num_predict: maxTokens, num_ctx: numCtx ?? 32768 },
692
715
  }),
693
716
  signal: AbortSignal.timeout(timeoutMs),
694
717
  });
@@ -723,8 +746,43 @@ class LlmClient {
723
746
  catch { /* skip malformed lines */ }
724
747
  }
725
748
  }
749
+ // Flush ollama's accumulated memory every N calls. ollama's runner RSS grows
750
+ // ~171 MB/call and isn't freed between requests — on RAM-constrained boxes
751
+ // this swap-thrashes long consolidations. After the Nth call, unload the
752
+ // model (keep_alive:0) + wait for the runner to exit + release, so the next
753
+ // call reloads fresh (low RSS) instead of accumulating to thrash. Opt-in via
754
+ // ollamaFlushEvery (0 = off).
755
+ const flushEvery = this.config.ollamaFlushEvery ?? 0;
756
+ if (flushEvery > 0) {
757
+ this.ollamaCallCount++;
758
+ if (this.ollamaCallCount >= flushEvery) {
759
+ await this.flushOllama(model);
760
+ this.ollamaCallCount = 0;
761
+ }
762
+ }
726
763
  return result.trim();
727
764
  }
765
+ /**
766
+ * Flush ollama's accumulated memory: unload the model (keep_alive:0) so the
767
+ * runner exits + releases its per-request RSS growth, then wait for the release
768
+ * before the next call reloads fresh. The runner takes >90 s to exit after
769
+ * keep_alive:0 (measured), so the wait is generous (ollamaFlushWaitMs, default
770
+ * 180 s). Best-effort — a flush failure just means no release this cycle.
771
+ */
772
+ async flushOllama(model) {
773
+ const url = `${this.config.baseUrl.replace(/\/$/, "")}/api/generate`;
774
+ try {
775
+ await fetch(url, {
776
+ method: "POST",
777
+ headers: { "Content-Type": "application/json" },
778
+ body: JSON.stringify({ model, keep_alive: 0 }),
779
+ signal: AbortSignal.timeout(60_000),
780
+ });
781
+ }
782
+ catch { /* best-effort flush */ }
783
+ const waitMs = this.config.ollamaFlushWaitMs ?? 180_000;
784
+ await new Promise((r) => setTimeout(r, waitMs));
785
+ }
728
786
  /**
729
787
  * Anthropic Messages API (/v1/messages).
730
788
  * Auth via x-api-key header.
package/dist/types.d.ts CHANGED
@@ -303,6 +303,33 @@ export interface HicortexConfig {
303
303
  * See #220.
304
304
  */
305
305
  enableThinking?: boolean;
306
+ /**
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.
314
+ */
315
+ numCtx?: number;
316
+ /**
317
+ * Flush ollama's accumulated memory every N ollama calls — workaround for
318
+ * ollama's per-request memory growth (the runner's RSS climbs ~171 MB/call and
319
+ * 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.
324
+ */
325
+ ollamaFlushEvery?: number;
326
+ /**
327
+ * Milliseconds to wait after an ollama flush (`keep_alive:0`) for the runner
328
+ * to exit + release its accumulated memory before the next call reloads.
329
+ * Default 180000 (3 min — the runner takes >90 s to exit after keep_alive:0;
330
+ * doubled for margin). Only relevant when `ollamaFlushEvery` > 0.
331
+ */
332
+ ollamaFlushWaitMs?: number;
306
333
  }
307
334
  /** A config-owned life-sphere domain (see HicortexConfig.domains). */
308
335
  export interface DomainDef {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamaze/hicortex",
3
- "version": "0.16.4",
3
+ "version": "0.16.6",
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": {