@joshuaswarren/openclaw-engram 9.0.28 → 9.0.29
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 +4 -0
- package/dist/index.js +145 -19
- package/dist/index.js.map +1 -1
- package/openclaw.plugin.json +29 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ AI agents forget everything between conversations. Engram fixes that.
|
|
|
34
34
|
- **Objective-state recall** — Engram can now store normalized file, process, and tool outcomes and, when `objectiveStateRecallEnabled` is enabled, inject the most relevant objective-state snapshots back into recall context as a separate `Objective State` section.
|
|
35
35
|
- **Causal trajectory graph foundation** — Engram can now persist typed `goal -> action -> observation -> outcome -> follow-up` chains when `causalTrajectoryMemoryEnabled` is enabled and, with `actionGraphRecallEnabled`, emit deterministic action-conditioned edges into the causal graph for later trajectory-aware retrieval.
|
|
36
36
|
- **Causal trajectory recall** — Engram can now, when `causalTrajectoryRecallEnabled` is enabled, inject prompt-relevant causal chains back into recall context as a separate `Causal Trajectories` section with lightweight match explainability.
|
|
37
|
+
- **Trust-zone store foundation** — Engram can now, when `trustZonesEnabled` is enabled, persist typed quarantine, working, and trusted records with provenance metadata into a dedicated trust-zone store for later promotion and defense slices.
|
|
37
38
|
- **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.
|
|
38
39
|
|
|
39
40
|
## Quick Start
|
|
@@ -189,6 +190,9 @@ Key settings:
|
|
|
189
190
|
| `causalTrajectoryStoreDir` | `{memoryDir}/state/causal-trajectories` | Root directory for causal-trajectory records |
|
|
190
191
|
| `causalTrajectoryRecallEnabled` | `false` | Inject prompt-relevant causal trajectories into recall context |
|
|
191
192
|
| `actionGraphRecallEnabled` | `false` | Write action-conditioned causal-stage edges from typed trajectory records into the causal graph |
|
|
193
|
+
| `trustZonesEnabled` | `false` | Enable the trust-zone memory foundation for quarantine, working, and trusted records |
|
|
194
|
+
| `quarantinePromotionEnabled` | `false` | Reserve future promotion flows from quarantine into higher-trust zones |
|
|
195
|
+
| `trustZoneStoreDir` | `{memoryDir}/state/trust-zones` | Root directory for trust-zone records |
|
|
192
196
|
|
|
193
197
|
Full reference: [Config Reference](docs/config-reference.md)
|
|
194
198
|
|
package/dist/index.js
CHANGED
|
@@ -296,6 +296,9 @@ function parseConfig(raw) {
|
|
|
296
296
|
causalTrajectoryStoreDir: typeof cfg.causalTrajectoryStoreDir === "string" && cfg.causalTrajectoryStoreDir.trim().length > 0 ? cfg.causalTrajectoryStoreDir.trim() : path.join(memoryDir, "state", "causal-trajectories"),
|
|
297
297
|
causalTrajectoryRecallEnabled: cfg.causalTrajectoryRecallEnabled === true,
|
|
298
298
|
actionGraphRecallEnabled: cfg.actionGraphRecallEnabled === true,
|
|
299
|
+
trustZonesEnabled: cfg.trustZonesEnabled === true,
|
|
300
|
+
quarantinePromotionEnabled: cfg.quarantinePromotionEnabled === true,
|
|
301
|
+
trustZoneStoreDir: typeof cfg.trustZoneStoreDir === "string" && cfg.trustZoneStoreDir.trim().length > 0 ? cfg.trustZoneStoreDir.trim() : path.join(memoryDir, "state", "trust-zones"),
|
|
299
302
|
// Local LLM Provider (v2.1)
|
|
300
303
|
localLlmEnabled: cfg.localLlmEnabled === true || cfg.localLlmEnabled === "true",
|
|
301
304
|
// default: false
|
|
@@ -14213,9 +14216,9 @@ function assertSafePathSegment(value, field) {
|
|
|
14213
14216
|
}
|
|
14214
14217
|
return value;
|
|
14215
14218
|
}
|
|
14216
|
-
function assertIsoRecordedAt(value) {
|
|
14219
|
+
function assertIsoRecordedAt(value, field = "recordedAt") {
|
|
14217
14220
|
if (!/^\d{4}-\d{2}-\d{2}T/.test(value)) {
|
|
14218
|
-
throw new Error(
|
|
14221
|
+
throw new Error(`${field} must be an ISO timestamp`);
|
|
14219
14222
|
}
|
|
14220
14223
|
return value;
|
|
14221
14224
|
}
|
|
@@ -23718,7 +23721,7 @@ promotionCandidates: ${res.promotionCandidateCount}`
|
|
|
23718
23721
|
}
|
|
23719
23722
|
|
|
23720
23723
|
// src/cli.ts
|
|
23721
|
-
import
|
|
23724
|
+
import path54 from "path";
|
|
23722
23725
|
import { access as access3, readFile as readFile37, readdir as readdir24, unlink as unlink7 } from "fs/promises";
|
|
23723
23726
|
import { createHash as createHash10 } from "crypto";
|
|
23724
23727
|
|
|
@@ -24599,8 +24602,8 @@ function gatherCandidates(input, warnings) {
|
|
|
24599
24602
|
const record = rec;
|
|
24600
24603
|
const content = typeof record.content === "string" ? record.content : null;
|
|
24601
24604
|
if (!content) continue;
|
|
24602
|
-
const
|
|
24603
|
-
if (!
|
|
24605
|
+
const path56 = typeof record.path === "string" ? record.path : "";
|
|
24606
|
+
if (!path56.startsWith("transcripts/") && !path56.includes("/transcripts/")) continue;
|
|
24604
24607
|
rows.push(...parseJsonl(content, warnings));
|
|
24605
24608
|
}
|
|
24606
24609
|
return rows;
|
|
@@ -26234,6 +26237,111 @@ async function runCompatChecks(options) {
|
|
|
26234
26237
|
};
|
|
26235
26238
|
}
|
|
26236
26239
|
|
|
26240
|
+
// src/trust-zones.ts
|
|
26241
|
+
import path53 from "path";
|
|
26242
|
+
import { mkdir as mkdir35, writeFile as writeFile32 } from "fs/promises";
|
|
26243
|
+
function validateMetadata3(raw) {
|
|
26244
|
+
return validateStringRecord(raw, "metadata");
|
|
26245
|
+
}
|
|
26246
|
+
function validateZone(raw, field) {
|
|
26247
|
+
const value = assertString2(raw, field);
|
|
26248
|
+
if (!["quarantine", "working", "trusted"].includes(value)) {
|
|
26249
|
+
throw new Error(`${field} must be one of quarantine|working|trusted`);
|
|
26250
|
+
}
|
|
26251
|
+
return value;
|
|
26252
|
+
}
|
|
26253
|
+
function validateKind(raw) {
|
|
26254
|
+
const value = assertString2(raw, "kind");
|
|
26255
|
+
if (!["memory", "artifact", "state", "trajectory", "external"].includes(value)) {
|
|
26256
|
+
throw new Error("kind must be one of memory|artifact|state|trajectory|external");
|
|
26257
|
+
}
|
|
26258
|
+
return value;
|
|
26259
|
+
}
|
|
26260
|
+
function validateProvenance(raw) {
|
|
26261
|
+
if (!isRecord2(raw)) throw new Error("provenance must be an object");
|
|
26262
|
+
const sourceClass = assertString2(raw.sourceClass, "provenance.sourceClass");
|
|
26263
|
+
if (!["tool_output", "web_content", "subagent_trace", "system_memory", "user_input", "manual"].includes(sourceClass)) {
|
|
26264
|
+
throw new Error("provenance.sourceClass must be one of tool_output|web_content|subagent_trace|system_memory|user_input|manual");
|
|
26265
|
+
}
|
|
26266
|
+
return {
|
|
26267
|
+
sourceClass,
|
|
26268
|
+
observedAt: assertIsoRecordedAt(assertString2(raw.observedAt, "provenance.observedAt"), "provenance.observedAt"),
|
|
26269
|
+
sessionKey: optionalString(raw.sessionKey),
|
|
26270
|
+
sourceId: optionalString(raw.sourceId),
|
|
26271
|
+
evidenceHash: optionalString(raw.evidenceHash)
|
|
26272
|
+
};
|
|
26273
|
+
}
|
|
26274
|
+
function resolveTrustZoneStoreDir(memoryDir, overrideDir) {
|
|
26275
|
+
if (typeof overrideDir === "string" && overrideDir.trim().length > 0) {
|
|
26276
|
+
return overrideDir.trim();
|
|
26277
|
+
}
|
|
26278
|
+
return path53.join(memoryDir, "state", "trust-zones");
|
|
26279
|
+
}
|
|
26280
|
+
function validateTrustZoneRecord(raw) {
|
|
26281
|
+
if (!isRecord2(raw)) throw new Error("trust-zone record must be an object");
|
|
26282
|
+
if (raw.schemaVersion !== 1) throw new Error("schemaVersion must be 1");
|
|
26283
|
+
return {
|
|
26284
|
+
schemaVersion: 1,
|
|
26285
|
+
recordId: assertSafePathSegment(assertString2(raw.recordId, "recordId"), "recordId"),
|
|
26286
|
+
zone: validateZone(raw.zone, "zone"),
|
|
26287
|
+
recordedAt: assertIsoRecordedAt(assertString2(raw.recordedAt, "recordedAt")),
|
|
26288
|
+
kind: validateKind(raw.kind),
|
|
26289
|
+
summary: assertString2(raw.summary, "summary"),
|
|
26290
|
+
provenance: validateProvenance(raw.provenance),
|
|
26291
|
+
promotedFromZone: raw.promotedFromZone === void 0 ? void 0 : validateZone(raw.promotedFromZone, "promotedFromZone"),
|
|
26292
|
+
entityRefs: optionalStringArray2(raw.entityRefs, "entityRefs"),
|
|
26293
|
+
tags: optionalStringArray2(raw.tags, "tags"),
|
|
26294
|
+
metadata: validateMetadata3(raw.metadata)
|
|
26295
|
+
};
|
|
26296
|
+
}
|
|
26297
|
+
async function readTrustZoneRecords(options) {
|
|
26298
|
+
const rootDir = resolveTrustZoneStoreDir(options.memoryDir, options.trustZoneStoreDir);
|
|
26299
|
+
const files = await listJsonFiles(path53.join(rootDir, "zones"));
|
|
26300
|
+
const records = [];
|
|
26301
|
+
const invalidRecords = [];
|
|
26302
|
+
for (const filePath of files) {
|
|
26303
|
+
try {
|
|
26304
|
+
records.push(validateTrustZoneRecord(await readJsonFile(filePath)));
|
|
26305
|
+
} catch (error) {
|
|
26306
|
+
invalidRecords.push({
|
|
26307
|
+
path: filePath,
|
|
26308
|
+
error: error instanceof Error ? error.message : String(error)
|
|
26309
|
+
});
|
|
26310
|
+
}
|
|
26311
|
+
}
|
|
26312
|
+
return { files, records, invalidRecords };
|
|
26313
|
+
}
|
|
26314
|
+
async function getTrustZoneStoreStatus(options) {
|
|
26315
|
+
const rootDir = resolveTrustZoneStoreDir(options.memoryDir, options.trustZoneStoreDir);
|
|
26316
|
+
const zonesDir = path53.join(rootDir, "zones");
|
|
26317
|
+
const { files, records, invalidRecords } = await readTrustZoneRecords(options);
|
|
26318
|
+
records.sort((a, b) => b.recordedAt.localeCompare(a.recordedAt));
|
|
26319
|
+
const byZone = {};
|
|
26320
|
+
const byKind = {};
|
|
26321
|
+
for (const record of records) {
|
|
26322
|
+
byZone[record.zone] = (byZone[record.zone] ?? 0) + 1;
|
|
26323
|
+
byKind[record.kind] = (byKind[record.kind] ?? 0) + 1;
|
|
26324
|
+
}
|
|
26325
|
+
return {
|
|
26326
|
+
enabled: options.enabled,
|
|
26327
|
+
promotionEnabled: options.promotionEnabled,
|
|
26328
|
+
rootDir,
|
|
26329
|
+
zonesDir,
|
|
26330
|
+
records: {
|
|
26331
|
+
total: files.length,
|
|
26332
|
+
valid: records.length,
|
|
26333
|
+
invalid: invalidRecords.length,
|
|
26334
|
+
byZone,
|
|
26335
|
+
byKind,
|
|
26336
|
+
latestRecordId: records[0]?.recordId,
|
|
26337
|
+
latestRecordedAt: records[0]?.recordedAt,
|
|
26338
|
+
latestZone: records[0]?.zone
|
|
26339
|
+
},
|
|
26340
|
+
latestRecord: records[0],
|
|
26341
|
+
invalidRecords
|
|
26342
|
+
};
|
|
26343
|
+
}
|
|
26344
|
+
|
|
26237
26345
|
// src/cli.ts
|
|
26238
26346
|
function rankCandidateForKeep(a, b) {
|
|
26239
26347
|
const aConfidence = typeof a.frontmatter.confidence === "number" ? a.frontmatter.confidence : 0;
|
|
@@ -26430,6 +26538,14 @@ async function runCausalTrajectoryStatusCliCommand(options) {
|
|
|
26430
26538
|
enabled: options.causalTrajectoryMemoryEnabled
|
|
26431
26539
|
});
|
|
26432
26540
|
}
|
|
26541
|
+
async function runTrustZoneStatusCliCommand(options) {
|
|
26542
|
+
return getTrustZoneStoreStatus({
|
|
26543
|
+
memoryDir: options.memoryDir,
|
|
26544
|
+
trustZoneStoreDir: options.trustZoneStoreDir,
|
|
26545
|
+
enabled: options.trustZonesEnabled,
|
|
26546
|
+
promotionEnabled: options.quarantinePromotionEnabled
|
|
26547
|
+
});
|
|
26548
|
+
}
|
|
26433
26549
|
async function runSessionCheckCliCommand(options) {
|
|
26434
26550
|
return analyzeSessionIntegrity({ memoryDir: options.memoryDir });
|
|
26435
26551
|
}
|
|
@@ -26657,7 +26773,7 @@ function policyVersionForValues(values, config) {
|
|
|
26657
26773
|
return createHash10("sha256").update(JSON.stringify(normalized)).digest("hex").slice(0, 12);
|
|
26658
26774
|
}
|
|
26659
26775
|
async function readRuntimePolicySnapshot2(config, fileName) {
|
|
26660
|
-
const filePath =
|
|
26776
|
+
const filePath = path54.join(config.memoryDir, "state", fileName);
|
|
26661
26777
|
const snapshot = await readRuntimePolicySnapshot(filePath, {
|
|
26662
26778
|
maxStaleDecayThreshold: config.lifecycleArchiveDecayThreshold
|
|
26663
26779
|
});
|
|
@@ -27241,14 +27357,14 @@ async function resolveMemoryDirForNamespace(orchestrator, namespace) {
|
|
|
27241
27357
|
const ns = (namespace ?? "").trim();
|
|
27242
27358
|
if (!ns) return orchestrator.config.memoryDir;
|
|
27243
27359
|
if (!orchestrator.config.namespacesEnabled) return orchestrator.config.memoryDir;
|
|
27244
|
-
const candidate =
|
|
27360
|
+
const candidate = path54.join(orchestrator.config.memoryDir, "namespaces", ns);
|
|
27245
27361
|
if (ns === orchestrator.config.defaultNamespace) {
|
|
27246
27362
|
return await exists2(candidate) ? candidate : orchestrator.config.memoryDir;
|
|
27247
27363
|
}
|
|
27248
27364
|
return candidate;
|
|
27249
27365
|
}
|
|
27250
27366
|
async function readAllMemoryFiles(memoryDir) {
|
|
27251
|
-
const roots = [
|
|
27367
|
+
const roots = [path54.join(memoryDir, "facts"), path54.join(memoryDir, "corrections")];
|
|
27252
27368
|
const out = [];
|
|
27253
27369
|
const walk = async (dir) => {
|
|
27254
27370
|
let entries;
|
|
@@ -27259,7 +27375,7 @@ async function readAllMemoryFiles(memoryDir) {
|
|
|
27259
27375
|
}
|
|
27260
27376
|
for (const entry of entries) {
|
|
27261
27377
|
const entryName = typeof entry.name === "string" ? entry.name : entry.name.toString("utf-8");
|
|
27262
|
-
const fullPath =
|
|
27378
|
+
const fullPath = path54.join(dir, entryName);
|
|
27263
27379
|
if (entry.isDirectory()) {
|
|
27264
27380
|
await walk(fullPath);
|
|
27265
27381
|
continue;
|
|
@@ -27588,6 +27704,16 @@ function registerCli(api, orchestrator) {
|
|
|
27588
27704
|
console.log(JSON.stringify(status, null, 2));
|
|
27589
27705
|
console.log("OK");
|
|
27590
27706
|
});
|
|
27707
|
+
cmd.command("trust-zone-status").description("Show trust-zone store status, zoned record counts, and latest stored record").action(async () => {
|
|
27708
|
+
const status = await runTrustZoneStatusCliCommand({
|
|
27709
|
+
memoryDir: orchestrator.config.memoryDir,
|
|
27710
|
+
trustZoneStoreDir: orchestrator.config.trustZoneStoreDir,
|
|
27711
|
+
trustZonesEnabled: orchestrator.config.trustZonesEnabled,
|
|
27712
|
+
quarantinePromotionEnabled: orchestrator.config.quarantinePromotionEnabled
|
|
27713
|
+
});
|
|
27714
|
+
console.log(JSON.stringify(status, null, 2));
|
|
27715
|
+
console.log("OK");
|
|
27716
|
+
});
|
|
27591
27717
|
cmd.command("conversation-index-health").description("Show conversation index backend health and index stats").action(async () => {
|
|
27592
27718
|
const health = await runConversationIndexHealthCliCommand(orchestrator);
|
|
27593
27719
|
console.log(JSON.stringify(health, null, 2));
|
|
@@ -28237,7 +28363,7 @@ function registerCli(api, orchestrator) {
|
|
|
28237
28363
|
}
|
|
28238
28364
|
});
|
|
28239
28365
|
cmd.command("identity").description("Show agent identity reflections").action(async () => {
|
|
28240
|
-
const workspaceDir =
|
|
28366
|
+
const workspaceDir = path54.join(process.env.HOME ?? "~", ".openclaw", "workspace");
|
|
28241
28367
|
const identity = await orchestrator.storage.readIdentity(workspaceDir);
|
|
28242
28368
|
if (!identity) {
|
|
28243
28369
|
console.log("No identity file found.");
|
|
@@ -28460,8 +28586,8 @@ function registerCli(api, orchestrator) {
|
|
|
28460
28586
|
const options = args[0] ?? {};
|
|
28461
28587
|
const threadId = options.thread;
|
|
28462
28588
|
const top = parseInt(options.top ?? "10", 10);
|
|
28463
|
-
const memoryDir =
|
|
28464
|
-
const threading = new ThreadingManager(
|
|
28589
|
+
const memoryDir = path54.join(process.env.HOME ?? "~", ".openclaw", "workspace", "memory", "local");
|
|
28590
|
+
const threading = new ThreadingManager(path54.join(memoryDir, "threads"));
|
|
28465
28591
|
if (threadId) {
|
|
28466
28592
|
const thread = await threading.loadThread(threadId);
|
|
28467
28593
|
if (!thread) {
|
|
@@ -28937,9 +29063,9 @@ async function recordObjectiveStateSnapshotsFromAgentMessages(options) {
|
|
|
28937
29063
|
}
|
|
28938
29064
|
|
|
28939
29065
|
// src/index.ts
|
|
28940
|
-
import { readFile as readFile38, writeFile as
|
|
29066
|
+
import { readFile as readFile38, writeFile as writeFile33 } from "fs/promises";
|
|
28941
29067
|
import { readFileSync as readFileSync4 } from "fs";
|
|
28942
|
-
import
|
|
29068
|
+
import path55 from "path";
|
|
28943
29069
|
import os6 from "os";
|
|
28944
29070
|
var ENGRAM_REGISTERED_GUARD = "__openclawEngramRegistered";
|
|
28945
29071
|
var ENGRAM_HOOK_APIS = "__openclawEngramHookApis";
|
|
@@ -28947,7 +29073,7 @@ function loadPluginConfigFromFile() {
|
|
|
28947
29073
|
try {
|
|
28948
29074
|
const explicitConfigPath = process.env.OPENCLAW_ENGRAM_CONFIG_PATH || process.env.OPENCLAW_CONFIG_PATH;
|
|
28949
29075
|
const homeDir = process.env.HOME ?? os6.homedir();
|
|
28950
|
-
const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath :
|
|
29076
|
+
const configPath = explicitConfigPath && explicitConfigPath.length > 0 ? explicitConfigPath : path55.join(homeDir, ".openclaw", "openclaw.json");
|
|
28951
29077
|
const content = readFileSync4(configPath, "utf-8");
|
|
28952
29078
|
const config = JSON.parse(content);
|
|
28953
29079
|
const pluginEntry = config?.plugins?.entries?.["openclaw-engram"];
|
|
@@ -29197,11 +29323,11 @@ Use this context naturally when relevant. Never quote or expose this memory cont
|
|
|
29197
29323
|
`session reset via API for ${sessionKey}, new sessionId=${result.sessionId}`
|
|
29198
29324
|
);
|
|
29199
29325
|
const safeSessionKey = sanitizeSessionKeyForFilename(sessionKey);
|
|
29200
|
-
const signalPath =
|
|
29326
|
+
const signalPath = path55.join(
|
|
29201
29327
|
workspaceDir,
|
|
29202
29328
|
`.compaction-reset-signal-${safeSessionKey}`
|
|
29203
29329
|
);
|
|
29204
|
-
await
|
|
29330
|
+
await writeFile33(
|
|
29205
29331
|
signalPath,
|
|
29206
29332
|
JSON.stringify({
|
|
29207
29333
|
sessionKey,
|
|
@@ -29228,7 +29354,7 @@ Use this context naturally when relevant. Never quote or expose this memory cont
|
|
|
29228
29354
|
);
|
|
29229
29355
|
async function ensureHourlySummaryCron(api2) {
|
|
29230
29356
|
const jobId = "engram-hourly-summary";
|
|
29231
|
-
const cronFilePath =
|
|
29357
|
+
const cronFilePath = path55.join(os6.homedir(), ".openclaw", "cron", "jobs.json");
|
|
29232
29358
|
try {
|
|
29233
29359
|
let jobsData = { version: 1, jobs: [] };
|
|
29234
29360
|
try {
|
|
@@ -29269,7 +29395,7 @@ Use this context naturally when relevant. Never quote or expose this memory cont
|
|
|
29269
29395
|
state: {}
|
|
29270
29396
|
};
|
|
29271
29397
|
jobsData.jobs.push(newJob);
|
|
29272
|
-
await
|
|
29398
|
+
await writeFile33(cronFilePath, JSON.stringify(jobsData, null, 2), "utf-8");
|
|
29273
29399
|
log.info("auto-registered hourly summary cron job");
|
|
29274
29400
|
} catch (err) {
|
|
29275
29401
|
log.error("failed to auto-register hourly summary cron job:", err);
|