openclaw-memory-atmem 2.2.6-beta.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 +31 -2
- package/dist/index.js +153 -57
- package/dist/src/delegated-identity.js +62 -0
- package/dist/src/rpc-client.js +1 -1
- package/dist/src/setup.js +1 -1
- package/dist/src/tool-errors.js +31 -0
- package/dist/src/tool-observations.js +115 -0
- package/dist/src/tool-result-comparison.js +45 -0
- package/dist/src/types.js +0 -6
- package/openclaw.plugin.json +19 -4
- package/package.json +3 -3
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 --
|
|
8
|
+
python -m pip install --upgrade atmem==2.2.7b1
|
|
9
9
|
atmem openclaw install
|
|
10
10
|
```
|
|
11
11
|
|
|
@@ -22,7 +22,8 @@ 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.
|
|
25
|
+
Bridge `2.2.7-beta.1` includes exact task-context delivery, the shared
|
|
26
|
+
calibrated retrieval decision, and delegated context correlation. When the host
|
|
26
27
|
supplies `taskId` in hook context, the bridge requests only that governed task,
|
|
27
28
|
checks its byte digest, contributes it separately from recalled memory, and
|
|
28
29
|
confirms exposure. With no `taskId`, task delivery stays off and existing
|
|
@@ -34,3 +35,31 @@ In shadow mode the bridge observes native-memory changes without injecting AtMem
|
|
|
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
36
|
|
|
36
37
|
See the repository [OpenClaw setup](../../docs/openclaw-setup.md) and [control-plane guarantees](../../docs/control-plane.md).
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
### Delegated local CLI identity and tool evidence in 2.2.7b1
|
|
41
|
+
|
|
42
|
+
The default delegated user mapping requires `senderIsOwner: true`. Setting
|
|
43
|
+
`requireOwner: false` alone no longer bypasses identity checks. A dedicated local
|
|
44
|
+
CLI process can use the explicit [isolated mapping configuration](../../docs/delegated-context-provider.md#isolated-local-cli-identity-226)
|
|
45
|
+
with matching workspace, agent and exact session generation. If CLI origin is
|
|
46
|
+
absent, an explicitly configured private state directory and an unrouted
|
|
47
|
+
`agent --local` process with no configured shared channels establish the local
|
|
48
|
+
process boundary. Shared-channel and explicit non-owner turns are refused.
|
|
49
|
+
|
|
50
|
+
Tool completion hooks use the event's invocation ID or, when absent, the host
|
|
51
|
+
context's ID. The verifier requires matching tool, turn and scope and a request
|
|
52
|
+
preceding completion. Missing completion observations remain `incomplete_evidence`;
|
|
53
|
+
actual terminal host tool-event results supply fallback observations when typed
|
|
54
|
+
completion hooks are absent. Only matched requests close, with content-free
|
|
55
|
+
bounded caching and duplicate suppression. Fully observed
|
|
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
|
@@ -23,6 +23,10 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
23
23
|
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
|
+
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";
|
|
29
|
+
import { delegatedIdentity, isolatedCliProcess } from "./src/delegated-identity.js";
|
|
26
30
|
import { AtmemClient } from "./src/rpc-client.js";
|
|
27
31
|
import { runSetup } from "./src/setup.js";
|
|
28
32
|
import { NOT_OWNER_MESSAGE, NO_IDENTITY_MESSAGE, describeDecision, isConversationOwner, ok, resolveBoundTaskForTool, refusal, sessionIdentityForTool, } from "./src/task-tools.js";
|
|
@@ -40,6 +44,7 @@ const TAKEOVER_GUIDANCE = "<atmem_memory_provider>\n" +
|
|
|
40
44
|
"</atmem_memory_provider>";
|
|
41
45
|
const INJECT_RE = /<(relevant_memories|user_persona|working_memory|episodic_memory|procedural_memory|atmem_control_plane|atmem_memory_provider)>[\s\S]*?<\/(relevant_memories|user_persona|working_memory|episodic_memory|procedural_memory|atmem_control_plane|atmem_memory_provider)>\s*/g;
|
|
42
46
|
const PROMPT_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
47
|
+
const MAX_PENDING_PROMPTS = 256;
|
|
43
48
|
function parseConfig(raw) {
|
|
44
49
|
const cfg = (raw ?? {});
|
|
45
50
|
const dbPath = expandHome(String(cfg.dbPath ?? "~/.atmem/memories.db"));
|
|
@@ -65,6 +70,14 @@ function parseConfig(raw) {
|
|
|
65
70
|
const delegatedContext = {
|
|
66
71
|
userId: String(cfg.delegatedContext?.userId ?? "").trim(),
|
|
67
72
|
requireOwner: cfg.delegatedContext?.requireOwner !== false,
|
|
73
|
+
localOperator: cfg.delegatedContext?.localOperator ? {
|
|
74
|
+
isolated: cfg.delegatedContext.localOperator.isolated === true,
|
|
75
|
+
stateDir: expandHome(String(cfg.delegatedContext.localOperator.stateDir ?? "")),
|
|
76
|
+
agentId: String(cfg.delegatedContext.localOperator.agentId ?? "").trim(),
|
|
77
|
+
workspaceDir: expandHome(String(cfg.delegatedContext.localOperator.workspaceDir ?? "")),
|
|
78
|
+
sessionKey: String(cfg.delegatedContext.localOperator.sessionKey ?? "").trim(),
|
|
79
|
+
sessionId: String(cfg.delegatedContext.localOperator.sessionId ?? "").trim(),
|
|
80
|
+
} : undefined,
|
|
68
81
|
};
|
|
69
82
|
return {
|
|
70
83
|
command: String(cfg.command ?? "atmem"),
|
|
@@ -86,10 +99,11 @@ function parseConfig(raw) {
|
|
|
86
99
|
maxRecords: Number(cfg.recall?.maxRecords ?? 3),
|
|
87
100
|
maxChars: Number(cfg.recall?.maxChars ?? 1200),
|
|
88
101
|
minScore: Number(cfg.recall?.minScore ?? 0.3),
|
|
102
|
+
requireDirectSupport: cfg.recall?.requireDirectSupport !== false,
|
|
89
103
|
timeoutMs: Number(cfg.recall?.timeoutMs ?? 4000),
|
|
90
104
|
},
|
|
91
105
|
persona: {
|
|
92
|
-
enabled: cfg.persona?.enabled
|
|
106
|
+
enabled: cfg.persona?.enabled === true,
|
|
93
107
|
maxChars: Number(cfg.persona?.maxChars ?? 600),
|
|
94
108
|
ttlSeconds: Number(cfg.persona?.ttlSeconds ?? 300),
|
|
95
109
|
},
|
|
@@ -329,9 +343,8 @@ function register(api) {
|
|
|
329
343
|
stop: () => blackboxClient.close(),
|
|
330
344
|
});
|
|
331
345
|
}
|
|
332
|
-
// Per-turn recall state. Semantic admission uses a short-lived SQLite handoff
|
|
333
|
-
// because OpenClaw may run prompt hooks and agent tools in separate runtimes.
|
|
334
346
|
const pendingPrompts = new Map();
|
|
347
|
+
const pendingPromptTimers = new Map();
|
|
335
348
|
const observedTurnInputs = new Map();
|
|
336
349
|
const inboundAttachments = new Map();
|
|
337
350
|
const inboundAttachmentGeneration = new Map();
|
|
@@ -377,14 +390,20 @@ function register(api) {
|
|
|
377
390
|
const workspace = cfg.agentWorkspaces[agentIdFor(ctx)];
|
|
378
391
|
return workspace ? `ws_${digestText(workspace).slice(0, 16)}` : undefined;
|
|
379
392
|
};
|
|
393
|
+
const verifiedIsolatedProcess = isolatedCliProcess(cfg.delegatedContext, api.config);
|
|
380
394
|
const delegatedUserIdFor = (ctx) => {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
395
|
+
const identity = delegatedIdentity(cfg.delegatedContext, ctx, cfg.agentWorkspaces[agentIdFor(ctx)], verifiedIsolatedProcess);
|
|
396
|
+
if (!identity.userId && cfg.delegatedContext.userId) {
|
|
397
|
+
api.logger.warn(`${TAG} delegated identity withheld: ${identity.reason}. ` +
|
|
398
|
+
"Use authenticated owner metadata or an explicitly isolated, scope-bound CLI mapping; " +
|
|
399
|
+
"provider HMAC authentication does not establish sender identity.");
|
|
400
|
+
}
|
|
401
|
+
return identity.userId;
|
|
386
402
|
};
|
|
387
403
|
const scopedKey = (value, ctx) => `${agentIdFor(ctx)}:${value}`;
|
|
404
|
+
// Lifecycle hooks must resolve one key. An llm_input-only event.sessionId
|
|
405
|
+
// must not redirect confirmation away from before_prompt_build's entry.
|
|
406
|
+
const sessionKeyFor = (ctx) => scopedKey(ctx.sessionKey ?? ctx.sessionId ?? ctx.runId ?? "default-session", ctx);
|
|
388
407
|
const contextIds = (ctx) => [...new Set([ctx.runId, ctx.sessionKey, ctx.sessionId].filter((value) => Boolean(value)).map((value) => scopedKey(value, ctx)))];
|
|
389
408
|
const callFor = (ctx, name, args, timeoutMs = cfg.recall.timeoutMs) => {
|
|
390
409
|
const scoped = cfg.controlPlane.enabled && !Object.keys(cfg.agentSubjects).length
|
|
@@ -586,8 +605,14 @@ function register(api) {
|
|
|
586
605
|
await observeTurnInput(event.prompt, ctx, "before_model_resolve", Array.isArray(event.attachments) ? event.attachments.length : 0);
|
|
587
606
|
});
|
|
588
607
|
api.on("llm_input", async (event, ctx) => {
|
|
589
|
-
const sessionKey =
|
|
608
|
+
const sessionKey = sessionKeyFor(ctx);
|
|
590
609
|
const pending = pendingPrompts.get(sessionKey);
|
|
610
|
+
if (!pending &&
|
|
611
|
+
cfg.controlPlane.enabled &&
|
|
612
|
+
Boolean(cfg.delegatedContext.userId) &&
|
|
613
|
+
event.prompt.trim().length > 0) {
|
|
614
|
+
api.logger.warn(`${TAG} no pending prompt state for llm_input; exact delegated delivery confirmation is unavailable`);
|
|
615
|
+
}
|
|
591
616
|
if (pending?.delegatedContext !== undefined) {
|
|
592
617
|
const exact = pending.delegatedContext;
|
|
593
618
|
const promptOccurrences = exact ? event.prompt.split(exact).length - 1 : 0;
|
|
@@ -615,7 +640,7 @@ function register(api) {
|
|
|
615
640
|
if (delivered && pending.exposureId) {
|
|
616
641
|
await callFor(ctx, "control_exposure_shown", { exposure_id: pending.exposureId }, cfg.recall.timeoutMs);
|
|
617
642
|
}
|
|
618
|
-
|
|
643
|
+
cachePendingPrompt(sessionKey, { ...pending, delegatedContext: undefined });
|
|
619
644
|
}
|
|
620
645
|
await recordBlackbox("model.input", event.runId, ctx, {
|
|
621
646
|
provider: event.provider,
|
|
@@ -635,10 +660,10 @@ function register(api) {
|
|
|
635
660
|
const visibleText = responses.map(String).join("");
|
|
636
661
|
const assistantVisibleTextSha256 = digestText(visibleText);
|
|
637
662
|
const modelOutputBundleSha256 = digestJson(responses);
|
|
638
|
-
const sessionKey =
|
|
663
|
+
const sessionKey = sessionKeyFor(ctx);
|
|
639
664
|
const pending = pendingPrompts.get(sessionKey);
|
|
640
665
|
if (pending) {
|
|
641
|
-
|
|
666
|
+
cachePendingPrompt(sessionKey, {
|
|
642
667
|
...pending,
|
|
643
668
|
assistantVisibleTextSha256,
|
|
644
669
|
modelOutputBundleSha256,
|
|
@@ -711,7 +736,7 @@ function register(api) {
|
|
|
711
736
|
const now = Date.now();
|
|
712
737
|
for (const [key, value] of pendingPrompts) {
|
|
713
738
|
if (now - value.ts > PROMPT_CACHE_TTL_MS)
|
|
714
|
-
|
|
739
|
+
deletePendingPrompt(key);
|
|
715
740
|
}
|
|
716
741
|
for (const [key, value] of observedTurnInputs) {
|
|
717
742
|
if (now - value.observedAt > PROMPT_CACHE_TTL_MS)
|
|
@@ -722,6 +747,30 @@ function register(api) {
|
|
|
722
747
|
inboundAttachments.delete(key);
|
|
723
748
|
}
|
|
724
749
|
};
|
|
750
|
+
function deletePendingPrompt(key) {
|
|
751
|
+
const timer = pendingPromptTimers.get(key);
|
|
752
|
+
if (timer)
|
|
753
|
+
clearTimeout(timer);
|
|
754
|
+
pendingPromptTimers.delete(key);
|
|
755
|
+
pendingPrompts.delete(key);
|
|
756
|
+
}
|
|
757
|
+
function cachePendingPrompt(key, value) {
|
|
758
|
+
deletePendingPrompt(key);
|
|
759
|
+
pendingPrompts.set(key, value);
|
|
760
|
+
const timer = setTimeout(() => {
|
|
761
|
+
const current = pendingPrompts.get(key);
|
|
762
|
+
if (current?.ts === value.ts)
|
|
763
|
+
deletePendingPrompt(key);
|
|
764
|
+
}, PROMPT_CACHE_TTL_MS);
|
|
765
|
+
timer.unref?.();
|
|
766
|
+
pendingPromptTimers.set(key, timer);
|
|
767
|
+
while (pendingPrompts.size > MAX_PENDING_PROMPTS) {
|
|
768
|
+
const oldest = pendingPrompts.keys().next().value;
|
|
769
|
+
if (oldest === undefined)
|
|
770
|
+
break;
|
|
771
|
+
deletePendingPrompt(oldest);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
725
774
|
async function personaBlock(sessionKey, ctx) {
|
|
726
775
|
if (!cfg.persona.enabled)
|
|
727
776
|
return { block: "", recordIds: [] };
|
|
@@ -753,10 +802,10 @@ function register(api) {
|
|
|
753
802
|
if (!userText)
|
|
754
803
|
return;
|
|
755
804
|
await observeTurnInput(userText, ctx, "before_prompt_build");
|
|
756
|
-
const sessionKey =
|
|
805
|
+
const sessionKey = sessionKeyFor(ctx);
|
|
757
806
|
const takeoverGuidance = cfg.takeoverActive ? TAKEOVER_GUIDANCE : "";
|
|
758
|
-
pendingPrompts.set(sessionKey, { text: userText, ts: Date.now() });
|
|
759
807
|
sweep();
|
|
808
|
+
cachePendingPrompt(sessionKey, { text: userText, ts: Date.now() });
|
|
760
809
|
if (cfg.controlPlane.enabled) {
|
|
761
810
|
try {
|
|
762
811
|
const prepared = (await callFor(ctx, "control_prepare", {
|
|
@@ -789,7 +838,14 @@ function register(api) {
|
|
|
789
838
|
`sha256:${digestText(taskPrepared.context)}` !== taskPrepared.context_sha256)) {
|
|
790
839
|
throw new Error("governed task context failed exact handoff digest validation");
|
|
791
840
|
}
|
|
792
|
-
|
|
841
|
+
if (prepared.authority === "delegated" &&
|
|
842
|
+
prepared.inject &&
|
|
843
|
+
(!prepared.context ||
|
|
844
|
+
!prepared.context_sha256 ||
|
|
845
|
+
digestText(prepared.context) !== prepared.context_sha256)) {
|
|
846
|
+
throw new Error("delegated context failed exact handoff digest validation");
|
|
847
|
+
}
|
|
848
|
+
cachePendingPrompt(sessionKey, {
|
|
793
849
|
text: userText,
|
|
794
850
|
ts: Date.now(),
|
|
795
851
|
exposureId: prepared.exposure_id,
|
|
@@ -815,13 +871,6 @@ function register(api) {
|
|
|
815
871
|
task_reason_codes: taskPrepared?.reason_codes ?? [],
|
|
816
872
|
});
|
|
817
873
|
}
|
|
818
|
-
if (prepared.authority === "delegated" &&
|
|
819
|
-
prepared.inject &&
|
|
820
|
-
(!prepared.context ||
|
|
821
|
-
!prepared.context_sha256 ||
|
|
822
|
-
digestText(prepared.context) !== prepared.context_sha256)) {
|
|
823
|
-
throw new Error("delegated context failed exact handoff digest validation");
|
|
824
|
-
}
|
|
825
874
|
if (prepared.authority === "delegated" || prepared.authority === "atmem_fallback") {
|
|
826
875
|
await recordBlackbox("context.provider_authorization", undefined, ctx, {
|
|
827
876
|
disposition: prepared.decision ?? "provider_failure",
|
|
@@ -835,20 +884,43 @@ function register(api) {
|
|
|
835
884
|
success: prepared.decision === "inject" || prepared.decision === "withhold",
|
|
836
885
|
}, undefined, { contextReceiptId: prepared.context_receipt_id });
|
|
837
886
|
}
|
|
887
|
+
const taskContext = taskPrepared?.disposition === "injected"
|
|
888
|
+
? taskPrepared.context ?? ""
|
|
889
|
+
: "";
|
|
890
|
+
let contextEnvelope = {};
|
|
891
|
+
if (prepared.inject && prepared.context) {
|
|
892
|
+
contextEnvelope = prepared.authority === "delegated"
|
|
893
|
+
? {
|
|
894
|
+
prependContext: prepared.context,
|
|
895
|
+
...(taskContext ? { appendContext: taskContext } : {}),
|
|
896
|
+
}
|
|
897
|
+
: {
|
|
898
|
+
appendContext: [prepared.context, taskContext].filter(Boolean).join("\n\n"),
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
else if (taskContext) {
|
|
902
|
+
contextEnvelope = { appendContext: taskContext };
|
|
903
|
+
}
|
|
904
|
+
const contextLocations = [
|
|
905
|
+
contextEnvelope.prependContext ? "prependContext" : "",
|
|
906
|
+
contextEnvelope.appendContext ? "appendContext" : "",
|
|
907
|
+
].filter(Boolean);
|
|
908
|
+
const deliveredContext = [
|
|
909
|
+
contextEnvelope.prependContext,
|
|
910
|
+
contextEnvelope.appendContext,
|
|
911
|
+
].filter((value) => Boolean(value)).join("\n\n");
|
|
838
912
|
await recordBlackbox("context.disposition", undefined, ctx, {
|
|
839
|
-
disposition:
|
|
913
|
+
disposition: deliveredContext
|
|
840
914
|
? "injected"
|
|
841
915
|
: prepared.mode === "shadow" && (prepared.preview_context ?? "")
|
|
842
916
|
? "withheld_by_policy"
|
|
843
917
|
: "no_relevant_memory",
|
|
844
|
-
context_sha256: digestText(
|
|
845
|
-
context_block_sha256: digestText(
|
|
846
|
-
context_envelope_sha256: digestJson(
|
|
847
|
-
? { prependContext: prepared.context ?? "" }
|
|
848
|
-
: { appendContext: prepared.context ?? "" }),
|
|
918
|
+
context_sha256: digestText(deliveredContext),
|
|
919
|
+
context_block_sha256: digestText(deliveredContext),
|
|
920
|
+
context_envelope_sha256: digestJson(contextEnvelope),
|
|
849
921
|
context_receipt_sha256: prepared.manifest_sha256,
|
|
850
922
|
digest_profile: "atmem-context-envelope-canonical-json-v1",
|
|
851
|
-
context_chars:
|
|
923
|
+
context_chars: deliveredContext.length,
|
|
852
924
|
candidate_ids: prepared.candidate_ids ?? [],
|
|
853
925
|
candidates_considered: prepared.retrieval?.eligible_candidate_count ?? 0,
|
|
854
926
|
retrieval_support_class: prepared.retrieval?.decision?.support_class ?? "not_recorded",
|
|
@@ -856,34 +928,20 @@ function register(api) {
|
|
|
856
928
|
retrieval_calibration_version: prepared.retrieval?.decision?.calibration_version,
|
|
857
929
|
exposure_id: prepared.exposure_id,
|
|
858
930
|
mode: prepared.mode,
|
|
859
|
-
context_location:
|
|
860
|
-
? prepared.authority === "delegated" ? "prependContext" : "appendContext"
|
|
861
|
-
: "none",
|
|
931
|
+
context_location: contextLocations.length ? contextLocations.join("+") : "none",
|
|
862
932
|
}, undefined, {
|
|
863
933
|
contextReceiptId: prepared.context_receipt_id,
|
|
864
934
|
});
|
|
865
|
-
if (
|
|
935
|
+
if (Object.keys(contextEnvelope).length) {
|
|
866
936
|
api.logger.info(`${TAG} memory control plane ${prepared.mode ?? "active"} context exposed`);
|
|
867
|
-
return
|
|
868
|
-
? {
|
|
869
|
-
prependContext: prepared.context,
|
|
870
|
-
appendContext: taskPrepared?.disposition === "injected"
|
|
871
|
-
? taskPrepared.context
|
|
872
|
-
: undefined,
|
|
873
|
-
}
|
|
874
|
-
: {
|
|
875
|
-
appendContext: [
|
|
876
|
-
prepared.context,
|
|
877
|
-
taskPrepared?.disposition === "injected" ? taskPrepared.context : "",
|
|
878
|
-
].filter(Boolean).join("\n\n"),
|
|
879
|
-
};
|
|
880
|
-
}
|
|
881
|
-
if (taskPrepared?.disposition === "injected" && taskPrepared.context) {
|
|
882
|
-
return { appendContext: taskPrepared.context };
|
|
937
|
+
return contextEnvelope;
|
|
883
938
|
}
|
|
884
939
|
return;
|
|
885
940
|
}
|
|
886
941
|
catch (error) {
|
|
942
|
+
// Any exact delegated bytes retained while constructing evidence are
|
|
943
|
+
// discarded immediately on the fail-closed path.
|
|
944
|
+
cachePendingPrompt(sessionKey, { text: userText, ts: Date.now() });
|
|
887
945
|
await recordBlackbox("context.disposition", undefined, ctx, {
|
|
888
946
|
disposition: "recall_failed",
|
|
889
947
|
context_block_sha256: digestText(""),
|
|
@@ -940,6 +998,8 @@ function register(api) {
|
|
|
940
998
|
max_records: cfg.recall.maxRecords,
|
|
941
999
|
max_chars: cfg.recall.maxChars,
|
|
942
1000
|
min_score: cfg.recall.minScore,
|
|
1001
|
+
require_direct_support: cfg.recall.requireDirectSupport,
|
|
1002
|
+
exclude_record_ids: personaRecordIds,
|
|
943
1003
|
reference_mode: cfg.cacheAware.enabled && cfg.cacheAware.compactReferences
|
|
944
1004
|
? "compact"
|
|
945
1005
|
: "full",
|
|
@@ -949,7 +1009,7 @@ function register(api) {
|
|
|
949
1009
|
recall = result.block;
|
|
950
1010
|
const current = pendingPrompts.get(sessionKey);
|
|
951
1011
|
if (current) {
|
|
952
|
-
|
|
1012
|
+
cachePendingPrompt(sessionKey, {
|
|
953
1013
|
...current,
|
|
954
1014
|
injectedRecordIds: result.record_ids ?? [],
|
|
955
1015
|
retrievalId: result.retrieval_id,
|
|
@@ -1002,7 +1062,7 @@ function register(api) {
|
|
|
1002
1062
|
? `ctxr_${digestJson({ componentEventIds, contextEnvelopeSha256 })}`
|
|
1003
1063
|
: undefined;
|
|
1004
1064
|
if (current) {
|
|
1005
|
-
|
|
1065
|
+
cachePendingPrompt(sessionKey, { ...current, contextReceiptId });
|
|
1006
1066
|
}
|
|
1007
1067
|
await recordBlackbox("context.disposition", undefined, ctx, {
|
|
1008
1068
|
disposition: memoryContext
|
|
@@ -1017,6 +1077,9 @@ function register(api) {
|
|
|
1017
1077
|
context_chars: memoryContext.length,
|
|
1018
1078
|
candidate_ids: candidateIds,
|
|
1019
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",
|
|
1020
1083
|
mode: "direct",
|
|
1021
1084
|
context_location: contextLocation,
|
|
1022
1085
|
}, undefined, {
|
|
@@ -1027,8 +1090,26 @@ function register(api) {
|
|
|
1027
1090
|
if (Object.keys(result).length)
|
|
1028
1091
|
return result;
|
|
1029
1092
|
});
|
|
1093
|
+
// Latest CLI harnesses emit terminal results on the agent-event stream.
|
|
1094
|
+
// Feature-detect both API generations; never infer completion from a request.
|
|
1095
|
+
const runContext = api.runContext ?? (api.setRunContext && api.getRunContext ? {
|
|
1096
|
+
setRunContext: api.setRunContext.bind(api), getRunContext: api.getRunContext.bind(api),
|
|
1097
|
+
} : undefined);
|
|
1098
|
+
const subscribe = api.agent?.events?.registerAgentEventSubscription?.bind(api.agent.events)
|
|
1099
|
+
?? api.registerAgentEventSubscription?.bind(api);
|
|
1100
|
+
const toolObservations = cfg.controlPlane.blackboxEnabled && subscribe
|
|
1101
|
+
? new ToolObservations(observationContext(runContext, digestJson({
|
|
1102
|
+
command: cfg.command, args: cfg.commandArgs, state: cfg.controlPlane.statePath,
|
|
1103
|
+
subjects: cfg.agentSubjects, workspaces: cfg.agentWorkspaces,
|
|
1104
|
+
})), digestJson) : undefined;
|
|
1105
|
+
if (toolObservations && subscribe)
|
|
1106
|
+
subscribe({
|
|
1107
|
+
id: "atmem-terminal-tool-observations", streams: ["tool"],
|
|
1108
|
+
handle: event => toolObservations.observe(event),
|
|
1109
|
+
});
|
|
1030
1110
|
// ---- flight recorder + takeover enforcement --------------------------
|
|
1031
1111
|
api.on("before_tool_call", async (event, ctx) => {
|
|
1112
|
+
toolObservations?.request(event.toolName, event.toolCallId ?? ctx.toolCallId, { ...ctx, runId: event.runId ?? ctx.runId, agentId: agentIdFor(ctx) });
|
|
1032
1113
|
await recordBlackbox("tool.requested", event.runId, ctx, {
|
|
1033
1114
|
tool_name: event.toolName,
|
|
1034
1115
|
tool_canonical_name: canonicalToolName(event.toolName),
|
|
@@ -1038,7 +1119,7 @@ function register(api) {
|
|
|
1038
1119
|
derived_path_sha256: Array.isArray(event.derivedPaths)
|
|
1039
1120
|
? event.derivedPaths.map((value) => digestText(String(value)))
|
|
1040
1121
|
: [],
|
|
1041
|
-
}, event.toolCallId);
|
|
1122
|
+
}, event.toolCallId ?? ctx.toolCallId);
|
|
1042
1123
|
if (!cfg.takeoverActive || !cfg.nativeWorkspaces.some((workspace) => touchesNativeMemory(event, workspace)))
|
|
1043
1124
|
return;
|
|
1044
1125
|
const reason = "AtMem takeover blocked access to OpenClaw's frozen native memory " +
|
|
@@ -1051,26 +1132,41 @@ function register(api) {
|
|
|
1051
1132
|
error_category: "blocked_by_memory_boundary",
|
|
1052
1133
|
result_sha256: digestText(reason),
|
|
1053
1134
|
duration_ms: 0,
|
|
1054
|
-
}, event.toolCallId);
|
|
1135
|
+
}, event.toolCallId ?? ctx.toolCallId);
|
|
1055
1136
|
api.logger.warn(`${TAG} ${reason} Tool: ${event.toolName}`);
|
|
1056
1137
|
return { block: true, blockReason: reason };
|
|
1057
1138
|
});
|
|
1058
1139
|
api.on("after_tool_call", async (event, ctx) => {
|
|
1140
|
+
toolObservations?.completed(event.toolCallId ?? ctx.toolCallId, { ...ctx, runId: event.runId ?? ctx.runId, agentId: agentIdFor(ctx) });
|
|
1059
1141
|
await recordBlackbox("tool.completed", event.runId, ctx, {
|
|
1060
1142
|
tool_name: event.toolName,
|
|
1061
1143
|
tool_canonical_name: canonicalToolName(event.toolName),
|
|
1062
1144
|
result_sha256: digestJson(event.result ?? null),
|
|
1145
|
+
...(!event.error ? progressCardComparison(event.toolName, event.result, digestJson) : {}),
|
|
1063
1146
|
outcome: event.error ? "error" : "completed",
|
|
1064
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,
|
|
1065
1151
|
duration_ms: event.durationMs ?? 0,
|
|
1066
|
-
}, event.toolCallId);
|
|
1152
|
+
}, event.toolCallId ?? ctx.toolCallId);
|
|
1067
1153
|
});
|
|
1068
1154
|
// ---- auto-capture: user turn through the pipeline, assistant as digest -
|
|
1069
1155
|
api.on("agent_end", async (event, ctx) => {
|
|
1070
|
-
|
|
1156
|
+
await toolObservations?.flush({ ...ctx, runId: event.runId ?? ctx.runId, agentId: agentIdFor(ctx) }, async (saved) => {
|
|
1157
|
+
await recordBlackbox("tool.completed", saved.ctx.runId, saved.ctx, {
|
|
1158
|
+
tool_name: saved.name, tool_canonical_name: canonicalToolName(saved.name),
|
|
1159
|
+
result_sha256: saved.result.digest, outcome: saved.result.error ? "error" : "completed",
|
|
1160
|
+
error_category: saved.result.error ? "tool_error" : undefined,
|
|
1161
|
+
error_reason: saved.result.reason,
|
|
1162
|
+
error_sha256: saved.result.errorDigest,
|
|
1163
|
+
reason: "observed_terminal_host_tool_event",
|
|
1164
|
+
}, saved.callId);
|
|
1165
|
+
});
|
|
1166
|
+
const sessionKey = sessionKeyFor(ctx);
|
|
1071
1167
|
const observedTurnInputKey = turnInputKey(ctx);
|
|
1072
1168
|
const cached = pendingPrompts.get(sessionKey);
|
|
1073
|
-
|
|
1169
|
+
deletePendingPrompt(sessionKey);
|
|
1074
1170
|
const userText = cached?.text?.replace(INJECT_RE, "").trim();
|
|
1075
1171
|
try {
|
|
1076
1172
|
if (cfg.takeoverActive) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
3
|
+
/** Verify the dedicated CLI process when the host omits origin metadata.
|
|
4
|
+
* This is an explicit local operator trust boundary, never shared-channel auth.
|
|
5
|
+
*/
|
|
6
|
+
export function isolatedCliProcess(config, hostConfig, runtime = { argv: process.argv.slice(2), stateDir: process.env.OPENCLAW_STATE_DIR, uid: process.getuid?.() }) {
|
|
7
|
+
const local = config.localOperator;
|
|
8
|
+
if (!local?.isolated || !local.stateDir || !runtime.stateDir || !hostConfig)
|
|
9
|
+
return false;
|
|
10
|
+
if (!runtime.argv.includes("agent") || !runtime.argv.includes("--local"))
|
|
11
|
+
return false;
|
|
12
|
+
const routed = ["--deliver", "--channel", "--to", "--reply-channel", "--reply-to", "--reply-account"];
|
|
13
|
+
if (runtime.argv.some(arg => routed.some(flag => arg === flag || arg.startsWith(flag + "="))))
|
|
14
|
+
return false;
|
|
15
|
+
const channels = hostConfig.channels;
|
|
16
|
+
if (channels !== undefined && (!channels || typeof channels !== "object" || Array.isArray(channels) || Object.keys(channels).length))
|
|
17
|
+
return false;
|
|
18
|
+
try {
|
|
19
|
+
const directory = lstatSync(local.stateDir);
|
|
20
|
+
return path.isAbsolute(local.stateDir) && directory.isDirectory() && !directory.isSymbolicLink()
|
|
21
|
+
&& (directory.mode & 0o077) === 0 && directory.uid === runtime.uid
|
|
22
|
+
&& realpathSync(local.stateDir) === realpathSync(runtime.stateDir);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Host metadata and operator configuration only; never prompt/model input. */
|
|
29
|
+
export function delegatedIdentity(config, ctx, configuredWorkspace, verifiedIsolatedProcess = false) {
|
|
30
|
+
if (!config.userId)
|
|
31
|
+
return { reason: "delegated_user_unconfigured" };
|
|
32
|
+
if (ctx.senderIsOwner === false)
|
|
33
|
+
return { reason: "delegated_sender_not_owner" };
|
|
34
|
+
if (ctx.senderIsOwner !== undefined && ctx.senderIsOwner !== true) {
|
|
35
|
+
return { reason: "delegated_owner_metadata_invalid" };
|
|
36
|
+
}
|
|
37
|
+
if (config.requireOwner) {
|
|
38
|
+
return ctx.senderIsOwner === true
|
|
39
|
+
? { userId: config.userId }
|
|
40
|
+
: { reason: "delegated_owner_metadata_missing" };
|
|
41
|
+
}
|
|
42
|
+
// Legacy requireOwner:false is no longer an unrestricted identity bypass.
|
|
43
|
+
const local = config.localOperator;
|
|
44
|
+
if (!local || local.isolated !== true) {
|
|
45
|
+
return { reason: "delegated_local_isolation_required" };
|
|
46
|
+
}
|
|
47
|
+
if ((ctx.messageProvider !== "cli" && !(ctx.messageProvider === undefined && verifiedIsolatedProcess)) || ctx.channel || ctx.channelId ||
|
|
48
|
+
ctx.accountId || ctx.chatId || ctx.senderId) {
|
|
49
|
+
return { reason: "delegated_local_origin_unverified" };
|
|
50
|
+
}
|
|
51
|
+
if (!local.agentId || !local.sessionKey || !local.sessionId || !local.workspaceDir ||
|
|
52
|
+
!ctx.runId || !ctx.workspaceDir || !configuredWorkspace ||
|
|
53
|
+
!path.isAbsolute(local.workspaceDir) || !path.isAbsolute(ctx.workspaceDir) ||
|
|
54
|
+
!path.isAbsolute(configuredWorkspace) ||
|
|
55
|
+
ctx.agentId !== local.agentId || ctx.sessionKey !== local.sessionKey ||
|
|
56
|
+
ctx.sessionId !== local.sessionId ||
|
|
57
|
+
path.resolve(ctx.workspaceDir) !== path.resolve(local.workspaceDir) ||
|
|
58
|
+
path.resolve(configuredWorkspace) !== path.resolve(local.workspaceDir)) {
|
|
59
|
+
return { reason: "delegated_local_scope_mismatch" };
|
|
60
|
+
}
|
|
61
|
+
return { userId: config.userId };
|
|
62
|
+
}
|
package/dist/src/rpc-client.js
CHANGED
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:
|
|
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
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { toolErrorReason } from "./tool-errors.js";
|
|
3
|
+
const CACHE_KEY = Symbol.for("atmem.tool-observation-cache.v1");
|
|
4
|
+
const sharedCache = globalThis;
|
|
5
|
+
const cache = sharedCache[CACHE_KEY] ??= new Map();
|
|
6
|
+
/** Some hosts deny run-context writes from tool hooks after registry reload.
|
|
7
|
+
* Keep a bounded content-minimized process fallback, isolated by installation config.
|
|
8
|
+
*/
|
|
9
|
+
export function observationContext(host, scope) {
|
|
10
|
+
const keyFor = (runId, namespace) => JSON.stringify([scope, runId, namespace]);
|
|
11
|
+
const sweep = () => {
|
|
12
|
+
for (const [key, entry] of cache)
|
|
13
|
+
if (entry.expires <= Date.now())
|
|
14
|
+
cache.delete(key);
|
|
15
|
+
while (cache.size >= 2048)
|
|
16
|
+
cache.delete(cache.keys().next().value);
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
setRunContext(patch) {
|
|
20
|
+
sweep();
|
|
21
|
+
cache.set(keyFor(patch.runId, patch.namespace), { value: structuredClone(patch.value), expires: Date.now() + 600_000 });
|
|
22
|
+
try {
|
|
23
|
+
host?.setRunContext(patch);
|
|
24
|
+
}
|
|
25
|
+
catch { /* Process fallback remains bounded. */ }
|
|
26
|
+
return true;
|
|
27
|
+
},
|
|
28
|
+
getRunContext(params) {
|
|
29
|
+
sweep();
|
|
30
|
+
const entry = cache.get(keyFor(params.runId, params.namespace));
|
|
31
|
+
if (entry)
|
|
32
|
+
return structuredClone(entry.value);
|
|
33
|
+
try {
|
|
34
|
+
return host?.getRunContext(params);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const INDEX = "atmem-tool-observations-v1";
|
|
43
|
+
const namespace = (id) => INDEX + "-" + createHash("sha256").update(id).digest("hex");
|
|
44
|
+
/** The CLI exposes its native names; the request hook exposes OpenClaw names. */
|
|
45
|
+
function canonical(name) {
|
|
46
|
+
const unwrapped = name.replace(/^mcp__openclaw__/, "");
|
|
47
|
+
return { Read: "read", Bash: "exec" }[unwrapped] ?? unwrapped;
|
|
48
|
+
}
|
|
49
|
+
/** Stores scope, names, digests and redacted error diagnostics in bounded ephemeral context. */
|
|
50
|
+
export class ToolObservations {
|
|
51
|
+
access;
|
|
52
|
+
digest;
|
|
53
|
+
constructor(access, digest) {
|
|
54
|
+
this.access = access;
|
|
55
|
+
this.digest = digest;
|
|
56
|
+
}
|
|
57
|
+
request(name, id, ctx) {
|
|
58
|
+
if (!id || !ctx.runId || !ctx.agentId || !ctx.sessionId || !ctx.sessionKey)
|
|
59
|
+
return;
|
|
60
|
+
const runId = ctx.runId;
|
|
61
|
+
const ids = this.access.getRunContext({ runId, namespace: INDEX }) ?? [];
|
|
62
|
+
if (ids.includes(id) || ids.length >= 256)
|
|
63
|
+
return;
|
|
64
|
+
// Never store the full hook context: it may contain host config/credentials.
|
|
65
|
+
const scope = { runId, agentId: ctx.agentId, sessionId: ctx.sessionId, sessionKey: ctx.sessionKey };
|
|
66
|
+
if (this.access.setRunContext({ runId, namespace: namespace(id), value: { ctx: scope, name, callId: id } })) {
|
|
67
|
+
this.access.setRunContext({ runId, namespace: INDEX, value: [...ids, id] });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
completed(id, ctx) {
|
|
71
|
+
if (!id || !ctx.runId)
|
|
72
|
+
return;
|
|
73
|
+
const key = { runId: ctx.runId, namespace: namespace(id) };
|
|
74
|
+
const saved = this.access.getRunContext(key);
|
|
75
|
+
if (saved && saved.ctx.sessionId === ctx.sessionId && saved.ctx.sessionKey === ctx.sessionKey && saved.ctx.agentId === ctx.agentId) {
|
|
76
|
+
this.access.setRunContext({ ...key, value: { ...saved, hookCompleted: true } });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
observe(event) {
|
|
80
|
+
const d = event.data;
|
|
81
|
+
if (event.stream !== "tool" || d.phase !== "result" || typeof d.toolCallId !== "string" ||
|
|
82
|
+
typeof d.name !== "string" || typeof d.isError !== "boolean" ||
|
|
83
|
+
!Object.hasOwn(d, "result") || d.incomplete === true || d.synthetic === true)
|
|
84
|
+
return;
|
|
85
|
+
const key = { runId: event.runId, namespace: namespace(d.toolCallId) };
|
|
86
|
+
const saved = this.access.getRunContext(key);
|
|
87
|
+
if (!saved || event.sessionKey !== saved.ctx.sessionKey ||
|
|
88
|
+
(event.sessionId !== undefined && event.sessionId !== saved.ctx.sessionId) ||
|
|
89
|
+
(event.agentId !== undefined && event.agentId !== saved.ctx.agentId) ||
|
|
90
|
+
canonical(d.name) !== canonical(saved.name))
|
|
91
|
+
return;
|
|
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 };
|
|
95
|
+
const conflict = saved.conflict || (saved.result !== undefined &&
|
|
96
|
+
(saved.result.digest !== result.digest || saved.result.error !== result.error ||
|
|
97
|
+
saved.result.errorDigest !== result.errorDigest));
|
|
98
|
+
this.access.setRunContext({ ...key, value: { ...saved, result, conflict } });
|
|
99
|
+
}
|
|
100
|
+
async flush(ctx, record) {
|
|
101
|
+
if (!ctx.runId)
|
|
102
|
+
return;
|
|
103
|
+
const runId = ctx.runId;
|
|
104
|
+
const ids = this.access.getRunContext({ runId, namespace: INDEX }) ?? [];
|
|
105
|
+
for (const id of ids) {
|
|
106
|
+
const key = { runId, namespace: namespace(id) };
|
|
107
|
+
const saved = this.access.getRunContext(key);
|
|
108
|
+
if (saved && !saved.hookCompleted && !saved.conflict && saved.result &&
|
|
109
|
+
saved.ctx.sessionKey === ctx.sessionKey && saved.ctx.sessionId === ctx.sessionId && saved.ctx.agentId === ctx.agentId) {
|
|
110
|
+
await record(saved);
|
|
111
|
+
this.access.setRunContext({ ...key, value: { ...saved, hookCompleted: true } });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/src/types.js
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal structural types for the OpenClaw plugin API surface this plugin
|
|
3
|
-
* uses. Kept local (instead of importing "openclaw/plugin-sdk/core") so the
|
|
4
|
-
* plugin builds without the SDK on the compile path; the shapes match the
|
|
5
|
-
* hook/tool contracts observed in OpenClaw's plugin SDK.
|
|
6
|
-
*/
|
|
7
1
|
export {};
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
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": "
|
|
91
|
+
"description": "Optional always-on persona snapshot; disabled by default to avoid unrelated context",
|
|
91
92
|
"properties": {
|
|
92
|
-
"enabled": { "type": "boolean", "default":
|
|
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
|
}
|
|
@@ -150,7 +151,21 @@
|
|
|
150
151
|
"requireOwner": {
|
|
151
152
|
"type": "boolean",
|
|
152
153
|
"default": true,
|
|
153
|
-
"description": "
|
|
154
|
+
"description": "Require authenticated owner metadata (default). False requires the explicit isolated localOperator mapping; it is not an ownership bypass."
|
|
155
|
+
},
|
|
156
|
+
"localOperator": {
|
|
157
|
+
"type": "object",
|
|
158
|
+
"additionalProperties": false,
|
|
159
|
+
"required": ["isolated", "agentId", "workspaceDir", "sessionKey", "sessionId"],
|
|
160
|
+
"description": "Only for a dedicated local CLI process with channels disabled by the operator. Requires host messageProvider=cli, workspace and exact session generation; channel-originated turns are refused.",
|
|
161
|
+
"properties": {
|
|
162
|
+
"isolated": { "type": "boolean", "const": true, "description": "Operator assertion that this process is isolated from shared channels and other users." },
|
|
163
|
+
"stateDir": { "type": "string", "minLength": 1, "description": "Optional dedicated owner-only OPENCLAW_STATE_DIR. Required when a direct agent --local process omits CLI origin metadata; channels and delivery flags must be absent." },
|
|
164
|
+
"agentId": { "type": "string", "minLength": 1 },
|
|
165
|
+
"workspaceDir": { "type": "string", "minLength": 1 },
|
|
166
|
+
"sessionKey": { "type": "string", "minLength": 1 },
|
|
167
|
+
"sessionId": { "type": "string", "minLength": 1 }
|
|
168
|
+
}
|
|
154
169
|
}
|
|
155
170
|
}
|
|
156
171
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-memory-atmem",
|
|
3
|
-
"version": "2.2.
|
|
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,8 +40,8 @@
|
|
|
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/task-tools.mjs && node test/delegated-context-contract.mjs",
|
|
44
|
-
"smoke": "node test/smoke.mjs && node test/task-journey.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
|
+
"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",
|
|
47
47
|
"record-hook-context": "node test/lib/record-hook-context.mjs"
|