@gamaze/hicortex 0.16.6 → 0.16.7

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.
@@ -23,3 +23,9 @@ export declare function readPositiveConfig(config: Record<string, unknown>, key:
23
23
  * treated as truthy.
24
24
  */
25
25
  export declare function readStrictBoolean(config: Record<string, unknown>, key: string): boolean | undefined;
26
+ /**
27
+ * Read a non-negative finite number (allows 0, unlike readPositiveConfig).
28
+ * Returns `def` when absent OR invalid. Used for keys where 0 is a valid "off"
29
+ * value (e.g. ollamaFlushEvery).
30
+ */
31
+ export declare function readNonNegativeConfig(config: Record<string, unknown>, key: string, def: number): number;
@@ -15,6 +15,7 @@
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
16
  exports.readPositiveConfig = readPositiveConfig;
17
17
  exports.readStrictBoolean = readStrictBoolean;
18
+ exports.readNonNegativeConfig = readNonNegativeConfig;
18
19
  /**
19
20
  * Read a positive finite number from a config object. Returns `def` when the
20
21
  * key is absent OR present-but-invalid (with a warn in the latter case).
@@ -43,3 +44,17 @@ function readStrictBoolean(config, key) {
43
44
  console.warn(`[hicortex] config "${key}" = ${String(v)} is not a boolean — ignored.`);
44
45
  return undefined;
45
46
  }
47
+ /**
48
+ * Read a non-negative finite number (allows 0, unlike readPositiveConfig).
49
+ * Returns `def` when absent OR invalid. Used for keys where 0 is a valid "off"
50
+ * value (e.g. ollamaFlushEvery).
51
+ */
52
+ function readNonNegativeConfig(config, key, def) {
53
+ const v = config[key];
54
+ if (v === undefined)
55
+ return def;
56
+ if (typeof v === "number" && Number.isFinite(v) && v >= 0)
57
+ return v;
58
+ console.warn(`[hicortex] config "${key}" = ${String(v)} is not a non-negative finite number — using default ${def}.`);
59
+ return def;
60
+ }
package/dist/llm.d.ts CHANGED
@@ -271,7 +271,9 @@ export declare class LlmClient {
271
271
  * runner exits + releases its per-request RSS growth, then wait for the release
272
272
  * before the next call reloads fresh. The runner takes >90 s to exit after
273
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.
274
+ * 180 s). Logs the flush so the wait is distinguishable from a hang. If the
275
+ * unload request fails (ollama down), the wait is skipped — no dead time for a
276
+ * release that can't have happened. See #229 review.
275
277
  */
276
278
  private flushOllama;
277
279
  /**
package/dist/llm.js CHANGED
@@ -212,7 +212,7 @@ function applyTierTuningOverlay(llmConfig, savedConfig) {
212
212
  llmConfig.numCtx = (0, config_read_js_1.readPositiveConfig)(savedConfig, "numCtx", 2048);
213
213
  }
214
214
  if (savedConfig.ollamaFlushEvery !== undefined) {
215
- llmConfig.ollamaFlushEvery = (0, config_read_js_1.readPositiveConfig)(savedConfig, "ollamaFlushEvery", 0);
215
+ llmConfig.ollamaFlushEvery = (0, config_read_js_1.readNonNegativeConfig)(savedConfig, "ollamaFlushEvery", 0);
216
216
  }
217
217
  if (savedConfig.ollamaFlushWaitMs !== undefined) {
218
218
  llmConfig.ollamaFlushWaitMs = (0, config_read_js_1.readPositiveConfig)(savedConfig, "ollamaFlushWaitMs", 180000);
@@ -553,7 +553,20 @@ class LlmClient {
553
553
  // so ONLY this tier gets the smaller context window — the heavy tiers keep 32768
554
554
  // (preserves detectChunkSize's chunk-sizing). thinking is not threaded (scoring is
555
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);
556
+ const result = await this.complete(this.config.model, prompt, tokens, 600_000, undefined, this.config.numCtx ?? 2048);
557
+ // Periodic flush: scoped to scoring (the fast tier) only — heavy tiers
558
+ // (distill/reflect/classify) don't flush, avoiding a multi-minute pause
559
+ // mid-distillation + unloading the wrong model. Override-routed tiers never
560
+ // reach here (completeWithOverride builds a fresh client). See #229 review.
561
+ const flushEvery = this.config.ollamaFlushEvery ?? 0;
562
+ if (this.config.provider === "ollama" && flushEvery > 0) {
563
+ this.ollamaCallCount++;
564
+ if (this.ollamaCallCount >= flushEvery) {
565
+ await this.flushOllama(this.config.model);
566
+ this.ollamaCallCount = 0;
567
+ }
568
+ }
569
+ return result;
557
570
  }
558
571
  /**
559
572
  * Reflect-tier completion (nightly reflection, needs reasoning).
@@ -746,20 +759,6 @@ class LlmClient {
746
759
  catch { /* skip malformed lines */ }
747
760
  }
748
761
  }
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
- }
763
762
  return result.trim();
764
763
  }
765
764
  /**
@@ -767,10 +766,14 @@ class LlmClient {
767
766
  * runner exits + releases its per-request RSS growth, then wait for the release
768
767
  * before the next call reloads fresh. The runner takes >90 s to exit after
769
768
  * 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.
769
+ * 180 s). Logs the flush so the wait is distinguishable from a hang. If the
770
+ * unload request fails (ollama down), the wait is skipped — no dead time for a
771
+ * release that can't have happened. See #229 review.
771
772
  */
772
773
  async flushOllama(model) {
773
774
  const url = `${this.config.baseUrl.replace(/\/$/, "")}/api/generate`;
775
+ const waitMs = this.config.ollamaFlushWaitMs ?? 180_000;
776
+ console.log(`[hicortex] ollama flush: unloading ${model} after ${this.ollamaCallCount} scoring calls, waiting ${waitMs / 1000}s for memory release…`);
774
777
  try {
775
778
  await fetch(url, {
776
779
  method: "POST",
@@ -778,10 +781,12 @@ class LlmClient {
778
781
  body: JSON.stringify({ model, keep_alive: 0 }),
779
782
  signal: AbortSignal.timeout(60_000),
780
783
  });
784
+ await new Promise((r) => setTimeout(r, waitMs));
785
+ console.log(`[hicortex] ollama flush: complete`);
786
+ }
787
+ catch {
788
+ console.warn(`[hicortex] ollama flush: unload request failed (ollama unreachable?) — skipping wait`);
781
789
  }
782
- catch { /* best-effort flush */ }
783
- const waitMs = this.config.ollamaFlushWaitMs ?? 180_000;
784
- await new Promise((r) => setTimeout(r, waitMs));
785
790
  }
786
791
  /**
787
792
  * Anthropic Messages API (/v1/messages).
package/dist/types.d.ts CHANGED
@@ -314,13 +314,17 @@ export interface HicortexConfig {
314
314
  */
315
315
  numCtx?: number;
316
316
  /**
317
- * Flush ollama's accumulated memory every N ollama calls — workaround for
317
+ * Flush ollama's accumulated memory every N scoring calls — workaround for
318
318
  * ollama's per-request memory growth (the runner's RSS climbs ~171 MB/call and
319
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.
320
+ * during long consolidations. Default 0 (off). When >0, every Nth scoring call
321
+ * (`completeFast`) triggers a `keep_alive:0` unload + an `ollamaFlushWaitMs`
322
+ * pause for the runner to exit + release, then the next call reloads fresh.
323
+ * N=15 caps a cycle at ~2.5 GB. Scoped to the fast tier (scoring) only. Note:
324
+ * N counts **logical** scoring calls, not raw HTTP requests — `complete()`
325
+ * retries up to 4× on timeout, so under retry pressure the actual accumulation
326
+ * may be up to 4×N calls' worth. In practice the flush prevents the thrash that
327
+ * causes retries, keeping the count accurate.
324
328
  */
325
329
  ollamaFlushEvery?: number;
326
330
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamaze/hicortex",
3
- "version": "0.16.6",
3
+ "version": "0.16.7",
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": {