@gamaze/hicortex 0.16.3 → 0.16.5
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/config-read.d.ts +25 -0
- package/dist/config-read.js +45 -0
- package/dist/llm.d.ts +21 -0
- package/dist/llm.js +104 -29
- package/dist/mcp-server.js +4 -0
- package/dist/nightly.js +7 -24
- package/dist/types.d.ts +31 -0
- package/package.json +1 -1
|
@@ -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,12 @@ 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;
|
|
32
|
+
/** Context window for the ollama fast tier (completeOllama). Falls back to 2048. */
|
|
33
|
+
numCtx?: number;
|
|
28
34
|
/** Optional separate endpoint for reflect-tier LLM (e.g. remote Ollama with larger model). */
|
|
29
35
|
reflectBaseUrl?: string;
|
|
30
36
|
reflectApiKey?: string;
|
|
@@ -87,6 +93,21 @@ export type { ModelTierOverride } from "./types.js";
|
|
|
87
93
|
* a dead score apiKey/provider under an ollama base.
|
|
88
94
|
*/
|
|
89
95
|
export declare function applyModelsBlock(saved: Record<string, unknown> | null): Record<string, unknown> | null;
|
|
96
|
+
/**
|
|
97
|
+
* Validate + copy the heavy-phase tuning keys (#220: maxTokens + enableThinking)
|
|
98
|
+
* from the saved disk config onto a runtime LlmConfig. Called by BOTH LlmConfig
|
|
99
|
+
* construction sites — the daemon in mcp-server.ts (runs distill) AND
|
|
100
|
+
* resolveSavedLlmConfig below (the nightly runs reflect + classify) — so every
|
|
101
|
+
* process that runs a heavy phase honors the keys, and a future site calling
|
|
102
|
+
* this inherits them by construction.
|
|
103
|
+
*
|
|
104
|
+
* Both keys are optional; absent = call-site defaults (8192 / thinking kwarg
|
|
105
|
+
* omitted). Wrong-typed values warn and are dropped (readPositiveConfig /
|
|
106
|
+
* readStrictBoolean) — notably a JSON slip `"enableThinking": "false"` (string)
|
|
107
|
+
* is rejected rather than coerced to truthy thinking-on, which would silently
|
|
108
|
+
* invert the fix this key exists to apply.
|
|
109
|
+
*/
|
|
110
|
+
export declare function applyTierTuningOverlay(llmConfig: LlmConfig, savedConfig: Record<string, unknown> | null | undefined): void;
|
|
90
111
|
/**
|
|
91
112
|
* Resolve an LlmConfig from a saved ~/.hicortex/config.json object.
|
|
92
113
|
*
|
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,34 @@ 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
|
+
if (savedConfig.numCtx !== undefined) {
|
|
212
|
+
llmConfig.numCtx = (0, config_read_js_1.readPositiveConfig)(savedConfig, "numCtx", 2048);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
185
215
|
/**
|
|
186
216
|
* Resolve an LlmConfig from a saved ~/.hicortex/config.json object.
|
|
187
217
|
*
|
|
@@ -232,6 +262,12 @@ function resolveSavedLlmConfig(savedConfig, findBinary = findClaudeBinary) {
|
|
|
232
262
|
llmConfig.reflectApiKey = savedConfig.reflectApiKey ?? llmConfig.apiKey;
|
|
233
263
|
llmConfig.reflectProvider = savedConfig.reflectProvider ?? llmConfig.provider;
|
|
234
264
|
}
|
|
265
|
+
// Heavy-phase tuning (#220: maxTokens + enableThinking). The nightly runs
|
|
266
|
+
// reflect + classify (consolidation), the daemon runs distill — both need the
|
|
267
|
+
// keys, so the overlay is applied at both construction sites.
|
|
268
|
+
if (llmConfig) {
|
|
269
|
+
applyTierTuningOverlay(llmConfig, savedConfig);
|
|
270
|
+
}
|
|
235
271
|
// Optional classify tier (memory tag classification). Same overlay pattern
|
|
236
272
|
// as distillModel/distillBaseUrl: when absent, completeClassify falls back
|
|
237
273
|
// to the reflect tier — zero behavior change for existing installs.
|
|
@@ -504,28 +540,40 @@ class LlmClient {
|
|
|
504
540
|
/**
|
|
505
541
|
* Fast-tier completion (importance scoring, simple tasks).
|
|
506
542
|
*/
|
|
507
|
-
async completeFast(prompt, maxTokens
|
|
508
|
-
|
|
543
|
+
async completeFast(prompt, maxTokens) {
|
|
544
|
+
const tokens = maxTokens ?? this.config.maxTokens ?? 2048;
|
|
545
|
+
// Scoring is the fast tier: numCtx (default 2048, ~850-token prompts) is threaded
|
|
546
|
+
// so ONLY this tier gets the smaller context window — the heavy tiers keep 32768
|
|
547
|
+
// (preserves detectChunkSize's chunk-sizing). thinking is not threaded (scoring is
|
|
548
|
+
// excluded from the thinking toggle; ollama is think:false regardless).
|
|
549
|
+
return this.complete(this.config.model, prompt, tokens, 600_000, undefined, this.config.numCtx ?? 2048);
|
|
509
550
|
}
|
|
510
551
|
/**
|
|
511
552
|
* Reflect-tier completion (nightly reflection, needs reasoning).
|
|
512
553
|
* Routes to reflectBaseUrl/reflectProvider if configured (e.g. remote Ollama with larger model).
|
|
513
554
|
*/
|
|
514
|
-
async completeReflect(prompt, maxTokens
|
|
555
|
+
async completeReflect(prompt, maxTokens) {
|
|
556
|
+
const tokens = maxTokens ?? this.config.maxTokens ?? 8192;
|
|
557
|
+
const thinking = this.config.enableThinking;
|
|
515
558
|
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,
|
|
559
|
+
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
560
|
}
|
|
518
|
-
return this.complete(this.config.reflectModel, prompt,
|
|
561
|
+
return this.complete(this.config.reflectModel, prompt, tokens, 900_000, thinking);
|
|
519
562
|
}
|
|
520
563
|
/**
|
|
521
564
|
* Distillation-tier completion (session knowledge extraction).
|
|
522
565
|
* Routes to distillBaseUrl/distillProvider if configured (e.g. remote Ollama with faster model).
|
|
523
566
|
*/
|
|
524
|
-
async completeDistill(prompt, maxTokens
|
|
567
|
+
async completeDistill(prompt, maxTokens) {
|
|
568
|
+
const tokens = maxTokens ?? this.config.maxTokens ?? 8192;
|
|
569
|
+
// enableThinking is threaded explicitly (not read inside the shared
|
|
570
|
+
// completeOpenAiCompat) so the chat_template_kwargs kwarg is scoped to the
|
|
571
|
+
// heavy phases — distill/reflect/classify — and never reaches scoring. See #220.
|
|
572
|
+
const thinking = this.config.enableThinking;
|
|
525
573
|
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,
|
|
574
|
+
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
575
|
}
|
|
528
|
-
return this.complete(this.config.distillModel ?? this.config.model, prompt,
|
|
576
|
+
return this.complete(this.config.distillModel ?? this.config.model, prompt, tokens, 900_000, thinking);
|
|
529
577
|
}
|
|
530
578
|
/**
|
|
531
579
|
* Classification-tier completion (memory tag classification).
|
|
@@ -538,33 +586,40 @@ class LlmClient {
|
|
|
538
586
|
* - Only classifyModel set → the classify model on the reflect endpoint
|
|
539
587
|
* when one is configured, else on the base endpoint.
|
|
540
588
|
*/
|
|
541
|
-
async completeClassify(prompt, maxTokens
|
|
589
|
+
async completeClassify(prompt, maxTokens) {
|
|
590
|
+
const tokens = maxTokens ?? this.config.maxTokens ?? 8192;
|
|
591
|
+
const thinking = this.config.enableThinking;
|
|
542
592
|
if (!this.config.classifyModel && !this.config.classifyBaseUrl) {
|
|
543
|
-
return this.completeReflect(prompt,
|
|
593
|
+
return this.completeReflect(prompt, tokens);
|
|
544
594
|
}
|
|
545
595
|
const model = this.config.classifyModel ?? this.config.reflectModel;
|
|
546
596
|
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,
|
|
597
|
+
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
598
|
}
|
|
549
599
|
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,
|
|
600
|
+
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
601
|
}
|
|
552
|
-
return this.complete(model, prompt,
|
|
602
|
+
return this.complete(model, prompt, tokens, 900_000, thinking);
|
|
553
603
|
}
|
|
554
604
|
/**
|
|
555
605
|
* Complete with overridden baseUrl/apiKey/provider (used for reflect tier with separate endpoint).
|
|
556
606
|
* Creates a temporary LlmClient to avoid mutating shared config under concurrent calls.
|
|
557
607
|
*/
|
|
558
|
-
async completeWithOverride(baseUrl, apiKey, provider, model, prompt, maxTokens, timeoutMs) {
|
|
608
|
+
async completeWithOverride(baseUrl, apiKey, provider, model, prompt, maxTokens, timeoutMs, thinking) {
|
|
559
609
|
const tempClient = new LlmClient({
|
|
560
610
|
...this.config,
|
|
561
611
|
baseUrl,
|
|
562
612
|
apiKey,
|
|
563
613
|
provider,
|
|
564
614
|
});
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
615
|
+
// numCtx intentionally NOT forwarded here: only the heavy tiers route through
|
|
616
|
+
// completeWithOverride (there is no fastBaseUrl/fast-tier override), and they
|
|
617
|
+
// must land on completeOllama's 32768 default to preserve detectChunkSize. If a
|
|
618
|
+
// fast-tier override is ever added, thread numCtx here too — else the fast tier
|
|
619
|
+
// would silently revert to 32768 (the original bug, reintroduced).
|
|
620
|
+
return tempClient.complete(model, prompt, maxTokens, timeoutMs, thinking);
|
|
621
|
+
}
|
|
622
|
+
async complete(model, prompt, maxTokens, timeoutMs, thinking, numCtx) {
|
|
568
623
|
if (this.isRateLimited) {
|
|
569
624
|
throw new RateLimitError(this.rateLimitedUntil - Date.now());
|
|
570
625
|
}
|
|
@@ -572,7 +627,7 @@ class LlmClient {
|
|
|
572
627
|
let lastErr;
|
|
573
628
|
for (let attempt = 0; attempt <= retryDelays.length; attempt++) {
|
|
574
629
|
try {
|
|
575
|
-
return await this.completeOnce(model, prompt, maxTokens, timeoutMs);
|
|
630
|
+
return await this.completeOnce(model, prompt, maxTokens, timeoutMs, thinking, numCtx);
|
|
576
631
|
}
|
|
577
632
|
catch (err) {
|
|
578
633
|
lastErr = err instanceof Error ? err : new Error(String(err));
|
|
@@ -589,17 +644,20 @@ class LlmClient {
|
|
|
589
644
|
}
|
|
590
645
|
throw lastErr;
|
|
591
646
|
}
|
|
592
|
-
async completeOnce(model, prompt, maxTokens, timeoutMs) {
|
|
647
|
+
async completeOnce(model, prompt, maxTokens, timeoutMs, thinking, numCtx) {
|
|
593
648
|
if (this.config.provider === "claude-cli") {
|
|
594
649
|
return this.completeClaude(model, prompt, timeoutMs);
|
|
595
650
|
}
|
|
596
651
|
if (this.config.provider === "ollama") {
|
|
597
|
-
return this.completeOllama(model, prompt, maxTokens, timeoutMs);
|
|
652
|
+
return this.completeOllama(model, prompt, maxTokens, timeoutMs, numCtx);
|
|
598
653
|
}
|
|
599
654
|
if (this.config.provider === "anthropic") {
|
|
600
655
|
return this.completeAnthropic(model, prompt, maxTokens, timeoutMs);
|
|
601
656
|
}
|
|
602
|
-
|
|
657
|
+
// thinking is threaded from each heavy phase (distill/reflect/classify), so
|
|
658
|
+
// the kwarg is emitted for those tiers on the openai-compat path; completeFast
|
|
659
|
+
// (scoring) never passes it (PR #227 review F1).
|
|
660
|
+
return this.completeOpenAiCompat(model, prompt, maxTokens, timeoutMs, thinking);
|
|
603
661
|
}
|
|
604
662
|
/**
|
|
605
663
|
* Claude CLI: shell out to `claude -p` for subscription users.
|
|
@@ -627,7 +685,7 @@ class LlmClient {
|
|
|
627
685
|
/**
|
|
628
686
|
* Ollama: use /api/generate with think:false (important for qwen3.5 models).
|
|
629
687
|
*/
|
|
630
|
-
async completeOllama(model, prompt, maxTokens, timeoutMs) {
|
|
688
|
+
async completeOllama(model, prompt, maxTokens, timeoutMs, numCtx) {
|
|
631
689
|
const url = `${this.config.baseUrl.replace(/\/$/, "")}/api/generate`;
|
|
632
690
|
// Ollama can take minutes to process large contexts — use streaming to avoid
|
|
633
691
|
// Node.js fetch headers timeout (default ~300s kills long Ollama inferences)
|
|
@@ -639,7 +697,14 @@ class LlmClient {
|
|
|
639
697
|
prompt,
|
|
640
698
|
stream: true,
|
|
641
699
|
think: false,
|
|
642
|
-
|
|
700
|
+
// num_ctx: threaded from completeFast (the fast/scoring tier) via the numCtx
|
|
701
|
+
// param. The fast tier passes the numCtx config key (default 2048; scoring
|
|
702
|
+
// prompts are ~850 tokens) to minimize the KV/prompt-cache footprint that, at
|
|
703
|
+
// 32768, accumulated past available RAM on memory-constrained boxes during long
|
|
704
|
+
// consolidations and swap-thrashed the nightly. The heavy tiers
|
|
705
|
+
// (completeDistill/Reflect/Classify) do NOT pass numCtx → default 32768, which
|
|
706
|
+
// preserves detectChunkSize's chunk-sizing (it packs ~60% of context per chunk).
|
|
707
|
+
options: { num_predict: maxTokens, num_ctx: numCtx ?? 32768 },
|
|
643
708
|
}),
|
|
644
709
|
signal: AbortSignal.timeout(timeoutMs),
|
|
645
710
|
});
|
|
@@ -711,7 +776,7 @@ class LlmClient {
|
|
|
711
776
|
/**
|
|
712
777
|
* OpenAI-compatible /v1/chat/completions (works for OpenAI, OpenRouter, etc).
|
|
713
778
|
*/
|
|
714
|
-
async completeOpenAiCompat(model, prompt, maxTokens, timeoutMs) {
|
|
779
|
+
async completeOpenAiCompat(model, prompt, maxTokens, timeoutMs, thinking) {
|
|
715
780
|
const baseUrl = this.config.baseUrl.replace(/\/$/, "");
|
|
716
781
|
// Some providers include the API version in the base URL already
|
|
717
782
|
const hasVersion = /\/v\d+\/?$/.test(baseUrl);
|
|
@@ -724,14 +789,24 @@ class LlmClient {
|
|
|
724
789
|
if (this.config.apiKey) {
|
|
725
790
|
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
726
791
|
}
|
|
792
|
+
// Qwen3 thinking mode: when on, the model can burn the whole token budget on
|
|
793
|
+
// an unclosed <think> block and emit nothing (probed 2026-08-04). `thinking`
|
|
794
|
+
// is threaded here ONLY from completeDistill, so the kwarg is emitted solely
|
|
795
|
+
// for the distill tier — reflect/classify/fast never send it (PR #227 F1).
|
|
796
|
+
// mlx-lm forwards chat_template_kwargs to the chat template (mirrors the
|
|
797
|
+
// ollama path's think:false). See #220.
|
|
798
|
+
const body = {
|
|
799
|
+
model,
|
|
800
|
+
messages: [{ role: "user", content: prompt }],
|
|
801
|
+
max_tokens: maxTokens,
|
|
802
|
+
};
|
|
803
|
+
if (thinking !== undefined) {
|
|
804
|
+
body.chat_template_kwargs = { enable_thinking: thinking };
|
|
805
|
+
}
|
|
727
806
|
const resp = await fetch(url, {
|
|
728
807
|
method: "POST",
|
|
729
808
|
headers,
|
|
730
|
-
body: JSON.stringify(
|
|
731
|
-
model,
|
|
732
|
-
messages: [{ role: "user", content: prompt }],
|
|
733
|
-
max_tokens: maxTokens,
|
|
734
|
-
}),
|
|
809
|
+
body: JSON.stringify(body),
|
|
735
810
|
signal: AbortSignal.timeout(timeoutMs),
|
|
736
811
|
});
|
|
737
812
|
if (resp.status === 429)
|
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
-
|
|
192
|
-
|
|
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,37 @@ 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;
|
|
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;
|
|
285
316
|
}
|
|
286
317
|
/** A config-owned life-sphere domain (see HicortexConfig.domains). */
|
|
287
318
|
export interface DomainDef {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamaze/hicortex",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.5",
|
|
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": {
|