@joshuaswarren/openclaw-engram 9.0.14 → 9.0.16

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/README.md CHANGED
@@ -5,6 +5,22 @@
5
5
  [![npm version](https://img.shields.io/npm/v/@joshuaswarren/openclaw-engram)](https://www.npmjs.com/package/@joshuaswarren/openclaw-engram)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
7
 
8
+ ## Product Thesis
9
+
10
+ Engram is being built around three requirements:
11
+
12
+ - **Memory that improves action outcomes**
13
+ - **Memory that survives long horizons and failures**
14
+ - **Memory that can defend itself**
15
+
16
+ That product thesis drives the roadmap order:
17
+
18
+ 1. Evaluation harness and shadow-mode measurement
19
+ 2. Objective-state and causal trajectory memory
20
+ 3. Trust-zoned memory promotion and poisoning defense
21
+ 4. Harmonic retrieval over abstractions plus anchors
22
+ 5. Creation-memory, commitments, and recoverability
23
+
8
24
  ## Why Engram?
9
25
 
10
26
  AI agents forget everything between conversations. Engram fixes that.
@@ -13,6 +29,8 @@ AI agents forget everything between conversations. Engram fixes that.
13
29
  - **Smart recall** — Before each conversation, Engram injects the most relevant memories into the agent's context. Your agents remember what they need, when they need it.
14
30
  - **Local-first** — All memory data stays on your filesystem as plain markdown files. No cloud dependency, no vendor lock-in, fully portable.
15
31
  - **Pluggable search** — Choose from six search backends: QMD (hybrid BM25+vector+reranking), LanceDB, Meilisearch, Orama, remote HTTP, or bring your own.
32
+ - **Memory OS features** — Graph recall, temporal memory tree, lifecycle policy, compounding, shared context, memory boxes, and identity continuity can be enabled progressively as your install grows.
33
+ - **Benchmark-first roadmap** — Engram now has an evaluation-harness foundation so memory improvements can be measured on real agent trajectories instead of subjective recall demos.
16
34
  - **Zero-config start** — Install, add an API key, restart. Engram works out of the box with sensible defaults and progressively unlocks advanced features as you enable them.
17
35
 
18
36
  ## Quick Start
@@ -121,6 +139,7 @@ Engram's capabilities are organized into feature families that you can enable pr
121
139
  | **Compounding** | Weekly synthesis that surfaces patterns and recurring mistakes |
122
140
  | **Hot/Cold Tiering** | Automatic migration of aging memories to cold storage |
123
141
  | **Behavior Loop Tuning** | Runtime self-tuning of extraction and recall parameters |
142
+ | **Evaluation Harness Foundation** | Tracks benchmark packs and run summaries so future PRs can be gated on memory quality instead of anecdotes |
124
143
 
125
144
  Start with defaults, then enable features as needed. See [Enable All Features](docs/enable-all-v8.md) for a full-feature config profile.
126
145
 
@@ -130,6 +149,9 @@ Start with defaults, then enable features as needed. See [Enable All Features](d
130
149
  openclaw engram stats # Memory counts, search status, health
131
150
  openclaw engram search "your query" # Search memories from CLI
132
151
  openclaw engram compat --strict # Compatibility check
152
+ openclaw engram benchmark-status # Benchmark/eval harness packs, runs, latest summary
153
+ openclaw engram benchmark-validate <path> # Validate a benchmark manifest or pack directory
154
+ openclaw engram benchmark-import <path> # Import a validated benchmark pack into the eval store
133
155
  openclaw engram conversation-index-health # Conversation index status
134
156
  openclaw engram graph-health # Entity graph status
135
157
  openclaw engram tier-status # Hot/cold tier metrics
@@ -149,6 +171,9 @@ Key settings:
149
171
  | `searchBackend` | `"qmd"` | Search engine: `qmd`, `orama`, `lancedb`, `meilisearch`, `remote`, `noop` |
150
172
  | `qmdEnabled` | `true` | Enable QMD hybrid search |
151
173
  | `memoryDir` | `~/.openclaw/workspace/memory/local` | Memory storage root |
174
+ | `evalHarnessEnabled` | `false` | Enable the evaluation harness foundation for benchmark packs and run summaries |
175
+ | `evalShadowModeEnabled` | `false` | Reserve shadow-mode measurement paths for future benchmark instrumentation |
176
+ | `evalStoreDir` | `{memoryDir}/state/evals` | Root directory for benchmark packs and run summaries |
152
177
 
153
178
  Full reference: [Config Reference](docs/config-reference.md)
154
179
 
@@ -158,6 +183,7 @@ Full reference: [Config Reference](docs/config-reference.md)
158
183
  - [Search Backends](docs/search-backends.md) — Choosing and configuring search engines
159
184
  - [Writing a Search Backend](docs/writing-a-search-backend.md) — Build your own adapter
160
185
  - [Config Reference](docs/config-reference.md) — Every setting with defaults
186
+ - [Evaluation Harness](docs/evaluation-harness.md) — Benchmark pack and run-summary format
161
187
  - [Architecture Overview](docs/architecture/overview.md) — System design and storage layout
162
188
  - [Retrieval Pipeline](docs/architecture/retrieval-pipeline.md) — How recall works
163
189
  - [Memory Lifecycle](docs/architecture/memory-lifecycle.md) — Write, consolidation, expiry
@@ -166,6 +192,7 @@ Full reference: [Config Reference](docs/config-reference.md)
166
192
  - [Namespaces](docs/namespaces.md) — Multi-agent memory isolation
167
193
  - [Shared Context](docs/shared-context.md) — Cross-agent intelligence
168
194
  - [Identity Continuity](docs/identity-continuity.md) — Consistent agent personality
195
+ - [Agentic Memory Roadmap](docs/plans/2026-03-06-engram-agentic-memory-roadmap.md) — Benchmark-first roadmap and PR slices
169
196
 
170
197
  ## Developer Install
171
198
 
package/dist/index.js CHANGED
@@ -281,6 +281,9 @@ function parseConfig(raw) {
281
281
  conversationRecallTopK: typeof cfg.conversationRecallTopK === "number" ? cfg.conversationRecallTopK : 3,
282
282
  conversationRecallMaxChars: typeof cfg.conversationRecallMaxChars === "number" ? cfg.conversationRecallMaxChars : 2500,
283
283
  conversationRecallTimeoutMs: typeof cfg.conversationRecallTimeoutMs === "number" ? cfg.conversationRecallTimeoutMs : 800,
284
+ evalHarnessEnabled: cfg.evalHarnessEnabled === true,
285
+ evalShadowModeEnabled: cfg.evalShadowModeEnabled === true,
286
+ evalStoreDir: typeof cfg.evalStoreDir === "string" && cfg.evalStoreDir.trim().length > 0 ? cfg.evalStoreDir.trim() : path.join(memoryDir, "state", "evals"),
284
287
  // Local LLM Provider (v2.1)
285
288
  localLlmEnabled: cfg.localLlmEnabled === true || cfg.localLlmEnabled === "true",
286
289
  // default: false
@@ -22908,8 +22911,8 @@ promotionCandidates: ${res.promotionCandidateCount}`
22908
22911
  }
22909
22912
 
22910
22913
  // src/cli.ts
22911
- import path50 from "path";
22912
- import { access as access3, readFile as readFile36, readdir as readdir22, unlink as unlink7 } from "fs/promises";
22914
+ import path51 from "path";
22915
+ import { access as access3, readFile as readFile37, readdir as readdir23, unlink as unlink7 } from "fs/promises";
22913
22916
  import { createHash as createHash10 } from "crypto";
22914
22917
 
22915
22918
  // src/transfer/export-json.ts
@@ -23789,8 +23792,8 @@ function gatherCandidates(input, warnings) {
23789
23792
  const record = rec;
23790
23793
  const content = typeof record.content === "string" ? record.content : null;
23791
23794
  if (!content) continue;
23792
- const path52 = typeof record.path === "string" ? record.path : "";
23793
- if (!path52.startsWith("transcripts/") && !path52.includes("/transcripts/")) continue;
23795
+ const path53 = typeof record.path === "string" ? record.path : "";
23796
+ if (!path53.startsWith("transcripts/") && !path53.includes("/transcripts/")) continue;
23794
23797
  rows.push(...parseJsonl(content, warnings));
23795
23798
  }
23796
23799
  return rows;
@@ -25398,6 +25401,277 @@ async function runCompatChecks(options) {
25398
25401
  };
25399
25402
  }
25400
25403
 
25404
+ // src/evals.ts
25405
+ import path50 from "path";
25406
+ import { cp, mkdir as mkdir33, readFile as readFile36, readdir as readdir22, rm as rm5, stat as stat11 } from "fs/promises";
25407
+ function isRecord(value) {
25408
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25409
+ }
25410
+ function assertString(value, field) {
25411
+ if (typeof value !== "string" || value.trim().length === 0) {
25412
+ throw new Error(`${field} must be a non-empty string`);
25413
+ }
25414
+ return value.trim();
25415
+ }
25416
+ function optionalStringArray(value, field) {
25417
+ if (value === void 0) return void 0;
25418
+ if (!Array.isArray(value)) {
25419
+ throw new Error(`${field} must be an array of strings`);
25420
+ }
25421
+ const out = value.filter((item) => typeof item === "string").map((item) => item.trim()).filter((item) => item.length > 0);
25422
+ if (out.length !== value.length) {
25423
+ throw new Error(`${field} must be an array of non-empty strings`);
25424
+ }
25425
+ return out;
25426
+ }
25427
+ function resolveEvalStoreDir(memoryDir, overrideDir) {
25428
+ if (typeof overrideDir === "string" && overrideDir.trim().length > 0) {
25429
+ return overrideDir.trim();
25430
+ }
25431
+ return path50.join(memoryDir, "state", "evals");
25432
+ }
25433
+ function assertSafeBenchmarkId(benchmarkId) {
25434
+ if (benchmarkId === "." || benchmarkId === ".." || benchmarkId.includes("/") || benchmarkId.includes("\\")) {
25435
+ throw new Error("benchmarkId must be a safe path segment");
25436
+ }
25437
+ return benchmarkId;
25438
+ }
25439
+ function validateEvalBenchmarkManifest(raw) {
25440
+ if (!isRecord(raw)) throw new Error("benchmark manifest must be an object");
25441
+ if (raw.schemaVersion !== 1) throw new Error("schemaVersion must be 1");
25442
+ if (!Array.isArray(raw.cases)) throw new Error("cases must be an array");
25443
+ const cases = raw.cases.map((item, index) => {
25444
+ if (!isRecord(item)) throw new Error(`cases[${index}] must be an object`);
25445
+ return {
25446
+ id: assertString(item.id, `cases[${index}].id`),
25447
+ prompt: assertString(item.prompt, `cases[${index}].prompt`),
25448
+ expectedSignals: optionalStringArray(item.expectedSignals, `cases[${index}].expectedSignals`),
25449
+ notes: typeof item.notes === "string" && item.notes.trim().length > 0 ? item.notes.trim() : void 0
25450
+ };
25451
+ });
25452
+ return {
25453
+ schemaVersion: 1,
25454
+ benchmarkId: assertString(raw.benchmarkId, "benchmarkId"),
25455
+ title: assertString(raw.title, "title"),
25456
+ description: typeof raw.description === "string" && raw.description.trim().length > 0 ? raw.description.trim() : void 0,
25457
+ tags: optionalStringArray(raw.tags, "tags"),
25458
+ sourceLinks: optionalStringArray(raw.sourceLinks, "sourceLinks"),
25459
+ cases
25460
+ };
25461
+ }
25462
+ function validateEvalRunSummary(raw) {
25463
+ if (!isRecord(raw)) throw new Error("eval run summary must be an object");
25464
+ if (raw.schemaVersion !== 1) throw new Error("schemaVersion must be 1");
25465
+ const status = assertString(raw.status, "status");
25466
+ if (!["running", "completed", "failed", "partial"].includes(status)) {
25467
+ throw new Error("status must be one of running|completed|failed|partial");
25468
+ }
25469
+ const totalCases = Number(raw.totalCases);
25470
+ const passedCases = Number(raw.passedCases);
25471
+ const failedCases = Number(raw.failedCases);
25472
+ if (!Number.isFinite(totalCases) || totalCases < 0) throw new Error("totalCases must be a non-negative number");
25473
+ if (!Number.isFinite(passedCases) || passedCases < 0) throw new Error("passedCases must be a non-negative number");
25474
+ if (!Number.isFinite(failedCases) || failedCases < 0) throw new Error("failedCases must be a non-negative number");
25475
+ const metrics = isRecord(raw.metrics) ? {
25476
+ recallPrecisionAtK: typeof raw.metrics.recallPrecisionAtK === "number" ? raw.metrics.recallPrecisionAtK : void 0,
25477
+ actionOutcomeScore: typeof raw.metrics.actionOutcomeScore === "number" ? raw.metrics.actionOutcomeScore : void 0,
25478
+ objectiveStateCoverage: typeof raw.metrics.objectiveStateCoverage === "number" ? raw.metrics.objectiveStateCoverage : void 0,
25479
+ causalPathRecall: typeof raw.metrics.causalPathRecall === "number" ? raw.metrics.causalPathRecall : void 0,
25480
+ trustViolationRate: typeof raw.metrics.trustViolationRate === "number" ? raw.metrics.trustViolationRate : void 0,
25481
+ creationRecoveryScore: typeof raw.metrics.creationRecoveryScore === "number" ? raw.metrics.creationRecoveryScore : void 0
25482
+ } : void 0;
25483
+ return {
25484
+ schemaVersion: 1,
25485
+ runId: assertString(raw.runId, "runId"),
25486
+ benchmarkId: assertString(raw.benchmarkId, "benchmarkId"),
25487
+ status,
25488
+ startedAt: assertString(raw.startedAt, "startedAt"),
25489
+ completedAt: typeof raw.completedAt === "string" && raw.completedAt.trim().length > 0 ? raw.completedAt.trim() : void 0,
25490
+ totalCases,
25491
+ passedCases,
25492
+ failedCases,
25493
+ metrics,
25494
+ notes: typeof raw.notes === "string" && raw.notes.trim().length > 0 ? raw.notes.trim() : void 0,
25495
+ gitRef: typeof raw.gitRef === "string" && raw.gitRef.trim().length > 0 ? raw.gitRef.trim() : void 0
25496
+ };
25497
+ }
25498
+ async function listJsonFiles(dir) {
25499
+ try {
25500
+ const entries = await readdir22(dir, { withFileTypes: true });
25501
+ const out = [];
25502
+ for (const entry of entries) {
25503
+ const fullPath = path50.join(dir, entry.name);
25504
+ if (entry.isDirectory()) {
25505
+ out.push(...await listJsonFiles(fullPath));
25506
+ } else if (entry.isFile() && entry.name.endsWith(".json")) {
25507
+ out.push(fullPath);
25508
+ }
25509
+ }
25510
+ return out.sort();
25511
+ } catch {
25512
+ return [];
25513
+ }
25514
+ }
25515
+ async function listNamedFiles(dir, fileName) {
25516
+ try {
25517
+ const entries = await readdir22(dir, { withFileTypes: true });
25518
+ const out = [];
25519
+ for (const entry of entries) {
25520
+ const fullPath = path50.join(dir, entry.name);
25521
+ if (entry.isDirectory()) {
25522
+ out.push(...await listNamedFiles(fullPath, fileName));
25523
+ } else if (entry.isFile() && entry.name === fileName) {
25524
+ out.push(fullPath);
25525
+ }
25526
+ }
25527
+ return out.sort();
25528
+ } catch {
25529
+ return [];
25530
+ }
25531
+ }
25532
+ async function readJsonFile2(filePath) {
25533
+ return JSON.parse(await readFile36(filePath, "utf-8"));
25534
+ }
25535
+ async function resolveBenchmarkManifestPath(sourcePath) {
25536
+ const info = await stat11(sourcePath);
25537
+ if (info.isDirectory()) {
25538
+ return {
25539
+ sourceKind: "directory",
25540
+ manifestPath: path50.join(sourcePath, "manifest.json")
25541
+ };
25542
+ }
25543
+ if (info.isFile()) {
25544
+ return {
25545
+ sourceKind: "file",
25546
+ manifestPath: sourcePath
25547
+ };
25548
+ }
25549
+ throw new Error("benchmark pack source must be a file or directory");
25550
+ }
25551
+ async function validateEvalBenchmarkPack(sourcePath) {
25552
+ const trimmedSourcePath = sourcePath.trim();
25553
+ if (trimmedSourcePath.length === 0) {
25554
+ throw new Error("benchmark pack path must be a non-empty string");
25555
+ }
25556
+ const { manifestPath } = await resolveBenchmarkManifestPath(trimmedSourcePath);
25557
+ const manifest = validateEvalBenchmarkManifest(await readJsonFile2(manifestPath));
25558
+ return {
25559
+ sourcePath: trimmedSourcePath,
25560
+ manifestPath,
25561
+ benchmarkId: assertSafeBenchmarkId(manifest.benchmarkId),
25562
+ title: manifest.title,
25563
+ totalCases: manifest.cases.length,
25564
+ tags: [...manifest.tags ?? []],
25565
+ sourceLinks: [...manifest.sourceLinks ?? []]
25566
+ };
25567
+ }
25568
+ async function importEvalBenchmarkPack(options) {
25569
+ const summary = await validateEvalBenchmarkPack(options.sourcePath);
25570
+ const rootDir = resolveEvalStoreDir(options.memoryDir, options.evalStoreDir);
25571
+ const benchmarkDir = path50.join(rootDir, "benchmarks");
25572
+ const targetDir = path50.join(benchmarkDir, summary.benchmarkId);
25573
+ const { sourceKind, manifestPath } = await resolveBenchmarkManifestPath(summary.sourcePath);
25574
+ let overwritten = false;
25575
+ try {
25576
+ await stat11(targetDir);
25577
+ if (options.force !== true) {
25578
+ throw new Error(`benchmark pack already exists at ${targetDir}; rerun with force to replace it`);
25579
+ }
25580
+ overwritten = true;
25581
+ await rm5(targetDir, { recursive: true, force: true });
25582
+ } catch (error) {
25583
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
25584
+ throw error;
25585
+ }
25586
+ }
25587
+ await mkdir33(benchmarkDir, { recursive: true });
25588
+ if (sourceKind === "directory") {
25589
+ await cp(summary.sourcePath, targetDir, { recursive: true });
25590
+ } else {
25591
+ await mkdir33(targetDir, { recursive: true });
25592
+ await cp(manifestPath, path50.join(targetDir, "manifest.json"));
25593
+ }
25594
+ return {
25595
+ ...summary,
25596
+ targetDir,
25597
+ overwritten
25598
+ };
25599
+ }
25600
+ async function getEvalHarnessStatus(options) {
25601
+ const rootDir = resolveEvalStoreDir(options.memoryDir, options.evalStoreDir);
25602
+ const benchmarkDir = path50.join(rootDir, "benchmarks");
25603
+ const runsDir = path50.join(rootDir, "runs");
25604
+ const benchmarkFiles = await listNamedFiles(benchmarkDir, "manifest.json");
25605
+ const runFiles = await listJsonFiles(runsDir);
25606
+ const invalidBenchmarks = [];
25607
+ const invalidRuns = [];
25608
+ const manifests = [];
25609
+ for (const filePath of benchmarkFiles) {
25610
+ try {
25611
+ manifests.push(validateEvalBenchmarkManifest(await readJsonFile2(filePath)));
25612
+ } catch (error) {
25613
+ invalidBenchmarks.push({
25614
+ path: filePath,
25615
+ error: error instanceof Error ? error.message : String(error)
25616
+ });
25617
+ }
25618
+ }
25619
+ const runs = [];
25620
+ for (const filePath of runFiles) {
25621
+ try {
25622
+ runs.push(validateEvalRunSummary(await readJsonFile2(filePath)));
25623
+ } catch (error) {
25624
+ invalidRuns.push({
25625
+ path: filePath,
25626
+ error: error instanceof Error ? error.message : String(error)
25627
+ });
25628
+ }
25629
+ }
25630
+ runs.sort((a, b) => {
25631
+ const aTime = Date.parse(a.completedAt ?? a.startedAt);
25632
+ const bTime = Date.parse(b.completedAt ?? b.startedAt);
25633
+ return (Number.isNaN(bTime) ? 0 : bTime) - (Number.isNaN(aTime) ? 0 : aTime);
25634
+ });
25635
+ const latestRun = runs[0];
25636
+ const tags = /* @__PURE__ */ new Set();
25637
+ const sourceLinks = /* @__PURE__ */ new Set();
25638
+ let totalCases = 0;
25639
+ for (const manifest of manifests) {
25640
+ totalCases += manifest.cases.length;
25641
+ for (const tag of manifest.tags ?? []) tags.add(tag);
25642
+ for (const link of manifest.sourceLinks ?? []) sourceLinks.add(link);
25643
+ }
25644
+ return {
25645
+ enabled: options.enabled,
25646
+ shadowModeEnabled: options.shadowModeEnabled,
25647
+ rootDir,
25648
+ benchmarkDir,
25649
+ runsDir,
25650
+ benchmarks: {
25651
+ total: benchmarkFiles.length,
25652
+ valid: manifests.length,
25653
+ invalid: invalidBenchmarks.length,
25654
+ totalCases,
25655
+ tags: [...tags].sort(),
25656
+ sourceLinks: [...sourceLinks].sort()
25657
+ },
25658
+ runs: {
25659
+ total: runFiles.length,
25660
+ invalid: invalidRuns.length,
25661
+ completed: runs.filter((run) => run.status === "completed").length,
25662
+ failed: runs.filter((run) => run.status === "failed").length,
25663
+ partial: runs.filter((run) => run.status === "partial").length,
25664
+ running: runs.filter((run) => run.status === "running").length,
25665
+ latestRunId: latestRun?.runId,
25666
+ latestBenchmarkId: latestRun?.benchmarkId,
25667
+ latestCompletedAt: latestRun?.completedAt
25668
+ },
25669
+ latestRun,
25670
+ invalidBenchmarks,
25671
+ invalidRuns
25672
+ };
25673
+ }
25674
+
25401
25675
  // src/cli.ts
25402
25676
  function rankCandidateForKeep(a, b) {
25403
25677
  const aConfidence = typeof a.frontmatter.confidence === "number" ? a.frontmatter.confidence : 0;
@@ -25554,6 +25828,25 @@ async function runGraphHealthCliCommand(options) {
25554
25828
  includeRepairGuidance: options.includeRepairGuidance
25555
25829
  });
25556
25830
  }
25831
+ async function runBenchmarkStatusCliCommand(options) {
25832
+ return getEvalHarnessStatus({
25833
+ memoryDir: options.memoryDir,
25834
+ evalStoreDir: options.evalStoreDir,
25835
+ enabled: options.evalHarnessEnabled,
25836
+ shadowModeEnabled: options.evalShadowModeEnabled
25837
+ });
25838
+ }
25839
+ async function runBenchmarkValidateCliCommand(options) {
25840
+ return validateEvalBenchmarkPack(options.path);
25841
+ }
25842
+ async function runBenchmarkImportCliCommand(options) {
25843
+ return importEvalBenchmarkPack({
25844
+ sourcePath: options.path,
25845
+ memoryDir: options.memoryDir,
25846
+ evalStoreDir: options.evalStoreDir,
25847
+ force: options.force === true
25848
+ });
25849
+ }
25557
25850
  async function runSessionCheckCliCommand(options) {
25558
25851
  return analyzeSessionIntegrity({ memoryDir: options.memoryDir });
25559
25852
  }
@@ -25781,7 +26074,7 @@ function policyVersionForValues(values, config) {
25781
26074
  return createHash10("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 12);
25782
26075
  }
25783
26076
  async function readRuntimePolicySnapshot2(config, fileName) {
25784
- const filePath = path50.join(config.memoryDir, "state", fileName);
26077
+ const filePath = path51.join(config.memoryDir, "state", fileName);
25785
26078
  const snapshot = await readRuntimePolicySnapshot(filePath, {
25786
26079
  maxStaleDecayThreshold: config.lifecycleArchiveDecayThreshold
25787
26080
  });
@@ -26281,7 +26574,7 @@ async function withTimeout(promise, timeoutMs, timeoutMessage) {
26281
26574
  }
26282
26575
  async function runReplayCliCommand(orchestrator, options) {
26283
26576
  const extractionIdleTimeoutMs = Number.isFinite(options.extractionIdleTimeoutMs) ? Math.max(1e3, Math.floor(options.extractionIdleTimeoutMs)) : 15 * 6e4;
26284
- const inputRaw = await readFile36(options.inputPath, "utf-8");
26577
+ const inputRaw = await readFile37(options.inputPath, "utf-8");
26285
26578
  const registry = buildReplayNormalizerRegistry([
26286
26579
  openclawReplayNormalizer,
26287
26580
  claudeReplayNormalizer,
@@ -26346,7 +26639,7 @@ async function runReplayCliCommand(orchestrator, options) {
26346
26639
  async function getPluginVersion() {
26347
26640
  try {
26348
26641
  const pkgPath = new URL("../package.json", import.meta.url);
26349
- const raw = await readFile36(pkgPath, "utf-8");
26642
+ const raw = await readFile37(pkgPath, "utf-8");
26350
26643
  const parsed = JSON.parse(raw);
26351
26644
  return parsed.version ?? "unknown";
26352
26645
  } catch {
@@ -26365,32 +26658,32 @@ async function resolveMemoryDirForNamespace(orchestrator, namespace) {
26365
26658
  const ns = (namespace ?? "").trim();
26366
26659
  if (!ns) return orchestrator.config.memoryDir;
26367
26660
  if (!orchestrator.config.namespacesEnabled) return orchestrator.config.memoryDir;
26368
- const candidate = path50.join(orchestrator.config.memoryDir, "namespaces", ns);
26661
+ const candidate = path51.join(orchestrator.config.memoryDir, "namespaces", ns);
26369
26662
  if (ns === orchestrator.config.defaultNamespace) {
26370
26663
  return await exists2(candidate) ? candidate : orchestrator.config.memoryDir;
26371
26664
  }
26372
26665
  return candidate;
26373
26666
  }
26374
26667
  async function readAllMemoryFiles(memoryDir) {
26375
- const roots = [path50.join(memoryDir, "facts"), path50.join(memoryDir, "corrections")];
26668
+ const roots = [path51.join(memoryDir, "facts"), path51.join(memoryDir, "corrections")];
26376
26669
  const out = [];
26377
26670
  const walk = async (dir) => {
26378
26671
  let entries;
26379
26672
  try {
26380
- entries = await readdir22(dir, { withFileTypes: true });
26673
+ entries = await readdir23(dir, { withFileTypes: true });
26381
26674
  } catch {
26382
26675
  return;
26383
26676
  }
26384
26677
  for (const entry of entries) {
26385
26678
  const entryName = typeof entry.name === "string" ? entry.name : entry.name.toString("utf-8");
26386
- const fullPath = path50.join(dir, entryName);
26679
+ const fullPath = path51.join(dir, entryName);
26387
26680
  if (entry.isDirectory()) {
26388
26681
  await walk(fullPath);
26389
26682
  continue;
26390
26683
  }
26391
26684
  if (!entry.isFile() || !entryName.endsWith(".md")) continue;
26392
26685
  try {
26393
- const raw = await readFile36(fullPath, "utf-8");
26686
+ const raw = await readFile37(fullPath, "utf-8");
26394
26687
  const parsed = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
26395
26688
  if (!parsed) continue;
26396
26689
  const fmRaw = parsed[1];
@@ -26651,6 +26944,36 @@ function registerCli(api, orchestrator) {
26651
26944
  }
26652
26945
  console.log("OK");
26653
26946
  });
26947
+ cmd.command("benchmark-status").description("Show benchmark/evaluation harness status, benchmark packs, and latest run summary").action(async () => {
26948
+ const status = await runBenchmarkStatusCliCommand({
26949
+ memoryDir: orchestrator.config.memoryDir,
26950
+ evalStoreDir: orchestrator.config.evalStoreDir,
26951
+ evalHarnessEnabled: orchestrator.config.evalHarnessEnabled,
26952
+ evalShadowModeEnabled: orchestrator.config.evalShadowModeEnabled
26953
+ });
26954
+ console.log(JSON.stringify(status, null, 2));
26955
+ console.log("OK");
26956
+ });
26957
+ cmd.command("benchmark-validate").description("Validate a benchmark manifest file or pack directory without importing it").argument("<path>", "Path to a benchmark manifest JSON file or a directory with manifest.json").action(async (...args) => {
26958
+ const inputPath = args[0];
26959
+ const summary = await runBenchmarkValidateCliCommand({
26960
+ path: typeof inputPath === "string" ? inputPath : ""
26961
+ });
26962
+ console.log(JSON.stringify(summary, null, 2));
26963
+ console.log("OK");
26964
+ });
26965
+ cmd.command("benchmark-import").description("Validate and import a benchmark manifest file or pack directory into Engram's eval store").argument("<path>", "Path to a benchmark manifest JSON file or a directory with manifest.json").option("--force", "Replace an existing imported benchmark pack with the same benchmarkId").action(async (...args) => {
26966
+ const inputPath = args[0];
26967
+ const options = args[1] ?? {};
26968
+ const summary = await runBenchmarkImportCliCommand({
26969
+ path: typeof inputPath === "string" ? inputPath : "",
26970
+ memoryDir: orchestrator.config.memoryDir,
26971
+ evalStoreDir: orchestrator.config.evalStoreDir,
26972
+ force: options.force === true
26973
+ });
26974
+ console.log(JSON.stringify(summary, null, 2));
26975
+ console.log("OK");
26976
+ });
26654
26977
  cmd.command("conversation-index-health").description("Show conversation index backend health and index stats").action(async () => {
26655
26978
  const health = await runConversationIndexHealthCliCommand(orchestrator);
26656
26979
  console.log(JSON.stringify(health, null, 2));
@@ -27300,7 +27623,7 @@ function registerCli(api, orchestrator) {
27300
27623
  }
27301
27624
  });
27302
27625
  cmd.command("identity").description("Show agent identity reflections").action(async () => {
27303
- const workspaceDir = path50.join(process.env.HOME ?? "~", ".openclaw", "workspace");
27626
+ const workspaceDir = path51.join(process.env.HOME ?? "~", ".openclaw", "workspace");
27304
27627
  const identity = await orchestrator.storage.readIdentity(workspaceDir);
27305
27628
  if (!identity) {
27306
27629
  console.log("No identity file found.");
@@ -27523,8 +27846,8 @@ function registerCli(api, orchestrator) {
27523
27846
  const options = args[0] ?? {};
27524
27847
  const threadId = options.thread;
27525
27848
  const top = parseInt(options.top ?? "10", 10);
27526
- const memoryDir = path50.join(process.env.HOME ?? "~", ".openclaw", "workspace", "memory", "local");
27527
- const threading = new ThreadingManager(path50.join(memoryDir, "threads"));
27849
+ const memoryDir = path51.join(process.env.HOME ?? "~", ".openclaw", "workspace", "memory", "local");
27850
+ const threading = new ThreadingManager(path51.join(memoryDir, "threads"));
27528
27851
  if (threadId) {
27529
27852
  const thread = await threading.loadThread(threadId);
27530
27853
  if (!thread) {
@@ -27697,9 +28020,9 @@ function parseDuration(duration) {
27697
28020
  }
27698
28021
 
27699
28022
  // src/index.ts
27700
- import { readFile as readFile37, writeFile as writeFile29 } from "fs/promises";
28023
+ import { readFile as readFile38, writeFile as writeFile29 } from "fs/promises";
27701
28024
  import { readFileSync as readFileSync4 } from "fs";
27702
- import path51 from "path";
28025
+ import path52 from "path";
27703
28026
  import os6 from "os";
27704
28027
  var ENGRAM_REGISTERED_GUARD = "__openclawEngramRegistered";
27705
28028
  var ENGRAM_HOOK_APIS = "__openclawEngramHookApis";
@@ -27707,7 +28030,7 @@ function loadPluginConfigFromFile() {
27707
28030
  try {
27708
28031
  const explicitConfigPath = process.env.OPENCLAW_ENGRAM_CONFIG_PATH || process.env.OPENCLAW_CONFIG_PATH;
27709
28032
  const homeDir = process.env.HOME ?? os6.homedir();
27710
- const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path51.join(homeDir, ".openclaw", "openclaw.json");
28033
+ const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path52.join(homeDir, ".openclaw", "openclaw.json");
27711
28034
  const content = readFileSync4(configPath, "utf-8");
27712
28035
  const config = JSON.parse(content);
27713
28036
  const pluginEntry = config?.plugins?.entries?.["openclaw-engram"];
@@ -27944,7 +28267,7 @@ Use this context naturally when relevant. Never quote or expose this memory cont
27944
28267
  `session reset via API for ${sessionKey}, new sessionId=${result.sessionId}`
27945
28268
  );
27946
28269
  const safeSessionKey = sanitizeSessionKeyForFilename(sessionKey);
27947
- const signalPath = path51.join(
28270
+ const signalPath = path52.join(
27948
28271
  workspaceDir,
27949
28272
  `.compaction-reset-signal-${safeSessionKey}`
27950
28273
  );
@@ -27975,11 +28298,11 @@ Use this context naturally when relevant. Never quote or expose this memory cont
27975
28298
  );
27976
28299
  async function ensureHourlySummaryCron(api2) {
27977
28300
  const jobId = "engram-hourly-summary";
27978
- const cronFilePath = path51.join(os6.homedir(), ".openclaw", "cron", "jobs.json");
28301
+ const cronFilePath = path52.join(os6.homedir(), ".openclaw", "cron", "jobs.json");
27979
28302
  try {
27980
28303
  let jobsData = { version: 1, jobs: [] };
27981
28304
  try {
27982
- const content = await readFile37(cronFilePath, "utf-8");
28305
+ const content = await readFile38(cronFilePath, "utf-8");
27983
28306
  jobsData = JSON.parse(content);
27984
28307
  } catch {
27985
28308
  }