openclaw-memory-atmem 2.2.6 → 2.3.2

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,7 +5,7 @@ This npm package is the host bridge for AtMem. It is not a standalone memory eng
5
5
  Use the Python-owned installer:
6
6
 
7
7
  ```bash
8
- python -m pip install --upgrade atmem==2.2.6
8
+ python -m pip install --upgrade atmem==2.3.2
9
9
  atmem openclaw install
10
10
  ```
11
11
 
@@ -22,7 +22,7 @@ Existing AtMem 2.1 users run `atmem openclaw upgrade` after upgrading the Python
22
22
  package. This preserves the current memory mode and migration, verifies the new
23
23
  bridge with a self-test flight, and rolls back the bridge on failure.
24
24
 
25
- Bridge `2.2.6` includes exact task-context delivery, the shared
25
+ Bridge `2.3.2` includes durable execution-event spooling in addition to exact task-context delivery and the shared
26
26
  calibrated retrieval decision, and delegated context correlation. When the host
27
27
  supplies `taskId` in hook context, the bridge requests only that governed task,
28
28
  checks its byte digest, contributes it separately from recalled memory, and
@@ -32,7 +32,11 @@ bridge does not claim it can block OpenClaw execution.
32
32
 
33
33
  In shadow mode the bridge observes native-memory changes without injecting AtMem context. In active mode it exposes compatible memory search/get tools, model-semantic capture, bounded recall and native-path protection. `atmem control restore` restores the saved OpenClaw configuration and native memory.
34
34
 
35
- The bridge also supplies Agent Black Box hooks. It records model/tool lifecycle digests and bounded metadata—not raw prompts, responses, parameters or results—so `atmem blackbox verify RUN_ID` can check timeline integrity and observed tool-hook closure. See the [Agent Black Box guide](../../docs/agent-blackbox.md) for the exact boundary.
35
+ The bridge also supplies Agent Black Box hooks. With default full capture it sends
36
+ exact host-observed prompts, model boundaries, tool arguments/results and supported
37
+ multimodal parts into AtMem's encrypted evidence vault, alongside digest-bound
38
+ lifecycle metadata. Metadata-only and recorder-off modes remain explicit operator
39
+ choices. See the [Agent Black Box guide](../../docs/agent-blackbox.md) for the exact boundary.
36
40
 
37
41
  See the repository [OpenClaw setup](../../docs/openclaw-setup.md) and [control-plane guarantees](../../docs/control-plane.md).
38
42
 
@@ -54,3 +58,12 @@ actual terminal host tool-event results supply fallback observations when typed
54
58
  completion hooks are absent. Only matched requests close, with content-free
55
59
  bounded caching and duplicate suppression. Fully observed
56
60
  terminal tool errors close evidence as `completed_with_tool_errors`, not success.
61
+
62
+ Local development follow-up: automatic recall now requires calibrated direct
63
+ query support by default (`recall.requireDirectSupport: true`). Persona is an
64
+ explicit opt-in (`persona.enabled: true`); setup defaults it off to avoid adding
65
+ unrelated personal facts to every request. When enabled, persona records are
66
+ excluded from recall so each appears once. Existing explicit persona settings
67
+ are respected; set `persona.enabled: false` to adopt selective recall. Explicit
68
+ memory search remains available. The legacy rank-only mode is available with
69
+ `recall.requireDirectSupport: false`; it can admit weak matches.
package/dist/index.js CHANGED
@@ -20,10 +20,12 @@
20
20
  import os from "node:os";
21
21
  import path from "node:path";
22
22
  import { createHash, randomUUID } from "node:crypto";
23
- import { createReadStream } from "node:fs";
24
23
  import { lstat, mkdir, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
25
24
  import { spawnSync } from "node:child_process";
26
25
  import { ToolObservations, observationContext } from "./src/tool-observations.js";
26
+ import { progressCardComparison } from "./src/tool-result-comparison.js";
27
+ import { toolErrorReason } from "./src/tool-errors.js";
28
+ import { ExecutionSpool } from "./src/execution-spool.js";
27
29
  import { delegatedIdentity, isolatedCliProcess } from "./src/delegated-identity.js";
28
30
  import { AtmemClient } from "./src/rpc-client.js";
29
31
  import { runSetup } from "./src/setup.js";
@@ -45,7 +47,10 @@ const PROMPT_CACHE_TTL_MS = 10 * 60 * 1000;
45
47
  const MAX_PENDING_PROMPTS = 256;
46
48
  function parseConfig(raw) {
47
49
  const cfg = (raw ?? {});
48
- const dbPath = expandHome(String(cfg.dbPath ?? "~/.atmem/memories.db"));
50
+ const selectedHome = process.env.ATMEM_HOME
51
+ ? expandHome(process.env.ATMEM_HOME)
52
+ : path.join(os.homedir(), ".atmem");
53
+ const dbPath = expandHome(String(cfg.dbPath ?? path.join(selectedHome, "memory", "memories.db")));
49
54
  const subject = String(cfg.subject ?? "default");
50
55
  const stringMap = (value) => value && typeof value === "object" && !Array.isArray(value)
51
56
  ? Object.fromEntries(Object.entries(value)
@@ -62,8 +67,8 @@ function parseConfig(raw) {
62
67
  ].filter(Boolean))];
63
68
  const controlPlane = {
64
69
  enabled: cfg.controlPlane?.enabled === true,
65
- statePath: expandHome(String(cfg.controlPlane?.statePath ?? "~/.atmem/control-plane.json")),
66
- blackboxEnabled: cfg.controlPlane?.enabled === true || cfg.controlPlane?.blackboxEnabled === true,
70
+ statePath: expandHome(String(cfg.controlPlane?.statePath ?? path.join(selectedHome, "config", "control-plane.json"))),
71
+ blackboxEnabled: Boolean(cfg.controlPlane) && cfg.controlPlane?.blackboxEnabled !== false,
67
72
  };
68
73
  const delegatedContext = {
69
74
  userId: String(cfg.delegatedContext?.userId ?? "").trim(),
@@ -97,10 +102,11 @@ function parseConfig(raw) {
97
102
  maxRecords: Number(cfg.recall?.maxRecords ?? 3),
98
103
  maxChars: Number(cfg.recall?.maxChars ?? 1200),
99
104
  minScore: Number(cfg.recall?.minScore ?? 0.3),
105
+ requireDirectSupport: cfg.recall?.requireDirectSupport !== false,
100
106
  timeoutMs: Number(cfg.recall?.timeoutMs ?? 4000),
101
107
  },
102
108
  persona: {
103
- enabled: cfg.persona?.enabled !== false,
109
+ enabled: cfg.persona?.enabled === true,
104
110
  maxChars: Number(cfg.persona?.maxChars ?? 600),
105
111
  ttlSeconds: Number(cfg.persona?.ttlSeconds ?? 300),
106
112
  },
@@ -226,7 +232,7 @@ async function writeAttachmentBinding(root, key, items) {
226
232
  const payload = {
227
233
  format: "atmem-openclaw-attachment-binding-v1",
228
234
  keySha256: createHash("sha256").update(key).digest("hex"),
229
- items,
235
+ items: items.map(({ dataBase64: _discarded, ...item }) => item),
230
236
  updatedAt: Date.now(),
231
237
  };
232
238
  await writeFile(temporary, JSON.stringify(payload), { encoding: "utf8", mode: 0o600 });
@@ -256,17 +262,57 @@ async function readAttachmentBinding(root, key) {
256
262
  }
257
263
  function modalityFromMime(mimeType) {
258
264
  const mime = mimeType.toLowerCase();
259
- if (mime.startsWith("image/"))
265
+ if (mime === "image" || mime.startsWith("image/"))
260
266
  return "image";
261
- if (mime.startsWith("audio/"))
267
+ if (mime === "audio" || mime.startsWith("audio/"))
262
268
  return "audio";
263
- if (mime.startsWith("video/"))
269
+ if (mime === "video" || mime.startsWith("video/"))
264
270
  return "video";
265
- if (mime === "application/pdf" || mime.startsWith("text/") || mime.includes("document")) {
271
+ if (mime === "document" || mime === "application/pdf" || mime.startsWith("text/") || mime.includes("document")) {
266
272
  return "document";
267
273
  }
268
274
  return null;
269
275
  }
276
+ function inlineMediaParts(value) {
277
+ const parts = [];
278
+ const seen = new Set();
279
+ const visit = (item) => {
280
+ if (typeof item === "string") {
281
+ const match = /^data:([^;,]+);base64,([A-Za-z0-9+/]+={0,2})$/.exec(item);
282
+ if (!match)
283
+ return;
284
+ const mimeType = match[1].toLowerCase();
285
+ const modality = modalityFromMime(mimeType);
286
+ if (!modality)
287
+ return;
288
+ const bytes = Buffer.from(match[2], "base64");
289
+ if (bytes.length > 100 * 1024 * 1024 || bytes.toString("base64") !== match[2])
290
+ return;
291
+ const digest = createHash("sha256").update(bytes).digest("hex");
292
+ if (seen.has(digest))
293
+ return;
294
+ seen.add(digest);
295
+ parts.push({
296
+ type: modality,
297
+ mime_type: mimeType,
298
+ data_base64: match[2],
299
+ representation: "model_input",
300
+ });
301
+ return;
302
+ }
303
+ if (Array.isArray(item)) {
304
+ for (const child of item)
305
+ visit(child);
306
+ return;
307
+ }
308
+ if (item && typeof item === "object") {
309
+ for (const child of Object.values(item))
310
+ visit(child);
311
+ }
312
+ };
313
+ visit(value);
314
+ return parts;
315
+ }
270
316
  function withinPath(candidate, root) {
271
317
  return candidate === root || candidate.startsWith(root + path.sep);
272
318
  }
@@ -279,12 +325,12 @@ async function hashInboundAttachment(filePath, mimeType) {
279
325
  throw new Error("OpenClaw attachment path is outside the managed media directory");
280
326
  }
281
327
  const before = await lstat(resolved);
282
- if (!before.isFile() || before.size > 512 * 1024 * 1024) {
328
+ // The 2.3 consumer-hardware profile is measured and bounded at 100 MiB per artifact.
329
+ if (!before.isFile() || before.size > 100 * 1024 * 1024) {
283
330
  throw new Error("OpenClaw attachment is not a bounded regular file");
284
331
  }
285
- const hash = createHash("sha256");
286
- for await (const chunk of createReadStream(resolved))
287
- hash.update(chunk);
332
+ const exactBytes = await readFile(resolved);
333
+ const hash = createHash("sha256").update(exactBytes);
288
334
  const after = await lstat(resolved);
289
335
  if (before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
290
336
  throw new Error("OpenClaw attachment changed while its digest was computed");
@@ -300,6 +346,53 @@ async function hashInboundAttachment(filePath, mimeType) {
300
346
  mimeType,
301
347
  bytes: before.size,
302
348
  capturedAt: Date.now(),
349
+ dataBase64: exactBytes.toString("base64"),
350
+ };
351
+ }
352
+ function managedMediaPath(reference) {
353
+ const match = /^media:\/\/inbound\/([A-Za-z0-9][A-Za-z0-9._-]{0,255})$/.exec(reference);
354
+ if (!match || match[1] === "." || match[1] === "..")
355
+ return null;
356
+ const root = process.env.ATMEM_OPENCLAW_MEDIA_ROOT
357
+ ? expandHome(process.env.ATMEM_OPENCLAW_MEDIA_ROOT)
358
+ : path.join(os.homedir(), ".openclaw", "media");
359
+ return path.join(root, "inbound", match[1]);
360
+ }
361
+ function attachmentFields(value) {
362
+ const selected = [];
363
+ const seen = new Set();
364
+ const add = (reference, mimeType) => {
365
+ if (typeof reference !== "string" || !reference)
366
+ return;
367
+ const resolved = reference.startsWith("media://") ? managedMediaPath(reference) : reference;
368
+ if (!resolved || seen.has(resolved))
369
+ return;
370
+ seen.add(resolved);
371
+ selected.push({
372
+ path: resolved,
373
+ type: typeof mimeType === "string" && mimeType
374
+ ? mimeType
375
+ : "application/octet-stream",
376
+ });
377
+ };
378
+ const visit = (item) => {
379
+ if (Array.isArray(item)) {
380
+ for (const child of item)
381
+ visit(child);
382
+ return;
383
+ }
384
+ if (!item || typeof item !== "object")
385
+ return;
386
+ const row = item;
387
+ add(row.path ?? row.url, row.contentType ?? row.mimeType ?? row.kind);
388
+ if (row.__openclaw && typeof row.__openclaw === "object") {
389
+ visit(row.__openclaw.media);
390
+ }
391
+ };
392
+ visit(value);
393
+ return {
394
+ paths: selected.map((item) => item.path),
395
+ types: selected.map((item) => item.type),
303
396
  };
304
397
  }
305
398
  function recordIdFromPath(value) {
@@ -311,7 +404,16 @@ function recordIdFromPath(value) {
311
404
  }
312
405
  function register(api) {
313
406
  const cfg = parseConfig(api.pluginConfig);
314
- const attachmentBindingRoot = path.join(path.dirname(cfg.controlPlane.enabled ? cfg.controlPlane.statePath : cfg.dbPath), "openclaw-attachment-bindings");
407
+ const stateDirectory = path.dirname(cfg.controlPlane.statePath);
408
+ const selectedHome = process.env.ATMEM_HOME
409
+ ? expandHome(process.env.ATMEM_HOME)
410
+ : path.basename(stateDirectory) === "config"
411
+ ? path.dirname(stateDirectory)
412
+ : stateDirectory;
413
+ const runtimeRoot = path.basename(stateDirectory) === "config" || process.env.ATMEM_HOME
414
+ ? path.join(selectedHome, "runtime")
415
+ : selectedHome;
416
+ const attachmentBindingRoot = path.join(runtimeRoot, "openclaw-attachment-bindings");
315
417
  const client = new AtmemClient({
316
418
  command: cfg.command,
317
419
  args: cfg.commandArgs,
@@ -326,6 +428,7 @@ function register(api) {
326
428
  log: (message) => api.logger.debug?.(`${TAG} ${message}`),
327
429
  logError: (message) => api.logger.warn(`${TAG} ${message}`),
328
430
  });
431
+ const executionSpool = new ExecutionSpool(path.join(runtimeRoot, "openclaw-execution-spool.json"));
329
432
  // Let long-lived hosts close the stdio child during lifecycle shutdown.
330
433
  // The client's bounded idle shutdown also covers one-shot local runners.
331
434
  api.registerService?.({
@@ -434,13 +537,27 @@ function register(api) {
434
537
  ? normalized.slice("openclaw".length)
435
538
  : normalized;
436
539
  };
540
+ const governedMemoryTools = new Set([
541
+ "memory_search",
542
+ "memory_get",
543
+ "memory_remember",
544
+ "atmem_observe",
545
+ "atmem_forget_artifact",
546
+ "atmem_trace",
547
+ "atmem_tasks",
548
+ "atmem_task_progress",
549
+ ]);
437
550
  const recordBlackbox = async (eventType, eventRunId, ctx, payload, toolCallId, correlation = {}) => {
438
551
  if (!cfg.controlPlane.blackboxEnabled)
439
552
  return;
440
553
  try {
441
- await blackboxClient.callTool("control_record_blackbox_event", {
554
+ await executionSpool.enqueue({
442
555
  event_type: eventType,
443
556
  run_id: flightRunId(eventRunId, ctx),
557
+ execution_id: ctx.executionId,
558
+ parent_execution_id: ctx.parentExecutionId,
559
+ attempt_id: ctx.attemptId,
560
+ retry_of_attempt_id: ctx.retryOfAttemptId,
444
561
  agent_id: agentIdFor(ctx),
445
562
  workspace_id: workspaceIdFor(ctx),
446
563
  subject_id: Object.keys(cfg.agentSubjects).length ? subjectFor(ctx) : undefined,
@@ -452,7 +569,8 @@ function register(api) {
452
569
  context_receipt_id: correlation.contextReceiptId,
453
570
  outcome_id: correlation.outcomeId,
454
571
  payload,
455
- }, cfg.recall.timeoutMs);
572
+ });
573
+ await executionSpool.flush((event) => blackboxClient.callTool("control_record_blackbox_event", event, cfg.recall.timeoutMs));
456
574
  }
457
575
  catch (error) {
458
576
  api.logger.warn(`${TAG} blackbox event ${eventType} was not recorded: ${error instanceof Error ? error.message : String(error)}`);
@@ -507,6 +625,9 @@ function register(api) {
507
625
  prompt_sha256: promptSha256,
508
626
  prompt_chars: prompt.length,
509
627
  images_count: imagesCount,
628
+ _atmem_evidence: {
629
+ parts: [{ type: "text", text: prompt }],
630
+ },
510
631
  });
511
632
  try {
512
633
  await stageInbound(prompt, ctx);
@@ -522,7 +643,7 @@ function register(api) {
522
643
  });
523
644
  await pending;
524
645
  };
525
- const bindInboundAttachments = async (keys, paths, types) => {
646
+ const bindInboundAttachments = async (keys, paths, types, ctx) => {
526
647
  const generation = ++nextAttachmentGeneration;
527
648
  for (const key of keys) {
528
649
  inboundAttachments.delete(key);
@@ -532,6 +653,20 @@ function register(api) {
532
653
  if (!paths.length)
533
654
  return;
534
655
  const items = await Promise.all(paths.map((filePath, index) => hashInboundAttachment(filePath, types[index] ?? types[0] ?? "application/octet-stream")));
656
+ for (const item of items) {
657
+ await recordBlackbox("turn.attachment", undefined, ctx, {
658
+ result_sha256: item.mediaSha256,
659
+ response_stream_bytes: item.bytes,
660
+ _atmem_evidence: {
661
+ parts: [{
662
+ type: item.modality,
663
+ mime_type: item.mimeType,
664
+ data_base64: item.dataBase64,
665
+ host_reference: item.hostReference,
666
+ }],
667
+ },
668
+ });
669
+ }
535
670
  for (const key of keys) {
536
671
  if (inboundAttachmentGeneration.get(key) === generation) {
537
672
  inboundAttachments.set(key, { items, ts: Date.now() });
@@ -557,7 +692,8 @@ function register(api) {
557
692
  : typeof rawType === "string"
558
693
  ? [rawType]
559
694
  : [];
560
- return { paths, types };
695
+ const current = attachmentFields(message);
696
+ return current.paths.length ? current : { paths, types };
561
697
  };
562
698
  const writtenMessageBindingKeys = (message, ctx) => {
563
699
  const idempotencyKey = typeof message.idempotencyKey === "string"
@@ -574,23 +710,57 @@ function register(api) {
574
710
  };
575
711
  api.on("message_received", async (event, ctx) => {
576
712
  const metadata = event.metadata ?? {};
577
- const paths = Array.isArray(metadata.mediaPaths)
713
+ // OpenClaw 2026.9 exposes stable media facts on the event. The metadata
714
+ // aliases are deprecated and can be absent even though the model receives
715
+ // the attachment, so use them only as a compatibility fallback.
716
+ const media = Array.isArray(event.media) ? event.media : [];
717
+ const currentFields = attachmentFields(media);
718
+ const current = currentFields.paths.map((path, index) => ({
719
+ path,
720
+ type: currentFields.types[index] ?? "application/octet-stream",
721
+ }));
722
+ const originalMedia = Array.isArray(event.originalMedia) ? event.originalMedia : [];
723
+ const originalFields = attachmentFields(originalMedia);
724
+ const original = originalFields.paths.map((path, index) => ({
725
+ path,
726
+ type: originalFields.types[index] ?? "application/octet-stream",
727
+ }));
728
+ const legacyPaths = Array.isArray(metadata.mediaPaths)
578
729
  ? metadata.mediaPaths.filter((value) => typeof value === "string")
579
- : typeof metadata.mediaPath === "string"
580
- ? [metadata.mediaPath]
581
- : [];
582
- const types = Array.isArray(metadata.mediaTypes)
730
+ : typeof metadata.mediaPath === "string" ? [metadata.mediaPath] : [];
731
+ const legacyTypes = Array.isArray(metadata.mediaTypes)
583
732
  ? metadata.mediaTypes.filter((value) => typeof value === "string")
584
- : typeof metadata.mediaType === "string"
585
- ? [metadata.mediaType]
586
- : [];
733
+ : typeof metadata.mediaType === "string" ? [metadata.mediaType] : [];
734
+ const legacyOriginalPaths = Array.isArray(metadata.originalMediaPaths)
735
+ ? metadata.originalMediaPaths.filter((value) => typeof value === "string")
736
+ : typeof metadata.originalMediaPath === "string" ? [metadata.originalMediaPath] : [];
737
+ const legacyOriginalTypes = Array.isArray(metadata.originalMediaTypes)
738
+ ? metadata.originalMediaTypes.filter((value) => typeof value === "string")
739
+ : typeof metadata.originalMediaType === "string" ? [metadata.originalMediaType] : [];
740
+ const selected = current.length
741
+ ? current
742
+ : original.length
743
+ ? original
744
+ : legacyPaths.map((path, index) => ({
745
+ path,
746
+ type: legacyTypes[index] ?? legacyTypes[0] ?? "application/octet-stream",
747
+ })).concat(legacyOriginalPaths.map((path, index) => ({
748
+ path,
749
+ type: legacyOriginalTypes[index] ?? legacyOriginalTypes[0] ?? "application/octet-stream",
750
+ })));
751
+ const paths = selected.map((fact) => fact.path);
752
+ const types = selected.map((fact) => fact.type);
587
753
  const keys = [...new Set([
588
754
  event.sessionKey,
589
755
  event.runId,
590
756
  ...contextIds(ctx),
591
757
  ].filter((value) => Boolean(value)))];
592
758
  try {
593
- await bindInboundAttachments(keys, paths, types);
759
+ // A staging-pending event has not supplied a safe local path yet. Do not
760
+ // erase an earlier binding for the same turn while OpenClaw is staging it.
761
+ if (!paths.length && (event.mediaStagingPending || metadata.mediaStagingPending === true))
762
+ return;
763
+ await bindInboundAttachments(keys, paths, types, ctx);
594
764
  }
595
765
  catch (error) {
596
766
  api.logger.warn(`${TAG} inbound attachment provenance unavailable: ${error instanceof Error ? error.message : String(error)}`);
@@ -633,12 +803,21 @@ function register(api) {
633
803
  context_location: deliveredLocation,
634
804
  success: delivered,
635
805
  reason: delivered ? undefined : `expected one exact delegated segment; observed ${occurrences}`,
806
+ _atmem_evidence: {
807
+ context: exact,
808
+ delivered_location: deliveredLocation,
809
+ },
636
810
  }, undefined, { contextReceiptId: pending.contextReceiptId });
637
811
  if (delivered && pending.exposureId) {
638
812
  await callFor(ctx, "control_exposure_shown", { exposure_id: pending.exposureId }, cfg.recall.timeoutMs);
639
813
  }
640
814
  cachePendingPrompt(sessionKey, { ...pending, delegatedContext: undefined });
641
815
  }
816
+ const modelMediaParts = inlineMediaParts([
817
+ event.historyMessages,
818
+ event.messages,
819
+ event.images,
820
+ ]);
642
821
  await recordBlackbox("model.input", event.runId, ctx, {
643
822
  provider: event.provider,
644
823
  model: event.model,
@@ -650,6 +829,13 @@ function register(api) {
650
829
  history_count: Array.isArray(event.historyMessages) ? event.historyMessages.length : 0,
651
830
  images_count: event.imagesCount ?? 0,
652
831
  tools_count: Array.isArray(event.tools) ? event.tools.length : 0,
832
+ _atmem_evidence: {
833
+ prompt: event.prompt ?? "",
834
+ system_prompt: event.systemPrompt ?? "",
835
+ history_messages: event.historyMessages ?? [],
836
+ tools: event.tools ?? [],
837
+ parts: modelMediaParts,
838
+ },
653
839
  });
654
840
  });
655
841
  api.on("llm_output", async (event, ctx) => {
@@ -680,6 +866,10 @@ function register(api) {
680
866
  usage: event.usage ?? {},
681
867
  reasoning_effort: event.reasoningEffort,
682
868
  fast_mode: event.fastMode,
869
+ _atmem_evidence: {
870
+ assistant_texts: responses,
871
+ usage: event.usage ?? {},
872
+ },
683
873
  });
684
874
  });
685
875
  api.registerCli?.(({ program }) => {
@@ -995,6 +1185,8 @@ function register(api) {
995
1185
  max_records: cfg.recall.maxRecords,
996
1186
  max_chars: cfg.recall.maxChars,
997
1187
  min_score: cfg.recall.minScore,
1188
+ require_direct_support: cfg.recall.requireDirectSupport,
1189
+ exclude_record_ids: personaRecordIds,
998
1190
  reference_mode: cfg.cacheAware.enabled && cfg.cacheAware.compactReferences
999
1191
  ? "compact"
1000
1192
  : "full",
@@ -1072,8 +1264,15 @@ function register(api) {
1072
1264
  context_chars: memoryContext.length,
1073
1265
  candidate_ids: candidateIds,
1074
1266
  context_component_event_ids: componentEventIds,
1267
+ persona_record_count: personaRecordIds.length,
1268
+ recall_record_count: (current?.injectedRecordIds ?? []).length,
1269
+ context_selection_profile: cfg.recall.requireDirectSupport ? "direct-support-v1" : "rank-threshold-v1",
1075
1270
  mode: "direct",
1076
1271
  context_location: contextLocation,
1272
+ _atmem_evidence: {
1273
+ context: memoryContext,
1274
+ envelope: result,
1275
+ },
1077
1276
  }, undefined, {
1078
1277
  retrievalId: current?.retrievalId,
1079
1278
  contextEventId: current?.contextEventId ?? personaContextEventId,
@@ -1111,7 +1310,16 @@ function register(api) {
1111
1310
  derived_path_sha256: Array.isArray(event.derivedPaths)
1112
1311
  ? event.derivedPaths.map((value) => digestText(String(value)))
1113
1312
  : [],
1313
+ _atmem_evidence: {
1314
+ params: event.params ?? {},
1315
+ derived_paths: event.derivedPaths ?? [],
1316
+ },
1114
1317
  }, event.toolCallId ?? ctx.toolCallId);
1318
+ // These virtual tools operate through AtMem. Their arguments legitimately
1319
+ // contain values such as `corpus: "memory"`; they are not filesystem
1320
+ // access to OpenClaw's frozen MEMORY.md or memory/* tree.
1321
+ if (governedMemoryTools.has(canonicalToolName(event.toolName)))
1322
+ return;
1115
1323
  if (!cfg.takeoverActive || !cfg.nativeWorkspaces.some((workspace) => touchesNativeMemory(event, workspace)))
1116
1324
  return;
1117
1325
  const reason = "AtMem takeover blocked access to OpenClaw's frozen native memory " +
@@ -1134,9 +1342,18 @@ function register(api) {
1134
1342
  tool_name: event.toolName,
1135
1343
  tool_canonical_name: canonicalToolName(event.toolName),
1136
1344
  result_sha256: digestJson(event.result ?? null),
1345
+ ...(!event.error ? progressCardComparison(event.toolName, event.result, digestJson) : {}),
1137
1346
  outcome: event.error ? "error" : "completed",
1138
1347
  error_category: event.error ? "tool_error" : undefined,
1348
+ error_reason: event.error ? toolErrorReason(event.error) : undefined,
1349
+ error_sha256: event.error ? digestText(event.error) : undefined,
1350
+ result_present: event.result !== undefined,
1139
1351
  duration_ms: event.durationMs ?? 0,
1352
+ _atmem_evidence: {
1353
+ params: event.params ?? {},
1354
+ result: event.result ?? null,
1355
+ error: event.error ?? null,
1356
+ },
1140
1357
  }, event.toolCallId ?? ctx.toolCallId);
1141
1358
  });
1142
1359
  // ---- auto-capture: user turn through the pipeline, assistant as digest -
@@ -1146,11 +1363,25 @@ function register(api) {
1146
1363
  tool_name: saved.name, tool_canonical_name: canonicalToolName(saved.name),
1147
1364
  result_sha256: saved.result.digest, outcome: saved.result.error ? "error" : "completed",
1148
1365
  error_category: saved.result.error ? "tool_error" : undefined,
1366
+ error_reason: saved.result.reason,
1367
+ error_sha256: saved.result.errorDigest,
1149
1368
  reason: "observed_terminal_host_tool_event",
1150
1369
  }, saved.callId);
1151
1370
  });
1152
1371
  const sessionKey = sessionKeyFor(ctx);
1153
1372
  const observedTurnInputKey = turnInputKey(ctx);
1373
+ // Current OpenClaw WebChat persists exact attachment provenance on the
1374
+ // user transcript as __openclaw.media[].url = media://inbound/<id>. Some
1375
+ // earlier hooks omit it, so agent_end is the durable late-capture boundary.
1376
+ const completedTurnMedia = attachmentFields(event.messages);
1377
+ if (completedTurnMedia.paths.length) {
1378
+ try {
1379
+ await bindInboundAttachments([...new Set([sessionKey, event.runId, ...contextIds(ctx)].filter((value) => Boolean(value)))], completedTurnMedia.paths, completedTurnMedia.types, ctx);
1380
+ }
1381
+ catch (error) {
1382
+ api.logger.warn(`${TAG} completed-turn attachment capture unavailable: ${error instanceof Error ? error.message : String(error)}`);
1383
+ }
1384
+ }
1154
1385
  const cached = pendingPrompts.get(sessionKey);
1155
1386
  deletePendingPrompt(sessionKey);
1156
1387
  const userText = cached?.text?.replace(INJECT_RE, "").trim();
@@ -1254,6 +1485,10 @@ function register(api) {
1254
1485
  turn_messages_sha256: turnMessagesSha256,
1255
1486
  digest_profile: "atmem-turn-messages-canonical-json-v1",
1256
1487
  messages_count: Array.isArray(event.messages) ? event.messages.length : 0,
1488
+ _atmem_evidence: {
1489
+ messages: event.messages ?? [],
1490
+ error: event.error ?? null,
1491
+ },
1257
1492
  }, undefined, {
1258
1493
  retrievalId: cached?.retrievalId,
1259
1494
  contextEventId: cached?.contextEventId,
@@ -1274,8 +1509,10 @@ function register(api) {
1274
1509
  // by scanning the media directory or scraping its name from user text.
1275
1510
  const attachmentFields = writtenMessageAttachmentFields(message);
1276
1511
  const attachmentKeys = writtenMessageBindingKeys(message, ctx);
1277
- if (attachmentKeys.length) {
1278
- void bindInboundAttachments(attachmentKeys, attachmentFields.paths, attachmentFields.types).catch((error) => {
1512
+ // A sparse transcript message is normal in current OpenClaw. It must not
1513
+ // erase the binding already established by message_received.
1514
+ if (attachmentKeys.length && attachmentFields.paths.length) {
1515
+ void bindInboundAttachments(attachmentKeys, attachmentFields.paths, attachmentFields.types, ctx).catch((error) => {
1279
1516
  api.logger.warn(`${TAG} persisted attachment provenance unavailable: ${error instanceof Error ? error.message : String(error)}`);
1280
1517
  });
1281
1518
  }
@@ -0,0 +1,189 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes, randomUUID } from "node:crypto";
2
+ import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ const SPOOL_AAD = Buffer.from("atmem-openclaw-encrypted-execution-spool-v2", "utf8");
5
+ /** A bounded, atomic local spool. Removal requires a durable terminal receipt. */
6
+ export class ExecutionSpool {
7
+ filePath;
8
+ maxPending;
9
+ maxAgeMs;
10
+ keyPath;
11
+ state = null;
12
+ serial = Promise.resolve();
13
+ constructor(filePath, maxPending = 2048, maxAgeMs = 7 * 24 * 60 * 60 * 1000, keyPath = path.resolve(path.dirname(filePath), "..", ".atmem-evidence-keys", "openclaw-spool.key")) {
14
+ this.filePath = filePath;
15
+ this.maxPending = maxPending;
16
+ this.maxAgeMs = maxAgeMs;
17
+ this.keyPath = keyPath;
18
+ if (maxPending < 2)
19
+ throw new Error("execution spool capacity must be at least 2");
20
+ }
21
+ enqueue(value) {
22
+ return this.exclusive(async () => {
23
+ const state = await this.load();
24
+ this.expirePending(state, Date.now() - this.maxAgeMs);
25
+ const envelope = {
26
+ ...value,
27
+ event_id: randomUUID(),
28
+ producer_instance_id: state.producerInstanceId,
29
+ producer_epoch: state.producerEpoch,
30
+ producer_sequence: state.nextSequence++,
31
+ event_time: new Date().toISOString(),
32
+ };
33
+ state.pending.push(envelope);
34
+ if (state.pending.length > this.maxPending) {
35
+ const dropped = state.pending.length - this.maxPending + 1;
36
+ const removed = state.pending.splice(0, dropped);
37
+ state.pending.unshift({
38
+ event_type: "capture.gap",
39
+ run_id: String(envelope.run_id ?? "capture-spool"),
40
+ payload: {
41
+ reason: "spool_capacity_exceeded",
42
+ dropped_count: removed.length,
43
+ first_sequence: removed[0]?.producer_sequence,
44
+ last_sequence: removed.at(-1)?.producer_sequence,
45
+ },
46
+ event_id: randomUUID(),
47
+ producer_instance_id: state.producerInstanceId,
48
+ producer_epoch: state.producerEpoch,
49
+ producer_sequence: state.nextSequence++,
50
+ event_time: new Date().toISOString(),
51
+ });
52
+ }
53
+ await this.persist(state);
54
+ return envelope;
55
+ });
56
+ }
57
+ flush(send) {
58
+ return this.exclusive(async () => {
59
+ const state = await this.load();
60
+ let acknowledged = 0;
61
+ while (state.pending.length) {
62
+ const result = await send(state.pending[0]);
63
+ const receipt = result;
64
+ const accepted = receipt?.durably_accepted === true;
65
+ const classifiedConflict = receipt?.decision === "conflict" &&
66
+ receipt?.durably_classified === true;
67
+ if (!accepted && !classifiedConflict) {
68
+ throw new Error("AtMem did not confirm a durable terminal execution-event decision");
69
+ }
70
+ state.pending.shift();
71
+ acknowledged += 1;
72
+ await this.persist(state);
73
+ }
74
+ return acknowledged;
75
+ });
76
+ }
77
+ async snapshot() {
78
+ return this.exclusive(async () => ({ ...(await this.load()), pending: [...(await this.load()).pending] }));
79
+ }
80
+ exclusive(action) {
81
+ const next = this.serial.then(action, action);
82
+ this.serial = next.then(() => undefined, () => undefined);
83
+ return next;
84
+ }
85
+ async load() {
86
+ if (this.state)
87
+ return this.state;
88
+ try {
89
+ const wrapper = JSON.parse(await readFile(this.filePath, "utf8"));
90
+ if (wrapper.format !== "atmem-openclaw-encrypted-execution-spool-v2") {
91
+ throw new Error("unsupported execution spool format");
92
+ }
93
+ const key = await this.loadOrCreateKey();
94
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(wrapper.nonce, "base64"));
95
+ decipher.setAAD(SPOOL_AAD);
96
+ decipher.setAuthTag(Buffer.from(wrapper.tag, "base64"));
97
+ const plain = Buffer.concat([
98
+ decipher.update(Buffer.from(wrapper.ciphertext, "base64")), decipher.final(),
99
+ ]).toString("utf8");
100
+ const value = JSON.parse(plain);
101
+ if (value.format !== "atmem-openclaw-execution-spool-v1" || !Array.isArray(value.pending)) {
102
+ throw new Error("unsupported decrypted execution spool state");
103
+ }
104
+ // Pending events retain their original epoch. New events get a fresh
105
+ // epoch and sequence after every plugin process start.
106
+ this.state = {
107
+ ...value,
108
+ producerEpoch: randomUUID(),
109
+ nextSequence: 1,
110
+ };
111
+ }
112
+ catch (error) {
113
+ const code = error.code;
114
+ if (code !== "ENOENT")
115
+ throw error;
116
+ this.state = {
117
+ format: "atmem-openclaw-execution-spool-v1",
118
+ producerInstanceId: randomUUID(),
119
+ producerEpoch: randomUUID(),
120
+ nextSequence: 1,
121
+ pending: [],
122
+ };
123
+ }
124
+ await this.persist(this.state);
125
+ return this.state;
126
+ }
127
+ async persist(state) {
128
+ await mkdir(path.dirname(this.filePath), { recursive: true, mode: 0o700 });
129
+ const key = await this.loadOrCreateKey();
130
+ const nonce = randomBytes(12);
131
+ const cipher = createCipheriv("aes-256-gcm", key, nonce);
132
+ cipher.setAAD(SPOOL_AAD);
133
+ const ciphertext = Buffer.concat([
134
+ cipher.update(JSON.stringify(state), "utf8"), cipher.final(),
135
+ ]);
136
+ const wrapper = {
137
+ format: "atmem-openclaw-encrypted-execution-spool-v2",
138
+ suite: "AES-256-GCM",
139
+ nonce: nonce.toString("base64"),
140
+ ciphertext: ciphertext.toString("base64"),
141
+ tag: cipher.getAuthTag().toString("base64"),
142
+ };
143
+ const temporary = `${this.filePath}.${process.pid}.${randomUUID()}.tmp`;
144
+ await writeFile(temporary, `${JSON.stringify(wrapper)}\n`, { encoding: "utf8", mode: 0o600 });
145
+ await rename(temporary, this.filePath);
146
+ await chmod(this.filePath, 0o600);
147
+ }
148
+ async loadOrCreateKey() {
149
+ await mkdir(path.dirname(this.keyPath), { recursive: true, mode: 0o700 });
150
+ try {
151
+ const encoded = (await readFile(this.keyPath, "utf8")).trim();
152
+ const key = Buffer.from(encoded, "base64");
153
+ if (key.length !== 32)
154
+ throw new Error("execution spool key must be 256 bits");
155
+ return key;
156
+ }
157
+ catch (error) {
158
+ if (error.code !== "ENOENT")
159
+ throw error;
160
+ const key = randomBytes(32);
161
+ await writeFile(this.keyPath, `${key.toString("base64")}\n`, {
162
+ encoding: "utf8", mode: 0o600, flag: "wx",
163
+ });
164
+ await chmod(this.keyPath, 0o600);
165
+ return key;
166
+ }
167
+ }
168
+ expirePending(state, cutoff) {
169
+ const expired = state.pending.filter(event => Date.parse(event.event_time) < cutoff);
170
+ if (!expired.length)
171
+ return;
172
+ state.pending = state.pending.filter(event => Date.parse(event.event_time) >= cutoff);
173
+ state.pending.unshift({
174
+ event_type: "capture.gap",
175
+ run_id: String(expired.at(-1)?.run_id ?? "capture-spool"),
176
+ payload: {
177
+ reason: "spool_retention_expired",
178
+ dropped_count: expired.length,
179
+ first_sequence: expired[0].producer_sequence,
180
+ last_sequence: expired.at(-1)?.producer_sequence,
181
+ },
182
+ event_id: randomUUID(),
183
+ producer_instance_id: state.producerInstanceId,
184
+ producer_epoch: state.producerEpoch,
185
+ producer_sequence: state.nextSequence++,
186
+ event_time: new Date().toISOString(),
187
+ });
188
+ }
189
+ }
@@ -128,7 +128,7 @@ export class AtmemClient {
128
128
  capabilities: {},
129
129
  clientInfo: {
130
130
  name: "openclaw-memory-atmem",
131
- version: "2.2.6",
131
+ version: "2.3.2",
132
132
  },
133
133
  });
134
134
  this.notify("notifications/initialized", {});
package/dist/src/setup.js CHANGED
@@ -42,7 +42,7 @@ export function setupWrites(options) {
42
42
  minScore: 0.3,
43
43
  timeoutMs: 3000,
44
44
  },
45
- persona: { enabled: true, maxChars: 600, ttlSeconds: 300 },
45
+ persona: { enabled: false, maxChars: 600, ttlSeconds: 300 },
46
46
  capture: { enabled: true, captureAssistant: true },
47
47
  cacheAware: { enabled: true, compactReferences: true },
48
48
  tools: { enabled: true },
@@ -0,0 +1,31 @@
1
+ /** Retain a bounded diagnostic, never the full tool result or stack trace. */
2
+ export function toolErrorReason(value) {
3
+ let text = value;
4
+ if (value && typeof value === "object") {
5
+ const row = value;
6
+ text = row.error ?? row.message;
7
+ if (text && typeof text === "object")
8
+ text = text.message;
9
+ if (text === undefined && Array.isArray(row.content)) {
10
+ text = row.content.find(item => item?.type === "text" && typeof item.text === "string")?.text;
11
+ }
12
+ }
13
+ if (typeof text !== "string" || !text.trim())
14
+ return undefined;
15
+ const bounded = text.slice(0, 8192);
16
+ // OpenClaw wraps thrown web errors as untrusted API content. Extract only
17
+ // its narrow status prefix, never the page text or instructions beneath it.
18
+ if (bounded.startsWith("SECURITY NOTICE:")) {
19
+ const status = bounded.match(/<<<EXTERNAL_UNTRUSTED_CONTENT id="[a-f0-9]+">>>\r?\nSource: API\r?\n---\r?\n(Web fetch failed \([1-5][0-9]{2}\)):/);
20
+ return status?.[1];
21
+ }
22
+ return bounded.split(/[\r\n]/, 1)[0]
23
+ .replace(/https?:\/\/[^\s<>]+/gi, "[redacted-url]")
24
+ .replace(/\b(?:Bearer|Basic)\s+\S+/gi, "[redacted-auth]")
25
+ .replace(/\b(?:api[_-]?key|token|password|passwd|secret|authorization|cookie)\s*[:=]\s*(?:"[^"]*"|'[^']*'|\S+)/gi, "[redacted-credential]")
26
+ .replace(/\b(?:sk-|ghp_|github_pat_)[A-Za-z0-9_-]+/g, "[redacted-token]")
27
+ .replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, "[redacted-email]")
28
+ .replace(/(?:[A-Za-z]:\\|\/)[^\s<>"']+/g, "[redacted-path]")
29
+ .replace(/"[^"\r\n]*"|'[^'\r\n]*'/g, "[redacted-value]")
30
+ .replace(/[\x00-\x1f\x7f]/g, " ").trim().slice(0, 512) || undefined;
31
+ }
@@ -1,9 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { toolErrorReason } from "./tool-errors.js";
2
3
  const CACHE_KEY = Symbol.for("atmem.tool-observation-cache.v1");
3
4
  const sharedCache = globalThis;
4
5
  const cache = sharedCache[CACHE_KEY] ??= new Map();
5
6
  /** Some hosts deny run-context writes from tool hooks after registry reload.
6
- * Keep a bounded content-free process fallback, isolated by installation config.
7
+ * Keep a bounded content-minimized process fallback, isolated by installation config.
7
8
  */
8
9
  export function observationContext(host, scope) {
9
10
  const keyFor = (runId, namespace) => JSON.stringify([scope, runId, namespace]);
@@ -45,7 +46,7 @@ function canonical(name) {
45
46
  const unwrapped = name.replace(/^mcp__openclaw__/, "");
46
47
  return { Read: "read", Bash: "exec" }[unwrapped] ?? unwrapped;
47
48
  }
48
- /** Stores only scope, names and digests in bounded ephemeral observation context. */
49
+ /** Stores scope, names, digests and redacted error diagnostics in bounded ephemeral context. */
49
50
  export class ToolObservations {
50
51
  access;
51
52
  digest;
@@ -88,9 +89,12 @@ export class ToolObservations {
88
89
  (event.agentId !== undefined && event.agentId !== saved.ctx.agentId) ||
89
90
  canonical(d.name) !== canonical(saved.name))
90
91
  return;
91
- const result = { digest: this.digest(d.result), error: d.isError };
92
+ const result = { digest: this.digest(d.result), error: d.isError,
93
+ reason: d.isError ? toolErrorReason(d.error ?? d.result) : undefined,
94
+ errorDigest: d.isError && d.error !== undefined ? this.digest(d.error) : undefined };
92
95
  const conflict = saved.conflict || (saved.result !== undefined &&
93
- (saved.result.digest !== result.digest || saved.result.error !== result.error));
96
+ (saved.result.digest !== result.digest || saved.result.error !== result.error ||
97
+ saved.result.errorDigest !== result.errorDigest));
94
98
  this.access.setRunContext({ ...key, value: { ...saved, result, conflict } });
95
99
  }
96
100
  async flush(ctx, record) {
@@ -0,0 +1,45 @@
1
+ // Exact OpenClaw progress_card ↔ native relay equivalence. Never discard raw digests.
2
+ export function progressCardComparison(name, result, digest) {
3
+ if (name !== "progress_card")
4
+ return {};
5
+ const record = (v) => !!v && typeof v === "object" && !Array.isArray(v);
6
+ const keys = (v, expected) => Object.keys(v).sort().join(",") === [...expected].sort().join(",");
7
+ const native = Array.isArray(result);
8
+ if (!native && (!record(result) || !keys(result, ["content", "details"])))
9
+ return {};
10
+ const content = native ? result : result.content;
11
+ if (!Array.isArray(content) || content.length !== 2 || !content.every(v => record(v) && keys(v, ["type", "text"]) && v.type === (native ? "input_text" : "text") &&
12
+ typeof v.text === "string" && v.text.length <= 1024))
13
+ return {};
14
+ let payload;
15
+ try {
16
+ payload = JSON.parse(content[1].text);
17
+ }
18
+ catch {
19
+ return {};
20
+ }
21
+ if (!record(payload) || !keys(payload, ["revision", "steps"]))
22
+ return {};
23
+ const { revision, steps } = payload;
24
+ if (revision !== null && (!Number.isSafeInteger(revision) || revision < 1))
25
+ return {};
26
+ if (steps !== null && (!record(steps) || !keys(steps, ["completed", "total"]) ||
27
+ !Number.isSafeInteger(steps.completed) || !Number.isSafeInteger(steps.total) ||
28
+ steps.completed < 0 || steps.total < 1 || steps.total > 50 ||
29
+ steps.completed > steps.total))
30
+ return {};
31
+ if (revision === null && steps !== null)
32
+ return {};
33
+ const expected = revision === null ? "Progress card cleared" : steps === null
34
+ ? `Progress card updated (rev ${revision})`
35
+ : `Progress card updated (rev ${revision}, ${steps.completed}/${steps.total} done)`;
36
+ if (content[0].text !== expected || content[1].text !== JSON.stringify(payload, null, 2))
37
+ return {};
38
+ if (!native && digest(result.details) !== digest(payload))
39
+ return {};
40
+ return {
41
+ result_comparison_profile: "openclaw-progress-card-v1",
42
+ result_comparison_sha256: digest(payload),
43
+ result_observation_shape: native ? "native_input_text" : "tool_content_details",
44
+ };
45
+ }
@@ -2,7 +2,7 @@
2
2
  "id": "memory-atmem",
3
3
  "name": "Memory (atmem)",
4
4
  "description": "OpenClaw bridge installed and managed by the AtMem memory control plane.",
5
- "version": "2.2.6",
5
+ "version": "2.3.2",
6
6
  "commandAliases": ["memory-atmem"],
7
7
  "activation": {
8
8
  "onStartup": true,
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "dbPath": {
39
39
  "type": "string",
40
- "default": "~/.atmem/memories.db",
40
+ "default": "~/.atmem/memory/memories.db",
41
41
  "description": "SQLite database path (also auditable from outside OpenClaw via `atmem verify`)"
42
42
  },
43
43
  "subject": {
@@ -81,15 +81,16 @@
81
81
  "enabled": { "type": "boolean", "default": true },
82
82
  "maxRecords": { "type": "number", "default": 3, "description": "Max memories injected per turn" },
83
83
  "maxChars": { "type": "number", "default": 1200, "description": "Total character budget for the injected block" },
84
+ "requireDirectSupport": { "type": "boolean", "default": true, "description": "Require direct query relevance for automatic recall; rank score alone is insufficient" },
84
85
  "minScore": { "type": "number", "default": 0.3, "description": "Minimum relevance score; 0.3 requires a lexical match (priors alone never inject)" },
85
86
  "timeoutMs": { "type": "number", "default": 4000, "description": "Recall timeout; on timeout the turn proceeds without injection" }
86
87
  }
87
88
  },
88
89
  "persona": {
89
90
  "type": "object",
90
- "description": "L3 persona snapshot injected alongside recall",
91
+ "description": "Optional always-on persona snapshot; disabled by default to avoid unrelated context",
91
92
  "properties": {
92
- "enabled": { "type": "boolean", "default": true },
93
+ "enabled": { "type": "boolean", "default": false },
93
94
  "maxChars": { "type": "number", "default": 600, "description": "Character budget for the <user_persona> block" },
94
95
  "ttlSeconds": { "type": "number", "default": 300, "description": "Persona cache lifetime; also invalidated whenever new memory is captured" }
95
96
  }
@@ -128,13 +129,13 @@
128
129
  },
129
130
  "statePath": {
130
131
  "type": "string",
131
- "default": "~/.atmem/control-plane.json",
132
+ "default": "~/.atmem/config/control-plane.json",
132
133
  "description": "Digest-bound control file written by `atmem control`."
133
134
  },
134
135
  "blackboxEnabled": {
135
136
  "type": "boolean",
136
- "default": false,
137
- "description": "Record content-minimizing agent flights through the private control protocol. Managed automatically by the AtMem installer."
137
+ "default": true,
138
+ "description": "Record encrypted full-fidelity text, link/page, file, image, audio and video evidence plus metadata through the private control protocol. Set false to disable the recorder."
138
139
  }
139
140
  }
140
141
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-memory-atmem",
3
- "version": "2.2.6",
3
+ "version": "2.3.2",
4
4
  "description": "OpenClaw Agent Black Box and memory control-plane bridge for AtMem",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -40,7 +40,7 @@
40
40
  "scripts": {
41
41
  "build": "tsc -p tsconfig.json",
42
42
  "typecheck": "tsc -p tsconfig.json --noEmit",
43
- "test": "node test/manifest.mjs && node test/hook-context-compat.mjs && node test/setup.mjs && node test/hooks.mjs && node test/delegated-identity.mjs && node test/tool-observations.mjs && node test/task-tools.mjs && node test/delegated-context-contract.mjs",
43
+ "test": "node test/manifest.mjs && node test/hook-context-compat.mjs && node test/setup.mjs && node test/hooks.mjs && node test/delegated-identity.mjs && node test/tool-observations.mjs && node test/tool-result-comparison.mjs && node test/blackbox-diagnostics.mjs && node test/execution-spool.mjs && node test/task-tools.mjs && node test/delegated-context-contract.mjs",
44
44
  "smoke": "node test/smoke.mjs && node test/task-journey.mjs && node test/delegated-journey.mjs",
45
45
  "prepack": "npm run build && npm run typecheck && npm test && npm run smoke",
46
46
  "hook-context": "node test/hook-context-compat.mjs",