@gamaze/hicortex 0.16.3 → 0.16.4

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.
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Strict config readers for primitive values. Pure functions — no LlmConfig
3
+ * dependency — shared by the nightly preflight knobs and the distill-tier
4
+ * overlay (llm.ts / mcp-server.ts).
5
+ *
6
+ * The point is to reject wrong-typed config values AT THE BOUNDARY (disk →
7
+ * runtime) with a warn, rather than casting them straight through. The trap
8
+ * these guard: a JSON slip like `"enableThinking": "false"` (string,
9
+ * not boolean) casts to a truthy value downstream — for the Qwen chat template
10
+ * a non-empty string flips thinking ON, which is precisely the failure the key
11
+ * exists to prevent, with no error anywhere. Same class as the #225
12
+ * preflight-knob validation.
13
+ */
14
+ /**
15
+ * Read a positive finite number from a config object. Returns `def` when the
16
+ * key is absent OR present-but-invalid (with a warn in the latter case).
17
+ */
18
+ export declare function readPositiveConfig(config: Record<string, unknown>, key: string, def: number): number;
19
+ /**
20
+ * Read a strict boolean from a config object. Returns the boolean when valid,
21
+ * `undefined` when the key is absent OR present-but-not-a-boolean (with a warn
22
+ * in the latter case). Never coerces — `"false"` (string) is rejected, not
23
+ * treated as truthy.
24
+ */
25
+ export declare function readStrictBoolean(config: Record<string, unknown>, key: string): boolean | undefined;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ /**
3
+ * Strict config readers for primitive values. Pure functions — no LlmConfig
4
+ * dependency — shared by the nightly preflight knobs and the distill-tier
5
+ * overlay (llm.ts / mcp-server.ts).
6
+ *
7
+ * The point is to reject wrong-typed config values AT THE BOUNDARY (disk →
8
+ * runtime) with a warn, rather than casting them straight through. The trap
9
+ * these guard: a JSON slip like `"enableThinking": "false"` (string,
10
+ * not boolean) casts to a truthy value downstream — for the Qwen chat template
11
+ * a non-empty string flips thinking ON, which is precisely the failure the key
12
+ * exists to prevent, with no error anywhere. Same class as the #225
13
+ * preflight-knob validation.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.readPositiveConfig = readPositiveConfig;
17
+ exports.readStrictBoolean = readStrictBoolean;
18
+ /**
19
+ * Read a positive finite number from a config object. Returns `def` when the
20
+ * key is absent OR present-but-invalid (with a warn in the latter case).
21
+ */
22
+ function readPositiveConfig(config, key, def) {
23
+ const v = config[key];
24
+ if (v === undefined)
25
+ return def;
26
+ if (typeof v === "number" && Number.isFinite(v) && v > 0)
27
+ return v;
28
+ console.warn(`[hicortex] config "${key}" = ${String(v)} is not a positive finite number — using default ${def}.`);
29
+ return def;
30
+ }
31
+ /**
32
+ * Read a strict boolean from a config object. Returns the boolean when valid,
33
+ * `undefined` when the key is absent OR present-but-not-a-boolean (with a warn
34
+ * in the latter case). Never coerces — `"false"` (string) is rejected, not
35
+ * treated as truthy.
36
+ */
37
+ function readStrictBoolean(config, key) {
38
+ const v = config[key];
39
+ if (v === undefined)
40
+ return undefined;
41
+ if (typeof v === "boolean")
42
+ return v;
43
+ console.warn(`[hicortex] config "${key}" = ${String(v)} is not a boolean — ignored.`);
44
+ return undefined;
45
+ }
package/dist/llm.d.ts CHANGED
@@ -25,6 +25,10 @@ export interface LlmConfig {
25
25
  distillBaseUrl?: string;
26
26
  distillApiKey?: string;
27
27
  distillProvider?: string;
28
+ /** Max output tokens for all phases (distill/reflect/classify/scoring). Heavy phases fall back to 8192, scoring to 2048. */
29
+ maxTokens?: number;
30
+ /** Toggle thinking on the openai-compat path for the heavy phases. Absent = no kwarg sent. */
31
+ enableThinking?: boolean;
28
32
  /** Optional separate endpoint for reflect-tier LLM (e.g. remote Ollama with larger model). */
29
33
  reflectBaseUrl?: string;
30
34
  reflectApiKey?: string;
@@ -87,6 +91,21 @@ export type { ModelTierOverride } from "./types.js";
87
91
  * a dead score apiKey/provider under an ollama base.
88
92
  */
89
93
  export declare function applyModelsBlock(saved: Record<string, unknown> | null): Record<string, unknown> | null;
94
+ /**
95
+ * Validate + copy the heavy-phase tuning keys (#220: maxTokens + enableThinking)
96
+ * from the saved disk config onto a runtime LlmConfig. Called by BOTH LlmConfig
97
+ * construction sites — the daemon in mcp-server.ts (runs distill) AND
98
+ * resolveSavedLlmConfig below (the nightly runs reflect + classify) — so every
99
+ * process that runs a heavy phase honors the keys, and a future site calling
100
+ * this inherits them by construction.
101
+ *
102
+ * Both keys are optional; absent = call-site defaults (8192 / thinking kwarg
103
+ * omitted). Wrong-typed values warn and are dropped (readPositiveConfig /
104
+ * readStrictBoolean) — notably a JSON slip `"enableThinking": "false"` (string)
105
+ * is rejected rather than coerced to truthy thinking-on, which would silently
106
+ * invert the fix this key exists to apply.
107
+ */
108
+ export declare function applyTierTuningOverlay(llmConfig: LlmConfig, savedConfig: Record<string, unknown> | null | undefined): void;
90
109
  /**
91
110
  * Resolve an LlmConfig from a saved ~/.hicortex/config.json object.
92
111
  *
package/dist/llm.js CHANGED
@@ -18,6 +18,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.LlmClient = exports.RateLimitError = exports.resolveLlmConfigForCC = void 0;
19
19
  exports.resolveExplicitLlmConfig = resolveExplicitLlmConfig;
20
20
  exports.applyModelsBlock = applyModelsBlock;
21
+ exports.applyTierTuningOverlay = applyTierTuningOverlay;
21
22
  exports.resolveSavedLlmConfig = resolveSavedLlmConfig;
22
23
  exports.resolveClassifyProbeTarget = resolveClassifyProbeTarget;
23
24
  exports.findClaudeBinary = findClaudeBinary;
@@ -25,6 +26,7 @@ exports.claudeCliConfig = claudeCliConfig;
25
26
  exports.probeOllama = probeOllama;
26
27
  exports.probeOllamaModel = probeOllamaModel;
27
28
  exports.resolveDistillFallback = resolveDistillFallback;
29
+ const config_read_js_1 = require("./config-read.js");
28
30
  /**
29
31
  * Resolve LLM configuration from explicit config-file overrides or
30
32
  * Hicortex-specific env vars only. Returns null when nothing explicit is set.
@@ -182,6 +184,31 @@ function applyModelsBlock(saved) {
182
184
  }
183
185
  return { ...saved, ...mapped };
184
186
  }
187
+ /**
188
+ * Validate + copy the heavy-phase tuning keys (#220: maxTokens + enableThinking)
189
+ * from the saved disk config onto a runtime LlmConfig. Called by BOTH LlmConfig
190
+ * construction sites — the daemon in mcp-server.ts (runs distill) AND
191
+ * resolveSavedLlmConfig below (the nightly runs reflect + classify) — so every
192
+ * process that runs a heavy phase honors the keys, and a future site calling
193
+ * this inherits them by construction.
194
+ *
195
+ * Both keys are optional; absent = call-site defaults (8192 / thinking kwarg
196
+ * omitted). Wrong-typed values warn and are dropped (readPositiveConfig /
197
+ * readStrictBoolean) — notably a JSON slip `"enableThinking": "false"` (string)
198
+ * is rejected rather than coerced to truthy thinking-on, which would silently
199
+ * invert the fix this key exists to apply.
200
+ */
201
+ function applyTierTuningOverlay(llmConfig, savedConfig) {
202
+ if (!savedConfig)
203
+ return;
204
+ if (savedConfig.maxTokens !== undefined) {
205
+ llmConfig.maxTokens = (0, config_read_js_1.readPositiveConfig)(savedConfig, "maxTokens", 8192);
206
+ }
207
+ const thinking = (0, config_read_js_1.readStrictBoolean)(savedConfig, "enableThinking");
208
+ if (thinking !== undefined) {
209
+ llmConfig.enableThinking = thinking;
210
+ }
211
+ }
185
212
  /**
186
213
  * Resolve an LlmConfig from a saved ~/.hicortex/config.json object.
187
214
  *
@@ -232,6 +259,12 @@ function resolveSavedLlmConfig(savedConfig, findBinary = findClaudeBinary) {
232
259
  llmConfig.reflectApiKey = savedConfig.reflectApiKey ?? llmConfig.apiKey;
233
260
  llmConfig.reflectProvider = savedConfig.reflectProvider ?? llmConfig.provider;
234
261
  }
262
+ // Heavy-phase tuning (#220: maxTokens + enableThinking). The nightly runs
263
+ // reflect + classify (consolidation), the daemon runs distill — both need the
264
+ // keys, so the overlay is applied at both construction sites.
265
+ if (llmConfig) {
266
+ applyTierTuningOverlay(llmConfig, savedConfig);
267
+ }
235
268
  // Optional classify tier (memory tag classification). Same overlay pattern
236
269
  // as distillModel/distillBaseUrl: when absent, completeClassify falls back
237
270
  // to the reflect tier — zero behavior change for existing installs.
@@ -504,28 +537,39 @@ class LlmClient {
504
537
  /**
505
538
  * Fast-tier completion (importance scoring, simple tasks).
506
539
  */
507
- async completeFast(prompt, maxTokens = 2048) {
508
- return this.complete(this.config.model, prompt, maxTokens, 600_000);
540
+ async completeFast(prompt, maxTokens) {
541
+ 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);
509
546
  }
510
547
  /**
511
548
  * Reflect-tier completion (nightly reflection, needs reasoning).
512
549
  * Routes to reflectBaseUrl/reflectProvider if configured (e.g. remote Ollama with larger model).
513
550
  */
514
- async completeReflect(prompt, maxTokens = 8192) {
551
+ async completeReflect(prompt, maxTokens) {
552
+ const tokens = maxTokens ?? this.config.maxTokens ?? 8192;
553
+ const thinking = this.config.enableThinking;
515
554
  if (this.config.reflectBaseUrl) {
516
- return this.completeWithOverride(this.config.reflectBaseUrl, this.config.reflectApiKey ?? this.config.apiKey, this.config.reflectProvider ?? this.config.provider, this.config.reflectModel, prompt, maxTokens, 900_000);
555
+ return this.completeWithOverride(this.config.reflectBaseUrl, this.config.reflectApiKey ?? this.config.apiKey, this.config.reflectProvider ?? this.config.provider, this.config.reflectModel, prompt, tokens, 900_000, thinking);
517
556
  }
518
- return this.complete(this.config.reflectModel, prompt, maxTokens, 900_000);
557
+ return this.complete(this.config.reflectModel, prompt, tokens, 900_000, thinking);
519
558
  }
520
559
  /**
521
560
  * Distillation-tier completion (session knowledge extraction).
522
561
  * Routes to distillBaseUrl/distillProvider if configured (e.g. remote Ollama with faster model).
523
562
  */
524
- async completeDistill(prompt, maxTokens = 2048) {
563
+ async completeDistill(prompt, maxTokens) {
564
+ const tokens = maxTokens ?? this.config.maxTokens ?? 8192;
565
+ // enableThinking is threaded explicitly (not read inside the shared
566
+ // completeOpenAiCompat) so the chat_template_kwargs kwarg is scoped to the
567
+ // heavy phases — distill/reflect/classify — and never reaches scoring. See #220.
568
+ const thinking = this.config.enableThinking;
525
569
  if (this.config.distillBaseUrl) {
526
- return this.completeWithOverride(this.config.distillBaseUrl, this.config.distillApiKey ?? this.config.apiKey, this.config.distillProvider ?? this.config.provider, this.config.distillModel ?? this.config.model, prompt, maxTokens, 900_000);
570
+ return this.completeWithOverride(this.config.distillBaseUrl, this.config.distillApiKey ?? this.config.apiKey, this.config.distillProvider ?? this.config.provider, this.config.distillModel ?? this.config.model, prompt, tokens, 900_000, thinking);
527
571
  }
528
- return this.complete(this.config.distillModel ?? this.config.model, prompt, maxTokens, 900_000);
572
+ return this.complete(this.config.distillModel ?? this.config.model, prompt, tokens, 900_000, thinking);
529
573
  }
530
574
  /**
531
575
  * Classification-tier completion (memory tag classification).
@@ -538,33 +582,35 @@ class LlmClient {
538
582
  * - Only classifyModel set → the classify model on the reflect endpoint
539
583
  * when one is configured, else on the base endpoint.
540
584
  */
541
- async completeClassify(prompt, maxTokens = 8192) {
585
+ async completeClassify(prompt, maxTokens) {
586
+ const tokens = maxTokens ?? this.config.maxTokens ?? 8192;
587
+ const thinking = this.config.enableThinking;
542
588
  if (!this.config.classifyModel && !this.config.classifyBaseUrl) {
543
- return this.completeReflect(prompt, maxTokens);
589
+ return this.completeReflect(prompt, tokens);
544
590
  }
545
591
  const model = this.config.classifyModel ?? this.config.reflectModel;
546
592
  if (this.config.classifyBaseUrl) {
547
- return this.completeWithOverride(this.config.classifyBaseUrl, this.config.classifyApiKey ?? this.config.apiKey, this.config.classifyProvider ?? this.config.provider, model, prompt, maxTokens, 900_000);
593
+ return this.completeWithOverride(this.config.classifyBaseUrl, this.config.classifyApiKey ?? this.config.apiKey, this.config.classifyProvider ?? this.config.provider, model, prompt, tokens, 900_000, thinking);
548
594
  }
549
595
  if (this.config.reflectBaseUrl) {
550
- return this.completeWithOverride(this.config.reflectBaseUrl, this.config.reflectApiKey ?? this.config.apiKey, this.config.reflectProvider ?? this.config.provider, model, prompt, maxTokens, 900_000);
596
+ return this.completeWithOverride(this.config.reflectBaseUrl, this.config.reflectApiKey ?? this.config.apiKey, this.config.reflectProvider ?? this.config.provider, model, prompt, tokens, 900_000, thinking);
551
597
  }
552
- return this.complete(model, prompt, maxTokens, 900_000);
598
+ return this.complete(model, prompt, tokens, 900_000, thinking);
553
599
  }
554
600
  /**
555
601
  * Complete with overridden baseUrl/apiKey/provider (used for reflect tier with separate endpoint).
556
602
  * Creates a temporary LlmClient to avoid mutating shared config under concurrent calls.
557
603
  */
558
- async completeWithOverride(baseUrl, apiKey, provider, model, prompt, maxTokens, timeoutMs) {
604
+ async completeWithOverride(baseUrl, apiKey, provider, model, prompt, maxTokens, timeoutMs, thinking) {
559
605
  const tempClient = new LlmClient({
560
606
  ...this.config,
561
607
  baseUrl,
562
608
  apiKey,
563
609
  provider,
564
610
  });
565
- return tempClient.complete(model, prompt, maxTokens, timeoutMs);
611
+ return tempClient.complete(model, prompt, maxTokens, timeoutMs, thinking);
566
612
  }
567
- async complete(model, prompt, maxTokens, timeoutMs) {
613
+ async complete(model, prompt, maxTokens, timeoutMs, thinking) {
568
614
  if (this.isRateLimited) {
569
615
  throw new RateLimitError(this.rateLimitedUntil - Date.now());
570
616
  }
@@ -572,7 +618,7 @@ class LlmClient {
572
618
  let lastErr;
573
619
  for (let attempt = 0; attempt <= retryDelays.length; attempt++) {
574
620
  try {
575
- return await this.completeOnce(model, prompt, maxTokens, timeoutMs);
621
+ return await this.completeOnce(model, prompt, maxTokens, timeoutMs, thinking);
576
622
  }
577
623
  catch (err) {
578
624
  lastErr = err instanceof Error ? err : new Error(String(err));
@@ -589,7 +635,7 @@ class LlmClient {
589
635
  }
590
636
  throw lastErr;
591
637
  }
592
- async completeOnce(model, prompt, maxTokens, timeoutMs) {
638
+ async completeOnce(model, prompt, maxTokens, timeoutMs, thinking) {
593
639
  if (this.config.provider === "claude-cli") {
594
640
  return this.completeClaude(model, prompt, timeoutMs);
595
641
  }
@@ -599,7 +645,10 @@ class LlmClient {
599
645
  if (this.config.provider === "anthropic") {
600
646
  return this.completeAnthropic(model, prompt, maxTokens, timeoutMs);
601
647
  }
602
- return this.completeOpenAiCompat(model, prompt, maxTokens, timeoutMs);
648
+ // thinking is threaded from each heavy phase (distill/reflect/classify), so
649
+ // the kwarg is emitted for those tiers on the openai-compat path; completeFast
650
+ // (scoring) never passes it (PR #227 review F1).
651
+ return this.completeOpenAiCompat(model, prompt, maxTokens, timeoutMs, thinking);
603
652
  }
604
653
  /**
605
654
  * Claude CLI: shell out to `claude -p` for subscription users.
@@ -711,7 +760,7 @@ class LlmClient {
711
760
  /**
712
761
  * OpenAI-compatible /v1/chat/completions (works for OpenAI, OpenRouter, etc).
713
762
  */
714
- async completeOpenAiCompat(model, prompt, maxTokens, timeoutMs) {
763
+ async completeOpenAiCompat(model, prompt, maxTokens, timeoutMs, thinking) {
715
764
  const baseUrl = this.config.baseUrl.replace(/\/$/, "");
716
765
  // Some providers include the API version in the base URL already
717
766
  const hasVersion = /\/v\d+\/?$/.test(baseUrl);
@@ -724,14 +773,24 @@ class LlmClient {
724
773
  if (this.config.apiKey) {
725
774
  headers["Authorization"] = `Bearer ${this.config.apiKey}`;
726
775
  }
776
+ // Qwen3 thinking mode: when on, the model can burn the whole token budget on
777
+ // an unclosed <think> block and emit nothing (probed 2026-08-04). `thinking`
778
+ // is threaded here ONLY from completeDistill, so the kwarg is emitted solely
779
+ // for the distill tier — reflect/classify/fast never send it (PR #227 F1).
780
+ // mlx-lm forwards chat_template_kwargs to the chat template (mirrors the
781
+ // ollama path's think:false). See #220.
782
+ const body = {
783
+ model,
784
+ messages: [{ role: "user", content: prompt }],
785
+ max_tokens: maxTokens,
786
+ };
787
+ if (thinking !== undefined) {
788
+ body.chat_template_kwargs = { enable_thinking: thinking };
789
+ }
727
790
  const resp = await fetch(url, {
728
791
  method: "POST",
729
792
  headers,
730
- body: JSON.stringify({
731
- model,
732
- messages: [{ role: "user", content: prompt }],
733
- max_tokens: maxTokens,
734
- }),
793
+ body: JSON.stringify(body),
735
794
  signal: AbortSignal.timeout(timeoutMs),
736
795
  });
737
796
  if (resp.status === 429)
@@ -424,6 +424,10 @@ async function startServer(options = {}) {
424
424
  llmConfig.distillApiKey = savedConfig.distillApiKey ?? llmConfig.apiKey;
425
425
  llmConfig.distillProvider = savedConfig.distillProvider ?? llmConfig.provider;
426
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.
430
+ (0, llm_js_1.applyTierTuningOverlay)(llmConfig, savedConfig);
427
431
  // Apply separate reflect endpoint if configured (e.g. remote Ollama with larger model)
428
432
  if (savedConfig?.reflectBaseUrl) {
429
433
  llmConfig.reflectBaseUrl = savedConfig.reflectBaseUrl;
package/dist/nightly.js CHANGED
@@ -55,6 +55,7 @@ try {
55
55
  }
56
56
  catch { }
57
57
  const db_js_1 = require("./db.js");
58
+ const config_read_js_1 = require("./config-read.js");
58
59
  const llm_js_1 = require("./llm.js");
59
60
  const embedder_js_1 = require("./embedder.js");
60
61
  const storage = __importStar(require("./storage.js"));
@@ -187,27 +188,9 @@ function captureLockWaitMs() {
187
188
  const env = Number(process.env.HICORTEX_CAPTURE_LOCK_WAIT_MS);
188
189
  return Number.isFinite(env) && env >= 0 ? env : CAPTURE_LOCK_WAIT_MS;
189
190
  }
190
- /**
191
- * Read a positive finite number from a nightly config key, falling back to
192
- * `def` when the key is absent/invalid. Used by the pre-flight retry knobs
193
- * (#163): tuning knobs live in config, never hardcoded (cf. decayHalfLifeDays,
194
- * recallMinSimilarity).
195
- *
196
- * A value that is PRESENT but rejected (non-number, non-finite, or ≤ 0) warns
197
- * — e.g. an operator who sets `preflightAttempts: 0` intending "don't retry"
198
- * would otherwise silently get the default 3. (Single-try is
199
- * `preflightAttempts: 1`, so there's no functional gap — this just makes the
200
- * silent coercion visible, consistent with the fail-explicit pattern.)
201
- */
202
- function readPositiveConfig(config, key, def) {
203
- const v = config[key];
204
- if (v === undefined)
205
- return def;
206
- if (typeof v === "number" && Number.isFinite(v) && v > 0)
207
- return v;
208
- console.warn(`[hicortex] config "${key}" = ${String(v)} is not a positive finite number — using default ${def}.`);
209
- return def;
210
- }
191
+ // readPositiveConfig moved to ./config-read.ts (shared with the distill-tier
192
+ // overlay in llm.ts / mcp-server.ts). Validates positive-number config knobs
193
+ // at the disk→runtime boundary with a warn-on-rejected-value.
211
194
  const NIGHTLY_LOG_MAX_BYTES = 1024 * 1024; // 1 MB — years of normal runs
212
195
  /**
213
196
  * Keep ~/.hicortex/nightly.log bounded. The launchd plist and systemd unit
@@ -524,9 +507,9 @@ async function runClientNightly(config, dryRun, stateDir = HICORTEX_HOME, recapt
524
507
  // elapsed time can exceed it. Not a defect: the capture lock isn't held
525
508
  // during the retry and the cursor design is dup-over-loss, so a late success
526
509
  // is harmless. Just don't treat 2m45s as a hard wall-clock bound.
527
- const PREFLIGHT_TIMEOUT_MS = readPositiveConfig(config, "preflightTimeoutMs", 15_000);
528
- const PREFLIGHT_ATTEMPTS = Math.max(1, Math.floor(readPositiveConfig(config, "preflightAttempts", 3)));
529
- const PREFLIGHT_RETRY_GAP_MS = readPositiveConfig(config, "preflightRetryGapMs", 60_000);
510
+ const PREFLIGHT_TIMEOUT_MS = (0, config_read_js_1.readPositiveConfig)(config, "preflightTimeoutMs", 15_000);
511
+ const PREFLIGHT_ATTEMPTS = Math.max(1, Math.floor((0, config_read_js_1.readPositiveConfig)(config, "preflightAttempts", 3)));
512
+ const PREFLIGHT_RETRY_GAP_MS = (0, config_read_js_1.readPositiveConfig)(config, "preflightRetryGapMs", 60_000);
530
513
  let reachable = false;
531
514
  for (let attempt = 1; attempt <= PREFLIGHT_ATTEMPTS; attempt++) {
532
515
  try {
package/dist/types.d.ts CHANGED
@@ -282,6 +282,27 @@ export interface HicortexConfig {
282
282
  * dup-over-loss); just don't treat the nominal sum as a hard bound.
283
283
  */
284
284
  preflightRetryGapMs?: number;
285
+ /**
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
291
+ * when it finishes early. Read in llm.ts; see #220.
292
+ */
293
+ maxTokens?: number;
294
+ /**
295
+ * 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.
304
+ */
305
+ enableThinking?: boolean;
285
306
  }
286
307
  /** A config-owned life-sphere domain (see HicortexConfig.domains). */
287
308
  export interface DomainDef {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamaze/hicortex",
3
- "version": "0.16.3",
3
+ "version": "0.16.4",
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": {