@gamaze/hicortex 0.4.4 → 0.4.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/distiller.js +39 -15
- package/dist/index.js +1 -22
- package/dist/init.d.ts +1 -1
- package/dist/init.js +4 -4
- package/dist/llm.d.ts +27 -7
- package/dist/llm.js +63 -48
- package/dist/nightly.js +74 -5
- package/openclaw.plugin.json +2 -2
- package/package.json +1 -1
- package/skills/hicortex-learn/SKILL.md +2 -2
package/dist/distiller.js
CHANGED
|
@@ -205,15 +205,24 @@ async function distillSession(llm, conversation, projectName, date, chunkSizeCha
|
|
|
205
205
|
}
|
|
206
206
|
// Use provided chunk size or default to no chunking
|
|
207
207
|
const chunkSize = chunkSizeChars ?? MAX_TRANSCRIPT_CHARS;
|
|
208
|
-
// If transcript fits in one chunk, distill directly
|
|
208
|
+
// If transcript fits in one chunk, distill directly (errors propagate)
|
|
209
209
|
if (transcript.length <= chunkSize) {
|
|
210
210
|
return distillChunk(llm, transcript, projectName, date);
|
|
211
211
|
}
|
|
212
|
-
// Chunk large transcripts and distill each segment
|
|
212
|
+
// Chunk large transcripts and distill each segment.
|
|
213
|
+
//
|
|
214
|
+
// Partial success policy:
|
|
215
|
+
// - If SOME chunks succeed and SOME fail, return the partial results and
|
|
216
|
+
// log a warning. The caller gets *something* and can decide whether
|
|
217
|
+
// to count this as success.
|
|
218
|
+
// - If ALL chunks fail, throw — no useful output, and the caller needs
|
|
219
|
+
// to know this session hit a transient error.
|
|
213
220
|
const chunks = splitIntoChunks(transcript, chunkSize);
|
|
214
221
|
console.log(`[hicortex] Chunking ${transcript.length} chars into ${chunks.length} segments`);
|
|
215
222
|
const allEntries = [];
|
|
216
223
|
const seen = new Set();
|
|
224
|
+
let chunkFailures = 0;
|
|
225
|
+
let lastError = null;
|
|
217
226
|
for (let i = 0; i < chunks.length; i++) {
|
|
218
227
|
console.log(`[hicortex] Chunk ${i + 1}/${chunks.length} (${chunks[i].length} chars)`);
|
|
219
228
|
try {
|
|
@@ -230,30 +239,45 @@ async function distillSession(llm, conversation, projectName, date, chunkSizeCha
|
|
|
230
239
|
catch (err) {
|
|
231
240
|
const msg = err instanceof Error ? err.message : String(err);
|
|
232
241
|
console.error(`[hicortex] Chunk ${i + 1} failed: ${msg}`);
|
|
233
|
-
|
|
242
|
+
chunkFailures++;
|
|
243
|
+
lastError = err instanceof Error ? err : new Error(msg);
|
|
234
244
|
}
|
|
235
245
|
}
|
|
246
|
+
// If every chunk failed, the session wasn't actually processed. Throw so
|
|
247
|
+
// the nightly pipeline knows to retry this session next run.
|
|
248
|
+
if (chunkFailures === chunks.length) {
|
|
249
|
+
throw lastError ?? new Error("All distillation chunks failed");
|
|
250
|
+
}
|
|
251
|
+
if (chunkFailures > 0) {
|
|
252
|
+
console.warn(`[hicortex] Partial distillation: ${chunks.length - chunkFailures}/${chunks.length} chunks succeeded`);
|
|
253
|
+
}
|
|
236
254
|
return allEntries;
|
|
237
255
|
}
|
|
238
256
|
/**
|
|
239
257
|
* Distill a single chunk of conversation text.
|
|
258
|
+
*
|
|
259
|
+
* Behaviour contract:
|
|
260
|
+
* - Returns `[]` for legitimate empty results (NO_EXTRACT, empty LLM response,
|
|
261
|
+
* transcript produced no entries). These are terminal states — the chunk was
|
|
262
|
+
* processed successfully, there's just nothing worth keeping.
|
|
263
|
+
* - Throws for transient errors (LLM unreachable, HTTP 4xx/5xx, timeout, model
|
|
264
|
+
* not found, rate limit). These MUST propagate so the nightly pipeline can
|
|
265
|
+
* distinguish "nothing to extract" from "try again later" and avoid
|
|
266
|
+
* advancing the last-run watermark past sessions it never actually processed.
|
|
240
267
|
*/
|
|
241
268
|
async function distillChunk(llm, transcript, projectName, date) {
|
|
242
269
|
const prompt = (0, prompts_js_1.distillation)(projectName, date, transcript);
|
|
243
|
-
try
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
catch (err) {
|
|
253
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
254
|
-
console.error(`[hicortex] Distillation LLM error: ${msg}`);
|
|
270
|
+
// NOTE: Intentionally no try/catch here. Transient LLM errors (network
|
|
271
|
+
// failures, 4xx/5xx, model-not-found, timeouts) propagate up to the caller
|
|
272
|
+
// so the nightly pipeline can treat them as "retry later" instead of
|
|
273
|
+
// "processed successfully with zero extractions".
|
|
274
|
+
const result = await llm.completeDistill(prompt);
|
|
275
|
+
if (!result)
|
|
276
|
+
return [];
|
|
277
|
+
if (result === "NO_EXTRACT" || result.slice(0, 20).includes("NO_EXTRACT")) {
|
|
255
278
|
return [];
|
|
256
279
|
}
|
|
280
|
+
return parseDistilledEntries(result);
|
|
257
281
|
}
|
|
258
282
|
/**
|
|
259
283
|
* Split transcript text into chunks at natural boundaries (double newlines).
|
package/dist/index.js
CHANGED
|
@@ -466,28 +466,7 @@ async function autoConfigureLlm(pluginConfig, log) {
|
|
|
466
466
|
const msg = err instanceof Error ? err.message : String(err);
|
|
467
467
|
log(`[hicortex] LLM test failed (${llmConfig.baseUrl}): ${msg}`);
|
|
468
468
|
}
|
|
469
|
-
// Step 4:
|
|
470
|
-
if (llmConfig.provider === "zai") {
|
|
471
|
-
const altUrl = llmConfig.baseUrl.includes("/coding/")
|
|
472
|
-
? llmConfig.baseUrl.replace("/coding/", "/")
|
|
473
|
-
: llmConfig.baseUrl.replace("/api/paas/", "/api/coding/paas/");
|
|
474
|
-
log(`[hicortex] Trying alternate z.ai endpoint: ${altUrl}`);
|
|
475
|
-
llmConfig.baseUrl = altUrl;
|
|
476
|
-
const altClient = new llm_js_1.LlmClient(llmConfig);
|
|
477
|
-
try {
|
|
478
|
-
const response = await altClient.completeFast("Respond with just the word OK", 10);
|
|
479
|
-
if (response && response.length > 0) {
|
|
480
|
-
log(`[hicortex] LLM connection verified on alternate endpoint`);
|
|
481
|
-
persistProviderConfig(llmConfig, log);
|
|
482
|
-
return llmConfig;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
catch (err) {
|
|
486
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
487
|
-
log(`[hicortex] Alternate z.ai endpoint also failed: ${msg}`);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
// Step 5: Fall back — return the config anyway, log instructions
|
|
469
|
+
// Step 4: Fall back — return the config anyway, log instructions
|
|
491
470
|
log(`[hicortex] WARNING: Could not verify LLM connection. ` +
|
|
492
471
|
`Distillation and consolidation may fail. ` +
|
|
493
472
|
`To fix: add models.providers.${llmConfig.provider}.baseUrl to ~/.openclaw/openclaw.json ` +
|
package/dist/init.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Detection:
|
|
5
5
|
* 1. Local HC server running (localhost:8787)
|
|
6
|
-
* 2. Remote HC server (HICORTEX_SERVER_URL
|
|
6
|
+
* 2. Remote HC server (HICORTEX_SERVER_URL — any reachable host:port)
|
|
7
7
|
* 3. OC plugin installed (~/.openclaw/openclaw.json)
|
|
8
8
|
* 4. CC MCP already registered (~/.claude/settings.json)
|
|
9
9
|
* 5. Existing DB (~/.hicortex/ or ~/.openclaw/data/)
|
package/dist/init.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Detection:
|
|
6
6
|
* 1. Local HC server running (localhost:8787)
|
|
7
|
-
* 2. Remote HC server (HICORTEX_SERVER_URL
|
|
7
|
+
* 2. Remote HC server (HICORTEX_SERVER_URL — any reachable host:port)
|
|
8
8
|
* 3. OC plugin installed (~/.openclaw/openclaw.json)
|
|
9
9
|
* 4. CC MCP already registered (~/.claude/settings.json)
|
|
10
10
|
* 5. Existing DB (~/.hicortex/ or ~/.openclaw/data/)
|
|
@@ -185,10 +185,10 @@ When invoked with \`/learn <text>\`, store the learning in long-term memory via
|
|
|
185
185
|
|
|
186
186
|
## Example
|
|
187
187
|
|
|
188
|
-
\`/learn
|
|
188
|
+
\`/learn always check provider docs before assuming an API uses the same auth scheme as OpenAI\`
|
|
189
189
|
|
|
190
190
|
Becomes a call to hicortex_ingest with:
|
|
191
|
-
- content: "LEARNING:
|
|
191
|
+
- content: "LEARNING: always check provider docs before assuming an API uses the same auth scheme as OpenAI — header names and token formats vary widely (Bearer vs x-api-key vs custom)."
|
|
192
192
|
- memory_type: "lesson"
|
|
193
193
|
`;
|
|
194
194
|
const learnPath = (0, node_path_1.join)(CC_COMMANDS_DIR, "learn.md");
|
|
@@ -402,7 +402,7 @@ async function persistLlmConfig() {
|
|
|
402
402
|
options.push({
|
|
403
403
|
label: "Other provider (requires API key)",
|
|
404
404
|
save: async () => {
|
|
405
|
-
console.log("\n Providers: Anthropic, OpenAI, Google,
|
|
405
|
+
console.log("\n Providers: Anthropic, OpenAI, Google, OpenRouter, or any OpenAI-compatible endpoint");
|
|
406
406
|
const baseUrl = await ask(" Provider base URL: ");
|
|
407
407
|
if (!baseUrl) {
|
|
408
408
|
console.log(" ⚠ Cancelled.");
|
package/dist/llm.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Multi-provider LLM client for consolidation and distillation.
|
|
3
|
-
* Ported from hicortex/consolidate/llm.py.
|
|
4
3
|
*
|
|
5
4
|
* Resolution for OC adapter (resolveLlmConfig):
|
|
6
5
|
* 1. Plugin config (llmBaseUrl, llmApiKey, llmModel)
|
|
@@ -10,12 +9,14 @@
|
|
|
10
9
|
*
|
|
11
10
|
* Resolution for CC adapter (resolveLlmConfigForCC):
|
|
12
11
|
* 1. Explicit env vars (HICORTEX_LLM_BASE_URL + HICORTEX_LLM_API_KEY + HICORTEX_LLM_MODEL)
|
|
13
|
-
* 2. ANTHROPIC_API_KEY → Haiku (cheap, CC users always have this)
|
|
14
|
-
* 3. OPENAI_API_KEY → gpt-
|
|
15
|
-
* 4. GOOGLE_API_KEY → gemini-2.
|
|
16
|
-
* 5.
|
|
12
|
+
* 2. ANTHROPIC_API_KEY → Claude Haiku (cheap, CC users always have this)
|
|
13
|
+
* 3. OPENAI_API_KEY → gpt-5.4-nano
|
|
14
|
+
* 4. GOOGLE_API_KEY → gemini-2.5-flash
|
|
15
|
+
* 5. Claude CLI fallback (uses subscription, no API key needed)
|
|
16
|
+
* 6. Fallback: Ollama at http://localhost:11434
|
|
17
17
|
*
|
|
18
|
-
* Supports
|
|
18
|
+
* Supports any OpenAI-compatible endpoint plus first-class support for
|
|
19
|
+
* OpenAI, Anthropic, Google, Ollama, OpenRouter, and Claude CLI.
|
|
19
20
|
*/
|
|
20
21
|
export interface LlmConfig {
|
|
21
22
|
baseUrl: string;
|
|
@@ -68,6 +69,25 @@ export declare function claudeCliConfig(claudePath: string): LlmConfig;
|
|
|
68
69
|
* Returns the model name if available, null otherwise.
|
|
69
70
|
*/
|
|
70
71
|
export declare function probeOllama(baseUrl?: string): Promise<string | null>;
|
|
72
|
+
/**
|
|
73
|
+
* Pre-flight health check for a specific Ollama endpoint + model.
|
|
74
|
+
* Returns { ok, reason } so callers can log a clear abort message.
|
|
75
|
+
*
|
|
76
|
+
* - `ok: true` — endpoint reachable AND the requested model appears in
|
|
77
|
+
* `/api/tags`. Safe to proceed with a batch distillation run.
|
|
78
|
+
* - `ok: false, reason: "unreachable"` — network failure or non-2xx.
|
|
79
|
+
* - `ok: false, reason: "model_missing"` — endpoint is up but the
|
|
80
|
+
* model isn't listed (the exact case that caused data loss when
|
|
81
|
+
* mhac-pro's Ollama didn't have the distill model loaded).
|
|
82
|
+
*
|
|
83
|
+
* Matches on exact name OR name prefix ("qwen3.5:35b" matches "qwen3.5:35b-a3b").
|
|
84
|
+
*/
|
|
85
|
+
export declare function probeOllamaModel(baseUrl: string, modelName: string): Promise<{
|
|
86
|
+
ok: true;
|
|
87
|
+
} | {
|
|
88
|
+
ok: false;
|
|
89
|
+
reason: "unreachable" | "model_missing";
|
|
90
|
+
}>;
|
|
71
91
|
/**
|
|
72
92
|
* For batch operations (nightly pipeline), prefer Ollama when available.
|
|
73
93
|
* Claude CLI has strict rate limits that kill batch distillation.
|
|
@@ -116,7 +136,7 @@ export declare class LlmClient {
|
|
|
116
136
|
*/
|
|
117
137
|
private completeOllama;
|
|
118
138
|
/**
|
|
119
|
-
* Anthropic Messages API (/v1/messages).
|
|
139
|
+
* Anthropic Messages API (/v1/messages).
|
|
120
140
|
* Auth via x-api-key header.
|
|
121
141
|
*/
|
|
122
142
|
private completeAnthropic;
|
package/dist/llm.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* Multi-provider LLM client for consolidation and distillation.
|
|
4
|
-
* Ported from hicortex/consolidate/llm.py.
|
|
5
4
|
*
|
|
6
5
|
* Resolution for OC adapter (resolveLlmConfig):
|
|
7
6
|
* 1. Plugin config (llmBaseUrl, llmApiKey, llmModel)
|
|
@@ -11,12 +10,14 @@
|
|
|
11
10
|
*
|
|
12
11
|
* Resolution for CC adapter (resolveLlmConfigForCC):
|
|
13
12
|
* 1. Explicit env vars (HICORTEX_LLM_BASE_URL + HICORTEX_LLM_API_KEY + HICORTEX_LLM_MODEL)
|
|
14
|
-
* 2. ANTHROPIC_API_KEY → Haiku (cheap, CC users always have this)
|
|
15
|
-
* 3. OPENAI_API_KEY → gpt-
|
|
16
|
-
* 4. GOOGLE_API_KEY → gemini-2.
|
|
17
|
-
* 5.
|
|
13
|
+
* 2. ANTHROPIC_API_KEY → Claude Haiku (cheap, CC users always have this)
|
|
14
|
+
* 3. OPENAI_API_KEY → gpt-5.4-nano
|
|
15
|
+
* 4. GOOGLE_API_KEY → gemini-2.5-flash
|
|
16
|
+
* 5. Claude CLI fallback (uses subscription, no API key needed)
|
|
17
|
+
* 6. Fallback: Ollama at http://localhost:11434
|
|
18
18
|
*
|
|
19
|
-
* Supports
|
|
19
|
+
* Supports any OpenAI-compatible endpoint plus first-class support for
|
|
20
|
+
* OpenAI, Anthropic, Google, Ollama, OpenRouter, and Claude CLI.
|
|
20
21
|
*/
|
|
21
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
23
|
exports.LlmClient = exports.RateLimitError = void 0;
|
|
@@ -25,6 +26,7 @@ exports.resolveLlmConfigForCC = resolveLlmConfigForCC;
|
|
|
25
26
|
exports.findClaudeBinary = findClaudeBinary;
|
|
26
27
|
exports.claudeCliConfig = claudeCliConfig;
|
|
27
28
|
exports.probeOllama = probeOllama;
|
|
29
|
+
exports.probeOllamaModel = probeOllamaModel;
|
|
28
30
|
exports.preferOllamaForBatch = preferOllamaForBatch;
|
|
29
31
|
const node_fs_1 = require("node:fs");
|
|
30
32
|
const node_path_1 = require("node:path");
|
|
@@ -76,7 +78,7 @@ function resolveLlmConfigForCC(overrides) {
|
|
|
76
78
|
baseUrl: overrides.llmBaseUrl,
|
|
77
79
|
apiKey: overrides.llmApiKey,
|
|
78
80
|
model: overrides.llmModel ?? "claude-haiku-4-5-20251001",
|
|
79
|
-
reflectModel: overrides.reflectModel ?? overrides.llmModel ?? "claude-sonnet-4-
|
|
81
|
+
reflectModel: overrides.reflectModel ?? overrides.llmModel ?? "claude-sonnet-4-6",
|
|
80
82
|
provider,
|
|
81
83
|
};
|
|
82
84
|
}
|
|
@@ -90,7 +92,7 @@ function resolveLlmConfigForCC(overrides) {
|
|
|
90
92
|
baseUrl: hcBaseUrl,
|
|
91
93
|
apiKey: hcApiKey,
|
|
92
94
|
model: hcModel ?? "claude-haiku-4-5-20251001",
|
|
93
|
-
reflectModel: process.env.HICORTEX_REFLECT_MODEL ?? hcModel ?? "claude-sonnet-4-
|
|
95
|
+
reflectModel: process.env.HICORTEX_REFLECT_MODEL ?? hcModel ?? "claude-sonnet-4-6",
|
|
94
96
|
provider,
|
|
95
97
|
};
|
|
96
98
|
}
|
|
@@ -101,7 +103,7 @@ function resolveLlmConfigForCC(overrides) {
|
|
|
101
103
|
baseUrl: process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com",
|
|
102
104
|
apiKey: anthropicKey,
|
|
103
105
|
model: "claude-haiku-4-5-20251001",
|
|
104
|
-
reflectModel: "claude-sonnet-4-
|
|
106
|
+
reflectModel: "claude-sonnet-4-6",
|
|
105
107
|
provider: "anthropic",
|
|
106
108
|
};
|
|
107
109
|
}
|
|
@@ -111,8 +113,8 @@ function resolveLlmConfigForCC(overrides) {
|
|
|
111
113
|
return {
|
|
112
114
|
baseUrl,
|
|
113
115
|
apiKey: openaiKey,
|
|
114
|
-
model: "gpt-
|
|
115
|
-
reflectModel: "gpt-
|
|
116
|
+
model: "gpt-5.4-nano",
|
|
117
|
+
reflectModel: "gpt-5.4-nano",
|
|
116
118
|
provider: detectProvider(baseUrl),
|
|
117
119
|
};
|
|
118
120
|
}
|
|
@@ -121,8 +123,8 @@ function resolveLlmConfigForCC(overrides) {
|
|
|
121
123
|
return {
|
|
122
124
|
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
123
125
|
apiKey: googleKey,
|
|
124
|
-
model: "gemini-2.
|
|
125
|
-
reflectModel: "gemini-2.
|
|
126
|
+
model: "gemini-2.5-flash",
|
|
127
|
+
reflectModel: "gemini-2.5-flash",
|
|
126
128
|
provider: "google",
|
|
127
129
|
};
|
|
128
130
|
}
|
|
@@ -150,8 +152,6 @@ function detectProvider(url) {
|
|
|
150
152
|
return "openrouter";
|
|
151
153
|
if (u.includes("googleapis") || u.includes("generativelanguage"))
|
|
152
154
|
return "google";
|
|
153
|
-
if (u.includes("z.ai") || u.includes("zai"))
|
|
154
|
-
return "zai";
|
|
155
155
|
return "openai";
|
|
156
156
|
}
|
|
157
157
|
function readOpenClawConfig() {
|
|
@@ -162,7 +162,7 @@ function readOpenClawConfig() {
|
|
|
162
162
|
const primary = config?.agents?.defaults?.model?.primary;
|
|
163
163
|
if (!primary)
|
|
164
164
|
return null;
|
|
165
|
-
// primary format is "provider/model" (e.g. "
|
|
165
|
+
// primary format is "provider/model" (e.g. "openai/gpt-5", "anthropic/claude-sonnet-4-6")
|
|
166
166
|
if (typeof primary === "string" && (primary.includes("/") || primary.includes(":"))) {
|
|
167
167
|
const sep = primary.includes("/") ? "/" : ":";
|
|
168
168
|
const [providerHint, ...rest] = primary.split(sep);
|
|
@@ -216,7 +216,7 @@ function readOcAuthKey(provider) {
|
|
|
216
216
|
const raw = (0, node_fs_1.readFileSync)(authPath, "utf-8");
|
|
217
217
|
const auth = JSON.parse(raw);
|
|
218
218
|
const profiles = auth?.profiles ?? {};
|
|
219
|
-
// Look for a profile matching the provider (e.g. "
|
|
219
|
+
// Look for a profile matching the provider (e.g. "openai:default")
|
|
220
220
|
for (const [profileId, profile] of Object.entries(profiles)) {
|
|
221
221
|
const p = profile;
|
|
222
222
|
if (p?.provider === provider ||
|
|
@@ -245,8 +245,8 @@ function resolveFromEnv() {
|
|
|
245
245
|
return {
|
|
246
246
|
baseUrl,
|
|
247
247
|
apiKey: openaiKey,
|
|
248
|
-
model: process.env.OPENAI_MODEL ?? "gpt-
|
|
249
|
-
reflectModel: process.env.OPENAI_MODEL ?? "gpt-
|
|
248
|
+
model: process.env.OPENAI_MODEL ?? "gpt-5.4-nano",
|
|
249
|
+
reflectModel: process.env.OPENAI_MODEL ?? "gpt-5.4-nano",
|
|
250
250
|
provider,
|
|
251
251
|
};
|
|
252
252
|
}
|
|
@@ -255,8 +255,8 @@ function resolveFromEnv() {
|
|
|
255
255
|
return {
|
|
256
256
|
baseUrl: process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com",
|
|
257
257
|
apiKey: anthropicKey,
|
|
258
|
-
model: "claude-sonnet-4-
|
|
259
|
-
reflectModel: "claude-sonnet-4-
|
|
258
|
+
model: "claude-sonnet-4-6",
|
|
259
|
+
reflectModel: "claude-sonnet-4-6",
|
|
260
260
|
provider: "anthropic",
|
|
261
261
|
};
|
|
262
262
|
}
|
|
@@ -265,8 +265,8 @@ function resolveFromEnv() {
|
|
|
265
265
|
return {
|
|
266
266
|
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
267
267
|
apiKey: googleKey,
|
|
268
|
-
model: "gemini-2.
|
|
269
|
-
reflectModel: "gemini-2.
|
|
268
|
+
model: "gemini-2.5-flash",
|
|
269
|
+
reflectModel: "gemini-2.5-flash",
|
|
270
270
|
provider: "google",
|
|
271
271
|
};
|
|
272
272
|
}
|
|
@@ -280,33 +280,23 @@ function getEnvKeyForProvider(provider) {
|
|
|
280
280
|
return process.env.ANTHROPIC_API_KEY;
|
|
281
281
|
case "google":
|
|
282
282
|
return process.env.GOOGLE_API_KEY;
|
|
283
|
-
case "zai":
|
|
284
|
-
return process.env.ZAI_API_KEY ?? process.env.LLM_API_KEY;
|
|
285
283
|
default:
|
|
286
284
|
return undefined;
|
|
287
285
|
}
|
|
288
286
|
}
|
|
289
|
-
/**
|
|
287
|
+
/**
|
|
288
|
+
* Default base URLs for first-class supported providers.
|
|
289
|
+
*
|
|
290
|
+
* For any other provider, set llmBaseUrl explicitly in your config or use
|
|
291
|
+
* an OpenAI-compatible endpoint. The detectProvider() function will treat
|
|
292
|
+
* unknown URLs as openai-compatible by default.
|
|
293
|
+
*/
|
|
290
294
|
const PROVIDER_BASE_URLS = {
|
|
291
295
|
openai: "https://api.openai.com/v1",
|
|
292
296
|
anthropic: "https://api.anthropic.com",
|
|
293
297
|
google: "https://generativelanguage.googleapis.com/v1beta",
|
|
294
298
|
ollama: "http://localhost:11434",
|
|
295
299
|
openrouter: "https://openrouter.ai/api",
|
|
296
|
-
zai: "https://api.z.ai/api/anthropic",
|
|
297
|
-
groq: "https://api.groq.com/openai/v1",
|
|
298
|
-
deepseek: "https://api.deepseek.com",
|
|
299
|
-
mistral: "https://api.mistral.ai/v1",
|
|
300
|
-
together: "https://api.together.xyz/v1",
|
|
301
|
-
perplexity: "https://api.perplexity.ai",
|
|
302
|
-
nvidia: "https://integrate.api.nvidia.com/v1",
|
|
303
|
-
xai: "https://api.x.ai/v1",
|
|
304
|
-
venice: "https://api.venice.ai/api/v1",
|
|
305
|
-
minimax: "https://api.minimaxi.com/v1",
|
|
306
|
-
moonshot: "https://api.moonshot.ai/v1",
|
|
307
|
-
kimi: "https://api.kimi.com/coding",
|
|
308
|
-
chutes: "https://api.chutes.ai",
|
|
309
|
-
kilo: "https://api.kilo.ai/api/gateway",
|
|
310
300
|
};
|
|
311
301
|
function getDefaultUrlForProvider(provider) {
|
|
312
302
|
return PROVIDER_BASE_URLS[provider.toLowerCase()] ?? "https://api.openai.com/v1";
|
|
@@ -378,6 +368,35 @@ async function probeOllama(baseUrl = "http://localhost:11434") {
|
|
|
378
368
|
return null;
|
|
379
369
|
}
|
|
380
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Pre-flight health check for a specific Ollama endpoint + model.
|
|
373
|
+
* Returns { ok, reason } so callers can log a clear abort message.
|
|
374
|
+
*
|
|
375
|
+
* - `ok: true` — endpoint reachable AND the requested model appears in
|
|
376
|
+
* `/api/tags`. Safe to proceed with a batch distillation run.
|
|
377
|
+
* - `ok: false, reason: "unreachable"` — network failure or non-2xx.
|
|
378
|
+
* - `ok: false, reason: "model_missing"` — endpoint is up but the
|
|
379
|
+
* model isn't listed (the exact case that caused data loss when
|
|
380
|
+
* mhac-pro's Ollama didn't have the distill model loaded).
|
|
381
|
+
*
|
|
382
|
+
* Matches on exact name OR name prefix ("qwen3.5:35b" matches "qwen3.5:35b-a3b").
|
|
383
|
+
*/
|
|
384
|
+
async function probeOllamaModel(baseUrl, modelName) {
|
|
385
|
+
try {
|
|
386
|
+
const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/api/tags`, {
|
|
387
|
+
signal: AbortSignal.timeout(5000),
|
|
388
|
+
});
|
|
389
|
+
if (!resp.ok)
|
|
390
|
+
return { ok: false, reason: "unreachable" };
|
|
391
|
+
const data = (await resp.json());
|
|
392
|
+
const models = data.models ?? [];
|
|
393
|
+
const found = models.some((m) => m.name === modelName || m.name.startsWith(modelName + ":"));
|
|
394
|
+
return found ? { ok: true } : { ok: false, reason: "model_missing" };
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
return { ok: false, reason: "unreachable" };
|
|
398
|
+
}
|
|
399
|
+
}
|
|
381
400
|
/**
|
|
382
401
|
* For batch operations (nightly pipeline), prefer Ollama when available.
|
|
383
402
|
* Claude CLI has strict rate limits that kill batch distillation.
|
|
@@ -505,7 +524,7 @@ class LlmClient {
|
|
|
505
524
|
if (this.config.provider === "ollama") {
|
|
506
525
|
return this.completeOllama(model, prompt, maxTokens, timeoutMs);
|
|
507
526
|
}
|
|
508
|
-
if (this.config.provider === "anthropic"
|
|
527
|
+
if (this.config.provider === "anthropic") {
|
|
509
528
|
return this.completeAnthropic(model, prompt, maxTokens, timeoutMs);
|
|
510
529
|
}
|
|
511
530
|
return this.completeOpenAiCompat(model, prompt, maxTokens, timeoutMs);
|
|
@@ -586,7 +605,7 @@ class LlmClient {
|
|
|
586
605
|
return result.trim();
|
|
587
606
|
}
|
|
588
607
|
/**
|
|
589
|
-
* Anthropic Messages API (/v1/messages).
|
|
608
|
+
* Anthropic Messages API (/v1/messages).
|
|
590
609
|
* Auth via x-api-key header.
|
|
591
610
|
*/
|
|
592
611
|
async completeAnthropic(model, prompt, maxTokens, timeoutMs) {
|
|
@@ -622,8 +641,8 @@ class LlmClient {
|
|
|
622
641
|
*/
|
|
623
642
|
async completeOpenAiCompat(model, prompt, maxTokens, timeoutMs) {
|
|
624
643
|
const baseUrl = this.config.baseUrl.replace(/\/$/, "");
|
|
625
|
-
// Some providers
|
|
626
|
-
const hasVersion = /\/v\d+\/?$/.test(baseUrl)
|
|
644
|
+
// Some providers include the API version in the base URL already
|
|
645
|
+
const hasVersion = /\/v\d+\/?$/.test(baseUrl);
|
|
627
646
|
const url = hasVersion
|
|
628
647
|
? `${baseUrl}/chat/completions`
|
|
629
648
|
: `${baseUrl}/v1/chat/completions`;
|
|
@@ -647,10 +666,6 @@ class LlmClient {
|
|
|
647
666
|
this.handleRateLimit(resp);
|
|
648
667
|
if (!resp.ok) {
|
|
649
668
|
const text = await resp.text().catch(() => "");
|
|
650
|
-
// z.ai: "Insufficient balance" likely means wrong endpoint (coding vs paas)
|
|
651
|
-
if (text.includes("1113") || text.includes("Insufficient balance")) {
|
|
652
|
-
console.log(`[hicortex] LLM billing error. Check that llmBaseUrl matches your plan. Current: ${baseUrl}`);
|
|
653
|
-
}
|
|
654
669
|
throw new Error(`LLM API error ${resp.status}: ${text}`);
|
|
655
670
|
}
|
|
656
671
|
const data = (await resp.json());
|
package/dist/nightly.js
CHANGED
|
@@ -182,6 +182,24 @@ async function runNightly(options = {}) {
|
|
|
182
182
|
}
|
|
183
183
|
// Step 2: Distill each session
|
|
184
184
|
let memoriesIngested = 0;
|
|
185
|
+
let hadTransientFailure = false;
|
|
186
|
+
// Pre-flight health check for a remote distill endpoint.
|
|
187
|
+
// If the distill provider is Ollama on a remote host and that host (or the
|
|
188
|
+
// required model) is unreachable, abort BEFORE touching any sessions —
|
|
189
|
+
// prevents the data-loss bug where lastRun advances past sessions that
|
|
190
|
+
// were never actually processed.
|
|
191
|
+
if (batches.length > 0 && llmConfig.distillBaseUrl && (llmConfig.distillProvider ?? llmConfig.provider) === "ollama") {
|
|
192
|
+
const distillModel = llmConfig.distillModel ?? llmConfig.model;
|
|
193
|
+
const health = await (0, llm_js_1.probeOllamaModel)(llmConfig.distillBaseUrl, distillModel);
|
|
194
|
+
if (!health.ok) {
|
|
195
|
+
const reason = health.reason === "unreachable"
|
|
196
|
+
? `distill endpoint unreachable (${llmConfig.distillBaseUrl})`
|
|
197
|
+
: `distill model not loaded (${distillModel} missing on ${llmConfig.distillBaseUrl})`;
|
|
198
|
+
console.error(`[hicortex] ABORT: ${reason} — will retry next run, lastRun unchanged`);
|
|
199
|
+
hadTransientFailure = true;
|
|
200
|
+
batches.length = 0; // Skip the distillation loop entirely
|
|
201
|
+
}
|
|
202
|
+
}
|
|
185
203
|
// Detect safe chunk size based on model context window
|
|
186
204
|
const chunkSize = await (0, distiller_js_1.detectChunkSize)(llmConfig.provider, llmConfig.distillModel ?? llmConfig.model, llmConfig.baseUrl);
|
|
187
205
|
for (const batch of batches) {
|
|
@@ -190,6 +208,20 @@ async function runNightly(options = {}) {
|
|
|
190
208
|
console.log(`[hicortex] Skip ${batch.sessionId.slice(0, 8)} (${batch.projectName}): too short`);
|
|
191
209
|
continue;
|
|
192
210
|
}
|
|
211
|
+
// Server-mode per-session dedup: skip sessions already in the DB.
|
|
212
|
+
// Client mode gets this for free via the server's /ingest endpoint;
|
|
213
|
+
// server mode writes directly via storage.insertMemory and needs
|
|
214
|
+
// an explicit check. This makes retries of previously-failed runs
|
|
215
|
+
// idempotent.
|
|
216
|
+
if (!dryRun) {
|
|
217
|
+
const existing = db
|
|
218
|
+
.prepare("SELECT COUNT(*) as c FROM memories WHERE source_session = ?")
|
|
219
|
+
.get(batch.sessionId);
|
|
220
|
+
if (existing.c > 0) {
|
|
221
|
+
console.log(`[hicortex] Skip ${batch.sessionId.slice(0, 8)} (${batch.projectName}): already ingested`);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
193
225
|
console.log(`[hicortex] Distilling ${batch.sessionId.slice(0, 8)} (${batch.projectName}, ${batch.date})`);
|
|
194
226
|
if (dryRun) {
|
|
195
227
|
console.log(`[hicortex] [dry-run] Would distill ${transcript.length} chars`);
|
|
@@ -224,7 +256,8 @@ async function runNightly(options = {}) {
|
|
|
224
256
|
}
|
|
225
257
|
catch (err) {
|
|
226
258
|
const msg = err instanceof Error ? err.message : String(err);
|
|
227
|
-
console.error(`[hicortex] Distillation failed: ${msg}`);
|
|
259
|
+
console.error(`[hicortex] Distillation failed: ${msg} — will retry next run`);
|
|
260
|
+
hadTransientFailure = true;
|
|
228
261
|
}
|
|
229
262
|
}
|
|
230
263
|
console.log(`[hicortex] Distillation complete: ${memoriesIngested} new memories`);
|
|
@@ -241,8 +274,17 @@ async function runNightly(options = {}) {
|
|
|
241
274
|
console.log(`[hicortex] CLAUDE.md updated: ${injection.lessonsCount} lessons at ${injection.path}`);
|
|
242
275
|
}
|
|
243
276
|
// Step 5: Update last-run timestamp
|
|
277
|
+
// CRITICAL: only advance lastRun if every session was processed without
|
|
278
|
+
// a transient failure. Otherwise failed sessions would be permanently
|
|
279
|
+
// lost — they'd be older than the new lastRun and never retried.
|
|
244
280
|
if (!dryRun) {
|
|
245
|
-
|
|
281
|
+
if (hadTransientFailure) {
|
|
282
|
+
console.warn(`[hicortex] Not advancing lastRun — one or more sessions failed. ` +
|
|
283
|
+
`They will be retried on the next run.`);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
writeLastRun();
|
|
287
|
+
}
|
|
246
288
|
}
|
|
247
289
|
console.log(`[hicortex] Nightly pipeline complete.`);
|
|
248
290
|
}
|
|
@@ -329,6 +371,22 @@ async function runClientNightly(config, dryRun) {
|
|
|
329
371
|
writeLastRun();
|
|
330
372
|
return;
|
|
331
373
|
}
|
|
374
|
+
// Pre-flight health check for a remote distill endpoint (client mode).
|
|
375
|
+
// If the distill provider is Ollama on a remote host and the required model
|
|
376
|
+
// isn't loaded, abort BEFORE touching any sessions — same data-loss fix
|
|
377
|
+
// as server mode.
|
|
378
|
+
let hadTransientFailure = false;
|
|
379
|
+
if (llmConfig.distillBaseUrl && (llmConfig.distillProvider ?? llmConfig.provider) === "ollama") {
|
|
380
|
+
const distillModel = llmConfig.distillModel ?? llmConfig.model;
|
|
381
|
+
const health = await (0, llm_js_1.probeOllamaModel)(llmConfig.distillBaseUrl, distillModel);
|
|
382
|
+
if (!health.ok) {
|
|
383
|
+
const reason = health.reason === "unreachable"
|
|
384
|
+
? `distill endpoint unreachable (${llmConfig.distillBaseUrl})`
|
|
385
|
+
: `distill model not loaded (${distillModel} missing on ${llmConfig.distillBaseUrl})`;
|
|
386
|
+
console.error(`[hicortex] ABORT: ${reason} — will retry next run, lastRun unchanged`);
|
|
387
|
+
return; // Don't touch lastRun; next trigger retries the same sessions
|
|
388
|
+
}
|
|
389
|
+
}
|
|
332
390
|
// Distill each session and POST to server
|
|
333
391
|
let memoriesIngested = 0;
|
|
334
392
|
let sessionsSent = 0;
|
|
@@ -388,6 +446,7 @@ async function runClientNightly(config, dryRun) {
|
|
|
388
446
|
}
|
|
389
447
|
else {
|
|
390
448
|
console.error(`[hicortex] Ingest failed (${resp.status}): ${result.error}`);
|
|
449
|
+
hadTransientFailure = true;
|
|
391
450
|
}
|
|
392
451
|
}
|
|
393
452
|
if (sessionCount > 0) {
|
|
@@ -396,7 +455,8 @@ async function runClientNightly(config, dryRun) {
|
|
|
396
455
|
}
|
|
397
456
|
}
|
|
398
457
|
catch (err) {
|
|
399
|
-
console.error(`[hicortex]
|
|
458
|
+
console.error(`[hicortex] Distillation failed: ${err instanceof Error ? err.message : String(err)} — will retry next run`);
|
|
459
|
+
hadTransientFailure = true;
|
|
400
460
|
}
|
|
401
461
|
}
|
|
402
462
|
// Inject lessons from server into CLAUDE.md
|
|
@@ -408,8 +468,17 @@ async function runClientNightly(config, dryRun) {
|
|
|
408
468
|
console.error(`[hicortex] CLAUDE.md injection failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
409
469
|
}
|
|
410
470
|
}
|
|
411
|
-
if
|
|
412
|
-
|
|
471
|
+
// Only advance lastRun if every session was processed without a transient
|
|
472
|
+
// failure. Otherwise failed sessions would be permanently lost.
|
|
473
|
+
if (!dryRun) {
|
|
474
|
+
if (hadTransientFailure) {
|
|
475
|
+
console.warn(`[hicortex] Not advancing lastRun — one or more sessions failed. ` +
|
|
476
|
+
`They will be retried on the next run.`);
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
writeLastRun();
|
|
480
|
+
}
|
|
481
|
+
}
|
|
413
482
|
console.log(`[hicortex] Client nightly complete: ${memoriesIngested} memories from ${sessionsSent} sessions → ${serverUrl}`);
|
|
414
483
|
}
|
|
415
484
|
/**
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "hicortex",
|
|
3
3
|
"name": "Hicortex — Long-term Memory That Learns",
|
|
4
4
|
"description": "Your agents remember past decisions, avoid repeated mistakes, and get smarter every day. Nightly reflection generates actionable lessons that automatically update agent behavior.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.4.6",
|
|
6
6
|
"kind": "lifecycle",
|
|
7
7
|
"skills": ["./skills/hicortex-memory", "./skills/hicortex-learn", "./skills/hicortex-activate"],
|
|
8
8
|
"configSchema": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"properties": {
|
|
11
11
|
"licenseKey": {
|
|
12
12
|
"type": "string",
|
|
13
|
-
"description": "Hicortex license key (hctx-...). Leave empty for free tier (
|
|
13
|
+
"description": "Hicortex license key (hctx-...). Leave empty for free tier (250 memory cap)."
|
|
14
14
|
},
|
|
15
15
|
"llmBaseUrl": {
|
|
16
16
|
"type": "string",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamaze/hicortex",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "Human-like memory for self-improving AI agents. Automatic capturing, nightly reflection, and cross-agent learning. Works with Claude Code and OpenClaw.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -23,12 +23,12 @@ When invoked with `/learn <text>`, store the learning in long-term memory via hi
|
|
|
23
23
|
## Example
|
|
24
24
|
|
|
25
25
|
```
|
|
26
|
-
/learn
|
|
26
|
+
/learn always check provider docs before assuming an API uses the same auth scheme as OpenAI
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
Becomes:
|
|
30
30
|
```
|
|
31
|
-
hicortex_ingest(content="LEARNING:
|
|
31
|
+
hicortex_ingest(content="LEARNING: always check provider docs before assuming an API uses the same auth scheme as OpenAI — header names and token formats vary widely (Bearer vs x-api-key vs custom). (2026-04-07)", project="global", memory_type="lesson")
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
## Rules
|