openclaw-memory-atmem 2.2.7-beta.1 → 2.3.3
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 +9 -5
- package/dist/index.js +252 -29
- package/dist/src/execution-spool.js +189 -0
- package/dist/src/rpc-client.js +1 -1
- package/openclaw.plugin.json +5 -5
- package/package.json +2 -2
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.
|
|
8
|
+
python -m pip install --upgrade atmem==2.3.3
|
|
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.
|
|
25
|
+
Bridge `2.3.3` includes durable execution-event spooling in addition to exact task-context delivery and the shared
|
|
26
26
|
calibrated retrieval decision, and delegated context correlation. When the host
|
|
27
27
|
supplies `taskId` in hook context, the bridge requests only that governed task,
|
|
28
28
|
checks its byte digest, contributes it separately from recalled memory, and
|
|
@@ -32,12 +32,16 @@ bridge does not claim it can block OpenClaw execution.
|
|
|
32
32
|
|
|
33
33
|
In shadow mode the bridge observes native-memory changes without injecting AtMem context. In active mode it exposes compatible memory search/get tools, model-semantic capture, bounded recall and native-path protection. `atmem control restore` restores the saved OpenClaw configuration and native memory.
|
|
34
34
|
|
|
35
|
-
The bridge also supplies Agent Black Box hooks.
|
|
35
|
+
The bridge also supplies Agent Black Box hooks. With default full capture it sends
|
|
36
|
+
exact host-observed prompts, model boundaries, tool arguments/results and supported
|
|
37
|
+
multimodal parts into AtMem's encrypted evidence vault, alongside digest-bound
|
|
38
|
+
lifecycle metadata. Metadata-only and recorder-off modes remain explicit operator
|
|
39
|
+
choices. See the [Agent Black Box guide](../../docs/agent-blackbox.md) for the exact boundary.
|
|
36
40
|
|
|
37
41
|
See the repository [OpenClaw setup](../../docs/openclaw-setup.md) and [control-plane guarantees](../../docs/control-plane.md).
|
|
38
42
|
|
|
39
43
|
|
|
40
|
-
### Delegated local CLI identity and tool evidence in 2.2.
|
|
44
|
+
### Delegated local CLI identity and tool evidence in 2.2.6
|
|
41
45
|
|
|
42
46
|
The default delegated user mapping requires `senderIsOwner: true`. Setting
|
|
43
47
|
`requireOwner: false` alone no longer bypasses identity checks. A dedicated local
|
|
@@ -55,7 +59,7 @@ completion hooks are absent. Only matched requests close, with content-free
|
|
|
55
59
|
bounded caching and duplicate suppression. Fully observed
|
|
56
60
|
terminal tool errors close evidence as `completed_with_tool_errors`, not success.
|
|
57
61
|
|
|
58
|
-
|
|
62
|
+
Local development follow-up: automatic recall now requires calibrated direct
|
|
59
63
|
query support by default (`recall.requireDirectSupport: true`). Persona is an
|
|
60
64
|
explicit opt-in (`persona.enabled: true`); setup defaults it off to avoid adding
|
|
61
65
|
unrelated personal facts to every request. When enabled, persona records are
|
package/dist/index.js
CHANGED
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
import os from "node:os";
|
|
21
21
|
import path from "node:path";
|
|
22
22
|
import { createHash, randomUUID } from "node:crypto";
|
|
23
|
-
import { createReadStream } from "node:fs";
|
|
24
23
|
import { lstat, mkdir, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
25
24
|
import { spawnSync } from "node:child_process";
|
|
26
25
|
import { ToolObservations, observationContext } from "./src/tool-observations.js";
|
|
27
26
|
import { progressCardComparison } from "./src/tool-result-comparison.js";
|
|
28
27
|
import { toolErrorReason } from "./src/tool-errors.js";
|
|
28
|
+
import { ExecutionSpool } from "./src/execution-spool.js";
|
|
29
29
|
import { delegatedIdentity, isolatedCliProcess } from "./src/delegated-identity.js";
|
|
30
30
|
import { AtmemClient } from "./src/rpc-client.js";
|
|
31
31
|
import { runSetup } from "./src/setup.js";
|
|
@@ -47,7 +47,10 @@ const PROMPT_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
|
47
47
|
const MAX_PENDING_PROMPTS = 256;
|
|
48
48
|
function parseConfig(raw) {
|
|
49
49
|
const cfg = (raw ?? {});
|
|
50
|
-
const
|
|
50
|
+
const selectedHome = process.env.ATMEM_HOME
|
|
51
|
+
? expandHome(process.env.ATMEM_HOME)
|
|
52
|
+
: path.join(os.homedir(), ".atmem");
|
|
53
|
+
const dbPath = expandHome(String(cfg.dbPath ?? path.join(selectedHome, "memory", "memories.db")));
|
|
51
54
|
const subject = String(cfg.subject ?? "default");
|
|
52
55
|
const stringMap = (value) => value && typeof value === "object" && !Array.isArray(value)
|
|
53
56
|
? Object.fromEntries(Object.entries(value)
|
|
@@ -64,8 +67,8 @@ function parseConfig(raw) {
|
|
|
64
67
|
].filter(Boolean))];
|
|
65
68
|
const controlPlane = {
|
|
66
69
|
enabled: cfg.controlPlane?.enabled === true,
|
|
67
|
-
statePath: expandHome(String(cfg.controlPlane?.statePath ?? "
|
|
68
|
-
blackboxEnabled: cfg.controlPlane
|
|
70
|
+
statePath: expandHome(String(cfg.controlPlane?.statePath ?? path.join(selectedHome, "config", "control-plane.json"))),
|
|
71
|
+
blackboxEnabled: Boolean(cfg.controlPlane) && cfg.controlPlane?.blackboxEnabled !== false,
|
|
69
72
|
};
|
|
70
73
|
const delegatedContext = {
|
|
71
74
|
userId: String(cfg.delegatedContext?.userId ?? "").trim(),
|
|
@@ -229,7 +232,7 @@ async function writeAttachmentBinding(root, key, items) {
|
|
|
229
232
|
const payload = {
|
|
230
233
|
format: "atmem-openclaw-attachment-binding-v1",
|
|
231
234
|
keySha256: createHash("sha256").update(key).digest("hex"),
|
|
232
|
-
items,
|
|
235
|
+
items: items.map(({ dataBase64: _discarded, ...item }) => item),
|
|
233
236
|
updatedAt: Date.now(),
|
|
234
237
|
};
|
|
235
238
|
await writeFile(temporary, JSON.stringify(payload), { encoding: "utf8", mode: 0o600 });
|
|
@@ -259,17 +262,57 @@ async function readAttachmentBinding(root, key) {
|
|
|
259
262
|
}
|
|
260
263
|
function modalityFromMime(mimeType) {
|
|
261
264
|
const mime = mimeType.toLowerCase();
|
|
262
|
-
if (mime.startsWith("image/"))
|
|
265
|
+
if (mime === "image" || mime.startsWith("image/"))
|
|
263
266
|
return "image";
|
|
264
|
-
if (mime.startsWith("audio/"))
|
|
267
|
+
if (mime === "audio" || mime.startsWith("audio/"))
|
|
265
268
|
return "audio";
|
|
266
|
-
if (mime.startsWith("video/"))
|
|
269
|
+
if (mime === "video" || mime.startsWith("video/"))
|
|
267
270
|
return "video";
|
|
268
|
-
if (mime === "application/pdf" || mime.startsWith("text/") || mime.includes("document")) {
|
|
271
|
+
if (mime === "document" || mime === "application/pdf" || mime.startsWith("text/") || mime.includes("document")) {
|
|
269
272
|
return "document";
|
|
270
273
|
}
|
|
271
274
|
return null;
|
|
272
275
|
}
|
|
276
|
+
function inlineMediaParts(value) {
|
|
277
|
+
const parts = [];
|
|
278
|
+
const seen = new Set();
|
|
279
|
+
const visit = (item) => {
|
|
280
|
+
if (typeof item === "string") {
|
|
281
|
+
const match = /^data:([^;,]+);base64,([A-Za-z0-9+/]+={0,2})$/.exec(item);
|
|
282
|
+
if (!match)
|
|
283
|
+
return;
|
|
284
|
+
const mimeType = match[1].toLowerCase();
|
|
285
|
+
const modality = modalityFromMime(mimeType);
|
|
286
|
+
if (!modality)
|
|
287
|
+
return;
|
|
288
|
+
const bytes = Buffer.from(match[2], "base64");
|
|
289
|
+
if (bytes.length > 100 * 1024 * 1024 || bytes.toString("base64") !== match[2])
|
|
290
|
+
return;
|
|
291
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
292
|
+
if (seen.has(digest))
|
|
293
|
+
return;
|
|
294
|
+
seen.add(digest);
|
|
295
|
+
parts.push({
|
|
296
|
+
type: modality,
|
|
297
|
+
mime_type: mimeType,
|
|
298
|
+
data_base64: match[2],
|
|
299
|
+
representation: "model_input",
|
|
300
|
+
});
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (Array.isArray(item)) {
|
|
304
|
+
for (const child of item)
|
|
305
|
+
visit(child);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (item && typeof item === "object") {
|
|
309
|
+
for (const child of Object.values(item))
|
|
310
|
+
visit(child);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
visit(value);
|
|
314
|
+
return parts;
|
|
315
|
+
}
|
|
273
316
|
function withinPath(candidate, root) {
|
|
274
317
|
return candidate === root || candidate.startsWith(root + path.sep);
|
|
275
318
|
}
|
|
@@ -282,12 +325,12 @@ async function hashInboundAttachment(filePath, mimeType) {
|
|
|
282
325
|
throw new Error("OpenClaw attachment path is outside the managed media directory");
|
|
283
326
|
}
|
|
284
327
|
const before = await lstat(resolved);
|
|
285
|
-
|
|
328
|
+
// The 2.3 consumer-hardware profile is measured and bounded at 100 MiB per artifact.
|
|
329
|
+
if (!before.isFile() || before.size > 100 * 1024 * 1024) {
|
|
286
330
|
throw new Error("OpenClaw attachment is not a bounded regular file");
|
|
287
331
|
}
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
hash.update(chunk);
|
|
332
|
+
const exactBytes = await readFile(resolved);
|
|
333
|
+
const hash = createHash("sha256").update(exactBytes);
|
|
291
334
|
const after = await lstat(resolved);
|
|
292
335
|
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
|
|
293
336
|
throw new Error("OpenClaw attachment changed while its digest was computed");
|
|
@@ -303,6 +346,53 @@ async function hashInboundAttachment(filePath, mimeType) {
|
|
|
303
346
|
mimeType,
|
|
304
347
|
bytes: before.size,
|
|
305
348
|
capturedAt: Date.now(),
|
|
349
|
+
dataBase64: exactBytes.toString("base64"),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function managedMediaPath(reference) {
|
|
353
|
+
const match = /^media:\/\/inbound\/([A-Za-z0-9][A-Za-z0-9._-]{0,255})$/.exec(reference);
|
|
354
|
+
if (!match || match[1] === "." || match[1] === "..")
|
|
355
|
+
return null;
|
|
356
|
+
const root = process.env.ATMEM_OPENCLAW_MEDIA_ROOT
|
|
357
|
+
? expandHome(process.env.ATMEM_OPENCLAW_MEDIA_ROOT)
|
|
358
|
+
: path.join(os.homedir(), ".openclaw", "media");
|
|
359
|
+
return path.join(root, "inbound", match[1]);
|
|
360
|
+
}
|
|
361
|
+
function attachmentFields(value) {
|
|
362
|
+
const selected = [];
|
|
363
|
+
const seen = new Set();
|
|
364
|
+
const add = (reference, mimeType) => {
|
|
365
|
+
if (typeof reference !== "string" || !reference)
|
|
366
|
+
return;
|
|
367
|
+
const resolved = reference.startsWith("media://") ? managedMediaPath(reference) : reference;
|
|
368
|
+
if (!resolved || seen.has(resolved))
|
|
369
|
+
return;
|
|
370
|
+
seen.add(resolved);
|
|
371
|
+
selected.push({
|
|
372
|
+
path: resolved,
|
|
373
|
+
type: typeof mimeType === "string" && mimeType
|
|
374
|
+
? mimeType
|
|
375
|
+
: "application/octet-stream",
|
|
376
|
+
});
|
|
377
|
+
};
|
|
378
|
+
const visit = (item) => {
|
|
379
|
+
if (Array.isArray(item)) {
|
|
380
|
+
for (const child of item)
|
|
381
|
+
visit(child);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (!item || typeof item !== "object")
|
|
385
|
+
return;
|
|
386
|
+
const row = item;
|
|
387
|
+
add(row.path ?? row.url, row.contentType ?? row.mimeType ?? row.kind);
|
|
388
|
+
if (row.__openclaw && typeof row.__openclaw === "object") {
|
|
389
|
+
visit(row.__openclaw.media);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
visit(value);
|
|
393
|
+
return {
|
|
394
|
+
paths: selected.map((item) => item.path),
|
|
395
|
+
types: selected.map((item) => item.type),
|
|
306
396
|
};
|
|
307
397
|
}
|
|
308
398
|
function recordIdFromPath(value) {
|
|
@@ -314,7 +404,16 @@ function recordIdFromPath(value) {
|
|
|
314
404
|
}
|
|
315
405
|
function register(api) {
|
|
316
406
|
const cfg = parseConfig(api.pluginConfig);
|
|
317
|
-
const
|
|
407
|
+
const stateDirectory = path.dirname(cfg.controlPlane.statePath);
|
|
408
|
+
const selectedHome = process.env.ATMEM_HOME
|
|
409
|
+
? expandHome(process.env.ATMEM_HOME)
|
|
410
|
+
: path.basename(stateDirectory) === "config"
|
|
411
|
+
? path.dirname(stateDirectory)
|
|
412
|
+
: stateDirectory;
|
|
413
|
+
const runtimeRoot = path.basename(stateDirectory) === "config" || process.env.ATMEM_HOME
|
|
414
|
+
? path.join(selectedHome, "runtime")
|
|
415
|
+
: selectedHome;
|
|
416
|
+
const attachmentBindingRoot = path.join(runtimeRoot, "openclaw-attachment-bindings");
|
|
318
417
|
const client = new AtmemClient({
|
|
319
418
|
command: cfg.command,
|
|
320
419
|
args: cfg.commandArgs,
|
|
@@ -329,6 +428,7 @@ function register(api) {
|
|
|
329
428
|
log: (message) => api.logger.debug?.(`${TAG} ${message}`),
|
|
330
429
|
logError: (message) => api.logger.warn(`${TAG} ${message}`),
|
|
331
430
|
});
|
|
431
|
+
const executionSpool = new ExecutionSpool(path.join(runtimeRoot, "openclaw-execution-spool.json"));
|
|
332
432
|
// Let long-lived hosts close the stdio child during lifecycle shutdown.
|
|
333
433
|
// The client's bounded idle shutdown also covers one-shot local runners.
|
|
334
434
|
api.registerService?.({
|
|
@@ -437,13 +537,27 @@ function register(api) {
|
|
|
437
537
|
? normalized.slice("openclaw".length)
|
|
438
538
|
: normalized;
|
|
439
539
|
};
|
|
540
|
+
const governedMemoryTools = new Set([
|
|
541
|
+
"memory_search",
|
|
542
|
+
"memory_get",
|
|
543
|
+
"memory_remember",
|
|
544
|
+
"atmem_observe",
|
|
545
|
+
"atmem_forget_artifact",
|
|
546
|
+
"atmem_trace",
|
|
547
|
+
"atmem_tasks",
|
|
548
|
+
"atmem_task_progress",
|
|
549
|
+
]);
|
|
440
550
|
const recordBlackbox = async (eventType, eventRunId, ctx, payload, toolCallId, correlation = {}) => {
|
|
441
551
|
if (!cfg.controlPlane.blackboxEnabled)
|
|
442
552
|
return;
|
|
443
553
|
try {
|
|
444
|
-
await
|
|
554
|
+
await executionSpool.enqueue({
|
|
445
555
|
event_type: eventType,
|
|
446
556
|
run_id: flightRunId(eventRunId, ctx),
|
|
557
|
+
execution_id: ctx.executionId,
|
|
558
|
+
parent_execution_id: ctx.parentExecutionId,
|
|
559
|
+
attempt_id: ctx.attemptId,
|
|
560
|
+
retry_of_attempt_id: ctx.retryOfAttemptId,
|
|
447
561
|
agent_id: agentIdFor(ctx),
|
|
448
562
|
workspace_id: workspaceIdFor(ctx),
|
|
449
563
|
subject_id: Object.keys(cfg.agentSubjects).length ? subjectFor(ctx) : undefined,
|
|
@@ -455,7 +569,8 @@ function register(api) {
|
|
|
455
569
|
context_receipt_id: correlation.contextReceiptId,
|
|
456
570
|
outcome_id: correlation.outcomeId,
|
|
457
571
|
payload,
|
|
458
|
-
}
|
|
572
|
+
});
|
|
573
|
+
await executionSpool.flush((event) => blackboxClient.callTool("control_record_blackbox_event", event, cfg.recall.timeoutMs));
|
|
459
574
|
}
|
|
460
575
|
catch (error) {
|
|
461
576
|
api.logger.warn(`${TAG} blackbox event ${eventType} was not recorded: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -510,6 +625,9 @@ function register(api) {
|
|
|
510
625
|
prompt_sha256: promptSha256,
|
|
511
626
|
prompt_chars: prompt.length,
|
|
512
627
|
images_count: imagesCount,
|
|
628
|
+
_atmem_evidence: {
|
|
629
|
+
parts: [{ type: "text", text: prompt }],
|
|
630
|
+
},
|
|
513
631
|
});
|
|
514
632
|
try {
|
|
515
633
|
await stageInbound(prompt, ctx);
|
|
@@ -525,7 +643,7 @@ function register(api) {
|
|
|
525
643
|
});
|
|
526
644
|
await pending;
|
|
527
645
|
};
|
|
528
|
-
const bindInboundAttachments = async (keys, paths, types) => {
|
|
646
|
+
const bindInboundAttachments = async (keys, paths, types, ctx) => {
|
|
529
647
|
const generation = ++nextAttachmentGeneration;
|
|
530
648
|
for (const key of keys) {
|
|
531
649
|
inboundAttachments.delete(key);
|
|
@@ -535,6 +653,20 @@ function register(api) {
|
|
|
535
653
|
if (!paths.length)
|
|
536
654
|
return;
|
|
537
655
|
const items = await Promise.all(paths.map((filePath, index) => hashInboundAttachment(filePath, types[index] ?? types[0] ?? "application/octet-stream")));
|
|
656
|
+
for (const item of items) {
|
|
657
|
+
await recordBlackbox("turn.attachment", undefined, ctx, {
|
|
658
|
+
result_sha256: item.mediaSha256,
|
|
659
|
+
response_stream_bytes: item.bytes,
|
|
660
|
+
_atmem_evidence: {
|
|
661
|
+
parts: [{
|
|
662
|
+
type: item.modality,
|
|
663
|
+
mime_type: item.mimeType,
|
|
664
|
+
data_base64: item.dataBase64,
|
|
665
|
+
host_reference: item.hostReference,
|
|
666
|
+
}],
|
|
667
|
+
},
|
|
668
|
+
});
|
|
669
|
+
}
|
|
538
670
|
for (const key of keys) {
|
|
539
671
|
if (inboundAttachmentGeneration.get(key) === generation) {
|
|
540
672
|
inboundAttachments.set(key, { items, ts: Date.now() });
|
|
@@ -560,7 +692,8 @@ function register(api) {
|
|
|
560
692
|
: typeof rawType === "string"
|
|
561
693
|
? [rawType]
|
|
562
694
|
: [];
|
|
563
|
-
|
|
695
|
+
const current = attachmentFields(message);
|
|
696
|
+
return current.paths.length ? current : { paths, types };
|
|
564
697
|
};
|
|
565
698
|
const writtenMessageBindingKeys = (message, ctx) => {
|
|
566
699
|
const idempotencyKey = typeof message.idempotencyKey === "string"
|
|
@@ -577,23 +710,57 @@ function register(api) {
|
|
|
577
710
|
};
|
|
578
711
|
api.on("message_received", async (event, ctx) => {
|
|
579
712
|
const metadata = event.metadata ?? {};
|
|
580
|
-
|
|
713
|
+
// OpenClaw 2026.9 exposes stable media facts on the event. The metadata
|
|
714
|
+
// aliases are deprecated and can be absent even though the model receives
|
|
715
|
+
// the attachment, so use them only as a compatibility fallback.
|
|
716
|
+
const media = Array.isArray(event.media) ? event.media : [];
|
|
717
|
+
const currentFields = attachmentFields(media);
|
|
718
|
+
const current = currentFields.paths.map((path, index) => ({
|
|
719
|
+
path,
|
|
720
|
+
type: currentFields.types[index] ?? "application/octet-stream",
|
|
721
|
+
}));
|
|
722
|
+
const originalMedia = Array.isArray(event.originalMedia) ? event.originalMedia : [];
|
|
723
|
+
const originalFields = attachmentFields(originalMedia);
|
|
724
|
+
const original = originalFields.paths.map((path, index) => ({
|
|
725
|
+
path,
|
|
726
|
+
type: originalFields.types[index] ?? "application/octet-stream",
|
|
727
|
+
}));
|
|
728
|
+
const legacyPaths = Array.isArray(metadata.mediaPaths)
|
|
581
729
|
? metadata.mediaPaths.filter((value) => typeof value === "string")
|
|
582
|
-
: typeof metadata.mediaPath === "string"
|
|
583
|
-
|
|
584
|
-
: [];
|
|
585
|
-
const types = Array.isArray(metadata.mediaTypes)
|
|
730
|
+
: typeof metadata.mediaPath === "string" ? [metadata.mediaPath] : [];
|
|
731
|
+
const legacyTypes = Array.isArray(metadata.mediaTypes)
|
|
586
732
|
? metadata.mediaTypes.filter((value) => typeof value === "string")
|
|
587
|
-
: typeof metadata.mediaType === "string"
|
|
588
|
-
|
|
589
|
-
|
|
733
|
+
: typeof metadata.mediaType === "string" ? [metadata.mediaType] : [];
|
|
734
|
+
const legacyOriginalPaths = Array.isArray(metadata.originalMediaPaths)
|
|
735
|
+
? metadata.originalMediaPaths.filter((value) => typeof value === "string")
|
|
736
|
+
: typeof metadata.originalMediaPath === "string" ? [metadata.originalMediaPath] : [];
|
|
737
|
+
const legacyOriginalTypes = Array.isArray(metadata.originalMediaTypes)
|
|
738
|
+
? metadata.originalMediaTypes.filter((value) => typeof value === "string")
|
|
739
|
+
: typeof metadata.originalMediaType === "string" ? [metadata.originalMediaType] : [];
|
|
740
|
+
const selected = current.length
|
|
741
|
+
? current
|
|
742
|
+
: original.length
|
|
743
|
+
? original
|
|
744
|
+
: legacyPaths.map((path, index) => ({
|
|
745
|
+
path,
|
|
746
|
+
type: legacyTypes[index] ?? legacyTypes[0] ?? "application/octet-stream",
|
|
747
|
+
})).concat(legacyOriginalPaths.map((path, index) => ({
|
|
748
|
+
path,
|
|
749
|
+
type: legacyOriginalTypes[index] ?? legacyOriginalTypes[0] ?? "application/octet-stream",
|
|
750
|
+
})));
|
|
751
|
+
const paths = selected.map((fact) => fact.path);
|
|
752
|
+
const types = selected.map((fact) => fact.type);
|
|
590
753
|
const keys = [...new Set([
|
|
591
754
|
event.sessionKey,
|
|
592
755
|
event.runId,
|
|
593
756
|
...contextIds(ctx),
|
|
594
757
|
].filter((value) => Boolean(value)))];
|
|
595
758
|
try {
|
|
596
|
-
|
|
759
|
+
// A staging-pending event has not supplied a safe local path yet. Do not
|
|
760
|
+
// erase an earlier binding for the same turn while OpenClaw is staging it.
|
|
761
|
+
if (!paths.length && (event.mediaStagingPending || metadata.mediaStagingPending === true))
|
|
762
|
+
return;
|
|
763
|
+
await bindInboundAttachments(keys, paths, types, ctx);
|
|
597
764
|
}
|
|
598
765
|
catch (error) {
|
|
599
766
|
api.logger.warn(`${TAG} inbound attachment provenance unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -636,12 +803,21 @@ function register(api) {
|
|
|
636
803
|
context_location: deliveredLocation,
|
|
637
804
|
success: delivered,
|
|
638
805
|
reason: delivered ? undefined : `expected one exact delegated segment; observed ${occurrences}`,
|
|
806
|
+
_atmem_evidence: {
|
|
807
|
+
context: exact,
|
|
808
|
+
delivered_location: deliveredLocation,
|
|
809
|
+
},
|
|
639
810
|
}, undefined, { contextReceiptId: pending.contextReceiptId });
|
|
640
811
|
if (delivered && pending.exposureId) {
|
|
641
812
|
await callFor(ctx, "control_exposure_shown", { exposure_id: pending.exposureId }, cfg.recall.timeoutMs);
|
|
642
813
|
}
|
|
643
814
|
cachePendingPrompt(sessionKey, { ...pending, delegatedContext: undefined });
|
|
644
815
|
}
|
|
816
|
+
const modelMediaParts = inlineMediaParts([
|
|
817
|
+
event.historyMessages,
|
|
818
|
+
event.messages,
|
|
819
|
+
event.images,
|
|
820
|
+
]);
|
|
645
821
|
await recordBlackbox("model.input", event.runId, ctx, {
|
|
646
822
|
provider: event.provider,
|
|
647
823
|
model: event.model,
|
|
@@ -653,6 +829,13 @@ function register(api) {
|
|
|
653
829
|
history_count: Array.isArray(event.historyMessages) ? event.historyMessages.length : 0,
|
|
654
830
|
images_count: event.imagesCount ?? 0,
|
|
655
831
|
tools_count: Array.isArray(event.tools) ? event.tools.length : 0,
|
|
832
|
+
_atmem_evidence: {
|
|
833
|
+
prompt: event.prompt ?? "",
|
|
834
|
+
system_prompt: event.systemPrompt ?? "",
|
|
835
|
+
history_messages: event.historyMessages ?? [],
|
|
836
|
+
tools: event.tools ?? [],
|
|
837
|
+
parts: modelMediaParts,
|
|
838
|
+
},
|
|
656
839
|
});
|
|
657
840
|
});
|
|
658
841
|
api.on("llm_output", async (event, ctx) => {
|
|
@@ -683,6 +866,10 @@ function register(api) {
|
|
|
683
866
|
usage: event.usage ?? {},
|
|
684
867
|
reasoning_effort: event.reasoningEffort,
|
|
685
868
|
fast_mode: event.fastMode,
|
|
869
|
+
_atmem_evidence: {
|
|
870
|
+
assistant_texts: responses,
|
|
871
|
+
usage: event.usage ?? {},
|
|
872
|
+
},
|
|
686
873
|
});
|
|
687
874
|
});
|
|
688
875
|
api.registerCli?.(({ program }) => {
|
|
@@ -1082,6 +1269,10 @@ function register(api) {
|
|
|
1082
1269
|
context_selection_profile: cfg.recall.requireDirectSupport ? "direct-support-v1" : "rank-threshold-v1",
|
|
1083
1270
|
mode: "direct",
|
|
1084
1271
|
context_location: contextLocation,
|
|
1272
|
+
_atmem_evidence: {
|
|
1273
|
+
context: memoryContext,
|
|
1274
|
+
envelope: result,
|
|
1275
|
+
},
|
|
1085
1276
|
}, undefined, {
|
|
1086
1277
|
retrievalId: current?.retrievalId,
|
|
1087
1278
|
contextEventId: current?.contextEventId ?? personaContextEventId,
|
|
@@ -1119,7 +1310,16 @@ function register(api) {
|
|
|
1119
1310
|
derived_path_sha256: Array.isArray(event.derivedPaths)
|
|
1120
1311
|
? event.derivedPaths.map((value) => digestText(String(value)))
|
|
1121
1312
|
: [],
|
|
1313
|
+
_atmem_evidence: {
|
|
1314
|
+
params: event.params ?? {},
|
|
1315
|
+
derived_paths: event.derivedPaths ?? [],
|
|
1316
|
+
},
|
|
1122
1317
|
}, event.toolCallId ?? ctx.toolCallId);
|
|
1318
|
+
// These virtual tools operate through AtMem. Their arguments legitimately
|
|
1319
|
+
// contain values such as `corpus: "memory"`; they are not filesystem
|
|
1320
|
+
// access to OpenClaw's frozen MEMORY.md or memory/* tree.
|
|
1321
|
+
if (governedMemoryTools.has(canonicalToolName(event.toolName)))
|
|
1322
|
+
return;
|
|
1123
1323
|
if (!cfg.takeoverActive || !cfg.nativeWorkspaces.some((workspace) => touchesNativeMemory(event, workspace)))
|
|
1124
1324
|
return;
|
|
1125
1325
|
const reason = "AtMem takeover blocked access to OpenClaw's frozen native memory " +
|
|
@@ -1149,6 +1349,11 @@ function register(api) {
|
|
|
1149
1349
|
error_sha256: event.error ? digestText(event.error) : undefined,
|
|
1150
1350
|
result_present: event.result !== undefined,
|
|
1151
1351
|
duration_ms: event.durationMs ?? 0,
|
|
1352
|
+
_atmem_evidence: {
|
|
1353
|
+
params: event.params ?? {},
|
|
1354
|
+
result: event.result ?? null,
|
|
1355
|
+
error: event.error ?? null,
|
|
1356
|
+
},
|
|
1152
1357
|
}, event.toolCallId ?? ctx.toolCallId);
|
|
1153
1358
|
});
|
|
1154
1359
|
// ---- auto-capture: user turn through the pipeline, assistant as digest -
|
|
@@ -1165,6 +1370,18 @@ function register(api) {
|
|
|
1165
1370
|
});
|
|
1166
1371
|
const sessionKey = sessionKeyFor(ctx);
|
|
1167
1372
|
const observedTurnInputKey = turnInputKey(ctx);
|
|
1373
|
+
// Current OpenClaw WebChat persists exact attachment provenance on the
|
|
1374
|
+
// user transcript as __openclaw.media[].url = media://inbound/<id>. Some
|
|
1375
|
+
// earlier hooks omit it, so agent_end is the durable late-capture boundary.
|
|
1376
|
+
const completedTurnMedia = attachmentFields(event.messages);
|
|
1377
|
+
if (completedTurnMedia.paths.length) {
|
|
1378
|
+
try {
|
|
1379
|
+
await bindInboundAttachments([...new Set([sessionKey, event.runId, ...contextIds(ctx)].filter((value) => Boolean(value)))], completedTurnMedia.paths, completedTurnMedia.types, ctx);
|
|
1380
|
+
}
|
|
1381
|
+
catch (error) {
|
|
1382
|
+
api.logger.warn(`${TAG} completed-turn attachment capture unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1168
1385
|
const cached = pendingPrompts.get(sessionKey);
|
|
1169
1386
|
deletePendingPrompt(sessionKey);
|
|
1170
1387
|
const userText = cached?.text?.replace(INJECT_RE, "").trim();
|
|
@@ -1268,6 +1485,10 @@ function register(api) {
|
|
|
1268
1485
|
turn_messages_sha256: turnMessagesSha256,
|
|
1269
1486
|
digest_profile: "atmem-turn-messages-canonical-json-v1",
|
|
1270
1487
|
messages_count: Array.isArray(event.messages) ? event.messages.length : 0,
|
|
1488
|
+
_atmem_evidence: {
|
|
1489
|
+
messages: event.messages ?? [],
|
|
1490
|
+
error: event.error ?? null,
|
|
1491
|
+
},
|
|
1271
1492
|
}, undefined, {
|
|
1272
1493
|
retrievalId: cached?.retrievalId,
|
|
1273
1494
|
contextEventId: cached?.contextEventId,
|
|
@@ -1288,8 +1509,10 @@ function register(api) {
|
|
|
1288
1509
|
// by scanning the media directory or scraping its name from user text.
|
|
1289
1510
|
const attachmentFields = writtenMessageAttachmentFields(message);
|
|
1290
1511
|
const attachmentKeys = writtenMessageBindingKeys(message, ctx);
|
|
1291
|
-
|
|
1292
|
-
|
|
1512
|
+
// A sparse transcript message is normal in current OpenClaw. It must not
|
|
1513
|
+
// erase the binding already established by message_received.
|
|
1514
|
+
if (attachmentKeys.length && attachmentFields.paths.length) {
|
|
1515
|
+
void bindInboundAttachments(attachmentKeys, attachmentFields.paths, attachmentFields.types, ctx).catch((error) => {
|
|
1293
1516
|
api.logger.warn(`${TAG} persisted attachment provenance unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
1294
1517
|
});
|
|
1295
1518
|
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const SPOOL_AAD = Buffer.from("atmem-openclaw-encrypted-execution-spool-v2", "utf8");
|
|
5
|
+
/** A bounded, atomic local spool. Removal requires a durable terminal receipt. */
|
|
6
|
+
export class ExecutionSpool {
|
|
7
|
+
filePath;
|
|
8
|
+
maxPending;
|
|
9
|
+
maxAgeMs;
|
|
10
|
+
keyPath;
|
|
11
|
+
state = null;
|
|
12
|
+
serial = Promise.resolve();
|
|
13
|
+
constructor(filePath, maxPending = 2048, maxAgeMs = 7 * 24 * 60 * 60 * 1000, keyPath = path.resolve(path.dirname(filePath), "..", ".atmem-evidence-keys", "openclaw-spool.key")) {
|
|
14
|
+
this.filePath = filePath;
|
|
15
|
+
this.maxPending = maxPending;
|
|
16
|
+
this.maxAgeMs = maxAgeMs;
|
|
17
|
+
this.keyPath = keyPath;
|
|
18
|
+
if (maxPending < 2)
|
|
19
|
+
throw new Error("execution spool capacity must be at least 2");
|
|
20
|
+
}
|
|
21
|
+
enqueue(value) {
|
|
22
|
+
return this.exclusive(async () => {
|
|
23
|
+
const state = await this.load();
|
|
24
|
+
this.expirePending(state, Date.now() - this.maxAgeMs);
|
|
25
|
+
const envelope = {
|
|
26
|
+
...value,
|
|
27
|
+
event_id: randomUUID(),
|
|
28
|
+
producer_instance_id: state.producerInstanceId,
|
|
29
|
+
producer_epoch: state.producerEpoch,
|
|
30
|
+
producer_sequence: state.nextSequence++,
|
|
31
|
+
event_time: new Date().toISOString(),
|
|
32
|
+
};
|
|
33
|
+
state.pending.push(envelope);
|
|
34
|
+
if (state.pending.length > this.maxPending) {
|
|
35
|
+
const dropped = state.pending.length - this.maxPending + 1;
|
|
36
|
+
const removed = state.pending.splice(0, dropped);
|
|
37
|
+
state.pending.unshift({
|
|
38
|
+
event_type: "capture.gap",
|
|
39
|
+
run_id: String(envelope.run_id ?? "capture-spool"),
|
|
40
|
+
payload: {
|
|
41
|
+
reason: "spool_capacity_exceeded",
|
|
42
|
+
dropped_count: removed.length,
|
|
43
|
+
first_sequence: removed[0]?.producer_sequence,
|
|
44
|
+
last_sequence: removed.at(-1)?.producer_sequence,
|
|
45
|
+
},
|
|
46
|
+
event_id: randomUUID(),
|
|
47
|
+
producer_instance_id: state.producerInstanceId,
|
|
48
|
+
producer_epoch: state.producerEpoch,
|
|
49
|
+
producer_sequence: state.nextSequence++,
|
|
50
|
+
event_time: new Date().toISOString(),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
await this.persist(state);
|
|
54
|
+
return envelope;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
flush(send) {
|
|
58
|
+
return this.exclusive(async () => {
|
|
59
|
+
const state = await this.load();
|
|
60
|
+
let acknowledged = 0;
|
|
61
|
+
while (state.pending.length) {
|
|
62
|
+
const result = await send(state.pending[0]);
|
|
63
|
+
const receipt = result;
|
|
64
|
+
const accepted = receipt?.durably_accepted === true;
|
|
65
|
+
const classifiedConflict = receipt?.decision === "conflict" &&
|
|
66
|
+
receipt?.durably_classified === true;
|
|
67
|
+
if (!accepted && !classifiedConflict) {
|
|
68
|
+
throw new Error("AtMem did not confirm a durable terminal execution-event decision");
|
|
69
|
+
}
|
|
70
|
+
state.pending.shift();
|
|
71
|
+
acknowledged += 1;
|
|
72
|
+
await this.persist(state);
|
|
73
|
+
}
|
|
74
|
+
return acknowledged;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async snapshot() {
|
|
78
|
+
return this.exclusive(async () => ({ ...(await this.load()), pending: [...(await this.load()).pending] }));
|
|
79
|
+
}
|
|
80
|
+
exclusive(action) {
|
|
81
|
+
const next = this.serial.then(action, action);
|
|
82
|
+
this.serial = next.then(() => undefined, () => undefined);
|
|
83
|
+
return next;
|
|
84
|
+
}
|
|
85
|
+
async load() {
|
|
86
|
+
if (this.state)
|
|
87
|
+
return this.state;
|
|
88
|
+
try {
|
|
89
|
+
const wrapper = JSON.parse(await readFile(this.filePath, "utf8"));
|
|
90
|
+
if (wrapper.format !== "atmem-openclaw-encrypted-execution-spool-v2") {
|
|
91
|
+
throw new Error("unsupported execution spool format");
|
|
92
|
+
}
|
|
93
|
+
const key = await this.loadOrCreateKey();
|
|
94
|
+
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(wrapper.nonce, "base64"));
|
|
95
|
+
decipher.setAAD(SPOOL_AAD);
|
|
96
|
+
decipher.setAuthTag(Buffer.from(wrapper.tag, "base64"));
|
|
97
|
+
const plain = Buffer.concat([
|
|
98
|
+
decipher.update(Buffer.from(wrapper.ciphertext, "base64")), decipher.final(),
|
|
99
|
+
]).toString("utf8");
|
|
100
|
+
const value = JSON.parse(plain);
|
|
101
|
+
if (value.format !== "atmem-openclaw-execution-spool-v1" || !Array.isArray(value.pending)) {
|
|
102
|
+
throw new Error("unsupported decrypted execution spool state");
|
|
103
|
+
}
|
|
104
|
+
// Pending events retain their original epoch. New events get a fresh
|
|
105
|
+
// epoch and sequence after every plugin process start.
|
|
106
|
+
this.state = {
|
|
107
|
+
...value,
|
|
108
|
+
producerEpoch: randomUUID(),
|
|
109
|
+
nextSequence: 1,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
const code = error.code;
|
|
114
|
+
if (code !== "ENOENT")
|
|
115
|
+
throw error;
|
|
116
|
+
this.state = {
|
|
117
|
+
format: "atmem-openclaw-execution-spool-v1",
|
|
118
|
+
producerInstanceId: randomUUID(),
|
|
119
|
+
producerEpoch: randomUUID(),
|
|
120
|
+
nextSequence: 1,
|
|
121
|
+
pending: [],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
await this.persist(this.state);
|
|
125
|
+
return this.state;
|
|
126
|
+
}
|
|
127
|
+
async persist(state) {
|
|
128
|
+
await mkdir(path.dirname(this.filePath), { recursive: true, mode: 0o700 });
|
|
129
|
+
const key = await this.loadOrCreateKey();
|
|
130
|
+
const nonce = randomBytes(12);
|
|
131
|
+
const cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
132
|
+
cipher.setAAD(SPOOL_AAD);
|
|
133
|
+
const ciphertext = Buffer.concat([
|
|
134
|
+
cipher.update(JSON.stringify(state), "utf8"), cipher.final(),
|
|
135
|
+
]);
|
|
136
|
+
const wrapper = {
|
|
137
|
+
format: "atmem-openclaw-encrypted-execution-spool-v2",
|
|
138
|
+
suite: "AES-256-GCM",
|
|
139
|
+
nonce: nonce.toString("base64"),
|
|
140
|
+
ciphertext: ciphertext.toString("base64"),
|
|
141
|
+
tag: cipher.getAuthTag().toString("base64"),
|
|
142
|
+
};
|
|
143
|
+
const temporary = `${this.filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
144
|
+
await writeFile(temporary, `${JSON.stringify(wrapper)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
145
|
+
await rename(temporary, this.filePath);
|
|
146
|
+
await chmod(this.filePath, 0o600);
|
|
147
|
+
}
|
|
148
|
+
async loadOrCreateKey() {
|
|
149
|
+
await mkdir(path.dirname(this.keyPath), { recursive: true, mode: 0o700 });
|
|
150
|
+
try {
|
|
151
|
+
const encoded = (await readFile(this.keyPath, "utf8")).trim();
|
|
152
|
+
const key = Buffer.from(encoded, "base64");
|
|
153
|
+
if (key.length !== 32)
|
|
154
|
+
throw new Error("execution spool key must be 256 bits");
|
|
155
|
+
return key;
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (error.code !== "ENOENT")
|
|
159
|
+
throw error;
|
|
160
|
+
const key = randomBytes(32);
|
|
161
|
+
await writeFile(this.keyPath, `${key.toString("base64")}\n`, {
|
|
162
|
+
encoding: "utf8", mode: 0o600, flag: "wx",
|
|
163
|
+
});
|
|
164
|
+
await chmod(this.keyPath, 0o600);
|
|
165
|
+
return key;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
expirePending(state, cutoff) {
|
|
169
|
+
const expired = state.pending.filter(event => Date.parse(event.event_time) < cutoff);
|
|
170
|
+
if (!expired.length)
|
|
171
|
+
return;
|
|
172
|
+
state.pending = state.pending.filter(event => Date.parse(event.event_time) >= cutoff);
|
|
173
|
+
state.pending.unshift({
|
|
174
|
+
event_type: "capture.gap",
|
|
175
|
+
run_id: String(expired.at(-1)?.run_id ?? "capture-spool"),
|
|
176
|
+
payload: {
|
|
177
|
+
reason: "spool_retention_expired",
|
|
178
|
+
dropped_count: expired.length,
|
|
179
|
+
first_sequence: expired[0].producer_sequence,
|
|
180
|
+
last_sequence: expired.at(-1)?.producer_sequence,
|
|
181
|
+
},
|
|
182
|
+
event_id: randomUUID(),
|
|
183
|
+
producer_instance_id: state.producerInstanceId,
|
|
184
|
+
producer_epoch: state.producerEpoch,
|
|
185
|
+
producer_sequence: state.nextSequence++,
|
|
186
|
+
event_time: new Date().toISOString(),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
package/dist/src/rpc-client.js
CHANGED
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.
|
|
5
|
+
"version": "2.3.3",
|
|
6
6
|
"commandAliases": ["memory-atmem"],
|
|
7
7
|
"activation": {
|
|
8
8
|
"onStartup": true,
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dbPath": {
|
|
39
39
|
"type": "string",
|
|
40
|
-
"default": "~/.atmem/memories.db",
|
|
40
|
+
"default": "~/.atmem/memory/memories.db",
|
|
41
41
|
"description": "SQLite database path (also auditable from outside OpenClaw via `atmem verify`)"
|
|
42
42
|
},
|
|
43
43
|
"subject": {
|
|
@@ -129,13 +129,13 @@
|
|
|
129
129
|
},
|
|
130
130
|
"statePath": {
|
|
131
131
|
"type": "string",
|
|
132
|
-
"default": "~/.atmem/control-plane.json",
|
|
132
|
+
"default": "~/.atmem/config/control-plane.json",
|
|
133
133
|
"description": "Digest-bound control file written by `atmem control`."
|
|
134
134
|
},
|
|
135
135
|
"blackboxEnabled": {
|
|
136
136
|
"type": "boolean",
|
|
137
|
-
"default":
|
|
138
|
-
"description": "Record
|
|
137
|
+
"default": true,
|
|
138
|
+
"description": "Record encrypted full-fidelity text, link/page, file, image, audio and video evidence plus metadata through the private control protocol. Set false to disable the recorder."
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-memory-atmem",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.3",
|
|
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/tool-result-comparison.mjs && node test/blackbox-diagnostics.mjs && node test/task-tools.mjs && node test/delegated-context-contract.mjs",
|
|
43
|
+
"test": "node test/manifest.mjs && node test/hook-context-compat.mjs && node test/setup.mjs && node test/hooks.mjs && node test/delegated-identity.mjs && node test/tool-observations.mjs && node test/tool-result-comparison.mjs && node test/blackbox-diagnostics.mjs && node test/execution-spool.mjs && node test/task-tools.mjs && node test/delegated-context-contract.mjs",
|
|
44
44
|
"smoke": "node test/smoke.mjs && node test/task-journey.mjs && node test/delegated-journey.mjs",
|
|
45
45
|
"prepack": "npm run build && npm run typecheck && npm test && npm run smoke",
|
|
46
46
|
"hook-context": "node test/hook-context-compat.mjs",
|