@gamaze/hicortex 0.4.5 → 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/llm.d.ts +19 -0
- package/dist/llm.js +30 -0
- package/dist/nightly.js +74 -5
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
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/llm.d.ts
CHANGED
|
@@ -69,6 +69,25 @@ export declare function claudeCliConfig(claudePath: string): LlmConfig;
|
|
|
69
69
|
* Returns the model name if available, null otherwise.
|
|
70
70
|
*/
|
|
71
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
|
+
}>;
|
|
72
91
|
/**
|
|
73
92
|
* For batch operations (nightly pipeline), prefer Ollama when available.
|
|
74
93
|
* Claude CLI has strict rate limits that kill batch distillation.
|
package/dist/llm.js
CHANGED
|
@@ -26,6 +26,7 @@ exports.resolveLlmConfigForCC = resolveLlmConfigForCC;
|
|
|
26
26
|
exports.findClaudeBinary = findClaudeBinary;
|
|
27
27
|
exports.claudeCliConfig = claudeCliConfig;
|
|
28
28
|
exports.probeOllama = probeOllama;
|
|
29
|
+
exports.probeOllamaModel = probeOllamaModel;
|
|
29
30
|
exports.preferOllamaForBatch = preferOllamaForBatch;
|
|
30
31
|
const node_fs_1 = require("node:fs");
|
|
31
32
|
const node_path_1 = require("node:path");
|
|
@@ -367,6 +368,35 @@ async function probeOllama(baseUrl = "http://localhost:11434") {
|
|
|
367
368
|
return null;
|
|
368
369
|
}
|
|
369
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
|
+
}
|
|
370
400
|
/**
|
|
371
401
|
* For batch operations (nightly pipeline), prefer Ollama when available.
|
|
372
402
|
* Claude CLI has strict rate limits that kill batch distillation.
|
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.4.
|
|
5
|
+
"version": "0.4.6",
|
|
6
6
|
"kind": "lifecycle",
|
|
7
7
|
"skills": ["./skills/hicortex-memory", "./skills/hicortex-learn", "./skills/hicortex-activate"],
|
|
8
8
|
"configSchema": {
|
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": {
|