@ngo-a/native-memory-citations 2026.6.7 → 2026.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,6 +13,25 @@ access; it is operator-controlled retrieval with an audit trail.
13
13
 
14
14
  ![Architecture](https://raw.githubusercontent.com/NGO-A/native-memory-citations/master/docs/architecture.svg)
15
15
 
16
+ ## Operating modes
17
+
18
+ The plugin runs in one of two modes, selected by the `mode` configuration key.
19
+
20
+ - **`bounded` (default).** The behavior described throughout this README: read-only
21
+ retrieval, keyword/substring search, extractive cited answers, no network calls, no
22
+ model calls, and no changes to host configuration. This is what a default install
23
+ does, and what every guarantee in this document refers to.
24
+ - **`enhanced` (opt-in).** Layers the three pillars of agentic memory - a local
25
+ knowledge graph, semantic and reranked recall, session-snapshot injection, and
26
+ observation tagging - on top of the bounded core, reusing the same access boundary,
27
+ redaction, and citation guarantees. Every enhanced capability is disabled by default
28
+ even in enhanced mode and is turned on explicitly, per feature.
29
+
30
+ Leaving configuration at its defaults keeps the plugin in bounded mode; an upgrade
31
+ changes nothing until you opt in. Enhanced-mode capabilities are introduced
32
+ incrementally beginning with the 2026.6.9 release - see
33
+ [Enhanced mode](#enhanced-mode-opt-in).
34
+
16
35
  ## Key capabilities
17
36
 
18
37
  - Operator-defined search scope, limited to workspace-relative roots.
@@ -27,18 +46,22 @@ access; it is operator-controlled retrieval with an audit trail.
27
46
 
28
47
  From npm (recommended):
29
48
 
30
- openclaw plugins install @ngo-a/native-memory-citations
49
+ ```sh
50
+ openclaw plugins install @ngo-a/native-memory-citations
51
+ ```
31
52
 
32
53
  From a local checkout (development):
33
54
 
34
- openclaw plugins install ./native-memory-citations
55
+ ```sh
56
+ openclaw plugins install ./native-memory-citations
57
+ ```
35
58
 
36
59
  Reload the Gateway after installing so the plugin host exposes the tools.
37
60
 
38
61
  ## Requirements
39
62
 
40
63
  - Node.js 22.19.0 or newer.
41
- - OpenClaw 2026.5.17 or newer (declared as a peer dependency).
64
+ - OpenClaw 2026.6.8 or newer (declared as a peer dependency).
42
65
  - A local OpenClaw workspace containing text memory files.
43
66
 
44
67
  Supported memory file types are `.md`, `.txt`, `.json`, `.jsonl`, `.yaml`, and
@@ -53,9 +76,9 @@ identity files, and tool references must be handled within clear boundaries.
53
76
 
54
77
  ## Tools
55
78
 
56
- - `native_memory_search` search the approved roots and return snippets with source paths, line numbers, and file SHA-256 hashes.
57
- - `native_memory_fetch` fetch a cited source by `sourceId` or a safe path, optionally checking an expected citation hash.
58
- - `native_memory_answer` build an extractive answer from cited snippets, and state plainly when no cited memory is found.
79
+ - `native_memory_search` - search the approved roots and return snippets with source paths, line numbers, and file SHA-256 hashes.
80
+ - `native_memory_fetch` - fetch a cited source by `sourceId` or a safe path, optionally checking an expected citation hash.
81
+ - `native_memory_answer` - build an extractive answer from cited snippets, and state plainly when no cited memory is found.
59
82
 
60
83
  ## Default scope
61
84
 
@@ -185,6 +208,103 @@ marks the result with `stale: true` and a `staleMessage` explaining the hash
185
208
  mismatch. Because hashes cover the full file, appending to a daily journal marks
186
209
  earlier citations stale even when the cited lines themselves are unchanged.
187
210
 
211
+ ## Enhanced mode (opt-in)
212
+
213
+ Enhanced mode layers the three pillars of agentic memory - storage, injection, and
214
+ recall - on top of the bounded core, while reusing the same access boundary,
215
+ redaction, and citation guarantees. It is opt-in and default-off: setting
216
+ `mode: "enhanced"` turns on the framework, and each pillar is then enabled
217
+ individually. Doing nothing leaves the plugin in bounded mode.
218
+
219
+ > **Availability (2026.6.9).** Enhanced mode is delivered incrementally. This release
220
+ > ships the bounded-mode guardrails, the enhanced config schema, plugin health checks,
221
+ > the deterministic zero-LLM knowledge-graph sidecar, and the enhanced lifecycle
222
+ > scaffolding. The richer recall, model-based, and wiki pillars - and full runtime
223
+ > dispatch/soak validation of the lifecycle hooks - are still pending (see Forthcoming).
224
+ > On any version, an unset or default configuration behaves exactly as bounded mode.
225
+
226
+ ### What 2026.6.9 ships
227
+
228
+ - **Bounded mode (default)** - unchanged guardrails and behavior (everything above).
229
+ - **Enhanced config schema + plugin health checks** - all enhanced keys are
230
+ schema-declared (so `openclaw doctor --fix` will not prune them), and the plugin
231
+ registers health checks visible to `openclaw doctor`.
232
+ - **Knowledge graph, zero-LLM** (`graph.enabled`). `native_memory_extract` writes typed
233
+ entity links (for example `works_at`, `invested_in`, `founded`) from your authorized
234
+ memory files into `memory/graph.jsonl` with no model call; `native_memory_graph`
235
+ queries it with a hard depth cap and cycle prevention. This pillar is functional in
236
+ this release.
237
+ - **Enhanced lifecycle scaffolding** (`injection.enabled`, `observations.enabled`,
238
+ `recall.snapshotFirst`). The code paths are present: `session_start` writes a capped
239
+ session snapshot, `before_prompt_build` injects it, and `agent_end` appends to
240
+ `memory/observations.jsonl` within `observations.maxBytes`. They require their host hook gates
241
+ (`hooks.allowPromptInjection` for injection, `hooks.allowConversationAccess` for the
242
+ observation append). On external-CLI runners or any other harness where in-process
243
+ lifecycle hooks do not dispatch, these hook-dependent features degrade cleanly: the
244
+ turn is unaffected and the three core cited-memory tools still work. **Runtime hook
245
+ dispatch and soak validation are still pending - treat these as experimental until
246
+ validated on the embedded runner.**
247
+
248
+ ### Forthcoming (not in 2026.6.9)
249
+
250
+ - Semantic recall fusion through the host `memory_search`, RRF reranking, intent
251
+ classification, and snapshot-first recall inside `native_memory_search` /
252
+ `native_memory_answer`.
253
+ - Model-based observation extraction (the current path appends without a model) and its
254
+ fail-open-under-slow-model validation. When it ships, the default will use the
255
+ host's configured summarization/fast model, not a provider-specific model name.
256
+ - The `memory-wiki` bridge.
257
+ - External gateway-perf, package-gauntlet, and long-soak validation lanes.
258
+
259
+ ### Dreaming requirement
260
+
261
+ Enhanced mode builds on OpenClaw's built-in dream cycle (Light -> REM -> Deep
262
+ consolidation), which is **off by default in OpenClaw**. When you enable enhanced
263
+ mode, the plugin enables dreaming for you and prints a notice explaining that it
264
+ *continues* - and does not replace - OpenClaw's dreaming, and that disabling dreaming
265
+ while enhanced mode is on will degrade or silently break these features. Bounded mode
266
+ never touches the dreaming setting. (In `2026.6.9` the guard's code path ships; its
267
+ host-config mutation on real startup is part of the dispatch validation still pending.)
268
+
269
+ ### Configuration (enhanced keys)
270
+
271
+ All keys default off/false; an absent block means bounded mode. Every key is part of
272
+ the plugin schema, so `openclaw doctor --fix` will not prune it. Keys for forthcoming
273
+ pillars - `recall.semantic`, `recall.rerank`, `recall.intentClassifier`,
274
+ `observations.model` and model-based `observations.extraction`, and `wikiBridge.enabled`
275
+ - are accepted by the schema but have no effect until those pillars ship. Omit
276
+ `observations.model` to use the host's configured summarization/fast model when
277
+ model-based extraction is implemented; if no host model is available, observation
278
+ tagging falls back to the no-model append path.
279
+
280
+ | Key | Type | Default | Description |
281
+ |-----|------|---------|-------------|
282
+ | `mode` | string | `"bounded"` | `"bounded"` or `"enhanced"`. |
283
+ | `graph.enabled` | boolean | `false` | Enable knowledge-graph extraction and the graph tools. |
284
+ | `graph.edgeTypes` | string[] | built-in set | Typed edges to extract. |
285
+ | `graph.maxDepth` | number | `3` | Hard cap on multi-hop traversal depth. |
286
+ | `recall.semantic` | boolean | `false` | Add semantic retrieval to search/answer. |
287
+ | `recall.rerank` | boolean | `false` | Rerank retrieved results by relevance. |
288
+ | `recall.snapshotFirst` | boolean | `false` | Check the session snapshot before deeper search. |
289
+ | `recall.intentClassifier` | boolean | `false` | Classify query intent to steer retrieval. |
290
+ | `injection.enabled` | boolean | `false` | Inject a capped session snapshot into context. |
291
+ | `injection.tokenCap` | number | `1300` | Maximum tokens injected per session. |
292
+ | `observations.enabled` | boolean | `false` | Tag per-turn observations. |
293
+ | `observations.model` | string | host default | Optional model profile for future extraction; when omitted, use the host's configured summarization/fast model. |
294
+ | `observations.extraction` | boolean | `true` | When `false`, record raw entries with no model call. |
295
+ | `observations.maxBytes` | number | `1048576` | Maximum retained size for `memory/observations.jsonl` in enhanced mode. |
296
+ | `dreaming.autoEnable` | boolean | `true` | In enhanced mode, enable host dreaming if it is off. |
297
+ | `dreaming.enforce` | boolean | `true` | Re-warn at startup if dreaming is disabled in enhanced mode. |
298
+ | `dreaming.blockToolsWhenOff` | boolean | `false` | If `true`, dreaming-dependent tools error instead of degrading when dreaming is off. |
299
+ | `wikiBridge.enabled` | boolean | `false` | Enrich a separately installed `memory-wiki` vault, if present. |
300
+
301
+ ### Compatibility
302
+
303
+ Enhanced mode requires a newer OpenClaw floor than bounded mode; the package declares
304
+ the required version per release. Hook-dependent features require a host harness that
305
+ dispatches in-process lifecycle hooks; external CLI runners and other non-dispatching
306
+ harnesses keep the core tools working and simply do not run those enhanced hooks.
307
+
188
308
  ## Security model
189
309
 
190
310
  The plugin enforces a two-layer model.
package/dist/core.d.ts CHANGED
@@ -3,6 +3,65 @@ export type PluginConfig = {
3
3
  allowedRoots?: string[];
4
4
  sharedMode?: boolean;
5
5
  maxFileBytes?: number;
6
+ mode?: "bounded" | "enhanced";
7
+ dreaming?: {
8
+ autoEnable?: boolean;
9
+ enforce?: boolean;
10
+ blockToolsWhenOff?: boolean;
11
+ };
12
+ graph?: {
13
+ enabled?: boolean;
14
+ edgeTypes?: GraphEdgeType[];
15
+ maxDepth?: number;
16
+ };
17
+ recall?: {
18
+ semantic?: boolean;
19
+ rerank?: boolean;
20
+ snapshotFirst?: boolean;
21
+ intentClassifier?: boolean;
22
+ };
23
+ injection?: {
24
+ enabled?: boolean;
25
+ tokenCap?: number;
26
+ };
27
+ observations?: {
28
+ enabled?: boolean;
29
+ model?: string;
30
+ extraction?: boolean;
31
+ maxBytes?: number;
32
+ };
33
+ wikiBridge?: {
34
+ enabled?: boolean;
35
+ };
36
+ };
37
+ export type GraphEdgeType = "works_at" | "invested_in" | "founded" | "advises" | "attended" | "mentions";
38
+ export type GraphEdge = {
39
+ from: string;
40
+ type: GraphEdgeType;
41
+ to: string;
42
+ sourceFile: string;
43
+ sourceLine: number;
44
+ extractedAt: string;
45
+ };
46
+ export type GraphExtractResult = {
47
+ enabled: boolean;
48
+ mode: "bounded" | "enhanced";
49
+ graphPath: string;
50
+ edgeCount: number;
51
+ skipped?: string;
52
+ };
53
+ export type GraphPath = {
54
+ nodes: string[];
55
+ edges: GraphEdge[];
56
+ };
57
+ export type GraphQueryResult = {
58
+ enabled: boolean;
59
+ mode: "bounded" | "enhanced";
60
+ query: string;
61
+ maxDepth: number;
62
+ edgeCount: number;
63
+ paths: GraphPath[];
64
+ skipped?: string;
6
65
  };
7
66
  export type SearchHit = {
8
67
  sourceId: string;
@@ -38,6 +97,9 @@ export type MemoryLogger = {
38
97
  error?: (message: string) => void;
39
98
  };
40
99
  export declare function workspaceFromConfig(config?: PluginConfig): string;
100
+ export declare function modeFromConfig(config?: PluginConfig): "bounded" | "enhanced";
101
+ export declare function isEnhancedMode(config?: PluginConfig): boolean;
102
+ export declare function graphEnabled(config?: PluginConfig): boolean;
41
103
  export declare function allowedRoots(config?: PluginConfig): string[];
42
104
  export declare function toSafePath(config: PluginConfig, requested: string): Promise<string>;
43
105
  export declare function sourceIdForPath(config: PluginConfig, absolutePath: string): string;
@@ -49,6 +111,13 @@ export declare function searchMemory(query: string, options?: {
49
111
  signal?: AbortSignal;
50
112
  logger?: MemoryLogger;
51
113
  }): Promise<SearchHit[]>;
114
+ export declare function extractMemoryGraph(config?: PluginConfig, options?: {
115
+ logger?: MemoryLogger;
116
+ }): Promise<GraphExtractResult>;
117
+ export declare function queryMemoryGraph(query: string, options?: {
118
+ maxDepth?: number;
119
+ config?: PluginConfig;
120
+ }): Promise<GraphQueryResult>;
52
121
  export declare function fetchMemorySource(input: {
53
122
  sourceId?: string;
54
123
  filePath?: string;
package/dist/core.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readdir, readFile, realpath, stat } from "node:fs/promises";
1
+ import { mkdir, readdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
2
2
  import { createHash } from "node:crypto";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
@@ -17,6 +17,8 @@ const DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
17
17
  const SCAN_CONCURRENCY = 8;
18
18
  const HIGH_ENTROPY_TOKEN_MIN_LENGTH = 24;
19
19
  const HIGH_ENTROPY_TOKEN_MIN_BITS_PER_CHAR = 4;
20
+ const DEFAULT_GRAPH_EDGE_TYPES = ["works_at", "invested_in", "founded", "advises", "attended", "mentions"];
21
+ const GRAPH_EXTRACTED_AT = "1970-01-01T00:00:00.000Z";
20
22
  const STOPWORDS = new Set([
21
23
  "the",
22
24
  "a",
@@ -93,6 +95,22 @@ function defaultWorkspace() {
93
95
  export function workspaceFromConfig(config = {}) {
94
96
  return path.resolve(config.workspace?.trim() || defaultWorkspace());
95
97
  }
98
+ export function modeFromConfig(config = {}) {
99
+ return config.mode === "enhanced" ? "enhanced" : "bounded";
100
+ }
101
+ export function isEnhancedMode(config = {}) {
102
+ return modeFromConfig(config) === "enhanced";
103
+ }
104
+ export function graphEnabled(config = {}) {
105
+ return isEnhancedMode(config) && config.graph?.enabled === true;
106
+ }
107
+ function graphPath(config = {}) {
108
+ return path.join(workspaceFromConfig(config), "memory", "graph.jsonl");
109
+ }
110
+ function enabledGraphEdgeTypes(config = {}) {
111
+ const configured = config.graph?.edgeTypes?.length ? config.graph.edgeTypes : DEFAULT_GRAPH_EDGE_TYPES;
112
+ return new Set(configured.filter((type) => DEFAULT_GRAPH_EDGE_TYPES.includes(type)));
113
+ }
96
114
  export function allowedRoots(config = {}) {
97
115
  const roots = config.allowedRoots && config.allowedRoots.length > 0
98
116
  ? config.allowedRoots
@@ -231,6 +249,14 @@ function publicHit(hit) {
231
249
  sha256: hit.sha256,
232
250
  };
233
251
  }
252
+ function graphSourceRoots(config) {
253
+ const workspace = workspaceFromConfig(config);
254
+ return [path.join(workspace, "memory"), path.join(workspace, "MEMORY.md")];
255
+ }
256
+ function isDerivedMemoryArtifact(config, file) {
257
+ const sourceId = sourceIdForPath(config, file);
258
+ return sourceId === "memory/graph.jsonl" || sourceId === "memory/observations.jsonl";
259
+ }
234
260
  async function collectFiles(root, logger) {
235
261
  const info = await stat(root).catch(() => null);
236
262
  if (!info) {
@@ -432,6 +458,173 @@ async function searchMemoryInternal(query, options = {}) {
432
458
  export async function searchMemory(query, options = {}) {
433
459
  return (await searchMemoryInternal(query, options)).map(publicHit);
434
460
  }
461
+ function normalizeEntity(value) {
462
+ return value
463
+ .replace(/[`*_()[\]{}]/g, "")
464
+ .replace(/\s+/g, " ")
465
+ .replace(/[.,;:!?]+$/g, "")
466
+ .trim();
467
+ }
468
+ function addEdge(edges, seen, edge) {
469
+ const normalized = {
470
+ ...edge,
471
+ from: normalizeEntity(edge.from),
472
+ to: normalizeEntity(edge.to),
473
+ extractedAt: GRAPH_EXTRACTED_AT,
474
+ };
475
+ if (!normalized.from || !normalized.to || normalized.from === normalized.to) {
476
+ return;
477
+ }
478
+ const key = `${normalized.from}\0${normalized.type}\0${normalized.to}\0${normalized.sourceFile}\0${normalized.sourceLine}`;
479
+ if (seen.has(key)) {
480
+ return;
481
+ }
482
+ seen.add(key);
483
+ edges.push(normalized);
484
+ }
485
+ function extractEdgesFromLine(line, sourceFile, sourceLine, allowedTypes) {
486
+ const edges = [];
487
+ const seen = new Set();
488
+ const cleaned = line.replace(/^\s*(?:[-*]|\d+[.)])\s+/, "").trim();
489
+ const properNoun = "([A-Z][\\w@.+-]*(?:\\s+[A-Z][\\w@.+-]*){0,5})";
490
+ const target = "([^.;\\n]{2,120})";
491
+ const patterns = [
492
+ { type: "works_at", re: new RegExp(`${properNoun}\\s+(?:works|worked)\\s+(?:at|for)\\s+${target}`, "i") },
493
+ { type: "invested_in", re: new RegExp(`${properNoun}\\s+(?:invested in|backs|backed)\\s+${target}`, "i") },
494
+ { type: "founded", re: new RegExp(`${properNoun}\\s+(?:founded|co-founded|started)\\s+${target}`, "i") },
495
+ { type: "advises", re: new RegExp(`${properNoun}\\s+(?:advises|advised|mentors|mentor(?:ed)?)\\s+${target}`, "i") },
496
+ { type: "attended", re: new RegExp(`${properNoun}\\s+(?:attended|went to)\\s+${target}`, "i") },
497
+ { type: "mentions", re: new RegExp(`${properNoun}\\s+(?:mentions|mentioned|discusses|discussed)\\s+${target}`, "i") },
498
+ ];
499
+ for (const pattern of patterns) {
500
+ if (!allowedTypes.has(pattern.type)) {
501
+ continue;
502
+ }
503
+ const match = cleaned.match(pattern.re);
504
+ if (match?.[1] && match[2]) {
505
+ addEdge(edges, seen, { from: match[1], type: pattern.type, to: match[2], sourceFile, sourceLine });
506
+ }
507
+ }
508
+ return edges;
509
+ }
510
+ async function readGraphEdges(config) {
511
+ const text = await readFile(graphPath(config), "utf8").catch(() => "");
512
+ if (!text.trim()) {
513
+ return [];
514
+ }
515
+ const edges = [];
516
+ for (const line of text.split(/\r?\n/g)) {
517
+ if (!line.trim()) {
518
+ continue;
519
+ }
520
+ const parsed = JSON.parse(line);
521
+ if (typeof parsed.from === "string"
522
+ && DEFAULT_GRAPH_EDGE_TYPES.includes(parsed.type)
523
+ && typeof parsed.to === "string"
524
+ && typeof parsed.sourceFile === "string"
525
+ && typeof parsed.sourceLine === "number"
526
+ && typeof parsed.extractedAt === "string") {
527
+ edges.push(parsed);
528
+ }
529
+ }
530
+ return edges;
531
+ }
532
+ export async function extractMemoryGraph(config = {}, options = {}) {
533
+ const mode = modeFromConfig(config);
534
+ const target = sourceIdForPath(config, graphPath(config));
535
+ if (!graphEnabled(config)) {
536
+ return {
537
+ enabled: false,
538
+ mode,
539
+ graphPath: target,
540
+ edgeCount: 0,
541
+ skipped: mode === "enhanced" ? "graph.enabled is false" : "mode is bounded",
542
+ };
543
+ }
544
+ const allowedTypes = enabledGraphEdgeTypes(config);
545
+ const fileSizeLimit = maxFileBytes(config);
546
+ const files = (await Promise.all(graphSourceRoots(config).map((root) => collectFiles(root, options.logger)))).flat();
547
+ const edges = [];
548
+ const seen = new Set();
549
+ for (const file of files.sort()) {
550
+ if (isDerivedMemoryArtifact(config, file)) {
551
+ continue;
552
+ }
553
+ const loaded = await loadFile(file, fileSizeLimit, options.logger);
554
+ if (!loaded) {
555
+ continue;
556
+ }
557
+ const sourceFile = sourceIdForPath(config, file);
558
+ loaded.rawLines.forEach((line, index) => {
559
+ for (const edge of extractEdgesFromLine(line, sourceFile, index + 1, allowedTypes)) {
560
+ addEdge(edges, seen, edge);
561
+ }
562
+ });
563
+ }
564
+ edges.sort((a, b) => a.from.localeCompare(b.from)
565
+ || a.type.localeCompare(b.type)
566
+ || a.to.localeCompare(b.to)
567
+ || a.sourceFile.localeCompare(b.sourceFile)
568
+ || a.sourceLine - b.sourceLine);
569
+ const file = graphPath(config);
570
+ await mkdir(path.dirname(file), { recursive: true });
571
+ await writeFile(file, `${edges.map((edge) => JSON.stringify(edge)).join("\n")}${edges.length ? "\n" : ""}`, "utf8");
572
+ return { enabled: true, mode, graphPath: target, edgeCount: edges.length };
573
+ }
574
+ export async function queryMemoryGraph(query, options = {}) {
575
+ const config = options.config ?? {};
576
+ const mode = modeFromConfig(config);
577
+ const maxDepth = clampInt(options.maxDepth ?? config.graph?.maxDepth, 3, 1, 6);
578
+ if (!graphEnabled(config)) {
579
+ return {
580
+ enabled: false,
581
+ mode,
582
+ query,
583
+ maxDepth,
584
+ edgeCount: 0,
585
+ paths: [],
586
+ skipped: mode === "enhanced" ? "graph.enabled is false" : "mode is bounded",
587
+ };
588
+ }
589
+ const edges = await readGraphEdges(config);
590
+ const q = normalizeEntity(query).toLowerCase();
591
+ const starts = new Set();
592
+ for (const edge of edges) {
593
+ if (edge.from.toLowerCase().includes(q) || edge.to.toLowerCase().includes(q)) {
594
+ starts.add(edge.from);
595
+ starts.add(edge.to);
596
+ }
597
+ }
598
+ const adjacency = new Map();
599
+ for (const edge of edges) {
600
+ adjacency.set(edge.from, [...(adjacency.get(edge.from) ?? []), edge]);
601
+ adjacency.set(edge.to, [...(adjacency.get(edge.to) ?? []), { ...edge, from: edge.to, to: edge.from }]);
602
+ }
603
+ const paths = [];
604
+ const queue = Array.from(starts).sort().map((node) => ({ nodes: [node], edges: [] }));
605
+ const seenPaths = new Set();
606
+ while (queue.length > 0 && paths.length < 50) {
607
+ const current = queue.shift();
608
+ if (current.edges.length > 0) {
609
+ const key = current.nodes.join("\0");
610
+ if (!seenPaths.has(key)) {
611
+ seenPaths.add(key);
612
+ paths.push(current);
613
+ }
614
+ }
615
+ if (current.edges.length >= maxDepth) {
616
+ continue;
617
+ }
618
+ const last = current.nodes[current.nodes.length - 1] ?? "";
619
+ for (const edge of adjacency.get(last) ?? []) {
620
+ if (current.nodes.includes(edge.to)) {
621
+ continue;
622
+ }
623
+ queue.push({ nodes: [...current.nodes, edge.to], edges: [...current.edges, edge] });
624
+ }
625
+ }
626
+ return { enabled: true, mode, query, maxDepth, edgeCount: edges.length, paths };
627
+ }
435
628
  export async function fetchMemorySource(input, config = {}) {
436
629
  const requested = input.sourceId || input.filePath;
437
630
  if (!requested) {
@@ -0,0 +1,48 @@
1
+ import { type PluginConfig } from "./core.js";
2
+ type PluginApiLike = {
3
+ pluginConfig?: PluginConfig;
4
+ config?: OpenClawConfigLike;
5
+ logger?: {
6
+ debug?: (message: string) => void;
7
+ info?: (message: string) => void;
8
+ warn?: (message: string) => void;
9
+ error?: (message: string) => void;
10
+ };
11
+ registerHook?: (events: string | string[], handler: (event: unknown, ctx: unknown) => unknown, opts?: unknown) => void;
12
+ on?: (event: string, handler: (event: unknown, ctx: unknown) => unknown, opts?: unknown) => void;
13
+ runtime?: {
14
+ config?: {
15
+ current?: () => unknown;
16
+ mutateConfigFile?: (params: {
17
+ afterWrite: {
18
+ mode: "auto" | "none" | "restart" | "reload";
19
+ };
20
+ mutate: (draft: Record<string, unknown>) => void;
21
+ }) => Promise<unknown>;
22
+ };
23
+ };
24
+ };
25
+ type OpenClawConfigLike = {
26
+ memory?: {
27
+ dreaming?: {
28
+ enabled?: boolean;
29
+ };
30
+ };
31
+ plugins?: {
32
+ entries?: Record<string, {
33
+ config?: {
34
+ dreaming?: {
35
+ enabled?: boolean;
36
+ };
37
+ };
38
+ } | unknown>;
39
+ };
40
+ };
41
+ declare function setDreamingEnabledOnConfig(draft: Record<string, unknown>): void;
42
+ export declare function registerEnhancedLifecycle(api: PluginApiLike): void;
43
+ export declare const enhancedLifecycleForTest: {
44
+ DREAMING_NOTICE: string;
45
+ PLUGIN_ID: string;
46
+ setDreamingEnabledOnConfig: typeof setDreamingEnabledOnConfig;
47
+ };
48
+ export {};