@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 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
- // Continue with remaining chunks — partial extraction is better than none
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
- const result = await llm.completeDistill(prompt);
245
- if (!result)
246
- return [];
247
- if (result === "NO_EXTRACT" || result.slice(0, 20).includes("NO_EXTRACT")) {
248
- return [];
249
- }
250
- return parseDistilledEntries(result);
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: If test failed and this is z.ai, try the alternate endpoint
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 or bedrock:8787)
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 or bedrock:8787)
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 z.ai API uses Bearer auth on all endpoints, not x-api-key\`
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: z.ai API uses Bearer auth on all three endpoints (paas, coding, anthropic). Not x-api-key. (2026-03-26)"
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, z.ai, OpenRouter, or any OpenAI-compatible");
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-4o-mini
15
- * 4. GOOGLE_API_KEY → gemini-2.0-flash
16
- * 5. Fallback: Ollama at http://localhost:11434
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: OpenAI, Anthropic, Google, OpenRouter, Ollama, z.ai, and 15+ more
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). Used for Anthropic and z.ai.
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-4o-mini
16
- * 4. GOOGLE_API_KEY → gemini-2.0-flash
17
- * 5. Fallback: Ollama at http://localhost:11434
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: OpenAI, Anthropic, Google, OpenRouter, Ollama, z.ai, and 15+ more
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-5-20250514",
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-5-20250514",
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-5-20250514",
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-4o-mini",
115
- reflectModel: "gpt-4o-mini",
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.0-flash",
125
- reflectModel: "gemini-2.0-flash",
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. "zai/glm-5-turbo", "openai/gpt-4o")
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. "zai:default")
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-4o-mini",
249
- reflectModel: process.env.OPENAI_MODEL ?? "gpt-4o-mini",
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-20250514",
259
- reflectModel: "claude-sonnet-4-20250514",
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.0-flash",
269
- reflectModel: "gemini-2.0-flash",
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
- /** Default base URLs for all OC-supported providers (from OC gateway binary). */
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" || this.config.provider === "zai") {
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). Used for Anthropic and z.ai.
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 (z.ai) include version in base URL already
626
- const hasVersion = /\/v\d+\/?$/.test(baseUrl) || baseUrl.includes("/paas/v");
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
- writeLastRun();
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] Failed: ${err instanceof Error ? err.message : String(err)}`);
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 (!dryRun)
412
- writeLastRun();
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
  /**
@@ -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.3.11",
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 (100 memory cap)."
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.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 z.ai API uses Bearer auth on all endpoints, not x-api-key
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: z.ai API uses Bearer auth on all three endpoints (paas, coding, anthropic). Not x-api-key. (2026-03-24)", project="global", memory_type="lesson")
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