@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 +27 -0
- package/dist/index.js +344 -21
- package/dist/index.js.map +1 -1
- package/openclaw.plugin.json +29 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,6 +5,22 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@joshuaswarren/openclaw-engram)
|
|
6
6
|
[](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
|
|
22912
|
-
import { access as access3, readFile as
|
|
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
|
|
23793
|
-
if (!
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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 = [
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
27527
|
-
const threading = new ThreadingManager(
|
|
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
|
|
28023
|
+
import { readFile as readFile38, writeFile as writeFile29 } from "fs/promises";
|
|
27701
28024
|
import { readFileSync as readFileSync4 } from "fs";
|
|
27702
|
-
import
|
|
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 :
|
|
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 =
|
|
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 =
|
|
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
|
|
28305
|
+
const content = await readFile38(cronFilePath, "utf-8");
|
|
27983
28306
|
jobsData = JSON.parse(content);
|
|
27984
28307
|
} catch {
|
|
27985
28308
|
}
|