openclaw-memory-atmem 2.2.6 → 2.2.7-beta.1

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.2.7b1
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.2.7-beta.1` includes exact task-context delivery, 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
@@ -37,7 +37,7 @@ The bridge also supplies Agent Black Box hooks. It records model/tool lifecycle
37
37
  See the repository [OpenClaw setup](../../docs/openclaw-setup.md) and [control-plane guarantees](../../docs/control-plane.md).
38
38
 
39
39
 
40
- ### Delegated local CLI identity and tool evidence in 2.2.6
40
+ ### Delegated local CLI identity and tool evidence in 2.2.7b1
41
41
 
42
42
  The default delegated user mapping requires `senderIsOwner: true`. Setting
43
43
  `requireOwner: false` alone no longer bypasses identity checks. A dedicated local
@@ -54,3 +54,12 @@ actual terminal host tool-event results supply fallback observations when typed
54
54
  completion hooks are absent. Only matched requests close, with content-free
55
55
  bounded caching and duplicate suppression. Fully observed
56
56
  terminal tool errors close evidence as `completed_with_tool_errors`, not success.
57
+
58
+ Beta 2.2.7b1: automatic recall now requires calibrated direct
59
+ query support by default (`recall.requireDirectSupport: true`). Persona is an
60
+ explicit opt-in (`persona.enabled: true`); setup defaults it off to avoid adding
61
+ unrelated personal facts to every request. When enabled, persona records are
62
+ excluded from recall so each appears once. Existing explicit persona settings
63
+ are respected; set `persona.enabled: false` to adopt selective recall. Explicit
64
+ memory search remains available. The legacy rank-only mode is available with
65
+ `recall.requireDirectSupport: false`; it can admit weak matches.
package/dist/index.js CHANGED
@@ -24,6 +24,8 @@ import { createReadStream } from "node:fs";
24
24
  import { lstat, mkdir, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
25
25
  import { spawnSync } from "node:child_process";
26
26
  import { ToolObservations, observationContext } from "./src/tool-observations.js";
27
+ import { progressCardComparison } from "./src/tool-result-comparison.js";
28
+ import { toolErrorReason } from "./src/tool-errors.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";
@@ -97,10 +99,11 @@ function parseConfig(raw) {
97
99
  maxRecords: Number(cfg.recall?.maxRecords ?? 3),
98
100
  maxChars: Number(cfg.recall?.maxChars ?? 1200),
99
101
  minScore: Number(cfg.recall?.minScore ?? 0.3),
102
+ requireDirectSupport: cfg.recall?.requireDirectSupport !== false,
100
103
  timeoutMs: Number(cfg.recall?.timeoutMs ?? 4000),
101
104
  },
102
105
  persona: {
103
- enabled: cfg.persona?.enabled !== false,
106
+ enabled: cfg.persona?.enabled === true,
104
107
  maxChars: Number(cfg.persona?.maxChars ?? 600),
105
108
  ttlSeconds: Number(cfg.persona?.ttlSeconds ?? 300),
106
109
  },
@@ -995,6 +998,8 @@ function register(api) {
995
998
  max_records: cfg.recall.maxRecords,
996
999
  max_chars: cfg.recall.maxChars,
997
1000
  min_score: cfg.recall.minScore,
1001
+ require_direct_support: cfg.recall.requireDirectSupport,
1002
+ exclude_record_ids: personaRecordIds,
998
1003
  reference_mode: cfg.cacheAware.enabled && cfg.cacheAware.compactReferences
999
1004
  ? "compact"
1000
1005
  : "full",
@@ -1072,6 +1077,9 @@ function register(api) {
1072
1077
  context_chars: memoryContext.length,
1073
1078
  candidate_ids: candidateIds,
1074
1079
  context_component_event_ids: componentEventIds,
1080
+ persona_record_count: personaRecordIds.length,
1081
+ recall_record_count: (current?.injectedRecordIds ?? []).length,
1082
+ context_selection_profile: cfg.recall.requireDirectSupport ? "direct-support-v1" : "rank-threshold-v1",
1075
1083
  mode: "direct",
1076
1084
  context_location: contextLocation,
1077
1085
  }, undefined, {
@@ -1134,8 +1142,12 @@ function register(api) {
1134
1142
  tool_name: event.toolName,
1135
1143
  tool_canonical_name: canonicalToolName(event.toolName),
1136
1144
  result_sha256: digestJson(event.result ?? null),
1145
+ ...(!event.error ? progressCardComparison(event.toolName, event.result, digestJson) : {}),
1137
1146
  outcome: event.error ? "error" : "completed",
1138
1147
  error_category: event.error ? "tool_error" : undefined,
1148
+ error_reason: event.error ? toolErrorReason(event.error) : undefined,
1149
+ error_sha256: event.error ? digestText(event.error) : undefined,
1150
+ result_present: event.result !== undefined,
1139
1151
  duration_ms: event.durationMs ?? 0,
1140
1152
  }, event.toolCallId ?? ctx.toolCallId);
1141
1153
  });
@@ -1146,6 +1158,8 @@ function register(api) {
1146
1158
  tool_name: saved.name, tool_canonical_name: canonicalToolName(saved.name),
1147
1159
  result_sha256: saved.result.digest, outcome: saved.result.error ? "error" : "completed",
1148
1160
  error_category: saved.result.error ? "tool_error" : undefined,
1161
+ error_reason: saved.result.reason,
1162
+ error_sha256: saved.result.errorDigest,
1149
1163
  reason: "observed_terminal_host_tool_event",
1150
1164
  }, saved.callId);
1151
1165
  });
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.2.7-beta.1",
6
6
  "commandAliases": ["memory-atmem"],
7
7
  "activation": {
8
8
  "onStartup": true,
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-memory-atmem",
3
- "version": "2.2.6",
3
+ "version": "2.2.7-beta.1",
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/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",