@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.
@@ -238,10 +238,15 @@ let compKey = "by_type";
238
238
 
239
239
  const $ = (id) => document.getElementById(id);
240
240
 
241
- // #264 WS2: human-term labels for the internal memory_type enum. The data
242
- // blob carries the RAW enum keys (a stable JSON contract, also relied on by
243
- // tests); only the rendered label changes. Unknown keys pass through.
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 WS2: human-term labels for the internal memory_type enum. Node data
393
- // carries the RAW enum (the /graph JSON contract filters on it); only the
394
- // rendered strings change. Unknown values pass through unchanged.
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",
@@ -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='lesson' carries the type, and the
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 === "lesson");
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: "lesson",
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 = 'lesson' GROUP BY domain`)
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 = 'lesson'
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 === "decision" ||
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 = 'decision'
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 = 'lesson'")
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 === "lesson")
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 = 'lesson' AND created_at BETWEEN ? AND ?
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 'episode',
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.
@@ -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[]): Promise<DistilledEntry[]>;
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 "episode" | "fact" |
68
- * "decision" — the three distillation-time types. "lesson" is deliberately
69
- * absent: lessons are the reflection stage's product, never distillation's
70
- * (#216). A missing/unknown tag defaults to "episode" so older distiller
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: "episode" | "fact" | "decision";
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 [F] and the other [E]).
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]/[F]/[D]) is already stripped by the parser, so a
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
- * episode (the pre-#216 default). `[L]` is explicitly rejected → episode: the
430
- * distiller must NEVER emit lessons (that's the reflection stage's job), so a
431
- * model that emits `[L]` is wrong and we do not propagate it as a lesson.
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 "F":
451
+ case "K":
452
+ case "k":
453
+ case "F": // legacy tag (was Fact)
436
454
  case "f":
437
- return "fact";
455
+ return "knowledge";
438
456
  case "D":
439
457
  case "d":
440
- return "decision";
441
- // E, e, L, l (rejected), undefined, or anything else → episode.
458
+ return "decisions";
459
+ // E, e, L, l (rejected), undefined, or anything else → experience.
442
460
  default:
443
- return "episode";
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 "episode" (backward compatible with pre-#216 distiller
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(/^\[([EFDefdLl])\]\s*/);
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 → episode (pre-#216 distiller output, or a model that
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: "episode" });
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: "episode",
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: "episode",
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 = 'lesson'
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 = 'lesson'").get().c;
18
- const totalEpisodes = db.prepare("SELECT COUNT(*) AS c FROM memories WHERE memory_type = 'episode'").get().c;
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 Lessons` block, or null on any
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 Lessons (auto-injected from long-term memory)\n` +
181
- `These are actionable lessons learned from past sessions:\n\n` +
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 Lessons` → the per-turn
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 important facts, decisions, or lessons.",
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: ["episode", "lesson", "fact", "decision"],
493
- description: "Type of memory (default: episode)",
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 ?? "episode",
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 lessons learned from past sessions. Auto-generated insights about mistakes to avoid.",
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 lessons found." }] };
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: ["episode", "lesson", "fact", "decision"],
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`,