@gamaze/hicortex 0.19.4 → 0.19.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/distiller.d.ts +46 -1
- package/dist/distiller.js +92 -9
- package/dist/mcp-server.js +8 -1
- package/dist/prompts.d.ts +17 -14
- package/dist/prompts.js +37 -17
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/distiller.d.ts
CHANGED
|
@@ -38,10 +38,24 @@ export declare function extractConversationText(messages: unknown[], redactionCo
|
|
|
38
38
|
* `droppedOut`, when provided, is filled with every entry the substance gate
|
|
39
39
|
* discarded (full text). Callers use it to build a durable audit trail (#156);
|
|
40
40
|
* omitting it leaves gate behaviour unchanged.
|
|
41
|
+
*
|
|
42
|
+
* `segmentLabel` (optional) identifies the caller's segment in the #339
|
|
43
|
+
* over-firing warning (e.g. the capture pipeline's segment_id). Purely for
|
|
44
|
+
* log correlation — omitting it falls back to "chunk".
|
|
41
45
|
*/
|
|
42
46
|
export declare function distillSession(llm: LlmClient, conversation: string, projectName: string, date: string, chunkSizeChars?: number, droppedOut?: string[],
|
|
43
47
|
/** Called with each chunk's token usage (#5 budget metering). Optional. */
|
|
44
|
-
onUsage?: (usage: LlmUsage) => void
|
|
48
|
+
onUsage?: (usage: LlmUsage) => void,
|
|
49
|
+
/** Segment identifier for the #339 NO_EXTRACT warning. Optional. */
|
|
50
|
+
segmentLabel?: string): Promise<DistilledEntry[]>;
|
|
51
|
+
/**
|
|
52
|
+
* The NO_EXTRACT check distillChunk applies to an LLM response. EXPORTED and
|
|
53
|
+
* shared (not copy-pasted) with scripts/distill-ab-check/, whose counts must
|
|
54
|
+
* classify empty verdicts exactly as production does (#339 CR finding 3).
|
|
55
|
+
* Tolerant by design: a literal NO_EXTRACT anywhere in the first 20 chars
|
|
56
|
+
* counts (models prepend stray whitespace or a short phrase).
|
|
57
|
+
*/
|
|
58
|
+
export declare function isNoExtractResponse(result: string): boolean;
|
|
45
59
|
/**
|
|
46
60
|
* Reject ONLY structurally-empty distiller fragments before they become
|
|
47
61
|
* memories (#156). The distiller occasionally emits leftovers that parse into
|
|
@@ -76,3 +90,34 @@ export interface DistilledEntry {
|
|
|
76
90
|
content: string;
|
|
77
91
|
memoryType: "experience" | "knowledge" | "decisions";
|
|
78
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Map a single-letter type tag to the stored memory_type. Unknown/absent →
|
|
95
|
+
* experience (the pre-#216 default). `[L]` is explicitly rejected →
|
|
96
|
+
* experience: the distiller must NEVER emit learnings (that's the reflection
|
|
97
|
+
* stage's job), so a model that emits `[L]` is wrong and we do not propagate
|
|
98
|
+
* it as a learning.
|
|
99
|
+
*
|
|
100
|
+
* The single-letter tags ([E]/[K]/[D]) are unchanged from the raw-enum era —
|
|
101
|
+
* the model is taught these as "EXPERIENCE/KNOWLEDGE/DECISIONS" concepts in prompts.ts
|
|
102
|
+
* (ordinary English the model understands), and only the resulting STORED
|
|
103
|
+
* value changed in #264 (episode→experience, fact→knowledge, decision→
|
|
104
|
+
* decisions). The tag letters stay stable so neither the prompt nor the
|
|
105
|
+
* parser needs to change; only this mapping table moves.
|
|
106
|
+
*
|
|
107
|
+
* EXPORTED (with parseDistilledEntries) for scripts/distill-ab-check/ (#339 CR
|
|
108
|
+
* finding 3): the A/B harness computes its counts from each variant build's own
|
|
109
|
+
* parser instead of a copy-pasted mirror, so harness numbers are by construction
|
|
110
|
+
* the numbers that build's production would store. tests/distill-ab-parser-contract.test.ts
|
|
111
|
+
* pins the src and dist parsers against the same corpus.
|
|
112
|
+
*/
|
|
113
|
+
export declare function typeFromTag(letter: string | undefined): DistilledEntry["memoryType"];
|
|
114
|
+
/**
|
|
115
|
+
* Parse distilled markdown into individual memory entries with type tags.
|
|
116
|
+
* Each bullet becomes a separate memory. The leading `[E]`/`[F]`/`[D]` type
|
|
117
|
+
* tag is extracted (→ memoryType), stripped from the stored content, and
|
|
118
|
+
* passed to `insertMemory` via the `memoryType` option (#216). Bullets with
|
|
119
|
+
* no tag default to "experience" (backward compatible with pre-#216 distiller
|
|
120
|
+
* output that never carried a tag). EXPORTED for the A/B harness — see
|
|
121
|
+
* typeFromTag's comment (#339 CR finding 3).
|
|
122
|
+
*/
|
|
123
|
+
export declare function parseDistilledEntries(markdown: string): DistilledEntry[];
|
package/dist/distiller.js
CHANGED
|
@@ -8,11 +8,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
8
8
|
exports.detectChunkSize = detectChunkSize;
|
|
9
9
|
exports.extractConversationText = extractConversationText;
|
|
10
10
|
exports.distillSession = distillSession;
|
|
11
|
+
exports.isNoExtractResponse = isNoExtractResponse;
|
|
11
12
|
exports.hasMinimalSubstance = hasMinimalSubstance;
|
|
13
|
+
exports.typeFromTag = typeFromTag;
|
|
14
|
+
exports.parseDistilledEntries = parseDistilledEntries;
|
|
12
15
|
const prompts_js_1 = require("./prompts.js");
|
|
13
16
|
const redact_js_1 = require("./redact.js");
|
|
14
17
|
const MAX_TRANSCRIPT_CHARS = 80_000;
|
|
15
18
|
const MIN_CONVERSATION_CHARS = 200;
|
|
19
|
+
// #339 (2026-08-24 postmortem): NO_EXTRACT over-firing visibility threshold.
|
|
20
|
+
// Real summary-led segments that the model wrongly abandoned ran 38-64K
|
|
21
|
+
// denoised chars; genuine pure-status noise is ~2.6K. A segment larger than
|
|
22
|
+
// this whose every chunk returns an empty LLM verdict is far more likely the
|
|
23
|
+
// model pattern-matching a bookkeeping-heavy OPENING to the ephemera gate than
|
|
24
|
+
// a legitimately empty segment — so it gets a warning line. Warning ONLY: no
|
|
25
|
+
// auto-retry (cost); the goal is that silent segment loss shows up in the
|
|
26
|
+
// nightly log instead of in a weeks-later eval.
|
|
27
|
+
//
|
|
28
|
+
// The threshold compares the PRE-CHUNKING conversation length, never the chunk
|
|
29
|
+
// length: default ollama chunking (numCtx 8192 → ~19.6K chars) and the
|
|
30
|
+
// small-model speed cap (20K) both keep every chunk at or below this number,
|
|
31
|
+
// so a chunk-level check would be unreachable exactly where the incident lived.
|
|
32
|
+
const NO_EXTRACT_WARN_MIN_CHARS = 20_000;
|
|
16
33
|
// Chunk size limits by model parameter count (for local/CPU inference)
|
|
17
34
|
// Small models are slow on CPU — cap input size to keep inference under ~60s
|
|
18
35
|
const SMALL_MODEL_PARAMS = 8_000_000_000; // 8B — threshold for "small"
|
|
@@ -225,10 +242,16 @@ function extractConversationText(messages, redactionConfig) {
|
|
|
225
242
|
* `droppedOut`, when provided, is filled with every entry the substance gate
|
|
226
243
|
* discarded (full text). Callers use it to build a durable audit trail (#156);
|
|
227
244
|
* omitting it leaves gate behaviour unchanged.
|
|
245
|
+
*
|
|
246
|
+
* `segmentLabel` (optional) identifies the caller's segment in the #339
|
|
247
|
+
* over-firing warning (e.g. the capture pipeline's segment_id). Purely for
|
|
248
|
+
* log correlation — omitting it falls back to "chunk".
|
|
228
249
|
*/
|
|
229
250
|
async function distillSession(llm, conversation, projectName, date, chunkSizeChars, droppedOut,
|
|
230
251
|
/** Called with each chunk's token usage (#5 budget metering). Optional. */
|
|
231
|
-
onUsage
|
|
252
|
+
onUsage,
|
|
253
|
+
/** Segment identifier for the #339 NO_EXTRACT warning. Optional. */
|
|
254
|
+
segmentLabel) {
|
|
232
255
|
if (conversation.length < MIN_CONVERSATION_CHARS) {
|
|
233
256
|
return [];
|
|
234
257
|
}
|
|
@@ -241,9 +264,11 @@ onUsage) {
|
|
|
241
264
|
const chunkSize = chunkSizeChars ?? MAX_TRANSCRIPT_CHARS;
|
|
242
265
|
// If transcript fits in one chunk, distill directly (errors propagate)
|
|
243
266
|
if (transcript.length <= chunkSize) {
|
|
244
|
-
const { entries, dropped } = await distillChunk(llm, transcript, projectName, date, onUsage);
|
|
267
|
+
const { entries, dropped, emptyVerdict } = await distillChunk(llm, transcript, projectName, date, onUsage);
|
|
245
268
|
if (droppedOut)
|
|
246
269
|
droppedOut.push(...dropped);
|
|
270
|
+
if (emptyVerdict)
|
|
271
|
+
warnSuspiciousEmptySegment(segmentLabel, conversation.length);
|
|
247
272
|
return entries;
|
|
248
273
|
}
|
|
249
274
|
// Chunk large transcripts and distill each segment.
|
|
@@ -259,11 +284,14 @@ onUsage) {
|
|
|
259
284
|
const allEntries = [];
|
|
260
285
|
const seen = new Set();
|
|
261
286
|
let chunkFailures = 0;
|
|
287
|
+
let emptyVerdicts = 0;
|
|
262
288
|
let lastError = null;
|
|
263
289
|
for (let i = 0; i < chunks.length; i++) {
|
|
264
290
|
console.log(`[hicortex] Chunk ${i + 1}/${chunks.length} (${chunks[i].length} chars)`);
|
|
265
291
|
try {
|
|
266
|
-
const { entries, dropped } = await distillChunk(llm, chunks[i], projectName, date, onUsage);
|
|
292
|
+
const { entries, dropped, emptyVerdict } = await distillChunk(llm, chunks[i], projectName, date, onUsage);
|
|
293
|
+
if (emptyVerdict)
|
|
294
|
+
emptyVerdicts++;
|
|
267
295
|
if (droppedOut)
|
|
268
296
|
droppedOut.push(...dropped);
|
|
269
297
|
for (const entry of entries) {
|
|
@@ -292,8 +320,38 @@ onUsage) {
|
|
|
292
320
|
if (chunkFailures > 0) {
|
|
293
321
|
console.warn(`[hicortex] Partial distillation: ${chunks.length - chunkFailures}/${chunks.length} chunks succeeded`);
|
|
294
322
|
}
|
|
323
|
+
// #339 segment-level net (CR finding 1): fire when the WHOLE segment produced
|
|
324
|
+
// zero memories and every processed chunk returned an empty LLM verdict. Fires
|
|
325
|
+
// only with zero chunk failures — failed chunks are already loudly visible,
|
|
326
|
+
// and "all failed" throws above. The size gate lives INSIDE
|
|
327
|
+
// warnSuspiciousEmptySegment (strict >, pre-chunking conversation length).
|
|
328
|
+
if (allEntries.length === 0 &&
|
|
329
|
+
chunkFailures === 0 &&
|
|
330
|
+
emptyVerdicts === chunks.length) {
|
|
331
|
+
warnSuspiciousEmptySegment(segmentLabel, conversation.length);
|
|
332
|
+
}
|
|
295
333
|
return allEntries;
|
|
296
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* #339 over-firing visibility net: an empty result this large is the silent-loss
|
|
337
|
+
* signature (real summary-led segments are 38-64K chars; legitimate pure-status
|
|
338
|
+
* noise is ~2.6K). Warning-only, content-free — segment id + size, nothing from
|
|
339
|
+
* the transcript. The empty SUCCESS semantics are unchanged (no throw, no
|
|
340
|
+
* retry): the cursor advances, but the loss is now VISIBLE in the nightly log
|
|
341
|
+
* instead of surfacing weeks later in an eval.
|
|
342
|
+
*
|
|
343
|
+
* The size check lives here, not at call sites, so no path can skip it. Strict
|
|
344
|
+
* comparison: exactly NO_EXTRACT_WARN_MIN_CHARS chars is a legitimate small
|
|
345
|
+
* segment and stays silent.
|
|
346
|
+
*/
|
|
347
|
+
function warnSuspiciousEmptySegment(segmentLabel, segmentChars) {
|
|
348
|
+
if (segmentChars <= NO_EXTRACT_WARN_MIN_CHARS)
|
|
349
|
+
return;
|
|
350
|
+
console.warn(`[hicortex] Suspicious empty distillation: zero memories for a ${segmentChars}-char ` +
|
|
351
|
+
`${segmentLabel ? `segment ${segmentLabel}` : "segment"} ` +
|
|
352
|
+
`(every chunk returned NO_EXTRACT or nothing parseable) — segments this large almost always ` +
|
|
353
|
+
`contain extractable material; if this repeats, suspect gate over-firing (logged only, not retried)`);
|
|
354
|
+
}
|
|
297
355
|
/**
|
|
298
356
|
* Distill a single chunk of conversation text.
|
|
299
357
|
*
|
|
@@ -309,6 +367,13 @@ onUsage) {
|
|
|
309
367
|
*
|
|
310
368
|
* `dropped` carries entries the substance gate rejected (full text) so the
|
|
311
369
|
* caller can surface them in a durable audit trail (#156).
|
|
370
|
+
*
|
|
371
|
+
* `emptyVerdict` is true when the chunk was processed successfully but the LLM's
|
|
372
|
+
* verdict contained nothing extractable: bare NO_EXTRACT, an empty response, or
|
|
373
|
+
* text that parses to zero bullets (prose — the silent twin of NO_EXTRACT,
|
|
374
|
+
* #339 CR finding 2). The caller aggregates these for the segment-level
|
|
375
|
+
* over-firing warning; entries extracted then dropped by the substance gate do
|
|
376
|
+
* NOT count (their drops are already logged).
|
|
312
377
|
*/
|
|
313
378
|
async function distillChunk(llm, transcript, projectName, date, onUsage) {
|
|
314
379
|
const prompt = (0, prompts_js_1.distillation)(projectName, date, transcript);
|
|
@@ -323,10 +388,8 @@ async function distillChunk(llm, transcript, projectName, date, onUsage) {
|
|
|
323
388
|
// trip a budget.
|
|
324
389
|
if (usage && onUsage)
|
|
325
390
|
onUsage(usage);
|
|
326
|
-
if (!result)
|
|
327
|
-
return { entries: [], dropped: [] };
|
|
328
|
-
if (result === "NO_EXTRACT" || result.slice(0, 20).includes("NO_EXTRACT")) {
|
|
329
|
-
return { entries: [], dropped: [] };
|
|
391
|
+
if (!result || isNoExtractResponse(result)) {
|
|
392
|
+
return { entries: [], dropped: [], emptyVerdict: true };
|
|
330
393
|
}
|
|
331
394
|
const parsed = parseDistilledEntries(result);
|
|
332
395
|
// Smoke alarm (PR #218 review): the prompt enforces topic-first, but models
|
|
@@ -356,7 +419,20 @@ async function distillChunk(llm, transcript, projectName, date, onUsage) {
|
|
|
356
419
|
}
|
|
357
420
|
console.log(`[hicortex] Substance gate: dropped ${dropped.length}/${parsed.length} content-free fragment(s)`);
|
|
358
421
|
}
|
|
359
|
-
|
|
422
|
+
// Parsed-zero bypass (#339 CR finding 2): a non-empty response with no
|
|
423
|
+
// NO_EXTRACT token that still parses to zero bullets is the silent twin of
|
|
424
|
+
// NO_EXTRACT — an empty verdict for the over-firing net.
|
|
425
|
+
return { entries, dropped, emptyVerdict: parsed.length === 0 };
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* The NO_EXTRACT check distillChunk applies to an LLM response. EXPORTED and
|
|
429
|
+
* shared (not copy-pasted) with scripts/distill-ab-check/, whose counts must
|
|
430
|
+
* classify empty verdicts exactly as production does (#339 CR finding 3).
|
|
431
|
+
* Tolerant by design: a literal NO_EXTRACT anywhere in the first 20 chars
|
|
432
|
+
* counts (models prepend stray whitespace or a short phrase).
|
|
433
|
+
*/
|
|
434
|
+
function isNoExtractResponse(result) {
|
|
435
|
+
return result === "NO_EXTRACT" || result.slice(0, 20).includes("NO_EXTRACT");
|
|
360
436
|
}
|
|
361
437
|
/**
|
|
362
438
|
* Split transcript text into chunks at natural boundaries (double newlines).
|
|
@@ -445,6 +521,12 @@ function hasMinimalSubstance(entry) {
|
|
|
445
521
|
* value changed in #264 (episode→experience, fact→knowledge, decision→
|
|
446
522
|
* decisions). The tag letters stay stable so neither the prompt nor the
|
|
447
523
|
* parser needs to change; only this mapping table moves.
|
|
524
|
+
*
|
|
525
|
+
* EXPORTED (with parseDistilledEntries) for scripts/distill-ab-check/ (#339 CR
|
|
526
|
+
* finding 3): the A/B harness computes its counts from each variant build's own
|
|
527
|
+
* parser instead of a copy-pasted mirror, so harness numbers are by construction
|
|
528
|
+
* the numbers that build's production would store. tests/distill-ab-parser-contract.test.ts
|
|
529
|
+
* pins the src and dist parsers against the same corpus.
|
|
448
530
|
*/
|
|
449
531
|
function typeFromTag(letter) {
|
|
450
532
|
switch (letter) {
|
|
@@ -467,7 +549,8 @@ function typeFromTag(letter) {
|
|
|
467
549
|
* tag is extracted (→ memoryType), stripped from the stored content, and
|
|
468
550
|
* passed to `insertMemory` via the `memoryType` option (#216). Bullets with
|
|
469
551
|
* no tag default to "experience" (backward compatible with pre-#216 distiller
|
|
470
|
-
* output that never carried a tag).
|
|
552
|
+
* output that never carried a tag). EXPORTED for the A/B harness — see
|
|
553
|
+
* typeFromTag's comment (#339 CR finding 3).
|
|
471
554
|
*/
|
|
472
555
|
function parseDistilledEntries(markdown) {
|
|
473
556
|
const entries = [];
|
package/dist/mcp-server.js
CHANGED
|
@@ -1198,7 +1198,14 @@ async function startServer(options = {}) {
|
|
|
1198
1198
|
distillUsage.prompt += u.prompt_tokens ?? 0;
|
|
1199
1199
|
distillUsage.completion += u.completion_tokens ?? 0;
|
|
1200
1200
|
distillUsage.total += u.total_tokens ?? 0;
|
|
1201
|
-
}
|
|
1201
|
+
},
|
|
1202
|
+
// #339: identify this POST in the NO_EXTRACT over-firing warning —
|
|
1203
|
+
// segment_id (incremental capture), else session_id (legacy), else none.
|
|
1204
|
+
typeof segment_id === "string" && segment_id
|
|
1205
|
+
? segment_id
|
|
1206
|
+
: typeof session_id === "string" && session_id
|
|
1207
|
+
? session_id
|
|
1208
|
+
: undefined);
|
|
1202
1209
|
// Phase 1 — embed every chunk up front (async). If ANY embed fails we
|
|
1203
1210
|
// never reach the insert, so nothing is stored.
|
|
1204
1211
|
const createdAt = new Date(date).toISOString();
|
package/dist/prompts.d.ts
CHANGED
|
@@ -19,20 +19,23 @@ export declare function reflection(memoriesBlock: string, recentLessons?: string
|
|
|
19
19
|
/**
|
|
20
20
|
* Distillation prompt. Extracts knowledge from a session transcript.
|
|
21
21
|
*
|
|
22
|
-
* LAYOUT (
|
|
23
|
-
*
|
|
24
|
-
* prefix
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
22
|
+
* LAYOUT (REVERTED 2026-08-24): transcript BEFORE the static instruction
|
|
23
|
+
* block — the pre-0.19.4 order. The #329 item-6 reorder (static-first, for
|
|
24
|
+
* provider prefix caching) was REVERTED after a deterministic A/B on real
|
|
25
|
+
* segments: with instructions first, the model over-fires NO_EXTRACT on
|
|
26
|
+
* summary-led and long mixed sessions (a real coding segment: 15 memories →
|
|
27
|
+
* 0; a real Hermes session: rich → 0; isolation proved the LAYOUT caused it,
|
|
28
|
+
* not the item-5 sentence, which is KEPT). Silent shape-dependent segment
|
|
29
|
+
* loss beats any caching win. Re-attempting instructions-first requires a
|
|
30
|
+
* gate fix that passes the A/B matrix harness first.
|
|
31
|
+
*
|
|
32
|
+
* #339 gate hardening (same day): the NO_EXTRACT rule now carries an explicit
|
|
33
|
+
* whole-transcript guard + counter-example (a summary-led session that
|
|
34
|
+
* contains later decisions MUST be extracted) — the over-firing mechanism was
|
|
35
|
+
* the model pattern-matching a bookkeeping-heavy OPENING to the ephemera gate
|
|
36
|
+
* and abandoning the whole segment. Companion visibility net (warning on
|
|
37
|
+
* large empty results) lives in distiller.ts; the release-gate harness is
|
|
38
|
+
* scripts/distill-ab-check/.
|
|
36
39
|
*/
|
|
37
40
|
export declare function distillation(projectName: string, date: string, transcript: string): string;
|
|
38
41
|
/**
|
package/dist/prompts.js
CHANGED
|
@@ -107,25 +107,31 @@ Respond with a JSON array. Empty array [] is a valid response.`;
|
|
|
107
107
|
/**
|
|
108
108
|
* Distillation prompt. Extracts knowledge from a session transcript.
|
|
109
109
|
*
|
|
110
|
-
* LAYOUT (
|
|
111
|
-
*
|
|
112
|
-
* prefix
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
110
|
+
* LAYOUT (REVERTED 2026-08-24): transcript BEFORE the static instruction
|
|
111
|
+
* block — the pre-0.19.4 order. The #329 item-6 reorder (static-first, for
|
|
112
|
+
* provider prefix caching) was REVERTED after a deterministic A/B on real
|
|
113
|
+
* segments: with instructions first, the model over-fires NO_EXTRACT on
|
|
114
|
+
* summary-led and long mixed sessions (a real coding segment: 15 memories →
|
|
115
|
+
* 0; a real Hermes session: rich → 0; isolation proved the LAYOUT caused it,
|
|
116
|
+
* not the item-5 sentence, which is KEPT). Silent shape-dependent segment
|
|
117
|
+
* loss beats any caching win. Re-attempting instructions-first requires a
|
|
118
|
+
* gate fix that passes the A/B matrix harness first.
|
|
119
|
+
*
|
|
120
|
+
* #339 gate hardening (same day): the NO_EXTRACT rule now carries an explicit
|
|
121
|
+
* whole-transcript guard + counter-example (a summary-led session that
|
|
122
|
+
* contains later decisions MUST be extracted) — the over-firing mechanism was
|
|
123
|
+
* the model pattern-matching a bookkeeping-heavy OPENING to the ephemera gate
|
|
124
|
+
* and abandoning the whole segment. Companion visibility net (warning on
|
|
125
|
+
* large empty results) lives in distiller.ts; the release-gate harness is
|
|
126
|
+
* scripts/distill-ab-check/.
|
|
124
127
|
*/
|
|
125
128
|
function distillation(projectName, date, transcript) {
|
|
126
129
|
return `You are a memory extraction agent. Analyze this AI session transcript and extract
|
|
127
130
|
knowledge worth remembering long-term.
|
|
128
131
|
|
|
132
|
+
SESSION TRANSCRIPT (project: ${projectName}, date: ${date}):
|
|
133
|
+
${transcript}
|
|
134
|
+
|
|
129
135
|
EXTRACT into this markdown format:
|
|
130
136
|
|
|
131
137
|
# Session Memory: ${date} - ${projectName}
|
|
@@ -213,6 +219,23 @@ user-confirmed standardization it embodies qualifies.
|
|
|
213
219
|
If EVERY item in the transcript is never-record ephemera, output ONLY:
|
|
214
220
|
"NO_EXTRACT" — zero memories is the correct result for a pure-status segment.
|
|
215
221
|
|
|
222
|
+
NO_EXTRACT guard (a verdict on the WHOLE transcript, never on its opening):
|
|
223
|
+
"NO_EXTRACT" requires that NO durable decision, knowledge, or correction
|
|
224
|
+
appears ANYWHERE in the transcript — including after long bookkeeping
|
|
225
|
+
stretches. The opening is not evidence about the rest: real sessions often
|
|
226
|
+
OPEN with bookkeeping (a compaction summary, a task notification, a status
|
|
227
|
+
recap) and CONTAIN extractable material later. Read to the END of the
|
|
228
|
+
transcript before deciding; NO_EXTRACT on a long, mixed session is almost
|
|
229
|
+
always a mistake — when in doubt, extract the durable items.
|
|
230
|
+
Counter-example (MUST be extracted, never NO_EXTRACT): a session opens with
|
|
231
|
+
"Session summary: continuing the API migration; prior PR merged, tests
|
|
232
|
+
green" but later the user confirms "standardize on the queue-based worker —
|
|
233
|
+
make it the documented default" and corrects the assistant: "no, don't gate
|
|
234
|
+
retries behind a flag — remove the flag entirely". That session yields at
|
|
235
|
+
least a [D] standardization and an [E] correction; the summary opening
|
|
236
|
+
changes nothing. Emitting NO_EXTRACT there would lose the only record of
|
|
237
|
+
both.
|
|
238
|
+
|
|
216
239
|
RULES:
|
|
217
240
|
- Extract MAX 20 items total (quality over quantity)
|
|
218
241
|
- Use EXACT names/versions/paths/numbers as they appear in the transcript —
|
|
@@ -231,9 +254,6 @@ RULES:
|
|
|
231
254
|
"[Strong Negative] User rejected per-agent billing"). The subject always comes first.
|
|
232
255
|
- Omit any section that has zero items (don't include empty sections)
|
|
233
256
|
- If nothing worth extracting, output ONLY: "NO_EXTRACT"
|
|
234
|
-
|
|
235
|
-
SESSION TRANSCRIPT (project: ${projectName}, date: ${date}):
|
|
236
|
-
${transcript}
|
|
237
257
|
`;
|
|
238
258
|
}
|
|
239
259
|
/**
|
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.19.
|
|
5
|
+
"version": "0.19.5",
|
|
6
6
|
"kind": "lifecycle",
|
|
7
7
|
"skills": ["./skills/hicortex-memory"],
|
|
8
8
|
"configSchema": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamaze/hicortex",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.5",
|
|
4
4
|
"description": "Persistent agent identity for AI agents \u2014 a hand-edited identity layer, nightly-distilled experience, and lessons injected every session, shared across your whole fleet. Works with Hermes, OpenClaw, Claude Code, and Pi.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|