openclaw-memory-atmem 2.2.6-beta.5 → 2.2.6
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 +22 -2
- package/dist/index.js +158 -63
- package/dist/src/delegated-identity.js +62 -0
- package/dist/src/rpc-client.js +1 -1
- package/dist/src/task-tools.js +23 -0
- package/dist/src/tool-observations.js +111 -0
- package/dist/src/types.js +0 -6
- package/openclaw.plugin.json +16 -2
- 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.6
|
|
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.6
|
|
25
|
+
Bridge `2.2.6` 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,22 @@ 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.6
|
|
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.
|
package/dist/index.js
CHANGED
|
@@ -23,9 +23,11 @@ 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 { delegatedIdentity, isolatedCliProcess } from "./src/delegated-identity.js";
|
|
26
28
|
import { AtmemClient } from "./src/rpc-client.js";
|
|
27
29
|
import { runSetup } from "./src/setup.js";
|
|
28
|
-
import { NOT_OWNER_MESSAGE, NO_IDENTITY_MESSAGE, describeDecision, isConversationOwner, ok, refusal, sessionIdentityForTool, } from "./src/task-tools.js";
|
|
30
|
+
import { NOT_OWNER_MESSAGE, NO_IDENTITY_MESSAGE, describeDecision, isConversationOwner, ok, resolveBoundTaskForTool, refusal, sessionIdentityForTool, } from "./src/task-tools.js";
|
|
29
31
|
const TAG = "[memory-atmem]";
|
|
30
32
|
const TAKEOVER_GUIDANCE = "<atmem_memory_provider>\n" +
|
|
31
33
|
"AtMem is the active durable-memory provider. " +
|
|
@@ -40,6 +42,7 @@ const TAKEOVER_GUIDANCE = "<atmem_memory_provider>\n" +
|
|
|
40
42
|
"</atmem_memory_provider>";
|
|
41
43
|
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
44
|
const PROMPT_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
45
|
+
const MAX_PENDING_PROMPTS = 256;
|
|
43
46
|
function parseConfig(raw) {
|
|
44
47
|
const cfg = (raw ?? {});
|
|
45
48
|
const dbPath = expandHome(String(cfg.dbPath ?? "~/.atmem/memories.db"));
|
|
@@ -65,6 +68,14 @@ function parseConfig(raw) {
|
|
|
65
68
|
const delegatedContext = {
|
|
66
69
|
userId: String(cfg.delegatedContext?.userId ?? "").trim(),
|
|
67
70
|
requireOwner: cfg.delegatedContext?.requireOwner !== false,
|
|
71
|
+
localOperator: cfg.delegatedContext?.localOperator ? {
|
|
72
|
+
isolated: cfg.delegatedContext.localOperator.isolated === true,
|
|
73
|
+
stateDir: expandHome(String(cfg.delegatedContext.localOperator.stateDir ?? "")),
|
|
74
|
+
agentId: String(cfg.delegatedContext.localOperator.agentId ?? "").trim(),
|
|
75
|
+
workspaceDir: expandHome(String(cfg.delegatedContext.localOperator.workspaceDir ?? "")),
|
|
76
|
+
sessionKey: String(cfg.delegatedContext.localOperator.sessionKey ?? "").trim(),
|
|
77
|
+
sessionId: String(cfg.delegatedContext.localOperator.sessionId ?? "").trim(),
|
|
78
|
+
} : undefined,
|
|
68
79
|
};
|
|
69
80
|
return {
|
|
70
81
|
command: String(cfg.command ?? "atmem"),
|
|
@@ -329,9 +340,8 @@ function register(api) {
|
|
|
329
340
|
stop: () => blackboxClient.close(),
|
|
330
341
|
});
|
|
331
342
|
}
|
|
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
343
|
const pendingPrompts = new Map();
|
|
344
|
+
const pendingPromptTimers = new Map();
|
|
335
345
|
const observedTurnInputs = new Map();
|
|
336
346
|
const inboundAttachments = new Map();
|
|
337
347
|
const inboundAttachmentGeneration = new Map();
|
|
@@ -377,14 +387,20 @@ function register(api) {
|
|
|
377
387
|
const workspace = cfg.agentWorkspaces[agentIdFor(ctx)];
|
|
378
388
|
return workspace ? `ws_${digestText(workspace).slice(0, 16)}` : undefined;
|
|
379
389
|
};
|
|
390
|
+
const verifiedIsolatedProcess = isolatedCliProcess(cfg.delegatedContext, api.config);
|
|
380
391
|
const delegatedUserIdFor = (ctx) => {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
392
|
+
const identity = delegatedIdentity(cfg.delegatedContext, ctx, cfg.agentWorkspaces[agentIdFor(ctx)], verifiedIsolatedProcess);
|
|
393
|
+
if (!identity.userId && cfg.delegatedContext.userId) {
|
|
394
|
+
api.logger.warn(`${TAG} delegated identity withheld: ${identity.reason}. ` +
|
|
395
|
+
"Use authenticated owner metadata or an explicitly isolated, scope-bound CLI mapping; " +
|
|
396
|
+
"provider HMAC authentication does not establish sender identity.");
|
|
397
|
+
}
|
|
398
|
+
return identity.userId;
|
|
386
399
|
};
|
|
387
400
|
const scopedKey = (value, ctx) => `${agentIdFor(ctx)}:${value}`;
|
|
401
|
+
// Lifecycle hooks must resolve one key. An llm_input-only event.sessionId
|
|
402
|
+
// must not redirect confirmation away from before_prompt_build's entry.
|
|
403
|
+
const sessionKeyFor = (ctx) => scopedKey(ctx.sessionKey ?? ctx.sessionId ?? ctx.runId ?? "default-session", ctx);
|
|
388
404
|
const contextIds = (ctx) => [...new Set([ctx.runId, ctx.sessionKey, ctx.sessionId].filter((value) => Boolean(value)).map((value) => scopedKey(value, ctx)))];
|
|
389
405
|
const callFor = (ctx, name, args, timeoutMs = cfg.recall.timeoutMs) => {
|
|
390
406
|
const scoped = cfg.controlPlane.enabled && !Object.keys(cfg.agentSubjects).length
|
|
@@ -586,8 +602,14 @@ function register(api) {
|
|
|
586
602
|
await observeTurnInput(event.prompt, ctx, "before_model_resolve", Array.isArray(event.attachments) ? event.attachments.length : 0);
|
|
587
603
|
});
|
|
588
604
|
api.on("llm_input", async (event, ctx) => {
|
|
589
|
-
const sessionKey =
|
|
605
|
+
const sessionKey = sessionKeyFor(ctx);
|
|
590
606
|
const pending = pendingPrompts.get(sessionKey);
|
|
607
|
+
if (!pending &&
|
|
608
|
+
cfg.controlPlane.enabled &&
|
|
609
|
+
Boolean(cfg.delegatedContext.userId) &&
|
|
610
|
+
event.prompt.trim().length > 0) {
|
|
611
|
+
api.logger.warn(`${TAG} no pending prompt state for llm_input; exact delegated delivery confirmation is unavailable`);
|
|
612
|
+
}
|
|
591
613
|
if (pending?.delegatedContext !== undefined) {
|
|
592
614
|
const exact = pending.delegatedContext;
|
|
593
615
|
const promptOccurrences = exact ? event.prompt.split(exact).length - 1 : 0;
|
|
@@ -615,7 +637,7 @@ function register(api) {
|
|
|
615
637
|
if (delivered && pending.exposureId) {
|
|
616
638
|
await callFor(ctx, "control_exposure_shown", { exposure_id: pending.exposureId }, cfg.recall.timeoutMs);
|
|
617
639
|
}
|
|
618
|
-
|
|
640
|
+
cachePendingPrompt(sessionKey, { ...pending, delegatedContext: undefined });
|
|
619
641
|
}
|
|
620
642
|
await recordBlackbox("model.input", event.runId, ctx, {
|
|
621
643
|
provider: event.provider,
|
|
@@ -635,10 +657,10 @@ function register(api) {
|
|
|
635
657
|
const visibleText = responses.map(String).join("");
|
|
636
658
|
const assistantVisibleTextSha256 = digestText(visibleText);
|
|
637
659
|
const modelOutputBundleSha256 = digestJson(responses);
|
|
638
|
-
const sessionKey =
|
|
660
|
+
const sessionKey = sessionKeyFor(ctx);
|
|
639
661
|
const pending = pendingPrompts.get(sessionKey);
|
|
640
662
|
if (pending) {
|
|
641
|
-
|
|
663
|
+
cachePendingPrompt(sessionKey, {
|
|
642
664
|
...pending,
|
|
643
665
|
assistantVisibleTextSha256,
|
|
644
666
|
modelOutputBundleSha256,
|
|
@@ -711,7 +733,7 @@ function register(api) {
|
|
|
711
733
|
const now = Date.now();
|
|
712
734
|
for (const [key, value] of pendingPrompts) {
|
|
713
735
|
if (now - value.ts > PROMPT_CACHE_TTL_MS)
|
|
714
|
-
|
|
736
|
+
deletePendingPrompt(key);
|
|
715
737
|
}
|
|
716
738
|
for (const [key, value] of observedTurnInputs) {
|
|
717
739
|
if (now - value.observedAt > PROMPT_CACHE_TTL_MS)
|
|
@@ -722,6 +744,30 @@ function register(api) {
|
|
|
722
744
|
inboundAttachments.delete(key);
|
|
723
745
|
}
|
|
724
746
|
};
|
|
747
|
+
function deletePendingPrompt(key) {
|
|
748
|
+
const timer = pendingPromptTimers.get(key);
|
|
749
|
+
if (timer)
|
|
750
|
+
clearTimeout(timer);
|
|
751
|
+
pendingPromptTimers.delete(key);
|
|
752
|
+
pendingPrompts.delete(key);
|
|
753
|
+
}
|
|
754
|
+
function cachePendingPrompt(key, value) {
|
|
755
|
+
deletePendingPrompt(key);
|
|
756
|
+
pendingPrompts.set(key, value);
|
|
757
|
+
const timer = setTimeout(() => {
|
|
758
|
+
const current = pendingPrompts.get(key);
|
|
759
|
+
if (current?.ts === value.ts)
|
|
760
|
+
deletePendingPrompt(key);
|
|
761
|
+
}, PROMPT_CACHE_TTL_MS);
|
|
762
|
+
timer.unref?.();
|
|
763
|
+
pendingPromptTimers.set(key, timer);
|
|
764
|
+
while (pendingPrompts.size > MAX_PENDING_PROMPTS) {
|
|
765
|
+
const oldest = pendingPrompts.keys().next().value;
|
|
766
|
+
if (oldest === undefined)
|
|
767
|
+
break;
|
|
768
|
+
deletePendingPrompt(oldest);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
725
771
|
async function personaBlock(sessionKey, ctx) {
|
|
726
772
|
if (!cfg.persona.enabled)
|
|
727
773
|
return { block: "", recordIds: [] };
|
|
@@ -753,10 +799,10 @@ function register(api) {
|
|
|
753
799
|
if (!userText)
|
|
754
800
|
return;
|
|
755
801
|
await observeTurnInput(userText, ctx, "before_prompt_build");
|
|
756
|
-
const sessionKey =
|
|
802
|
+
const sessionKey = sessionKeyFor(ctx);
|
|
757
803
|
const takeoverGuidance = cfg.takeoverActive ? TAKEOVER_GUIDANCE : "";
|
|
758
|
-
pendingPrompts.set(sessionKey, { text: userText, ts: Date.now() });
|
|
759
804
|
sweep();
|
|
805
|
+
cachePendingPrompt(sessionKey, { text: userText, ts: Date.now() });
|
|
760
806
|
if (cfg.controlPlane.enabled) {
|
|
761
807
|
try {
|
|
762
808
|
const prepared = (await callFor(ctx, "control_prepare", {
|
|
@@ -789,7 +835,14 @@ function register(api) {
|
|
|
789
835
|
`sha256:${digestText(taskPrepared.context)}` !== taskPrepared.context_sha256)) {
|
|
790
836
|
throw new Error("governed task context failed exact handoff digest validation");
|
|
791
837
|
}
|
|
792
|
-
|
|
838
|
+
if (prepared.authority === "delegated" &&
|
|
839
|
+
prepared.inject &&
|
|
840
|
+
(!prepared.context ||
|
|
841
|
+
!prepared.context_sha256 ||
|
|
842
|
+
digestText(prepared.context) !== prepared.context_sha256)) {
|
|
843
|
+
throw new Error("delegated context failed exact handoff digest validation");
|
|
844
|
+
}
|
|
845
|
+
cachePendingPrompt(sessionKey, {
|
|
793
846
|
text: userText,
|
|
794
847
|
ts: Date.now(),
|
|
795
848
|
exposureId: prepared.exposure_id,
|
|
@@ -809,17 +862,12 @@ function register(api) {
|
|
|
809
862
|
task_id: taskPrepared.task_id ?? ctx.taskId,
|
|
810
863
|
task_disposition: taskPrepared?.disposition ?? "withheld",
|
|
811
864
|
task_revision: taskPrepared?.revision,
|
|
812
|
-
|
|
865
|
+
...(taskPrepared?.context_sha256
|
|
866
|
+
? { task_context_sha256: taskPrepared.context_sha256.replace(/^sha256:/, "") }
|
|
867
|
+
: {}),
|
|
813
868
|
task_reason_codes: taskPrepared?.reason_codes ?? [],
|
|
814
869
|
});
|
|
815
870
|
}
|
|
816
|
-
if (prepared.authority === "delegated" &&
|
|
817
|
-
prepared.inject &&
|
|
818
|
-
(!prepared.context ||
|
|
819
|
-
!prepared.context_sha256 ||
|
|
820
|
-
digestText(prepared.context) !== prepared.context_sha256)) {
|
|
821
|
-
throw new Error("delegated context failed exact handoff digest validation");
|
|
822
|
-
}
|
|
823
871
|
if (prepared.authority === "delegated" || prepared.authority === "atmem_fallback") {
|
|
824
872
|
await recordBlackbox("context.provider_authorization", undefined, ctx, {
|
|
825
873
|
disposition: prepared.decision ?? "provider_failure",
|
|
@@ -833,51 +881,64 @@ function register(api) {
|
|
|
833
881
|
success: prepared.decision === "inject" || prepared.decision === "withhold",
|
|
834
882
|
}, undefined, { contextReceiptId: prepared.context_receipt_id });
|
|
835
883
|
}
|
|
884
|
+
const taskContext = taskPrepared?.disposition === "injected"
|
|
885
|
+
? taskPrepared.context ?? ""
|
|
886
|
+
: "";
|
|
887
|
+
let contextEnvelope = {};
|
|
888
|
+
if (prepared.inject && prepared.context) {
|
|
889
|
+
contextEnvelope = prepared.authority === "delegated"
|
|
890
|
+
? {
|
|
891
|
+
prependContext: prepared.context,
|
|
892
|
+
...(taskContext ? { appendContext: taskContext } : {}),
|
|
893
|
+
}
|
|
894
|
+
: {
|
|
895
|
+
appendContext: [prepared.context, taskContext].filter(Boolean).join("\n\n"),
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
else if (taskContext) {
|
|
899
|
+
contextEnvelope = { appendContext: taskContext };
|
|
900
|
+
}
|
|
901
|
+
const contextLocations = [
|
|
902
|
+
contextEnvelope.prependContext ? "prependContext" : "",
|
|
903
|
+
contextEnvelope.appendContext ? "appendContext" : "",
|
|
904
|
+
].filter(Boolean);
|
|
905
|
+
const deliveredContext = [
|
|
906
|
+
contextEnvelope.prependContext,
|
|
907
|
+
contextEnvelope.appendContext,
|
|
908
|
+
].filter((value) => Boolean(value)).join("\n\n");
|
|
836
909
|
await recordBlackbox("context.disposition", undefined, ctx, {
|
|
837
|
-
disposition:
|
|
910
|
+
disposition: deliveredContext
|
|
838
911
|
? "injected"
|
|
839
912
|
: prepared.mode === "shadow" && (prepared.preview_context ?? "")
|
|
840
913
|
? "withheld_by_policy"
|
|
841
914
|
: "no_relevant_memory",
|
|
842
|
-
context_sha256: digestText(
|
|
843
|
-
context_block_sha256: digestText(
|
|
844
|
-
context_envelope_sha256: digestJson(
|
|
845
|
-
? { prependContext: prepared.context ?? "" }
|
|
846
|
-
: { appendContext: prepared.context ?? "" }),
|
|
915
|
+
context_sha256: digestText(deliveredContext),
|
|
916
|
+
context_block_sha256: digestText(deliveredContext),
|
|
917
|
+
context_envelope_sha256: digestJson(contextEnvelope),
|
|
847
918
|
context_receipt_sha256: prepared.manifest_sha256,
|
|
848
919
|
digest_profile: "atmem-context-envelope-canonical-json-v1",
|
|
849
|
-
context_chars:
|
|
920
|
+
context_chars: deliveredContext.length,
|
|
850
921
|
candidate_ids: prepared.candidate_ids ?? [],
|
|
922
|
+
candidates_considered: prepared.retrieval?.eligible_candidate_count ?? 0,
|
|
923
|
+
retrieval_support_class: prepared.retrieval?.decision?.support_class ?? "not_recorded",
|
|
924
|
+
retrieval_reason_codes: prepared.retrieval?.decision?.reason_codes ?? [],
|
|
925
|
+
retrieval_calibration_version: prepared.retrieval?.decision?.calibration_version,
|
|
851
926
|
exposure_id: prepared.exposure_id,
|
|
852
927
|
mode: prepared.mode,
|
|
853
|
-
context_location:
|
|
854
|
-
? prepared.authority === "delegated" ? "prependContext" : "appendContext"
|
|
855
|
-
: "none",
|
|
928
|
+
context_location: contextLocations.length ? contextLocations.join("+") : "none",
|
|
856
929
|
}, undefined, {
|
|
857
930
|
contextReceiptId: prepared.context_receipt_id,
|
|
858
931
|
});
|
|
859
|
-
if (
|
|
932
|
+
if (Object.keys(contextEnvelope).length) {
|
|
860
933
|
api.logger.info(`${TAG} memory control plane ${prepared.mode ?? "active"} context exposed`);
|
|
861
|
-
return
|
|
862
|
-
? {
|
|
863
|
-
prependContext: prepared.context,
|
|
864
|
-
appendContext: taskPrepared?.disposition === "injected"
|
|
865
|
-
? taskPrepared.context
|
|
866
|
-
: undefined,
|
|
867
|
-
}
|
|
868
|
-
: {
|
|
869
|
-
appendContext: [
|
|
870
|
-
prepared.context,
|
|
871
|
-
taskPrepared?.disposition === "injected" ? taskPrepared.context : "",
|
|
872
|
-
].filter(Boolean).join("\n\n"),
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
if (taskPrepared?.disposition === "injected" && taskPrepared.context) {
|
|
876
|
-
return { appendContext: taskPrepared.context };
|
|
934
|
+
return contextEnvelope;
|
|
877
935
|
}
|
|
878
936
|
return;
|
|
879
937
|
}
|
|
880
938
|
catch (error) {
|
|
939
|
+
// Any exact delegated bytes retained while constructing evidence are
|
|
940
|
+
// discarded immediately on the fail-closed path.
|
|
941
|
+
cachePendingPrompt(sessionKey, { text: userText, ts: Date.now() });
|
|
881
942
|
await recordBlackbox("context.disposition", undefined, ctx, {
|
|
882
943
|
disposition: "recall_failed",
|
|
883
944
|
context_block_sha256: digestText(""),
|
|
@@ -943,7 +1004,7 @@ function register(api) {
|
|
|
943
1004
|
recall = result.block;
|
|
944
1005
|
const current = pendingPrompts.get(sessionKey);
|
|
945
1006
|
if (current) {
|
|
946
|
-
|
|
1007
|
+
cachePendingPrompt(sessionKey, {
|
|
947
1008
|
...current,
|
|
948
1009
|
injectedRecordIds: result.record_ids ?? [],
|
|
949
1010
|
retrievalId: result.retrieval_id,
|
|
@@ -996,7 +1057,7 @@ function register(api) {
|
|
|
996
1057
|
? `ctxr_${digestJson({ componentEventIds, contextEnvelopeSha256 })}`
|
|
997
1058
|
: undefined;
|
|
998
1059
|
if (current) {
|
|
999
|
-
|
|
1060
|
+
cachePendingPrompt(sessionKey, { ...current, contextReceiptId });
|
|
1000
1061
|
}
|
|
1001
1062
|
await recordBlackbox("context.disposition", undefined, ctx, {
|
|
1002
1063
|
disposition: memoryContext
|
|
@@ -1021,8 +1082,26 @@ function register(api) {
|
|
|
1021
1082
|
if (Object.keys(result).length)
|
|
1022
1083
|
return result;
|
|
1023
1084
|
});
|
|
1085
|
+
// Latest CLI harnesses emit terminal results on the agent-event stream.
|
|
1086
|
+
// Feature-detect both API generations; never infer completion from a request.
|
|
1087
|
+
const runContext = api.runContext ?? (api.setRunContext && api.getRunContext ? {
|
|
1088
|
+
setRunContext: api.setRunContext.bind(api), getRunContext: api.getRunContext.bind(api),
|
|
1089
|
+
} : undefined);
|
|
1090
|
+
const subscribe = api.agent?.events?.registerAgentEventSubscription?.bind(api.agent.events)
|
|
1091
|
+
?? api.registerAgentEventSubscription?.bind(api);
|
|
1092
|
+
const toolObservations = cfg.controlPlane.blackboxEnabled && subscribe
|
|
1093
|
+
? new ToolObservations(observationContext(runContext, digestJson({
|
|
1094
|
+
command: cfg.command, args: cfg.commandArgs, state: cfg.controlPlane.statePath,
|
|
1095
|
+
subjects: cfg.agentSubjects, workspaces: cfg.agentWorkspaces,
|
|
1096
|
+
})), digestJson) : undefined;
|
|
1097
|
+
if (toolObservations && subscribe)
|
|
1098
|
+
subscribe({
|
|
1099
|
+
id: "atmem-terminal-tool-observations", streams: ["tool"],
|
|
1100
|
+
handle: event => toolObservations.observe(event),
|
|
1101
|
+
});
|
|
1024
1102
|
// ---- flight recorder + takeover enforcement --------------------------
|
|
1025
1103
|
api.on("before_tool_call", async (event, ctx) => {
|
|
1104
|
+
toolObservations?.request(event.toolName, event.toolCallId ?? ctx.toolCallId, { ...ctx, runId: event.runId ?? ctx.runId, agentId: agentIdFor(ctx) });
|
|
1026
1105
|
await recordBlackbox("tool.requested", event.runId, ctx, {
|
|
1027
1106
|
tool_name: event.toolName,
|
|
1028
1107
|
tool_canonical_name: canonicalToolName(event.toolName),
|
|
@@ -1032,7 +1111,7 @@ function register(api) {
|
|
|
1032
1111
|
derived_path_sha256: Array.isArray(event.derivedPaths)
|
|
1033
1112
|
? event.derivedPaths.map((value) => digestText(String(value)))
|
|
1034
1113
|
: [],
|
|
1035
|
-
}, event.toolCallId);
|
|
1114
|
+
}, event.toolCallId ?? ctx.toolCallId);
|
|
1036
1115
|
if (!cfg.takeoverActive || !cfg.nativeWorkspaces.some((workspace) => touchesNativeMemory(event, workspace)))
|
|
1037
1116
|
return;
|
|
1038
1117
|
const reason = "AtMem takeover blocked access to OpenClaw's frozen native memory " +
|
|
@@ -1045,11 +1124,12 @@ function register(api) {
|
|
|
1045
1124
|
error_category: "blocked_by_memory_boundary",
|
|
1046
1125
|
result_sha256: digestText(reason),
|
|
1047
1126
|
duration_ms: 0,
|
|
1048
|
-
}, event.toolCallId);
|
|
1127
|
+
}, event.toolCallId ?? ctx.toolCallId);
|
|
1049
1128
|
api.logger.warn(`${TAG} ${reason} Tool: ${event.toolName}`);
|
|
1050
1129
|
return { block: true, blockReason: reason };
|
|
1051
1130
|
});
|
|
1052
1131
|
api.on("after_tool_call", async (event, ctx) => {
|
|
1132
|
+
toolObservations?.completed(event.toolCallId ?? ctx.toolCallId, { ...ctx, runId: event.runId ?? ctx.runId, agentId: agentIdFor(ctx) });
|
|
1053
1133
|
await recordBlackbox("tool.completed", event.runId, ctx, {
|
|
1054
1134
|
tool_name: event.toolName,
|
|
1055
1135
|
tool_canonical_name: canonicalToolName(event.toolName),
|
|
@@ -1057,14 +1137,22 @@ function register(api) {
|
|
|
1057
1137
|
outcome: event.error ? "error" : "completed",
|
|
1058
1138
|
error_category: event.error ? "tool_error" : undefined,
|
|
1059
1139
|
duration_ms: event.durationMs ?? 0,
|
|
1060
|
-
}, event.toolCallId);
|
|
1140
|
+
}, event.toolCallId ?? ctx.toolCallId);
|
|
1061
1141
|
});
|
|
1062
1142
|
// ---- auto-capture: user turn through the pipeline, assistant as digest -
|
|
1063
1143
|
api.on("agent_end", async (event, ctx) => {
|
|
1064
|
-
|
|
1144
|
+
await toolObservations?.flush({ ...ctx, runId: event.runId ?? ctx.runId, agentId: agentIdFor(ctx) }, async (saved) => {
|
|
1145
|
+
await recordBlackbox("tool.completed", saved.ctx.runId, saved.ctx, {
|
|
1146
|
+
tool_name: saved.name, tool_canonical_name: canonicalToolName(saved.name),
|
|
1147
|
+
result_sha256: saved.result.digest, outcome: saved.result.error ? "error" : "completed",
|
|
1148
|
+
error_category: saved.result.error ? "tool_error" : undefined,
|
|
1149
|
+
reason: "observed_terminal_host_tool_event",
|
|
1150
|
+
}, saved.callId);
|
|
1151
|
+
});
|
|
1152
|
+
const sessionKey = sessionKeyFor(ctx);
|
|
1065
1153
|
const observedTurnInputKey = turnInputKey(ctx);
|
|
1066
1154
|
const cached = pendingPrompts.get(sessionKey);
|
|
1067
|
-
|
|
1155
|
+
deletePendingPrompt(sessionKey);
|
|
1068
1156
|
const userText = cached?.text?.replace(INJECT_RE, "").trim();
|
|
1069
1157
|
try {
|
|
1070
1158
|
if (cfg.takeoverActive) {
|
|
@@ -1744,13 +1832,20 @@ function register(api) {
|
|
|
1744
1832
|
required: ["item_id", "status", "base_revision"],
|
|
1745
1833
|
},
|
|
1746
1834
|
async execute(toolCallId, params) {
|
|
1747
|
-
const
|
|
1748
|
-
if (!identity)
|
|
1749
|
-
return refusal(NO_IDENTITY_MESSAGE);
|
|
1750
|
-
const result = (await callFor(toolCtx, "control_propose_task_delta", {
|
|
1835
|
+
const resolution = await resolveBoundTaskForTool(toolCtx, (identity) => callFor(toolCtx, "control_prepare_task_context", {
|
|
1751
1836
|
...identity,
|
|
1752
1837
|
...taskScope(toolCtx),
|
|
1753
|
-
|
|
1838
|
+
host_run_id: toolCtx.runId,
|
|
1839
|
+
}));
|
|
1840
|
+
if (!resolution.ok) {
|
|
1841
|
+
return refusal(`${resolution.message} (${resolution.reasonCodes.join(", ")})`);
|
|
1842
|
+
}
|
|
1843
|
+
const result = (await callFor(toolCtx, "control_propose_task_delta", {
|
|
1844
|
+
...resolution.identity,
|
|
1845
|
+
...taskScope(toolCtx),
|
|
1846
|
+
// Redundant checked assertion. Authority came from the current
|
|
1847
|
+
// conversation focus above, never from the model's parameters.
|
|
1848
|
+
task_id: resolution.taskId,
|
|
1754
1849
|
base_revision: Number(params.base_revision ?? 0),
|
|
1755
1850
|
// Derived from stable host identifiers, never from payload content
|
|
1756
1851
|
// or a clock, so a retried tool call collapses to one decision.
|
|
@@ -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/task-tools.js
CHANGED
|
@@ -30,6 +30,29 @@ export function sessionIdentityForTool(ctx) {
|
|
|
30
30
|
session_epoch: sessionEpoch,
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
+
/** Resolve authority from the authenticated conversation, never model input. */
|
|
34
|
+
export async function resolveBoundTaskForTool(ctx, prepare) {
|
|
35
|
+
const identity = sessionIdentityForTool(ctx);
|
|
36
|
+
if (!identity) {
|
|
37
|
+
return { ok: false, message: NO_IDENTITY_MESSAGE, reasonCodes: ["host_identity_missing"] };
|
|
38
|
+
}
|
|
39
|
+
const prepared = (await prepare(identity));
|
|
40
|
+
if (prepared.disposition !== "injected" || !prepared.task_id) {
|
|
41
|
+
const reasons = (prepared.reason_codes ?? []).map(String);
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
message: "This conversation has no active governed task focus. " +
|
|
45
|
+
"No task progress was recorded.",
|
|
46
|
+
reasonCodes: reasons.length ? reasons : ["task_context_selection_required"],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
ok: true,
|
|
51
|
+
identity,
|
|
52
|
+
taskId: String(prepared.task_id),
|
|
53
|
+
revision: prepared.revision,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
33
56
|
/** Absence is never permission. Only an explicit affirmative is the owner. */
|
|
34
57
|
export function isConversationOwner(ctx) {
|
|
35
58
|
return ctx.senderIsOwner === true;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const CACHE_KEY = Symbol.for("atmem.tool-observation-cache.v1");
|
|
3
|
+
const sharedCache = globalThis;
|
|
4
|
+
const cache = sharedCache[CACHE_KEY] ??= new Map();
|
|
5
|
+
/** 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
|
+
*/
|
|
8
|
+
export function observationContext(host, scope) {
|
|
9
|
+
const keyFor = (runId, namespace) => JSON.stringify([scope, runId, namespace]);
|
|
10
|
+
const sweep = () => {
|
|
11
|
+
for (const [key, entry] of cache)
|
|
12
|
+
if (entry.expires <= Date.now())
|
|
13
|
+
cache.delete(key);
|
|
14
|
+
while (cache.size >= 2048)
|
|
15
|
+
cache.delete(cache.keys().next().value);
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
setRunContext(patch) {
|
|
19
|
+
sweep();
|
|
20
|
+
cache.set(keyFor(patch.runId, patch.namespace), { value: structuredClone(patch.value), expires: Date.now() + 600_000 });
|
|
21
|
+
try {
|
|
22
|
+
host?.setRunContext(patch);
|
|
23
|
+
}
|
|
24
|
+
catch { /* Process fallback remains bounded. */ }
|
|
25
|
+
return true;
|
|
26
|
+
},
|
|
27
|
+
getRunContext(params) {
|
|
28
|
+
sweep();
|
|
29
|
+
const entry = cache.get(keyFor(params.runId, params.namespace));
|
|
30
|
+
if (entry)
|
|
31
|
+
return structuredClone(entry.value);
|
|
32
|
+
try {
|
|
33
|
+
return host?.getRunContext(params);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const INDEX = "atmem-tool-observations-v1";
|
|
42
|
+
const namespace = (id) => INDEX + "-" + createHash("sha256").update(id).digest("hex");
|
|
43
|
+
/** The CLI exposes its native names; the request hook exposes OpenClaw names. */
|
|
44
|
+
function canonical(name) {
|
|
45
|
+
const unwrapped = name.replace(/^mcp__openclaw__/, "");
|
|
46
|
+
return { Read: "read", Bash: "exec" }[unwrapped] ?? unwrapped;
|
|
47
|
+
}
|
|
48
|
+
/** Stores only scope, names and digests in bounded ephemeral observation context. */
|
|
49
|
+
export class ToolObservations {
|
|
50
|
+
access;
|
|
51
|
+
digest;
|
|
52
|
+
constructor(access, digest) {
|
|
53
|
+
this.access = access;
|
|
54
|
+
this.digest = digest;
|
|
55
|
+
}
|
|
56
|
+
request(name, id, ctx) {
|
|
57
|
+
if (!id || !ctx.runId || !ctx.agentId || !ctx.sessionId || !ctx.sessionKey)
|
|
58
|
+
return;
|
|
59
|
+
const runId = ctx.runId;
|
|
60
|
+
const ids = this.access.getRunContext({ runId, namespace: INDEX }) ?? [];
|
|
61
|
+
if (ids.includes(id) || ids.length >= 256)
|
|
62
|
+
return;
|
|
63
|
+
// Never store the full hook context: it may contain host config/credentials.
|
|
64
|
+
const scope = { runId, agentId: ctx.agentId, sessionId: ctx.sessionId, sessionKey: ctx.sessionKey };
|
|
65
|
+
if (this.access.setRunContext({ runId, namespace: namespace(id), value: { ctx: scope, name, callId: id } })) {
|
|
66
|
+
this.access.setRunContext({ runId, namespace: INDEX, value: [...ids, id] });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
completed(id, ctx) {
|
|
70
|
+
if (!id || !ctx.runId)
|
|
71
|
+
return;
|
|
72
|
+
const key = { runId: ctx.runId, namespace: namespace(id) };
|
|
73
|
+
const saved = this.access.getRunContext(key);
|
|
74
|
+
if (saved && saved.ctx.sessionId === ctx.sessionId && saved.ctx.sessionKey === ctx.sessionKey && saved.ctx.agentId === ctx.agentId) {
|
|
75
|
+
this.access.setRunContext({ ...key, value: { ...saved, hookCompleted: true } });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
observe(event) {
|
|
79
|
+
const d = event.data;
|
|
80
|
+
if (event.stream !== "tool" || d.phase !== "result" || typeof d.toolCallId !== "string" ||
|
|
81
|
+
typeof d.name !== "string" || typeof d.isError !== "boolean" ||
|
|
82
|
+
!Object.hasOwn(d, "result") || d.incomplete === true || d.synthetic === true)
|
|
83
|
+
return;
|
|
84
|
+
const key = { runId: event.runId, namespace: namespace(d.toolCallId) };
|
|
85
|
+
const saved = this.access.getRunContext(key);
|
|
86
|
+
if (!saved || event.sessionKey !== saved.ctx.sessionKey ||
|
|
87
|
+
(event.sessionId !== undefined && event.sessionId !== saved.ctx.sessionId) ||
|
|
88
|
+
(event.agentId !== undefined && event.agentId !== saved.ctx.agentId) ||
|
|
89
|
+
canonical(d.name) !== canonical(saved.name))
|
|
90
|
+
return;
|
|
91
|
+
const result = { digest: this.digest(d.result), error: d.isError };
|
|
92
|
+
const conflict = saved.conflict || (saved.result !== undefined &&
|
|
93
|
+
(saved.result.digest !== result.digest || saved.result.error !== result.error));
|
|
94
|
+
this.access.setRunContext({ ...key, value: { ...saved, result, conflict } });
|
|
95
|
+
}
|
|
96
|
+
async flush(ctx, record) {
|
|
97
|
+
if (!ctx.runId)
|
|
98
|
+
return;
|
|
99
|
+
const runId = ctx.runId;
|
|
100
|
+
const ids = this.access.getRunContext({ runId, namespace: INDEX }) ?? [];
|
|
101
|
+
for (const id of ids) {
|
|
102
|
+
const key = { runId, namespace: namespace(id) };
|
|
103
|
+
const saved = this.access.getRunContext(key);
|
|
104
|
+
if (saved && !saved.hookCompleted && !saved.conflict && saved.result &&
|
|
105
|
+
saved.ctx.sessionKey === ctx.sessionKey && saved.ctx.sessionId === ctx.sessionId && saved.ctx.agentId === ctx.agentId) {
|
|
106
|
+
await record(saved);
|
|
107
|
+
this.access.setRunContext({ ...key, value: { ...saved, hookCompleted: true } });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
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.6
|
|
5
|
+
"version": "2.2.6",
|
|
6
6
|
"commandAliases": ["memory-atmem"],
|
|
7
7
|
"activation": {
|
|
8
8
|
"onStartup": true,
|
|
@@ -150,7 +150,21 @@
|
|
|
150
150
|
"requireOwner": {
|
|
151
151
|
"type": "boolean",
|
|
152
152
|
"default": true,
|
|
153
|
-
"description": "
|
|
153
|
+
"description": "Require authenticated owner metadata (default). False requires the explicit isolated localOperator mapping; it is not an ownership bypass."
|
|
154
|
+
},
|
|
155
|
+
"localOperator": {
|
|
156
|
+
"type": "object",
|
|
157
|
+
"additionalProperties": false,
|
|
158
|
+
"required": ["isolated", "agentId", "workspaceDir", "sessionKey", "sessionId"],
|
|
159
|
+
"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.",
|
|
160
|
+
"properties": {
|
|
161
|
+
"isolated": { "type": "boolean", "const": true, "description": "Operator assertion that this process is isolated from shared channels and other users." },
|
|
162
|
+
"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." },
|
|
163
|
+
"agentId": { "type": "string", "minLength": 1 },
|
|
164
|
+
"workspaceDir": { "type": "string", "minLength": 1 },
|
|
165
|
+
"sessionKey": { "type": "string", "minLength": 1 },
|
|
166
|
+
"sessionId": { "type": "string", "minLength": 1 }
|
|
167
|
+
}
|
|
154
168
|
}
|
|
155
169
|
}
|
|
156
170
|
}
|
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.6",
|
|
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/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"
|