@oni.bot/core 1.0.2 → 1.0.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/CHANGELOG.md +146 -146
- package/dist/agents/define-agent.d.ts.map +1 -1
- package/dist/agents/define-agent.js +7 -2
- package/dist/agents/define-agent.js.map +1 -1
- package/dist/checkpoint.d.ts.map +1 -1
- package/dist/checkpoint.js +7 -2
- package/dist/checkpoint.js.map +1 -1
- package/dist/checkpointers/postgres.d.ts.map +1 -1
- package/dist/checkpointers/postgres.js +43 -27
- package/dist/checkpointers/postgres.js.map +1 -1
- package/dist/circuit-breaker.d.ts +1 -0
- package/dist/circuit-breaker.d.ts.map +1 -1
- package/dist/circuit-breaker.js +13 -0
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/cli/dev.d.ts.map +1 -1
- package/dist/cli/dev.js +0 -1
- package/dist/cli/dev.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +0 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.js +12 -4
- package/dist/config/loader.js.map +1 -1
- package/dist/coordination/pubsub.d.ts +1 -0
- package/dist/coordination/pubsub.d.ts.map +1 -1
- package/dist/coordination/pubsub.js +31 -16
- package/dist/coordination/pubsub.js.map +1 -1
- package/dist/coordination/request-reply.d.ts +6 -0
- package/dist/coordination/request-reply.d.ts.map +1 -1
- package/dist/coordination/request-reply.js +56 -14
- package/dist/coordination/request-reply.js.map +1 -1
- package/dist/events/bus.d.ts +1 -0
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/events/bus.js +16 -10
- package/dist/events/bus.js.map +1 -1
- package/dist/functional.d.ts.map +1 -1
- package/dist/functional.js +3 -0
- package/dist/functional.js.map +1 -1
- package/dist/graph.d.ts.map +1 -1
- package/dist/graph.js +5 -2
- package/dist/graph.js.map +1 -1
- package/dist/guardrails/audit.d.ts +4 -1
- package/dist/guardrails/audit.d.ts.map +1 -1
- package/dist/guardrails/audit.js +18 -1
- package/dist/guardrails/audit.js.map +1 -1
- package/dist/harness/agent-loop.d.ts.map +1 -1
- package/dist/harness/agent-loop.js +471 -352
- package/dist/harness/agent-loop.js.map +1 -1
- package/dist/harness/context-compactor.d.ts +1 -0
- package/dist/harness/context-compactor.d.ts.map +1 -1
- package/dist/harness/context-compactor.js +43 -1
- package/dist/harness/context-compactor.js.map +1 -1
- package/dist/harness/harness.d.ts +6 -0
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js +32 -5
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/hooks-engine.d.ts.map +1 -1
- package/dist/harness/hooks-engine.js +12 -10
- package/dist/harness/hooks-engine.js.map +1 -1
- package/dist/harness/index.d.ts +3 -1
- package/dist/harness/index.d.ts.map +1 -1
- package/dist/harness/index.js +2 -0
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/memory-loader.d.ts +150 -0
- package/dist/harness/memory-loader.d.ts.map +1 -0
- package/dist/harness/memory-loader.js +714 -0
- package/dist/harness/memory-loader.js.map +1 -0
- package/dist/harness/safety-gate.d.ts.map +1 -1
- package/dist/harness/safety-gate.js +47 -26
- package/dist/harness/safety-gate.js.map +1 -1
- package/dist/harness/skill-loader.d.ts +7 -0
- package/dist/harness/skill-loader.d.ts.map +1 -1
- package/dist/harness/skill-loader.js +24 -8
- package/dist/harness/skill-loader.js.map +1 -1
- package/dist/harness/todo-module.d.ts.map +1 -1
- package/dist/harness/todo-module.js +13 -6
- package/dist/harness/todo-module.js.map +1 -1
- package/dist/harness/types.d.ts +7 -0
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/dist/harness/validate-args.js +18 -3
- package/dist/harness/validate-args.js.map +1 -1
- package/dist/hitl/interrupt.d.ts +2 -2
- package/dist/hitl/interrupt.d.ts.map +1 -1
- package/dist/hitl/interrupt.js +6 -4
- package/dist/hitl/interrupt.js.map +1 -1
- package/dist/hitl/resume.d.ts +10 -0
- package/dist/hitl/resume.d.ts.map +1 -1
- package/dist/hitl/resume.js +31 -0
- package/dist/hitl/resume.js.map +1 -1
- package/dist/injected.d.ts.map +1 -1
- package/dist/injected.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +28 -8
- package/dist/inspect.js.map +1 -1
- package/dist/lsp/client.d.ts +2 -0
- package/dist/lsp/client.d.ts.map +1 -1
- package/dist/lsp/client.js +62 -17
- package/dist/lsp/client.js.map +1 -1
- package/dist/lsp/index.d.ts.map +1 -1
- package/dist/lsp/index.js.map +1 -1
- package/dist/mcp/client.d.ts +2 -0
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +44 -13
- package/dist/mcp/client.js.map +1 -1
- package/dist/mcp/convert.js +1 -1
- package/dist/mcp/convert.js.map +1 -1
- package/dist/mcp/transport.d.ts +2 -0
- package/dist/mcp/transport.d.ts.map +1 -1
- package/dist/mcp/transport.js +33 -8
- package/dist/mcp/transport.js.map +1 -1
- package/dist/messages/index.d.ts.map +1 -1
- package/dist/messages/index.js +7 -1
- package/dist/messages/index.js.map +1 -1
- package/dist/models/anthropic.d.ts.map +1 -1
- package/dist/models/anthropic.js +25 -15
- package/dist/models/anthropic.js.map +1 -1
- package/dist/models/google.d.ts.map +1 -1
- package/dist/models/google.js +23 -7
- package/dist/models/google.js.map +1 -1
- package/dist/models/ollama.d.ts.map +1 -1
- package/dist/models/ollama.js +11 -1
- package/dist/models/ollama.js.map +1 -1
- package/dist/models/openai.d.ts.map +1 -1
- package/dist/models/openai.js +15 -3
- package/dist/models/openai.js.map +1 -1
- package/dist/models/openrouter.d.ts.map +1 -1
- package/dist/models/openrouter.js +14 -3
- package/dist/models/openrouter.js.map +1 -1
- package/dist/prebuilt/react-agent.d.ts.map +1 -1
- package/dist/prebuilt/react-agent.js +1 -0
- package/dist/prebuilt/react-agent.js.map +1 -1
- package/dist/pregel.d.ts +9 -6
- package/dist/pregel.d.ts.map +1 -1
- package/dist/pregel.js +89 -39
- package/dist/pregel.js.map +1 -1
- package/dist/retry.d.ts.map +1 -1
- package/dist/retry.js +7 -6
- package/dist/retry.js.map +1 -1
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +36 -9
- package/dist/store/index.js.map +1 -1
- package/dist/stream-events.d.ts.map +1 -1
- package/dist/stream-events.js +3 -9
- package/dist/stream-events.js.map +1 -1
- package/dist/swarm/graph.d.ts +15 -2
- package/dist/swarm/graph.d.ts.map +1 -1
- package/dist/swarm/graph.js +43 -14
- package/dist/swarm/graph.js.map +1 -1
- package/dist/swarm/index.d.ts +2 -1
- package/dist/swarm/index.d.ts.map +1 -1
- package/dist/swarm/index.js.map +1 -1
- package/dist/swarm/mailbox.d.ts.map +1 -1
- package/dist/swarm/mailbox.js +3 -1
- package/dist/swarm/mailbox.js.map +1 -1
- package/dist/swarm/mermaid.d.ts +2 -1
- package/dist/swarm/mermaid.d.ts.map +1 -1
- package/dist/swarm/mermaid.js +6 -3
- package/dist/swarm/mermaid.js.map +1 -1
- package/dist/swarm/pool.d.ts.map +1 -1
- package/dist/swarm/pool.js +18 -1
- package/dist/swarm/pool.js.map +1 -1
- package/dist/swarm/registry.d.ts.map +1 -1
- package/dist/swarm/registry.js +7 -0
- package/dist/swarm/registry.js.map +1 -1
- package/dist/swarm/scaling.d.ts +10 -1
- package/dist/swarm/scaling.d.ts.map +1 -1
- package/dist/swarm/scaling.js +85 -14
- package/dist/swarm/scaling.js.map +1 -1
- package/dist/swarm/snapshot.d.ts.map +1 -1
- package/dist/swarm/snapshot.js +10 -1
- package/dist/swarm/snapshot.js.map +1 -1
- package/dist/swarm/supervisor.js +20 -12
- package/dist/swarm/supervisor.js.map +1 -1
- package/dist/swarm/tracer.d.ts +3 -1
- package/dist/swarm/tracer.d.ts.map +1 -1
- package/dist/swarm/tracer.js +66 -15
- package/dist/swarm/tracer.js.map +1 -1
- package/dist/swarm/types.d.ts +1 -6
- package/dist/swarm/types.d.ts.map +1 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/types.d.ts +3 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// @oni.bot/core/harness — MemoryLoader
|
|
3
|
+
//
|
|
4
|
+
// Filesystem-native progressive disclosure memory.
|
|
5
|
+
// Five memory types across four token-budget tiers.
|
|
6
|
+
// Uses dynamic require('fs') — safe in non-Node environments.
|
|
7
|
+
// ============================================================
|
|
8
|
+
// ─── Lazy fs/path helpers ─────────────────────────────────────────────────
|
|
9
|
+
function getFs() {
|
|
10
|
+
try {
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
12
|
+
return require("fs");
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function getPath() {
|
|
19
|
+
try {
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
21
|
+
return require("path");
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function parseFrontmatter(content) {
|
|
28
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
29
|
+
const match = normalized.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
30
|
+
if (!match)
|
|
31
|
+
return { meta: {}, body: content };
|
|
32
|
+
const raw = match[1];
|
|
33
|
+
const body = match[2];
|
|
34
|
+
const meta = {};
|
|
35
|
+
for (const line of raw.split("\n")) {
|
|
36
|
+
const colonIdx = line.indexOf(":");
|
|
37
|
+
if (colonIdx === -1)
|
|
38
|
+
continue;
|
|
39
|
+
const key = line.slice(0, colonIdx).trim();
|
|
40
|
+
const val = line.slice(colonIdx + 1).trim();
|
|
41
|
+
if (!key || !val)
|
|
42
|
+
continue;
|
|
43
|
+
switch (key) {
|
|
44
|
+
case "type":
|
|
45
|
+
meta.type = val;
|
|
46
|
+
break;
|
|
47
|
+
case "tier": {
|
|
48
|
+
const parsed = parseInt(val, 10);
|
|
49
|
+
if (parsed >= 0 && parsed <= 3)
|
|
50
|
+
meta.tier = parsed;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
case "tags":
|
|
54
|
+
meta.tags = val.split(",").map((t) => t.trim()).filter(Boolean);
|
|
55
|
+
break;
|
|
56
|
+
case "summary":
|
|
57
|
+
meta.summary = val;
|
|
58
|
+
break;
|
|
59
|
+
case "triggers":
|
|
60
|
+
meta.triggers = val.split(",").map((t) => t.trim()).filter(Boolean);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { meta, body };
|
|
65
|
+
}
|
|
66
|
+
function estimateTokens(content) {
|
|
67
|
+
return Math.ceil(content.length / 4);
|
|
68
|
+
}
|
|
69
|
+
function inferTypeFromPath(relPath) {
|
|
70
|
+
const top = relPath.split(/[\\/]/)[0] ?? "";
|
|
71
|
+
const map = {
|
|
72
|
+
identity: "identity",
|
|
73
|
+
working: "working",
|
|
74
|
+
procedural: "procedural",
|
|
75
|
+
episodic: "episodic",
|
|
76
|
+
semantic: "semantic",
|
|
77
|
+
user: "user",
|
|
78
|
+
};
|
|
79
|
+
return map[top] ?? "semantic";
|
|
80
|
+
}
|
|
81
|
+
function inferTierFromPath(relPath, type) {
|
|
82
|
+
if (type === "identity")
|
|
83
|
+
return 0;
|
|
84
|
+
if (type === "working")
|
|
85
|
+
return 1;
|
|
86
|
+
if (type === "user")
|
|
87
|
+
return relPath.includes("RELATIONSHIPS") ? 2 : 1;
|
|
88
|
+
if (type === "procedural") {
|
|
89
|
+
if (relPath.endsWith("INDEX.md"))
|
|
90
|
+
return 1;
|
|
91
|
+
if (relPath.includes("examples"))
|
|
92
|
+
return 3;
|
|
93
|
+
return 2;
|
|
94
|
+
}
|
|
95
|
+
if (type === "episodic") {
|
|
96
|
+
if (relPath.endsWith("INDEX.md"))
|
|
97
|
+
return 1;
|
|
98
|
+
if (relPath.includes("archive"))
|
|
99
|
+
return 3;
|
|
100
|
+
return 2;
|
|
101
|
+
}
|
|
102
|
+
if (type === "semantic") {
|
|
103
|
+
const parts = relPath.split(/[\\/]/);
|
|
104
|
+
// root INDEX.md: semantic/INDEX.md → 2 parts
|
|
105
|
+
if (parts.length === 2 && parts[1] === "INDEX.md")
|
|
106
|
+
return 1;
|
|
107
|
+
// domain INDEX.md: semantic/<domain>/INDEX.md → 3 parts
|
|
108
|
+
if (parts.length === 3 && parts[2] === "INDEX.md")
|
|
109
|
+
return 2;
|
|
110
|
+
return 3;
|
|
111
|
+
}
|
|
112
|
+
return 2;
|
|
113
|
+
}
|
|
114
|
+
const STOPWORDS = new Set([
|
|
115
|
+
"the", "and", "for", "are", "but", "not", "you", "all", "can", "had", "her", "was", "one",
|
|
116
|
+
"our", "out", "day", "get", "has", "him", "his", "how", "its", "may", "new", "now", "old",
|
|
117
|
+
"see", "two", "who", "did", "does", "into", "than", "that", "this", "with", "have", "from",
|
|
118
|
+
"they", "will", "been", "when", "what", "were", "your", "said", "each", "which", "their",
|
|
119
|
+
"time", "about", "would", "there", "could", "other", "after", "first", "these", "those",
|
|
120
|
+
]);
|
|
121
|
+
// ─── MemoryLoader ─────────────────────────────────────────────────────────
|
|
122
|
+
export class MemoryLoader {
|
|
123
|
+
config;
|
|
124
|
+
units = new Map();
|
|
125
|
+
loaded = new Set();
|
|
126
|
+
static DEFAULT_BUDGETS = {
|
|
127
|
+
0: 800,
|
|
128
|
+
1: 2000,
|
|
129
|
+
2: 4000,
|
|
130
|
+
3: 8000,
|
|
131
|
+
};
|
|
132
|
+
constructor(config) {
|
|
133
|
+
this.config = {
|
|
134
|
+
budgets: { ...MemoryLoader.DEFAULT_BUDGETS, ...config.budgets },
|
|
135
|
+
episodicRecentDays: config.episodicRecentDays ?? 7,
|
|
136
|
+
autoIndex: config.autoIndex ?? true,
|
|
137
|
+
matchThreshold: config.matchThreshold ?? 0.2,
|
|
138
|
+
debug: config.debug ?? false,
|
|
139
|
+
root: config.root,
|
|
140
|
+
};
|
|
141
|
+
this.ensureDirectoryStructure();
|
|
142
|
+
this.scan();
|
|
143
|
+
}
|
|
144
|
+
// ── Factory ──────────────────────────────────────────────────────────────
|
|
145
|
+
static fromRoot(root, opts) {
|
|
146
|
+
return new MemoryLoader({ root, ...opts });
|
|
147
|
+
}
|
|
148
|
+
// ── Directory Bootstrap ──────────────────────────────────────────────────
|
|
149
|
+
ensureDirectoryStructure() {
|
|
150
|
+
const fs = getFs();
|
|
151
|
+
const path = getPath();
|
|
152
|
+
if (!fs || !path)
|
|
153
|
+
return;
|
|
154
|
+
const dirs = [
|
|
155
|
+
"identity", "working", "user",
|
|
156
|
+
"procedural", "episodic/recent", "episodic/archive", "semantic",
|
|
157
|
+
];
|
|
158
|
+
for (const dir of dirs) {
|
|
159
|
+
try {
|
|
160
|
+
const full = path.join(this.config.root, dir);
|
|
161
|
+
if (!fs.existsSync(full)) {
|
|
162
|
+
fs.mkdirSync(full, { recursive: true });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Silent degrade — non-Node environment
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// ── Scanning ─────────────────────────────────────────────────────────────
|
|
171
|
+
scan() {
|
|
172
|
+
this.units.clear();
|
|
173
|
+
const fs = getFs();
|
|
174
|
+
const path = getPath();
|
|
175
|
+
if (!fs || !path || !fs.existsSync(this.config.root))
|
|
176
|
+
return;
|
|
177
|
+
this.scanDirectory(this.config.root, "", fs, path);
|
|
178
|
+
this.log(`Scanned: ${this.units.size} memory units found`);
|
|
179
|
+
}
|
|
180
|
+
scanDirectory(absDir, relDir, fs, path) {
|
|
181
|
+
let entries;
|
|
182
|
+
try {
|
|
183
|
+
entries = fs.readdirSync(absDir, { withFileTypes: true });
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
for (const entry of entries) {
|
|
189
|
+
// Skip INDEX.md — it is metadata, not a memory unit
|
|
190
|
+
if (entry.name === "INDEX.md")
|
|
191
|
+
continue;
|
|
192
|
+
const relPath = relDir ? path.join(relDir, entry.name) : entry.name;
|
|
193
|
+
const absPath = path.join(absDir, entry.name);
|
|
194
|
+
if (entry.isDirectory()) {
|
|
195
|
+
this.scanDirectory(absPath, relPath, fs, path);
|
|
196
|
+
}
|
|
197
|
+
else if (entry.name.endsWith(".md")) {
|
|
198
|
+
this.registerFile(absPath, relPath);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
registerFile(absPath, relPath) {
|
|
203
|
+
const fs = getFs();
|
|
204
|
+
if (!fs)
|
|
205
|
+
return;
|
|
206
|
+
try {
|
|
207
|
+
const raw = fs.readFileSync(absPath, "utf-8");
|
|
208
|
+
const { meta, body } = parseFrontmatter(raw);
|
|
209
|
+
const type = meta.type ?? inferTypeFromPath(relPath);
|
|
210
|
+
const tier = meta.tier ?? inferTierFromPath(relPath, type);
|
|
211
|
+
const stat = fs.statSync(absPath);
|
|
212
|
+
const unit = {
|
|
213
|
+
key: relPath,
|
|
214
|
+
path: absPath,
|
|
215
|
+
type,
|
|
216
|
+
tier,
|
|
217
|
+
tags: meta.tags ?? this.extractTagsFromContent(body),
|
|
218
|
+
summary: meta.summary ?? this.extractSummary(body),
|
|
219
|
+
tokenCost: estimateTokens(raw),
|
|
220
|
+
triggers: meta.triggers ?? [],
|
|
221
|
+
updatedAt: stat.mtime.toISOString(),
|
|
222
|
+
};
|
|
223
|
+
this.units.set(relPath, unit);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// Skip unreadable files
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// ── Tag + Summary Extraction ──────────────────────────────────────────────
|
|
230
|
+
extractTagsFromContent(body) {
|
|
231
|
+
const headings = [...body.matchAll(/^##\s+(.+)$/gm)].map((m) => m[1].toLowerCase().replace(/\s+/g, "-"));
|
|
232
|
+
const firstLine = body.split("\n").find((l) => l.trim() && !l.startsWith("#")) ?? "";
|
|
233
|
+
const words = firstLine.toLowerCase().match(/\b[a-z]{4,}\b/g) ?? [];
|
|
234
|
+
return [...new Set([...headings, ...words.slice(0, 5)])];
|
|
235
|
+
}
|
|
236
|
+
extractSummary(body) {
|
|
237
|
+
const firstParagraph = body.split("\n\n").find((p) => p.trim() && !p.startsWith("#"));
|
|
238
|
+
if (!firstParagraph)
|
|
239
|
+
return "";
|
|
240
|
+
return firstParagraph.replace(/\n/g, " ").trim().slice(0, 120);
|
|
241
|
+
}
|
|
242
|
+
// ── Progressive Disclosure ────────────────────────────────────────────────
|
|
243
|
+
/**
|
|
244
|
+
* wake() — Load Tier 0 (identity). Always called first.
|
|
245
|
+
* Additive: already-loaded units are skipped.
|
|
246
|
+
*/
|
|
247
|
+
wake() {
|
|
248
|
+
return this.loadByTier(0);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* orient() — Load Tier 1 (working, user/PROFILE, INDEX files).
|
|
252
|
+
* Additive.
|
|
253
|
+
*/
|
|
254
|
+
orient() {
|
|
255
|
+
return this.loadByTier(1);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* match(task) — Load Tier 2 units relevant to the task string.
|
|
259
|
+
* Scored by tag-overlap × recency. Additive.
|
|
260
|
+
* Returns empty LoadResult if taskTokens is empty (no error).
|
|
261
|
+
*/
|
|
262
|
+
match(task) {
|
|
263
|
+
const taskTags = this.extractTagsFromString(task);
|
|
264
|
+
if (taskTags.length === 0) {
|
|
265
|
+
return { units: [], totalTokens: 0, budget: this.getBudget(2), dropped: [] };
|
|
266
|
+
}
|
|
267
|
+
const budget = this.getBudget(2);
|
|
268
|
+
const candidates = this.getUnitsByTier(2).filter((u) => !this.loaded.has(u.key));
|
|
269
|
+
const scored = candidates
|
|
270
|
+
.map((unit) => ({ unit, score: this.scoreRelevance(unit, taskTags) }))
|
|
271
|
+
.filter(({ score }) => score > 0 && score >= this.config.matchThreshold)
|
|
272
|
+
.sort((a, b) => b.score - a.score);
|
|
273
|
+
return this.applyBudget(scored.map((s) => s.unit), budget);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* query(topic) — Load Tier 3 units. Called only via the memory_query tool.
|
|
277
|
+
* Additive.
|
|
278
|
+
*/
|
|
279
|
+
query(topic) {
|
|
280
|
+
const topicTags = this.extractTagsFromString(topic);
|
|
281
|
+
if (topicTags.length === 0) {
|
|
282
|
+
return { units: [], totalTokens: 0, budget: this.getBudget(3), dropped: [] };
|
|
283
|
+
}
|
|
284
|
+
const budget = this.getBudget(3);
|
|
285
|
+
const candidates = this.getUnitsByTier(3).filter((u) => !this.loaded.has(u.key));
|
|
286
|
+
const scored = candidates
|
|
287
|
+
.map((unit) => ({ unit, score: this.scoreRelevance(unit, topicTags) }))
|
|
288
|
+
.filter(({ score }) => score > 0 && score >= this.config.matchThreshold)
|
|
289
|
+
.sort((a, b) => b.score - a.score);
|
|
290
|
+
return this.applyBudget(scored.map((s) => s.unit), budget);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* load(key) — Directly load a specific memory unit by key. Additive.
|
|
294
|
+
*/
|
|
295
|
+
load(key) {
|
|
296
|
+
const unit = this.units.get(key);
|
|
297
|
+
if (!unit)
|
|
298
|
+
return null;
|
|
299
|
+
const hydrated = this.hydrate(unit);
|
|
300
|
+
this.loaded.add(key);
|
|
301
|
+
return hydrated;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* loadType(type) — Load all units of a given memory type.
|
|
305
|
+
* Budget: 16000t fixed (intentional escape hatch). Additive.
|
|
306
|
+
*/
|
|
307
|
+
loadType(type) {
|
|
308
|
+
const units = [...this.units.values()].filter((u) => u.type === type && !this.loaded.has(u.key));
|
|
309
|
+
return this.applyBudget(units, 16000);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* buildSystemPrompt(tiers) — Assemble loaded units into a system prompt string.
|
|
313
|
+
* Default tiers: [0, 1, 2]
|
|
314
|
+
*/
|
|
315
|
+
buildSystemPrompt(tiers = [0, 1, 2]) {
|
|
316
|
+
const sections = [];
|
|
317
|
+
for (const [key, unit] of this.units) {
|
|
318
|
+
if (!this.loaded.has(key))
|
|
319
|
+
continue;
|
|
320
|
+
if (!tiers.includes(unit.tier))
|
|
321
|
+
continue;
|
|
322
|
+
if (!unit.content)
|
|
323
|
+
continue;
|
|
324
|
+
const header = `\n\n<!-- MEMORY: ${unit.type.toUpperCase()} | ${unit.key} | T${unit.tier} -->\n`;
|
|
325
|
+
sections.push(header + unit.content);
|
|
326
|
+
}
|
|
327
|
+
return sections.join("");
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* getLoadedManifest() — Compact list of loaded keys + costs.
|
|
331
|
+
* Format: "- <key> (T<tier>, <cost>t)" per line.
|
|
332
|
+
*/
|
|
333
|
+
getLoadedManifest() {
|
|
334
|
+
return [...this.loaded]
|
|
335
|
+
.map((key) => {
|
|
336
|
+
const unit = this.units.get(key);
|
|
337
|
+
return unit ? `- ${key} (T${unit.tier}, ${unit.tokenCost}t)` : `- ${key}`;
|
|
338
|
+
})
|
|
339
|
+
.join("\n");
|
|
340
|
+
}
|
|
341
|
+
/** resetSession() — Clear loaded set and release hydrated content. Does not remove files. */
|
|
342
|
+
resetSession() {
|
|
343
|
+
this.loaded.clear();
|
|
344
|
+
for (const unit of this.units.values()) {
|
|
345
|
+
unit.content = undefined;
|
|
346
|
+
}
|
|
347
|
+
this.log("Session reset");
|
|
348
|
+
}
|
|
349
|
+
// ── Write API ─────────────────────────────────────────────────────────────
|
|
350
|
+
/**
|
|
351
|
+
* persist() — Write a memory unit to disk.
|
|
352
|
+
* autoIndex: rebuildIndex() called synchronously before return when autoIndex === true.
|
|
353
|
+
* Returns MemoryUnit with content populated.
|
|
354
|
+
*/
|
|
355
|
+
persist(type, filename, content, subfolder) {
|
|
356
|
+
return this.persistInternal(type, filename, content, subfolder, false);
|
|
357
|
+
}
|
|
358
|
+
persistInternal(type, filename, content, subfolder, skipAutoIndex) {
|
|
359
|
+
const fs = getFs();
|
|
360
|
+
const path = getPath();
|
|
361
|
+
if (!fs || !path) {
|
|
362
|
+
// Silent degrade in non-Node environments — return a stub unit.
|
|
363
|
+
// Must NOT throw: callers (persistEpisodic, finally block) do not wrap this.
|
|
364
|
+
return {
|
|
365
|
+
key: `${type}/${filename}`,
|
|
366
|
+
path: "",
|
|
367
|
+
type,
|
|
368
|
+
tier: 1,
|
|
369
|
+
tags: [],
|
|
370
|
+
summary: "",
|
|
371
|
+
tokenCost: 0,
|
|
372
|
+
triggers: [],
|
|
373
|
+
updatedAt: new Date().toISOString(),
|
|
374
|
+
content,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
const folder = subfolder
|
|
378
|
+
? path.join(this.config.root, type, subfolder)
|
|
379
|
+
: path.join(this.config.root, type);
|
|
380
|
+
const absPath = path.join(folder, filename);
|
|
381
|
+
const relPath = path.relative(this.config.root, absPath);
|
|
382
|
+
const finalContent = /^---\r?\n/.test(content)
|
|
383
|
+
? content
|
|
384
|
+
: this.injectFrontmatter(content, type, relPath);
|
|
385
|
+
try {
|
|
386
|
+
fs.mkdirSync(folder, { recursive: true });
|
|
387
|
+
fs.writeFileSync(absPath, finalContent, "utf-8");
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
// Must NOT throw: called from finally blocks — return a stub on I/O failure.
|
|
391
|
+
return {
|
|
392
|
+
key: relPath,
|
|
393
|
+
path: absPath,
|
|
394
|
+
type,
|
|
395
|
+
tier: inferTierFromPath(relPath, type),
|
|
396
|
+
tags: [],
|
|
397
|
+
summary: "",
|
|
398
|
+
tokenCost: estimateTokens(finalContent),
|
|
399
|
+
triggers: [],
|
|
400
|
+
updatedAt: new Date().toISOString(),
|
|
401
|
+
content: finalContent,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
this.registerFile(absPath, relPath);
|
|
405
|
+
const unit = this.units.get(relPath);
|
|
406
|
+
if (!unit) {
|
|
407
|
+
// registerFile silently failed (e.g. post-write permission error) — return minimal stub
|
|
408
|
+
return {
|
|
409
|
+
key: relPath,
|
|
410
|
+
path: absPath,
|
|
411
|
+
type,
|
|
412
|
+
tier: inferTierFromPath(relPath, type),
|
|
413
|
+
tags: [],
|
|
414
|
+
summary: "",
|
|
415
|
+
tokenCost: estimateTokens(finalContent),
|
|
416
|
+
triggers: [],
|
|
417
|
+
updatedAt: new Date().toISOString(),
|
|
418
|
+
content: finalContent,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
unit.content = finalContent;
|
|
422
|
+
if (this.config.autoIndex && !skipAutoIndex) {
|
|
423
|
+
this.rebuildIndex(path.dirname(absPath));
|
|
424
|
+
}
|
|
425
|
+
this.log(`Persisted: ${relPath} (${unit.tokenCost}t)`);
|
|
426
|
+
return unit;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* persistEpisodic() — Write session log to episodic/recent/<date>_<sessionId>.md.
|
|
430
|
+
* Triggers compaction if recent/ has files older than episodicRecentDays.
|
|
431
|
+
*/
|
|
432
|
+
persistEpisodic(sessionId, content) {
|
|
433
|
+
const date = new Date().toISOString().split("T")[0];
|
|
434
|
+
const filename = `${date}_${sessionId}.md`;
|
|
435
|
+
// Skip autoIndex for episodic/recent — INDEX.md would pollute the dated file listing
|
|
436
|
+
const unit = this.persistInternal("episodic", filename, content, "recent", true);
|
|
437
|
+
this.compactEpisodicIfNeeded();
|
|
438
|
+
return unit;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* persistSemantic() — Write knowledge unit to semantic/<domain>/<topic>.md.
|
|
442
|
+
* Rebuilds both domain INDEX.md and root semantic INDEX.md.
|
|
443
|
+
*/
|
|
444
|
+
persistSemantic(domain, topic, content) {
|
|
445
|
+
const path = getPath();
|
|
446
|
+
const filename = `${topic.toLowerCase().replace(/\s+/g, "-")}.md`;
|
|
447
|
+
const unit = this.persist("semantic", filename, content, domain);
|
|
448
|
+
if (this.config.autoIndex && path) {
|
|
449
|
+
this.rebuildIndex(path.join(this.config.root, "semantic"));
|
|
450
|
+
}
|
|
451
|
+
return unit;
|
|
452
|
+
}
|
|
453
|
+
// ── INDEX.md Management ───────────────────────────────────────────────────
|
|
454
|
+
/**
|
|
455
|
+
* rebuildIndex() — Rewrite the INDEX.md for a given folder.
|
|
456
|
+
* INDEX.md files are write-only — never read back by the loader.
|
|
457
|
+
*/
|
|
458
|
+
rebuildIndex(folder) {
|
|
459
|
+
const fs = getFs();
|
|
460
|
+
const path = getPath();
|
|
461
|
+
if (!fs || !path)
|
|
462
|
+
return;
|
|
463
|
+
const relFolder = path.relative(this.config.root, folder);
|
|
464
|
+
const folderPrefix = folder.endsWith(path.sep) ? folder : folder + path.sep;
|
|
465
|
+
const units = [...this.units.values()].filter((u) => u.path.startsWith(folderPrefix));
|
|
466
|
+
if (units.length === 0)
|
|
467
|
+
return;
|
|
468
|
+
const rows = units
|
|
469
|
+
.map((u) => `| ${path.basename(u.key)} | ${u.summary.slice(0, 60)} | ${u.tags.slice(0, 4).join(",")} | T${u.tier} | ${u.tokenCost} |`)
|
|
470
|
+
.join("\n");
|
|
471
|
+
const allTags = [...new Set(units.flatMap((u) => u.tags))].slice(0, 8).join(", ");
|
|
472
|
+
const index = [
|
|
473
|
+
`---`,
|
|
474
|
+
`type: index`,
|
|
475
|
+
`folder: ${relFolder}`,
|
|
476
|
+
`indexed_at: ${new Date().toISOString()}`,
|
|
477
|
+
`---`,
|
|
478
|
+
``,
|
|
479
|
+
`# Index: ${relFolder || "root"}`,
|
|
480
|
+
`_token_budget: 200_`,
|
|
481
|
+
``,
|
|
482
|
+
`## Contents`,
|
|
483
|
+
``,
|
|
484
|
+
`| file | summary | tags | tier | tokens |`,
|
|
485
|
+
`|------|---------|------|------|--------|`,
|
|
486
|
+
rows,
|
|
487
|
+
``,
|
|
488
|
+
`## Routing Hints`,
|
|
489
|
+
``,
|
|
490
|
+
`- Load this folder when: ${allTags}`,
|
|
491
|
+
`- Units: ${units.length} | Total tokens: ${units.reduce((s, u) => s + u.tokenCost, 0)}`,
|
|
492
|
+
].join("\n");
|
|
493
|
+
try {
|
|
494
|
+
fs.writeFileSync(path.join(folder, "INDEX.md"), index, "utf-8");
|
|
495
|
+
this.log(`Rebuilt INDEX.md: ${relFolder}`);
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
// Silent degrade
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
/** rebuildAllIndexes() — Rebuild every INDEX.md in the tree. */
|
|
502
|
+
rebuildAllIndexes() {
|
|
503
|
+
const path = getPath();
|
|
504
|
+
if (!path)
|
|
505
|
+
return;
|
|
506
|
+
const dirs = new Set();
|
|
507
|
+
for (const unit of this.units.values()) {
|
|
508
|
+
dirs.add(path.dirname(unit.path));
|
|
509
|
+
}
|
|
510
|
+
for (const dir of dirs) {
|
|
511
|
+
this.rebuildIndex(dir);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// ── Diagnostic ────────────────────────────────────────────────────────────
|
|
515
|
+
stats() {
|
|
516
|
+
const byType = {};
|
|
517
|
+
const byTier = {};
|
|
518
|
+
let totalTokens = 0;
|
|
519
|
+
for (const unit of this.units.values()) {
|
|
520
|
+
byType[unit.type] = (byType[unit.type] ?? 0) + 1;
|
|
521
|
+
byTier[`T${unit.tier}`] = (byTier[`T${unit.tier}`] ?? 0) + 1;
|
|
522
|
+
totalTokens += unit.tokenCost;
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
totalUnits: this.units.size,
|
|
526
|
+
currentlyLoaded: this.loaded.size,
|
|
527
|
+
totalTokensOnDisk: totalTokens,
|
|
528
|
+
byType,
|
|
529
|
+
byTier,
|
|
530
|
+
root: this.config.root,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
// ── Tool ─────────────────────────────────────────────────────────────────
|
|
534
|
+
/**
|
|
535
|
+
* getQueryTool() — Returns the memory_query ToolDefinition.
|
|
536
|
+
* The `reason` param is prompting-only — not used in execute logic.
|
|
537
|
+
* `_ctx` is typed as ToolContext (base); MemoryLoader needs no harness-specific context.
|
|
538
|
+
*/
|
|
539
|
+
getQueryTool() {
|
|
540
|
+
return {
|
|
541
|
+
name: "memory_query",
|
|
542
|
+
description: `Retrieve deep knowledge from memory when you hit a gap mid-task.
|
|
543
|
+
Call this when: you need a specific procedure, domain fact, or past decision you don't currently have in context.
|
|
544
|
+
Do NOT call this speculatively — only when you've identified a concrete gap.
|
|
545
|
+
Returns: matching memory units or empty if nothing found.`,
|
|
546
|
+
schema: {
|
|
547
|
+
type: "object",
|
|
548
|
+
properties: {
|
|
549
|
+
topic: {
|
|
550
|
+
type: "string",
|
|
551
|
+
description: "Specific topic, skill name, or domain concept to retrieve. Be precise.",
|
|
552
|
+
},
|
|
553
|
+
reason: {
|
|
554
|
+
type: "string",
|
|
555
|
+
description: "One sentence: what gap are you filling and why do you need it now.",
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
required: ["topic", "reason"],
|
|
559
|
+
additionalProperties: false,
|
|
560
|
+
},
|
|
561
|
+
execute: (input, _ctx) => {
|
|
562
|
+
// reason intentionally unused — prompting-only
|
|
563
|
+
const result = this.query(input.topic);
|
|
564
|
+
if (result.units.length === 0) {
|
|
565
|
+
return {
|
|
566
|
+
found: false,
|
|
567
|
+
topic: input.topic,
|
|
568
|
+
message: "No matching memory found. Proceed with available context or escalate.",
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
found: true,
|
|
573
|
+
units: result.units.map((u) => ({ key: u.key, content: u.content })),
|
|
574
|
+
totalTokens: result.totalTokens,
|
|
575
|
+
droppedCount: result.dropped.length,
|
|
576
|
+
};
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
// ── Internal Helpers ──────────────────────────────────────────────────────
|
|
581
|
+
loadByTier(tier) {
|
|
582
|
+
const budget = this.getBudget(tier);
|
|
583
|
+
const units = this.getUnitsByTier(tier).filter((u) => !this.loaded.has(u.key));
|
|
584
|
+
return this.applyBudget(units, budget);
|
|
585
|
+
}
|
|
586
|
+
getUnitsByTier(tier) {
|
|
587
|
+
return [...this.units.values()].filter((u) => u.tier === tier);
|
|
588
|
+
}
|
|
589
|
+
getBudget(tier) {
|
|
590
|
+
return this.config.budgets[tier];
|
|
591
|
+
}
|
|
592
|
+
applyBudget(units, budget) {
|
|
593
|
+
const selected = [];
|
|
594
|
+
const dropped = [];
|
|
595
|
+
let used = 0;
|
|
596
|
+
for (const unit of units) {
|
|
597
|
+
if (used + unit.tokenCost <= budget) {
|
|
598
|
+
selected.push({ ...this.hydrate(unit) });
|
|
599
|
+
this.loaded.add(unit.key);
|
|
600
|
+
used += unit.tokenCost;
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
dropped.push(unit);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
this.log(`Loaded ${selected.length} units (${used}/${budget}t). Dropped: ${dropped.length}`);
|
|
607
|
+
return { units: selected, totalTokens: used, budget, dropped };
|
|
608
|
+
}
|
|
609
|
+
hydrate(unit) {
|
|
610
|
+
if (unit.content)
|
|
611
|
+
return unit;
|
|
612
|
+
const fs = getFs();
|
|
613
|
+
if (!fs) {
|
|
614
|
+
unit.content = `<!-- Failed to read: ${unit.path} -->`;
|
|
615
|
+
return unit;
|
|
616
|
+
}
|
|
617
|
+
try {
|
|
618
|
+
unit.content = fs.readFileSync(unit.path, "utf-8");
|
|
619
|
+
}
|
|
620
|
+
catch {
|
|
621
|
+
unit.content = `<!-- Failed to read: ${unit.path} -->`;
|
|
622
|
+
}
|
|
623
|
+
return unit;
|
|
624
|
+
}
|
|
625
|
+
scoreRelevance(unit, taskTags) {
|
|
626
|
+
const unitTagSet = new Set([...unit.tags, ...unit.triggers]);
|
|
627
|
+
const overlap = taskTags.filter((t) => unitTagSet.has(t)).length;
|
|
628
|
+
const tagScore = overlap / Math.max(taskTags.length, 1);
|
|
629
|
+
const rawAge = Date.now() - new Date(unit.updatedAt).getTime();
|
|
630
|
+
const ageMs = isNaN(rawAge) ? 0 : rawAge;
|
|
631
|
+
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
|
632
|
+
const recencyScore = unit.type === "episodic" ? Math.max(0, 1 - ageDays / 30) : 0;
|
|
633
|
+
return tagScore * 0.8 + recencyScore * 0.2;
|
|
634
|
+
}
|
|
635
|
+
extractTagsFromString(text) {
|
|
636
|
+
return (text
|
|
637
|
+
.toLowerCase()
|
|
638
|
+
.match(/\b[a-z]{3,}\b/g)
|
|
639
|
+
?.filter((w) => !STOPWORDS.has(w))
|
|
640
|
+
.slice(0, 20) ?? []);
|
|
641
|
+
}
|
|
642
|
+
injectFrontmatter(content, type, relPath) {
|
|
643
|
+
const tier = inferTierFromPath(relPath, type);
|
|
644
|
+
const tags = this.extractTagsFromContent(content);
|
|
645
|
+
const summary = this.extractSummary(content);
|
|
646
|
+
return [
|
|
647
|
+
"---",
|
|
648
|
+
`type: ${type}`,
|
|
649
|
+
`tier: ${tier}`,
|
|
650
|
+
`tags: ${tags.join(", ")}`,
|
|
651
|
+
`summary: ${summary.slice(0, 100)}`,
|
|
652
|
+
`created: ${new Date().toISOString()}`,
|
|
653
|
+
"---",
|
|
654
|
+
"",
|
|
655
|
+
content,
|
|
656
|
+
].join("\n");
|
|
657
|
+
}
|
|
658
|
+
compactEpisodicIfNeeded() {
|
|
659
|
+
const fs = getFs();
|
|
660
|
+
const path = getPath();
|
|
661
|
+
if (!fs || !path)
|
|
662
|
+
return;
|
|
663
|
+
const recentDir = path.join(this.config.root, "episodic", "recent");
|
|
664
|
+
const archiveDir = path.join(this.config.root, "episodic", "archive");
|
|
665
|
+
if (!fs.existsSync(recentDir))
|
|
666
|
+
return;
|
|
667
|
+
const cutoff = new Date();
|
|
668
|
+
cutoff.setDate(cutoff.getDate() - this.config.episodicRecentDays);
|
|
669
|
+
let files;
|
|
670
|
+
try {
|
|
671
|
+
files = fs.readdirSync(recentDir).filter((f) => f.endsWith(".md"));
|
|
672
|
+
}
|
|
673
|
+
catch {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
let archived = false;
|
|
677
|
+
for (const file of files) {
|
|
678
|
+
const dateMatch = file.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
679
|
+
if (!dateMatch)
|
|
680
|
+
continue;
|
|
681
|
+
// Parse as local-time midnight to match the local-time cutoff.
|
|
682
|
+
// new Date("YYYY-MM-DD") parses as UTC midnight which is off by up to
|
|
683
|
+
// ±24h relative to the local-time cutoff in non-UTC timezones.
|
|
684
|
+
const [y, m, d] = dateMatch[1].split("-").map(Number);
|
|
685
|
+
const fileDate = new Date(y, m - 1, d);
|
|
686
|
+
if (fileDate < cutoff) {
|
|
687
|
+
try {
|
|
688
|
+
const src = path.join(recentDir, file);
|
|
689
|
+
const dest = path.join(archiveDir, file);
|
|
690
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
691
|
+
fs.renameSync(src, dest);
|
|
692
|
+
const oldKey = path.relative(this.config.root, src);
|
|
693
|
+
const newKey = path.relative(this.config.root, dest);
|
|
694
|
+
this.units.delete(oldKey);
|
|
695
|
+
this.registerFile(dest, newKey);
|
|
696
|
+
this.log(`Archived episodic: ${file}`);
|
|
697
|
+
archived = true;
|
|
698
|
+
}
|
|
699
|
+
catch {
|
|
700
|
+
// Skip if move fails
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (archived && this.config.autoIndex) {
|
|
705
|
+
this.rebuildIndex(recentDir);
|
|
706
|
+
this.rebuildIndex(archiveDir);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
log(msg) {
|
|
710
|
+
if (this.config.debug)
|
|
711
|
+
console.log(`[MemoryLoader] ${msg}`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
//# sourceMappingURL=memory-loader.js.map
|