@gamaze/hicortex 0.18.1 → 0.18.3
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/assets/dashboard.html +8 -3
- package/assets/viz.html +9 -3
- package/dist/consolidate.js +7 -7
- package/dist/dashboard.js +3 -3
- package/dist/db.js +24 -1
- package/dist/distiller.d.ts +9 -7
- package/dist/distiller.js +37 -19
- package/dist/eval/recall-sweep.js +2 -2
- package/dist/eval/reflection-census.js +3 -3
- package/dist/hosted-boot.d.ts +61 -0
- package/dist/hosted-boot.js +72 -0
- package/dist/index.js +13 -13
- package/dist/init.d.ts +15 -0
- package/dist/init.js +99 -8
- package/dist/learnings-identity.js +4 -4
- package/dist/localhost-bypass.d.ts +27 -0
- package/dist/localhost-bypass.js +71 -0
- package/dist/mcp-server.js +115 -19
- package/dist/prompts.js +12 -12
- package/dist/recall-index.js +7 -2
- package/dist/retrieval.js +2 -1
- package/dist/seed-lesson.js +1 -1
- package/dist/status.d.ts +8 -0
- package/dist/status.js +15 -1
- package/dist/storage.js +4 -4
- package/dist/token-budget.d.ts +34 -0
- package/dist/token-budget.js +131 -0
- package/dist/type-classify.d.ts +29 -26
- package/dist/type-classify.js +52 -45
- package/dist/type-labels.d.ts +48 -17
- package/dist/type-labels.js +89 -18
- package/dist/types.d.ts +10 -1
- package/dist/viz.d.ts +9 -1
- package/dist/viz.js +11 -2
- package/hermes-plugin/hicortex/client.py +1 -1
- package/hermes-plugin/hicortex/provider.py +13 -13
- package/package.json +1 -1
package/assets/dashboard.html
CHANGED
|
@@ -238,10 +238,15 @@ let compKey = "by_type";
|
|
|
238
238
|
|
|
239
239
|
const $ = (id) => document.getElementById(id);
|
|
240
240
|
|
|
241
|
-
// #264
|
|
242
|
-
//
|
|
243
|
-
//
|
|
241
|
+
// #264 final: the data blob carries the CANONICAL human-term keys
|
|
242
|
+
// (knowledge/experience/decisions/learnings). Legacy raw keys are also mapped
|
|
243
|
+
// so a snapshot taken mid-migrate renders correctly. Unknown keys pass through.
|
|
244
244
|
const TYPE_LABELS = {
|
|
245
|
+
knowledge: "Knowledge",
|
|
246
|
+
experience: "Experience",
|
|
247
|
+
decisions: "Decisions",
|
|
248
|
+
learnings: "Learnings",
|
|
249
|
+
// Legacy raw enum (same types, renamed).
|
|
245
250
|
fact: "Knowledge",
|
|
246
251
|
episode: "Experience",
|
|
247
252
|
decision: "Decisions",
|
package/assets/viz.html
CHANGED
|
@@ -389,10 +389,16 @@
|
|
|
389
389
|
var selDomain = document.getElementById("f-domain");
|
|
390
390
|
var selType = document.getElementById("f-type");
|
|
391
391
|
|
|
392
|
-
// #264
|
|
393
|
-
//
|
|
394
|
-
//
|
|
392
|
+
// #264 final: node data carries the CANONICAL human-term values
|
|
393
|
+
// (knowledge/experience/decisions/learnings). Legacy raw values are also
|
|
394
|
+
// mapped so a mid-migrate snapshot renders correctly. Unknown values pass
|
|
395
|
+
// through unchanged.
|
|
395
396
|
var TYPE_LABELS = {
|
|
397
|
+
knowledge: "Knowledge",
|
|
398
|
+
experience: "Experience",
|
|
399
|
+
decisions: "Decisions",
|
|
400
|
+
learnings: "Learnings",
|
|
401
|
+
// Legacy raw enum (same types, renamed).
|
|
396
402
|
fact: "Knowledge",
|
|
397
403
|
episode: "Experience",
|
|
398
404
|
decision: "Decisions",
|
package/dist/consolidate.js
CHANGED
|
@@ -418,7 +418,7 @@ async function stageReflection(db, memories, llm, budget, embedFn, dryRun) {
|
|
|
418
418
|
const severity = String(lo.severity ?? "important");
|
|
419
419
|
const confidence = String(lo.confidence ?? "medium");
|
|
420
420
|
const sourcePattern = String(lo.source_pattern ?? "");
|
|
421
|
-
// No `## Lesson:` prefix: memory_type='
|
|
421
|
+
// No `## Lesson:` prefix: memory_type='learnings' carries the type, and the
|
|
422
422
|
// text is the topic-first first line (display reads the first line, not a
|
|
423
423
|
// header parse — see learnings-identity.ts / index.ts).
|
|
424
424
|
let content = `${lessonText}\n\n`;
|
|
@@ -443,7 +443,7 @@ async function stageReflection(db, memories, llm, budget, embedFn, dryRun) {
|
|
|
443
443
|
// the accidental 1−L2 scale and required cosine > 0.98 — the check
|
|
444
444
|
// effectively never fired. See isContradictionCandidate.
|
|
445
445
|
const similarLessons = storage.vectorSearch(db, embedding, 3)
|
|
446
|
-
.filter((n) => isContradictionCandidate(n.distance) && n.memory_type === "
|
|
446
|
+
.filter((n) => isContradictionCandidate(n.distance) && n.memory_type === "learnings");
|
|
447
447
|
let contradicted = false;
|
|
448
448
|
if (similarLessons.length > 0 && budget.use("contradiction_check")) {
|
|
449
449
|
const existingText = similarLessons[0].content.slice(0, 300);
|
|
@@ -470,7 +470,7 @@ async function stageReflection(db, memories, llm, budget, embedFn, dryRun) {
|
|
|
470
470
|
storage.insertMemory(db, content, embedding, {
|
|
471
471
|
sourceAgent: "hicortex/reflection",
|
|
472
472
|
project,
|
|
473
|
-
memoryType: "
|
|
473
|
+
memoryType: "learnings",
|
|
474
474
|
baseStrength: baseStrength[severity] ?? 0.8,
|
|
475
475
|
});
|
|
476
476
|
generated++;
|
|
@@ -509,7 +509,7 @@ function rebuildContentModuleIndex(db, domains, stateDir) {
|
|
|
509
509
|
.all();
|
|
510
510
|
const lessonRows = db
|
|
511
511
|
.prepare(`SELECT domain, COUNT(*) AS cnt FROM memories
|
|
512
|
-
WHERE domain IS NOT NULL AND memory_type = '
|
|
512
|
+
WHERE domain IS NOT NULL AND memory_type = 'learnings' GROUP BY domain`)
|
|
513
513
|
.all();
|
|
514
514
|
const memByDomain = new Map(memRows.map((r) => [r.domain, r.cnt]));
|
|
515
515
|
const lessonByDomain = new Map(lessonRows.map((r) => [r.domain, r.cnt]));
|
|
@@ -637,7 +637,7 @@ async function stageDomainCuration(db, llm, budget, dryRun, stateDir) {
|
|
|
637
637
|
}
|
|
638
638
|
const lessonRows = db
|
|
639
639
|
.prepare(`SELECT project, COUNT(*) as cnt FROM memories
|
|
640
|
-
WHERE project IS NOT NULL AND memory_type = '
|
|
640
|
+
WHERE project IS NOT NULL AND memory_type = 'learnings'
|
|
641
641
|
GROUP BY project`)
|
|
642
642
|
.all();
|
|
643
643
|
const lessonsByProject = new Map(lessonRows.map((r) => [r.project, r.cnt]));
|
|
@@ -997,7 +997,7 @@ const SUPERSESSION_BATCH_SIZE = 500;
|
|
|
997
997
|
* events, not mutable state, so there is nothing to supersede.
|
|
998
998
|
*/
|
|
999
999
|
function isSupersedableShape(mem) {
|
|
1000
|
-
return (mem.memory_type === "
|
|
1000
|
+
return (mem.memory_type === "decisions" ||
|
|
1001
1001
|
mem.content.includes("[Decisions Made]") ||
|
|
1002
1002
|
mem.content.includes("[Corrections & Rejections]") ||
|
|
1003
1003
|
mem.content.includes("[Facts Learned]") ||
|
|
@@ -1117,7 +1117,7 @@ async function stageSupersession(db, llm, budget, embedFn, dryRun, stateDir, opt
|
|
|
1117
1117
|
// in lockstep (an inline SQL copy, so drift here silently narrows scope).
|
|
1118
1118
|
`SELECT rowid AS __rowid, * FROM memories
|
|
1119
1119
|
WHERE rowid > ?
|
|
1120
|
-
AND (memory_type = '
|
|
1120
|
+
AND (memory_type = 'decisions'
|
|
1121
1121
|
OR content LIKE '%[Decisions Made]%'
|
|
1122
1122
|
OR content LIKE '%[Corrections & Rejections]%'
|
|
1123
1123
|
OR content LIKE '%[Facts Learned]%'
|
package/dist/dashboard.js
CHANGED
|
@@ -53,7 +53,7 @@ function countBy(db, col) {
|
|
|
53
53
|
function computeDashboardMetrics(db) {
|
|
54
54
|
const mem = db.prepare("SELECT COUNT(*) AS c FROM memories").get().c;
|
|
55
55
|
const lesson = db
|
|
56
|
-
.prepare("SELECT COUNT(*) AS c FROM memories WHERE memory_type = '
|
|
56
|
+
.prepare("SELECT COUNT(*) AS c FROM memories WHERE memory_type = 'learnings'")
|
|
57
57
|
.get().c;
|
|
58
58
|
const link = db.prepare("SELECT COUNT(*) AS c FROM memory_links").get().c;
|
|
59
59
|
const adoptionRow = db
|
|
@@ -228,7 +228,7 @@ function backfillSnapshots(db) {
|
|
|
228
228
|
const dayRows = byDay.get(d);
|
|
229
229
|
for (const r of dayRows) {
|
|
230
230
|
mem++;
|
|
231
|
-
if (r.memory_type === "
|
|
231
|
+
if (r.memory_type === "learnings")
|
|
232
232
|
lesson++;
|
|
233
233
|
byType[r.memory_type] = (byType[r.memory_type] ?? 0) + 1;
|
|
234
234
|
const domKey = r.domain ?? "(unscoped)";
|
|
@@ -380,7 +380,7 @@ function handleDashboardData(db, query, config) {
|
|
|
380
380
|
const lessonRows = db
|
|
381
381
|
.prepare(`SELECT id, content, created_at
|
|
382
382
|
FROM memories
|
|
383
|
-
WHERE memory_type = '
|
|
383
|
+
WHERE memory_type = 'learnings' AND created_at BETWEEN ? AND ?
|
|
384
384
|
ORDER BY created_at ASC`)
|
|
385
385
|
.all(dayStart, dayEnd);
|
|
386
386
|
// Stage outcomes for the day: dedup merges + supersession links that day.
|
package/dist/db.js
CHANGED
|
@@ -105,7 +105,7 @@ CREATE TABLE IF NOT EXISTS memories (
|
|
|
105
105
|
source_session TEXT,
|
|
106
106
|
project TEXT,
|
|
107
107
|
privacy TEXT DEFAULT 'WORK',
|
|
108
|
-
memory_type TEXT DEFAULT '
|
|
108
|
+
memory_type TEXT DEFAULT 'experience',
|
|
109
109
|
updated_at TIMESTAMP
|
|
110
110
|
);
|
|
111
111
|
|
|
@@ -488,6 +488,29 @@ const MIGRATIONS = [
|
|
|
488
488
|
`);
|
|
489
489
|
},
|
|
490
490
|
},
|
|
491
|
+
{
|
|
492
|
+
version: 13,
|
|
493
|
+
name: "memory_type_unified_terminology",
|
|
494
|
+
up: (db) => {
|
|
495
|
+
// #264 final step: rename the memory_type COLUMN VALUES from the raw
|
|
496
|
+
// internal enum (fact/episode/decision/lesson) to the unified human
|
|
497
|
+
// terms (knowledge/experience/decisions/learnings). The column itself,
|
|
498
|
+
// its default, and every SQL query/filter were updated in the same
|
|
499
|
+
// change; this migration converts existing rows in place. Idempotent:
|
|
500
|
+
// re-running against an already-migrated DB matches 0 rows per clause.
|
|
501
|
+
// The CREATE TABLE default is now 'experience' (was 'episode'); legacy
|
|
502
|
+
// rows with the old default value are rewritten here.
|
|
503
|
+
//
|
|
504
|
+
// Ordering is irrelevant (each clause keys on a distinct old value) and
|
|
505
|
+
// no clause can fire on another clause's output (the new values are
|
|
506
|
+
// disjoint from the old). The transaction wrapper in migrate() makes the
|
|
507
|
+
// whole migration atomic.
|
|
508
|
+
db.exec("UPDATE memories SET memory_type = 'knowledge' WHERE memory_type = 'fact'");
|
|
509
|
+
db.exec("UPDATE memories SET memory_type = 'experience' WHERE memory_type = 'episode'");
|
|
510
|
+
db.exec("UPDATE memories SET memory_type = 'decisions' WHERE memory_type = 'decision'");
|
|
511
|
+
db.exec("UPDATE memories SET memory_type = 'learnings' WHERE memory_type = 'lesson'");
|
|
512
|
+
},
|
|
513
|
+
},
|
|
491
514
|
];
|
|
492
515
|
/**
|
|
493
516
|
* Run all pending migrations against the database.
|
package/dist/distiller.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Simplified from hicortex/distiller.py — messages come from agent_end hook,
|
|
4
4
|
* not from filesystem scanning.
|
|
5
5
|
*/
|
|
6
|
-
import type { LlmClient } from "./llm.js";
|
|
6
|
+
import type { LlmClient, LlmUsage } from "./llm.js";
|
|
7
7
|
import { type RedactionConfig } from "./redact.js";
|
|
8
8
|
/**
|
|
9
9
|
* Estimate a safe chunk size in chars based on the LLM provider and model.
|
|
@@ -39,7 +39,9 @@ export declare function extractConversationText(messages: unknown[], redactionCo
|
|
|
39
39
|
* discarded (full text). Callers use it to build a durable audit trail (#156);
|
|
40
40
|
* omitting it leaves gate behaviour unchanged.
|
|
41
41
|
*/
|
|
42
|
-
export declare function distillSession(llm: LlmClient, conversation: string, projectName: string, date: string, chunkSizeChars?: number, droppedOut?: string[]
|
|
42
|
+
export declare function distillSession(llm: LlmClient, conversation: string, projectName: string, date: string, chunkSizeChars?: number, droppedOut?: string[],
|
|
43
|
+
/** Called with each chunk's token usage (#5 budget metering). Optional. */
|
|
44
|
+
onUsage?: (usage: LlmUsage) => void): Promise<DistilledEntry[]>;
|
|
43
45
|
/**
|
|
44
46
|
* Reject ONLY structurally-empty distiller fragments before they become
|
|
45
47
|
* memories (#156). The distiller occasionally emits leftovers that parse into
|
|
@@ -64,13 +66,13 @@ export declare function distillSession(llm: LlmClient, conversation: string, pro
|
|
|
64
66
|
export declare function hasMinimalSubstance(entry: string): boolean;
|
|
65
67
|
/**
|
|
66
68
|
* A parsed distillation entry: the stored content (type tag STRIPPED) plus the
|
|
67
|
-
* classified memory_type. `memoryType` is one of "
|
|
68
|
-
* "
|
|
69
|
-
* absent:
|
|
70
|
-
* (#216). A missing/unknown tag defaults to "
|
|
69
|
+
* classified memory_type. `memoryType` is one of "experience" | "knowledge" |
|
|
70
|
+
* "decisions" — the three distillation-time types. "learnings" is deliberately
|
|
71
|
+
* absent: learnings are the reflection stage's product, never distillation's
|
|
72
|
+
* (#216). A missing/unknown tag defaults to "experience" so older distiller
|
|
71
73
|
* output (pre-#216, no tag) stays backward-compatible.
|
|
72
74
|
*/
|
|
73
75
|
export interface DistilledEntry {
|
|
74
76
|
content: string;
|
|
75
|
-
memoryType: "
|
|
77
|
+
memoryType: "experience" | "knowledge" | "decisions";
|
|
76
78
|
}
|
package/dist/distiller.js
CHANGED
|
@@ -226,7 +226,9 @@ function extractConversationText(messages, redactionConfig) {
|
|
|
226
226
|
* discarded (full text). Callers use it to build a durable audit trail (#156);
|
|
227
227
|
* omitting it leaves gate behaviour unchanged.
|
|
228
228
|
*/
|
|
229
|
-
async function distillSession(llm, conversation, projectName, date, chunkSizeChars, droppedOut
|
|
229
|
+
async function distillSession(llm, conversation, projectName, date, chunkSizeChars, droppedOut,
|
|
230
|
+
/** Called with each chunk's token usage (#5 budget metering). Optional. */
|
|
231
|
+
onUsage) {
|
|
230
232
|
if (conversation.length < MIN_CONVERSATION_CHARS) {
|
|
231
233
|
return [];
|
|
232
234
|
}
|
|
@@ -239,7 +241,7 @@ async function distillSession(llm, conversation, projectName, date, chunkSizeCha
|
|
|
239
241
|
const chunkSize = chunkSizeChars ?? MAX_TRANSCRIPT_CHARS;
|
|
240
242
|
// If transcript fits in one chunk, distill directly (errors propagate)
|
|
241
243
|
if (transcript.length <= chunkSize) {
|
|
242
|
-
const { entries, dropped } = await distillChunk(llm, transcript, projectName, date);
|
|
244
|
+
const { entries, dropped } = await distillChunk(llm, transcript, projectName, date, onUsage);
|
|
243
245
|
if (droppedOut)
|
|
244
246
|
droppedOut.push(...dropped);
|
|
245
247
|
return entries;
|
|
@@ -261,13 +263,13 @@ async function distillSession(llm, conversation, projectName, date, chunkSizeCha
|
|
|
261
263
|
for (let i = 0; i < chunks.length; i++) {
|
|
262
264
|
console.log(`[hicortex] Chunk ${i + 1}/${chunks.length} (${chunks[i].length} chars)`);
|
|
263
265
|
try {
|
|
264
|
-
const { entries, dropped } = await distillChunk(llm, chunks[i], projectName, date);
|
|
266
|
+
const { entries, dropped } = await distillChunk(llm, chunks[i], projectName, date, onUsage);
|
|
265
267
|
if (droppedOut)
|
|
266
268
|
droppedOut.push(...dropped);
|
|
267
269
|
for (const entry of entries) {
|
|
268
270
|
// Deduplicate by normalized content (type tag does not participate —
|
|
269
271
|
// two chunks extracting the same fact should collapse regardless of
|
|
270
|
-
// whether one tagged it [
|
|
272
|
+
// whether one tagged it [K] and the other [E]).
|
|
271
273
|
const key = entry.content.toLowerCase().replace(/\s+/g, " ").slice(0, 100);
|
|
272
274
|
if (!seen.has(key)) {
|
|
273
275
|
seen.add(key);
|
|
@@ -308,13 +310,19 @@ async function distillSession(llm, conversation, projectName, date, chunkSizeCha
|
|
|
308
310
|
* `dropped` carries entries the substance gate rejected (full text) so the
|
|
309
311
|
* caller can surface them in a durable audit trail (#156).
|
|
310
312
|
*/
|
|
311
|
-
async function distillChunk(llm, transcript, projectName, date) {
|
|
313
|
+
async function distillChunk(llm, transcript, projectName, date, onUsage) {
|
|
312
314
|
const prompt = (0, prompts_js_1.distillation)(projectName, date, transcript);
|
|
313
315
|
// NOTE: Intentionally no try/catch here. Transient LLM errors (network
|
|
314
316
|
// failures, 4xx/5xx, model-not-found, timeouts) propagate up to the caller
|
|
315
317
|
// so the nightly pipeline can treat them as "retry later" instead of
|
|
316
318
|
// "processed successfully with zero extractions".
|
|
317
|
-
const { text: result } = await llm.completeDistill(prompt);
|
|
319
|
+
const { text: result, usage } = await llm.completeDistill(prompt);
|
|
320
|
+
// #5: report this chunk's token usage to the caller's budget meter. Optional
|
|
321
|
+
// (absent for callers that don't meter); a missing/undefined usage (claude-cli)
|
|
322
|
+
// is a no-op — consistent with the existing design that such tenants never
|
|
323
|
+
// trip a budget.
|
|
324
|
+
if (usage && onUsage)
|
|
325
|
+
onUsage(usage);
|
|
318
326
|
if (!result)
|
|
319
327
|
return { entries: [], dropped: [] };
|
|
320
328
|
if (result === "NO_EXTRACT" || result.slice(0, 20).includes("NO_EXTRACT")) {
|
|
@@ -325,7 +333,7 @@ async function distillChunk(llm, transcript, projectName, date) {
|
|
|
325
333
|
// sometimes ignore constraints (cf. the prior max-15-bullet failure). Count
|
|
326
334
|
// entries that still look actor-led or bracket-led so a format regression
|
|
327
335
|
// shows in nightly logs, not months later in the next eval. Non-blocking.
|
|
328
|
-
// Note: the type tag ([E]/[
|
|
336
|
+
// Note: the type tag ([E]/[K]/[D]) is already stripped by the parser, so a
|
|
329
337
|
// leading bracket here means a payload-bracket or a category-first regression.
|
|
330
338
|
const offTopic = parsed.filter((e) => /^\s*(user|ai|the user|assistant)\b/i.test(e.content) || /^\s*\[/.test(e.content)).length;
|
|
331
339
|
if (parsed.length > 0 && offTopic > 0) {
|
|
@@ -426,21 +434,31 @@ function hasMinimalSubstance(entry) {
|
|
|
426
434
|
}
|
|
427
435
|
/**
|
|
428
436
|
* Map a single-letter type tag to the stored memory_type. Unknown/absent →
|
|
429
|
-
*
|
|
430
|
-
* distiller must NEVER emit
|
|
431
|
-
* model that emits `[L]` is wrong and we do not propagate
|
|
437
|
+
* experience (the pre-#216 default). `[L]` is explicitly rejected →
|
|
438
|
+
* experience: the distiller must NEVER emit learnings (that's the reflection
|
|
439
|
+
* stage's job), so a model that emits `[L]` is wrong and we do not propagate
|
|
440
|
+
* it as a learning.
|
|
441
|
+
*
|
|
442
|
+
* The single-letter tags ([E]/[K]/[D]) are unchanged from the raw-enum era —
|
|
443
|
+
* the model is taught these as "EXPERIENCE/KNOWLEDGE/DECISIONS" concepts in prompts.ts
|
|
444
|
+
* (ordinary English the model understands), and only the resulting STORED
|
|
445
|
+
* value changed in #264 (episode→experience, fact→knowledge, decision→
|
|
446
|
+
* decisions). The tag letters stay stable so neither the prompt nor the
|
|
447
|
+
* parser needs to change; only this mapping table moves.
|
|
432
448
|
*/
|
|
433
449
|
function typeFromTag(letter) {
|
|
434
450
|
switch (letter) {
|
|
435
|
-
case "
|
|
451
|
+
case "K":
|
|
452
|
+
case "k":
|
|
453
|
+
case "F": // legacy tag (was Fact)
|
|
436
454
|
case "f":
|
|
437
|
-
return "
|
|
455
|
+
return "knowledge";
|
|
438
456
|
case "D":
|
|
439
457
|
case "d":
|
|
440
|
-
return "
|
|
441
|
-
// E, e, L, l (rejected), undefined, or anything else →
|
|
458
|
+
return "decisions";
|
|
459
|
+
// E, e, L, l (rejected), undefined, or anything else → experience.
|
|
442
460
|
default:
|
|
443
|
-
return "
|
|
461
|
+
return "experience";
|
|
444
462
|
}
|
|
445
463
|
}
|
|
446
464
|
/**
|
|
@@ -448,7 +466,7 @@ function typeFromTag(letter) {
|
|
|
448
466
|
* Each bullet becomes a separate memory. The leading `[E]`/`[F]`/`[D]` type
|
|
449
467
|
* tag is extracted (→ memoryType), stripped from the stored content, and
|
|
450
468
|
* passed to `insertMemory` via the `memoryType` option (#216). Bullets with
|
|
451
|
-
* no tag default to "
|
|
469
|
+
* no tag default to "experience" (backward compatible with pre-#216 distiller
|
|
452
470
|
* output that never carried a tag).
|
|
453
471
|
*/
|
|
454
472
|
function parseDistilledEntries(markdown) {
|
|
@@ -472,15 +490,15 @@ function parseDistilledEntries(markdown) {
|
|
|
472
490
|
// Extract an optional leading single-letter type tag: "[E]", "[F]",
|
|
473
491
|
// "[D]" (case-insensitive). The tag must be the very first token of the
|
|
474
492
|
// bullet — a bracket that appears later is payload, not a type tag.
|
|
475
|
-
const tagMatch = body.match(/^\[([
|
|
493
|
+
const tagMatch = body.match(/^\[([EFDKefdklL])\]\s*/);
|
|
476
494
|
if (tagMatch) {
|
|
477
495
|
const memoryType = typeFromTag(tagMatch[1].toUpperCase());
|
|
478
496
|
entries.push({ content: body.slice(tagMatch[0].length), memoryType });
|
|
479
497
|
}
|
|
480
498
|
else {
|
|
481
|
-
// No tag →
|
|
499
|
+
// No tag → experience (pre-#216 distiller output, or a model that
|
|
482
500
|
// skipped the tag). Keep the content verbatim.
|
|
483
|
-
entries.push({ content: body, memoryType: "
|
|
501
|
+
entries.push({ content: body, memoryType: "experience" });
|
|
484
502
|
}
|
|
485
503
|
}
|
|
486
504
|
}
|
|
@@ -431,7 +431,7 @@ async function buildCorpusDb(dbPath) {
|
|
|
431
431
|
const vec = await (0, embedder_js_1.embed)(mem.text);
|
|
432
432
|
const id = storage.insertMemory(db, mem.text, vec, {
|
|
433
433
|
sourceAgent: "eval-corpus",
|
|
434
|
-
memoryType: "
|
|
434
|
+
memoryType: "experience",
|
|
435
435
|
baseStrength: 0.5, // uniform — strength is NOT a discriminator here
|
|
436
436
|
});
|
|
437
437
|
idToTopic.set(id, mem.topic);
|
|
@@ -455,7 +455,7 @@ async function buildScopeDb(dbPath) {
|
|
|
455
455
|
const vec = await (0, embedder_js_1.embed)(mem.text);
|
|
456
456
|
const id = storage.insertMemory(db, mem.text, vec, {
|
|
457
457
|
sourceAgent: "eval-scope",
|
|
458
|
-
memoryType: "
|
|
458
|
+
memoryType: "experience",
|
|
459
459
|
baseStrength: 0.5, // uniform — strength is NOT a discriminator here
|
|
460
460
|
project: mem.project, // #203 scope label
|
|
461
461
|
});
|
|
@@ -11,11 +11,11 @@ exports.runReflectionCensus = runReflectionCensus;
|
|
|
11
11
|
function runReflectionCensus(db) {
|
|
12
12
|
const lessonsByDate = db
|
|
13
13
|
.prepare(`SELECT date(created_at) AS date, COUNT(*) AS count
|
|
14
|
-
FROM memories WHERE memory_type = '
|
|
14
|
+
FROM memories WHERE memory_type = 'learnings'
|
|
15
15
|
GROUP BY date ORDER BY date`)
|
|
16
16
|
.all();
|
|
17
|
-
const totalLessons = db.prepare("SELECT COUNT(*) AS c FROM memories WHERE memory_type = '
|
|
18
|
-
const totalEpisodes = db.prepare("SELECT COUNT(*) AS c FROM memories WHERE memory_type = '
|
|
17
|
+
const totalLessons = db.prepare("SELECT COUNT(*) AS c FROM memories WHERE memory_type = 'learnings'").get().c;
|
|
18
|
+
const totalEpisodes = db.prepare("SELECT COUNT(*) AS c FROM memories WHERE memory_type = 'experience'").get().c;
|
|
19
19
|
return {
|
|
20
20
|
lessonsByDate,
|
|
21
21
|
totalLessons,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hosted-mode boot assertions (#110 §1-§2, #271 — Phase 0B).
|
|
3
|
+
*
|
|
4
|
+
* Pure decision function — the side-effect (console.error + process.exit) is
|
|
5
|
+
* the caller's job (mcp-server.ts at boot), so the assertion logic is unit-
|
|
6
|
+
* testable in-process without spawning a child or intercepting process.exit.
|
|
7
|
+
*
|
|
8
|
+
* INERT unless hostedMode is true (self-hosted default). When true, the server
|
|
9
|
+
* must refuse to start under either condition:
|
|
10
|
+
* - HICORTEX_DB_PATH set (a tenant must not be redirectable to an attacker-
|
|
11
|
+
* chosen DB location — path-override attack);
|
|
12
|
+
* - the localhost auth-bypass marker file present (hosted is fail-closed —
|
|
13
|
+
* no bypass; a tenant dir provisioned from a restored tar could otherwise
|
|
14
|
+
* ship with the bypass active).
|
|
15
|
+
*
|
|
16
|
+
* Spec: specs/2026-07-27-hosted-service.md §1-§2 (Phase 0B, issue #271).
|
|
17
|
+
*/
|
|
18
|
+
export interface HostedBootInput {
|
|
19
|
+
/** Resolved hostedMode flag from config (absent/false → self-hosted). */
|
|
20
|
+
hostedMode: boolean;
|
|
21
|
+
/** Whether HICORTEX_DB_PATH is currently set in the environment. */
|
|
22
|
+
dbPathEnvSet: boolean;
|
|
23
|
+
/** Whether the localhost-bypass marker file exists in the home dir. */
|
|
24
|
+
bypassMarkerPresent: boolean;
|
|
25
|
+
}
|
|
26
|
+
export type HostedBootDecision = {
|
|
27
|
+
ok: true;
|
|
28
|
+
hostedMode: boolean;
|
|
29
|
+
} | {
|
|
30
|
+
ok: false;
|
|
31
|
+
hostedMode: true;
|
|
32
|
+
reason: "db-path-override" | "bypass-marker";
|
|
33
|
+
message: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Decide whether the server may boot under hosted-mode constraints. Returns
|
|
37
|
+
* `{ok:true}` for self-hosted (always — assertions never fire) or hosted with
|
|
38
|
+
* a clean environment; returns `{ok:false, message}` when a hosted constraint
|
|
39
|
+
* is violated (caller logs + exits non-zero).
|
|
40
|
+
*/
|
|
41
|
+
export declare function checkHostedBoot(input: HostedBootInput): HostedBootDecision;
|
|
42
|
+
/**
|
|
43
|
+
* Decide whether to emit the "Localhost auth bypass is disabled" boot warning
|
|
44
|
+
* (#271 — CR warning 4). Pure: the caller owns the console.warn side-effect,
|
|
45
|
+
* so this is unit-testable across the four input combinations without spawning
|
|
46
|
+
* a process or capturing stderr.
|
|
47
|
+
*
|
|
48
|
+
* Emits ONLY in self-hosted mode when the bypass marker is absent — the upgrade
|
|
49
|
+
* path (a user who upgraded without re-running init loses the bypass and sees
|
|
50
|
+
* 401s from localhost). Returns null in every other state:
|
|
51
|
+
* - self-hosted + marker present: bypass active, nothing to warn about;
|
|
52
|
+
* - hosted + marker absent: hosted is fail-closed by design, no bypass to warn;
|
|
53
|
+
* - hosted + marker present: checkHostedBoot already refused (unreachable here
|
|
54
|
+
* when called after a passed boot decision), and the failure message is the
|
|
55
|
+
* operator-facing one — a second warning would be noise.
|
|
56
|
+
*
|
|
57
|
+
* The marker is read from the canonical Hicortex home (HICORTEX_HOME), matching
|
|
58
|
+
* where `init` writes it — NOT from stateDir, which can drift when
|
|
59
|
+
* HICORTEX_DB_PATH relocates the DB (#271 CR warning 1).
|
|
60
|
+
*/
|
|
61
|
+
export declare function shouldEmitBypassWarning(hostedMode: boolean, bypassMarkerPresent: boolean): string | null;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkHostedBoot = checkHostedBoot;
|
|
4
|
+
exports.shouldEmitBypassWarning = shouldEmitBypassWarning;
|
|
5
|
+
/**
|
|
6
|
+
* Decide whether the server may boot under hosted-mode constraints. Returns
|
|
7
|
+
* `{ok:true}` for self-hosted (always — assertions never fire) or hosted with
|
|
8
|
+
* a clean environment; returns `{ok:false, message}` when a hosted constraint
|
|
9
|
+
* is violated (caller logs + exits non-zero).
|
|
10
|
+
*/
|
|
11
|
+
function checkHostedBoot(input) {
|
|
12
|
+
// KNOWN ESCAPE HATCH (CR M1, deferred to #110 Phase 0B item #2 — Docker):
|
|
13
|
+
// HICORTEX_HOME is the same class of env-var redirect as HICORTEX_DB_PATH
|
|
14
|
+
// (paths.ts honors it → a tenant who sets it points hostedMode/marker reads
|
|
15
|
+
// at an attacker-chosen dir with no config → hostedMode reads false → every
|
|
16
|
+
// assertion bypassed). It is NOT refused here because the per-tenant Docker
|
|
17
|
+
// template (#2) may legitimately use HICORTEX_HOME to give each tenant its
|
|
18
|
+
// own home dir. Resolution belongs with #2's tenant-home provisioning: either
|
|
19
|
+
// the orchestrator sanitizes HICORTEX_HOME (container sets it, tenant can't
|
|
20
|
+
// override), or this gate refuses it once the Docker design lands. Do NOT
|
|
21
|
+
// ship a hosted tenant before that decision is made.
|
|
22
|
+
const { hostedMode, dbPathEnvSet, bypassMarkerPresent } = input;
|
|
23
|
+
if (!hostedMode)
|
|
24
|
+
return { ok: true, hostedMode: false };
|
|
25
|
+
if (dbPathEnvSet) {
|
|
26
|
+
return {
|
|
27
|
+
ok: false,
|
|
28
|
+
hostedMode: true,
|
|
29
|
+
reason: "db-path-override",
|
|
30
|
+
message: `[hicortex] hostedMode is ON but HICORTEX_DB_PATH is set. ` +
|
|
31
|
+
`Hosted tenants must not allow DB-path overrides — refusing to start. ` +
|
|
32
|
+
`Unset HICORTEX_DB_PATH on hosted tenants.`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (bypassMarkerPresent) {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
hostedMode: true,
|
|
39
|
+
reason: "bypass-marker",
|
|
40
|
+
message: `[hicortex] hostedMode is ON but the localhost auth-bypass marker file ` +
|
|
41
|
+
`(.allow-localhost-bypass) is present. Hosted must be fail-closed — ` +
|
|
42
|
+
`refusing to start. Remove the marker file.`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return { ok: true, hostedMode: true };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Decide whether to emit the "Localhost auth bypass is disabled" boot warning
|
|
49
|
+
* (#271 — CR warning 4). Pure: the caller owns the console.warn side-effect,
|
|
50
|
+
* so this is unit-testable across the four input combinations without spawning
|
|
51
|
+
* a process or capturing stderr.
|
|
52
|
+
*
|
|
53
|
+
* Emits ONLY in self-hosted mode when the bypass marker is absent — the upgrade
|
|
54
|
+
* path (a user who upgraded without re-running init loses the bypass and sees
|
|
55
|
+
* 401s from localhost). Returns null in every other state:
|
|
56
|
+
* - self-hosted + marker present: bypass active, nothing to warn about;
|
|
57
|
+
* - hosted + marker absent: hosted is fail-closed by design, no bypass to warn;
|
|
58
|
+
* - hosted + marker present: checkHostedBoot already refused (unreachable here
|
|
59
|
+
* when called after a passed boot decision), and the failure message is the
|
|
60
|
+
* operator-facing one — a second warning would be noise.
|
|
61
|
+
*
|
|
62
|
+
* The marker is read from the canonical Hicortex home (HICORTEX_HOME), matching
|
|
63
|
+
* where `init` writes it — NOT from stateDir, which can drift when
|
|
64
|
+
* HICORTEX_DB_PATH relocates the DB (#271 CR warning 1).
|
|
65
|
+
*/
|
|
66
|
+
function shouldEmitBypassWarning(hostedMode, bypassMarkerPresent) {
|
|
67
|
+
if (!hostedMode && !bypassMarkerPresent) {
|
|
68
|
+
return ("[hicortex] Localhost auth bypass is disabled — run " +
|
|
69
|
+
"`npx @gamaze/hicortex init` to restore it.");
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -150,7 +150,7 @@ async function fetchOcIdentityBlock(agentId) {
|
|
|
150
150
|
return (0, learnings_identity_js_1.gateAndRenderIdentity)(data, THIS_HARNESS, { requireAgentEcho: agentId !== null });
|
|
151
151
|
}
|
|
152
152
|
/**
|
|
153
|
-
* Fetch /lessons and build the `## Hicortex
|
|
153
|
+
* Fetch /lessons and build the `## Hicortex Learnings` block, or null on any
|
|
154
154
|
* failure or when no lessons survive selection. Preserves the pre-0.13 lesson
|
|
155
155
|
* output; the caller prepends the `## Identity` block and adds separators.
|
|
156
156
|
*/
|
|
@@ -177,8 +177,8 @@ async function buildLessonsBlock(project) {
|
|
|
177
177
|
const meta = [severityMatch?.[1], typeMatch?.[1]].filter(Boolean).join(", ");
|
|
178
178
|
return `- ${title}${meta ? ` (${meta})` : ""}`;
|
|
179
179
|
});
|
|
180
|
-
return (`## Hicortex
|
|
181
|
-
`These are actionable
|
|
180
|
+
return (`## Hicortex Learnings (auto-injected from long-term memory)\n` +
|
|
181
|
+
`These are actionable Learnings from past sessions:\n\n` +
|
|
182
182
|
formatted.join("\n"));
|
|
183
183
|
}
|
|
184
184
|
// ---------------------------------------------------------------------------
|
|
@@ -354,7 +354,7 @@ exports.default = {
|
|
|
354
354
|
const agentId = (0, identity_store_js_1.sanitizeAgentId)(ctx?.agentId ?? "");
|
|
355
355
|
// Fetch all three concurrently with INDEPENDENT fail-soft: no block
|
|
356
356
|
// may ever cost another. Order in the injected output: `## Identity`
|
|
357
|
-
// (standing identity, 0.13) → `## Hicortex
|
|
357
|
+
// (standing identity, 0.13) → `## Hicortex Learnings` → the per-turn
|
|
358
358
|
// `## Memory recall (auto)` index (#193, closest to the prompt).
|
|
359
359
|
const [identityBlock, lessonsBlock, recallBlock] = await Promise.all([
|
|
360
360
|
fetchOcIdentityBlock(agentId).catch(() => null),
|
|
@@ -481,7 +481,7 @@ exports.default = {
|
|
|
481
481
|
}), { name: "hicortex_recent" });
|
|
482
482
|
api.registerTool((_ctx) => ({
|
|
483
483
|
name: "hicortex_ingest",
|
|
484
|
-
description: "Store a new memory in long-term storage. Use for
|
|
484
|
+
description: "Store a new memory in long-term storage. Use for Knowledge, Decisions, or Learnings.",
|
|
485
485
|
parameters: {
|
|
486
486
|
type: "object",
|
|
487
487
|
properties: {
|
|
@@ -489,8 +489,8 @@ exports.default = {
|
|
|
489
489
|
project: { type: "string", description: "Project this memory belongs to" },
|
|
490
490
|
memory_type: {
|
|
491
491
|
type: "string",
|
|
492
|
-
enum: ["
|
|
493
|
-
description: "Type of memory (default:
|
|
492
|
+
enum: ["knowledge", "experience", "decisions", "learnings", "fact", "episode", "decision", "lesson"],
|
|
493
|
+
description: "Type of memory (default: Experience). Accepted: Knowledge/Experience/Decisions/Learnings (legacy raw enum also accepted, normalized to the canonical term).",
|
|
494
494
|
},
|
|
495
495
|
},
|
|
496
496
|
required: ["content"],
|
|
@@ -501,7 +501,7 @@ exports.default = {
|
|
|
501
501
|
content: args.content,
|
|
502
502
|
source_agent: `openclaw/${context?.agentId ?? "manual"}`,
|
|
503
503
|
project: args.project,
|
|
504
|
-
memory_type: args.memory_type
|
|
504
|
+
memory_type: args.memory_type ? (0, type_labels_js_1.normalizeMemoryType)(args.memory_type) : "experience",
|
|
505
505
|
}, 15000);
|
|
506
506
|
if (!result.ok) {
|
|
507
507
|
return { error: `Ingest failed: ${result.data?.error ?? `HTTP ${result.status}`}` };
|
|
@@ -516,7 +516,7 @@ exports.default = {
|
|
|
516
516
|
}), { name: "hicortex_ingest" });
|
|
517
517
|
api.registerTool((_ctx) => ({
|
|
518
518
|
name: "hicortex_lessons",
|
|
519
|
-
description: "Get actionable
|
|
519
|
+
description: "Get actionable Learnings distilled from past sessions. Auto-generated insights about mistakes to avoid.",
|
|
520
520
|
parameters: {
|
|
521
521
|
type: "object",
|
|
522
522
|
properties: {
|
|
@@ -530,7 +530,7 @@ exports.default = {
|
|
|
530
530
|
return { error: `Lessons fetch failed: ${describeGetFailure(status, "/lessons")}` };
|
|
531
531
|
const lessons = data.lessons ?? [];
|
|
532
532
|
if (lessons.length === 0) {
|
|
533
|
-
return { content: [{ type: "text", text: "No
|
|
533
|
+
return { content: [{ type: "text", text: "No Learnings found." }] };
|
|
534
534
|
}
|
|
535
535
|
const text = lessons.map((l) => `- ${l.content.slice(0, 500)}`).join("\n");
|
|
536
536
|
return { content: [{ type: "text", text }] };
|
|
@@ -612,8 +612,8 @@ exports.default = {
|
|
|
612
612
|
project: { type: "string", description: "New project name" },
|
|
613
613
|
memory_type: {
|
|
614
614
|
type: "string",
|
|
615
|
-
enum: ["
|
|
616
|
-
description: "New memory type",
|
|
615
|
+
enum: ["knowledge", "experience", "decisions", "learnings", "fact", "episode", "decision", "lesson"],
|
|
616
|
+
description: "New memory type. Accepted: Knowledge/Experience/Decisions/Learnings (legacy raw enum also accepted, normalized to the canonical term).",
|
|
617
617
|
},
|
|
618
618
|
},
|
|
619
619
|
required: ["id"],
|
|
@@ -624,7 +624,7 @@ exports.default = {
|
|
|
624
624
|
id: args.id,
|
|
625
625
|
content: args.content,
|
|
626
626
|
project: args.project,
|
|
627
|
-
memory_type: args.memory_type,
|
|
627
|
+
memory_type: args.memory_type ? (0, type_labels_js_1.normalizeMemoryType)(args.memory_type) : undefined,
|
|
628
628
|
}, 15000);
|
|
629
629
|
if (result.status === 404) {
|
|
630
630
|
return { error: `Memory not found: ${args.id}` };
|
package/dist/init.d.ts
CHANGED
|
@@ -289,6 +289,21 @@ export declare function getPackageSpec(configDir?: string): string;
|
|
|
289
289
|
* later — the "looks configured but isn't" trap (#176). Never persist it.
|
|
290
290
|
*/
|
|
291
291
|
export declare function isEphemeralNpxPath(binPath: string): boolean;
|
|
292
|
+
/**
|
|
293
|
+
* Build the PATH the launchd/systemd supervisors receive (#276). Order:
|
|
294
|
+
* 1. the binary's own dir — so a SIBLING node wins for nvm/volta/npm-global
|
|
295
|
+
* installs (the version the global was installed under);
|
|
296
|
+
* 2. the dir of the node the supervisor should run under — resolved via
|
|
297
|
+
* `which node` (the symlink path, stable across upgrades); see
|
|
298
|
+
* resolveNodeDir(). This is the generic rescue: for bun/pnpm/yarn globals
|
|
299
|
+
* the bin dir has NO node sibling, and on Apple Silicon node lives in
|
|
300
|
+
* /opt/homebrew/bin. Baking the resolved node dir in fixes every package
|
|
301
|
+
* manager without enumerating them;
|
|
302
|
+
* 3. the standard locations — including /opt/homebrew/bin (Apple Silicon
|
|
303
|
+
* homebrew) as a belt-and-suspenders fallback for the no-sibling case.
|
|
304
|
+
* Deduped (preserving first-seen order); empties dropped.
|
|
305
|
+
*/
|
|
306
|
+
export declare function buildSupervisorPath(binaryArgs: string[]): string;
|
|
292
307
|
/**
|
|
293
308
|
* Install (or verify) the CC SessionStart hook that runs the canonical command
|
|
294
309
|
* `hicortex learnings-identity` (aliased as the legacy `lessons-context`,
|