dsh-mnemon-source-runtime 0.5.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js ADDED
@@ -0,0 +1,889 @@
1
+ import { n as RUNTIME_MEMORY_LIMITS, o as MAX_RUNTIME_MEMORY_LIMIT_BYTES, t as RUNTIME_ENTRY_DELIMITER } from "./contracts-BqaPCrGD.js";
2
+ import { createMemoryMutationReceipt, defineMemoryPlugin, defineMemorySource, installMemory, memoryConfigurationDigest, memoryInputRecord, memoryInputStringArray, memoryInputText, truncateMemoryText } from "dsh-mnemon/extension-sdk";
3
+ import { homedir } from "node:os";
4
+ import { basename, isAbsolute, join } from "node:path";
5
+ import z from "schemastery";
6
+ import { COMPOSABLE_MEMORY_API_VERSION } from "dsh-mnemon/contracts";
7
+ import { execFileSync } from "node:child_process";
8
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
9
+ import { createHash } from "node:crypto";
10
+ //#region src/config.ts
11
+ const Config = z.object({
12
+ dataDir: z.string(),
13
+ userDataDir: z.string(),
14
+ memoryLimitBytes: z.number().step(1).min(1).max(MAX_RUNTIME_MEMORY_LIMIT_BYTES),
15
+ userLimitBytes: z.number().step(1).min(1).max(MAX_RUNTIME_MEMORY_LIMIT_BYTES)
16
+ });
17
+ function runtimeSourceConfig(value, instanceKey) {
18
+ const config = Config(value);
19
+ const dataDir = config.dataDir ?? join(homedir(), ".mnemon", "sources", encodeURIComponent(instanceKey));
20
+ const userDataDir = config.userDataDir ?? dataDir;
21
+ if (!isAbsolute(dataDir) || !isAbsolute(userDataDir)) throw new Error("Runtime Source dataDir and userDataDir must be absolute");
22
+ return {
23
+ dataDir,
24
+ userDataDir,
25
+ memoryLimitBytes: config.memoryLimitBytes ?? 10240,
26
+ userLimitBytes: config.userLimitBytes ?? 4096
27
+ };
28
+ }
29
+ //#endregion
30
+ //#region src/git-branch.ts
31
+ const GIT_BRANCH_TIMEOUT_MS = 2e3;
32
+ /**
33
+ * Resolve the current git branch of a workspace root. Returns undefined when
34
+ * the directory is not a git working tree, HEAD is detached, or the probe
35
+ * fails or times out, so callers fall back to the unfiltered view.
36
+ */
37
+ function resolveGitBranch(cwd) {
38
+ const root = cwd?.trim();
39
+ if (root === void 0 || root === "") return void 0;
40
+ try {
41
+ const branch = execFileSync("git", [
42
+ "-C",
43
+ root,
44
+ "branch",
45
+ "--show-current"
46
+ ], {
47
+ encoding: "utf8",
48
+ timeout: GIT_BRANCH_TIMEOUT_MS,
49
+ stdio: [
50
+ "ignore",
51
+ "pipe",
52
+ "ignore"
53
+ ]
54
+ }).trim();
55
+ return branch === "" ? void 0 : branch;
56
+ } catch {
57
+ return;
58
+ }
59
+ }
60
+ //#endregion
61
+ //#region src/controller.ts
62
+ const LOCK_TIMEOUT_MS = 5e3;
63
+ const LOCK_STALE_MS = 3e4;
64
+ const LOCK_RETRY_MS = 20;
65
+ const MAX_ENTRY_BYTES = 8192;
66
+ var RuntimeMemoryCapacityError = class extends Error {
67
+ target;
68
+ used;
69
+ projected;
70
+ limit;
71
+ code = "runtime-capacity";
72
+ constructor(target, used, projected, limit) {
73
+ super(`Would exceed ${target} runtime memory capacity: ${projected} bytes (current ${used}, limit ${limit}). Archive and compact runtime memory before retrying.`);
74
+ this.target = target;
75
+ this.used = used;
76
+ this.projected = projected;
77
+ this.limit = limit;
78
+ this.name = "RuntimeMemoryCapacityError";
79
+ }
80
+ };
81
+ var RuntimeMemoryConflictError = class extends Error {
82
+ code = "revision-conflict";
83
+ constructor() {
84
+ super("runtime memory changed while archival was running; no compacted data was applied");
85
+ this.name = "RuntimeMemoryConflictError";
86
+ }
87
+ };
88
+ function isRecord(value) {
89
+ return typeof value === "object" && value !== null && !Array.isArray(value);
90
+ }
91
+ function isTarget(value) {
92
+ return value === "memory" || value === "user";
93
+ }
94
+ function isImportance(value) {
95
+ return value === "critical" || value === "normal" || value === "low";
96
+ }
97
+ const RUNTIME_BRANCH_NAME_RE = /^[A-Za-z0-9._/-]+$/u;
98
+ /** Validate a model-supplied branch scope. Returns the normalized list; an empty list means "no scope". */
99
+ function normalizeRuntimeBranches(value, field = "branches") {
100
+ if (!Array.isArray(value)) throw new Error(`${field} must be an array of git branch names`);
101
+ const branches = [];
102
+ const seen = /* @__PURE__ */ new Set();
103
+ for (const item of value) {
104
+ if (typeof item !== "string") throw new Error(`${field} must contain only git branch names`);
105
+ const branch = item.trim();
106
+ if (branch === "" || branch.length > 128 || !RUNTIME_BRANCH_NAME_RE.test(branch) || branch === "-" || branch.startsWith("/") || branch.startsWith("-") || branch.endsWith("/") || branch.endsWith(".") || branch.includes("..") || branch.includes("//") || branch.includes("@{")) throw new Error(`${field} must contain git branch names (letters, numbers, dot, underscore, slash, dash)`);
107
+ if (seen.has(branch)) throw new Error(`${field} must not repeat a branch name`);
108
+ seen.add(branch);
109
+ branches.push(branch);
110
+ }
111
+ if (branches.length > 16) throw new Error(`${field} supports at most 16 branch names`);
112
+ return branches;
113
+ }
114
+ /** Parse a stored branch scope. Returns undefined for absent or invalid data. */
115
+ function parseRuntimeBranches(value) {
116
+ if (value === void 0) return void 0;
117
+ if (!Array.isArray(value)) return void 0;
118
+ const branches = [];
119
+ for (const item of value) {
120
+ if (typeof item !== "string" || item === "" || item.length > 128 || !RUNTIME_BRANCH_NAME_RE.test(item)) return void 0;
121
+ branches.push(item);
122
+ }
123
+ return branches;
124
+ }
125
+ function entryMatchesBranch(entry, branch) {
126
+ return entry.branches === void 0 || entry.branches.length === 0 || entry.branches.includes(branch);
127
+ }
128
+ function scopeBranch(branch) {
129
+ const value = branch?.trim();
130
+ return value === void 0 || value === "" ? void 0 : value;
131
+ }
132
+ function normalizeContent(value, field) {
133
+ const content = value?.trim().replace(/\s+/gu, " ") ?? "";
134
+ if (content === "") throw new Error(`${field} is required`);
135
+ if (content.includes("§")) throw new Error(`${field} must not contain the reserved § entry delimiter`);
136
+ const bytes = Buffer.byteLength(content, "utf8");
137
+ if (bytes > MAX_ENTRY_BYTES) throw new Error(`${field} is too large (${bytes} bytes; max ${MAX_ENTRY_BYTES})`);
138
+ return content;
139
+ }
140
+ function parseEntry(value) {
141
+ if (!isRecord(value) || typeof value.content !== "string" || !isTarget(value.target) || !isImportance(value.importance)) return void 0;
142
+ if (typeof value.created_at !== "string" || typeof value.updated_at !== "string") return void 0;
143
+ const content = value.content.trim().replace(/\s+/gu, " ");
144
+ if (content === "" || content.includes("§")) return void 0;
145
+ if (value.target === "user" && value.branches !== void 0) return void 0;
146
+ const branches = parseRuntimeBranches(value.branches);
147
+ return {
148
+ content,
149
+ created_at: value.created_at,
150
+ updated_at: value.updated_at,
151
+ target: value.target,
152
+ importance: value.importance,
153
+ ...branches === void 0 ? {} : { branches }
154
+ };
155
+ }
156
+ function byteCount(entries, target) {
157
+ const content = entries.filter((entry) => entry.target === target).map((entry) => entry.content).join(RUNTIME_ENTRY_DELIMITER);
158
+ return Buffer.byteLength(content, "utf8");
159
+ }
160
+ function markdown(entries, target) {
161
+ const content = entries.filter((entry) => entry.target === target).map((entry) => entry.content).join(RUNTIME_ENTRY_DELIMITER);
162
+ return content === "" ? "" : `${content}\n`;
163
+ }
164
+ function revision(file) {
165
+ return createHash("sha256").update(JSON.stringify(file)).digest("hex");
166
+ }
167
+ function prepareMutation(before, request, now) {
168
+ if (!isTarget(request.target)) throw new Error("target must be memory or user");
169
+ if (![
170
+ "add",
171
+ "replace",
172
+ "remove"
173
+ ].includes(request.action)) throw new Error("action must be add, replace, or remove");
174
+ if (request.importance !== void 0 && !isImportance(request.importance)) throw new Error("importance must be critical, normal, or low");
175
+ if (request.target === "user" && request.branches !== void 0) throw new Error("branches applies to target=memory only");
176
+ const branchScope = request.branches === void 0 ? void 0 : normalizeRuntimeBranches(request.branches);
177
+ const entries = before.map((entry) => ({ ...entry }));
178
+ if (request.action === "add") {
179
+ const content = normalizeContent(request.content, "content");
180
+ const duplicate = entries.find((entry) => entry.target === request.target && entry.content === content);
181
+ if (duplicate !== void 0) return {
182
+ changed: false,
183
+ projectedEntries: entries,
184
+ compactableEntries: entries.filter((entry) => entry.target === request.target),
185
+ fields: {
186
+ message: "Entry already exists (no duplicate added).",
187
+ added: duplicate.content
188
+ }
189
+ };
190
+ const pendingEntry = {
191
+ content,
192
+ created_at: now,
193
+ updated_at: now,
194
+ target: request.target,
195
+ importance: request.importance ?? "normal",
196
+ ...branchScope !== void 0 && branchScope.length > 0 ? { branches: branchScope } : {}
197
+ };
198
+ return {
199
+ changed: true,
200
+ projectedEntries: [...entries, pendingEntry],
201
+ compactableEntries: entries.filter((entry) => entry.target === request.target),
202
+ pendingEntry,
203
+ fields: {
204
+ message: "Entry added.",
205
+ added: content
206
+ }
207
+ };
208
+ }
209
+ const oldText = normalizeContent(request.oldText, "oldText");
210
+ const matches = entries.map((entry, index) => entry.target === request.target && entry.content.includes(oldText) ? index : -1).filter((index) => index >= 0);
211
+ if (matches.length === 0) throw new Error(`No ${request.target} entry contains ${JSON.stringify(oldText)}.`);
212
+ if (matches.length > 1) throw new Error(`Multiple ${request.target} entries contain ${JSON.stringify(oldText)}; use a unique substring.`);
213
+ const index = matches[0];
214
+ const previous = entries[index];
215
+ const compactableEntries = entries.filter((entry, entryIndex) => entry.target === request.target && entryIndex !== index);
216
+ if (request.action === "replace") {
217
+ const content = normalizeContent(request.content, "content");
218
+ const pendingEntry = {
219
+ ...previous,
220
+ content,
221
+ updated_at: now,
222
+ importance: request.importance ?? previous.importance
223
+ };
224
+ if (branchScope !== void 0) {
225
+ if (branchScope.length === 0) delete pendingEntry.branches;
226
+ else pendingEntry.branches = branchScope;
227
+ }
228
+ entries[index] = pendingEntry;
229
+ return {
230
+ changed: true,
231
+ projectedEntries: entries,
232
+ compactableEntries,
233
+ pendingEntry,
234
+ excludedEntry: previous,
235
+ fields: {
236
+ message: "Entry replaced.",
237
+ replaced: {
238
+ from: previous.content,
239
+ to: content
240
+ }
241
+ }
242
+ };
243
+ }
244
+ return {
245
+ changed: true,
246
+ projectedEntries: entries.filter((_, entryIndex) => entryIndex !== index),
247
+ compactableEntries,
248
+ excludedEntry: previous,
249
+ fields: {
250
+ message: "Entry removed.",
251
+ removed: previous.content
252
+ }
253
+ };
254
+ }
255
+ function compactionCandidates(compacted, existing, target, now) {
256
+ const seen = /* @__PURE__ */ new Set();
257
+ return compacted.map((entry) => {
258
+ const content = normalizeContent(entry.content, "compacted content");
259
+ if (!isImportance(entry.importance)) throw new Error("compacted importance must be critical, normal, or low");
260
+ if (seen.has(content)) throw new Error("compacted runtime memory contains duplicate entries");
261
+ seen.add(content);
262
+ const unchanged = existing.find((current) => current.content === content);
263
+ const inheritedBranches = entry.branches ?? unchanged?.branches;
264
+ return {
265
+ content,
266
+ created_at: unchanged?.created_at ?? now,
267
+ updated_at: unchanged?.updated_at ?? now,
268
+ target,
269
+ importance: entry.importance,
270
+ ...inheritedBranches === void 0 ? {} : { branches: inheritedBranches }
271
+ };
272
+ });
273
+ }
274
+ function packCompactionCandidates(replacements, target, maxBytes) {
275
+ const priority = {
276
+ critical: 0,
277
+ normal: 1,
278
+ low: 2
279
+ };
280
+ const ranked = replacements.map((entry, index) => ({
281
+ entry,
282
+ index
283
+ })).sort((left, right) => priority[left.entry.importance] - priority[right.entry.importance] || left.index - right.index);
284
+ const selected = /* @__PURE__ */ new Set();
285
+ const packed = [];
286
+ for (const candidate of ranked) {
287
+ if (byteCount([...packed, candidate.entry], target) > maxBytes) continue;
288
+ packed.push(candidate.entry);
289
+ selected.add(candidate.index);
290
+ }
291
+ return replacements.filter((_, index) => selected.has(index));
292
+ }
293
+ function sleepSync(milliseconds) {
294
+ const buffer = new Int32Array(new SharedArrayBuffer(4));
295
+ Atomics.wait(buffer, 0, 0, milliseconds);
296
+ }
297
+ /**
298
+ * Single authority for hot memory. JSON is the durable source of truth;
299
+ * Markdown files are deterministic projections consumed by prompt assembly.
300
+ */
301
+ var RuntimeMemoryController = class RuntimeMemoryController {
302
+ now;
303
+ directory;
304
+ sourcePath;
305
+ userSourcePath;
306
+ memoryPath;
307
+ userPath;
308
+ lockPath;
309
+ limits;
310
+ queue = Promise.resolve();
311
+ localUserPath;
312
+ userController;
313
+ constructor(runner, now = () => /* @__PURE__ */ new Date(), limits = RUNTIME_MEMORY_LIMITS, userRunner) {
314
+ this.now = now;
315
+ for (const [target, limit] of Object.entries(limits)) if (!Number.isInteger(limit) || limit < 1 || limit > 1048576) throw new Error(`runtime ${target} memory limit must be an integer within 1..${MAX_RUNTIME_MEMORY_LIMIT_BYTES} bytes`);
316
+ this.limits = {
317
+ memory: limits.memory,
318
+ user: limits.user
319
+ };
320
+ this.directory = join(runner.effectiveDataDir(), "runtime");
321
+ this.sourcePath = join(this.directory, "memories.json");
322
+ this.memoryPath = join(this.directory, "MEMORY.md");
323
+ this.localUserPath = join(this.directory, "USER.md");
324
+ this.lockPath = join(this.directory, ".memories.lock");
325
+ const userDirectory = userRunner === void 0 ? this.directory : join(userRunner.effectiveDataDir(), "runtime");
326
+ this.userController = userDirectory === this.directory || userRunner === void 0 ? void 0 : new RuntimeMemoryController(userRunner, now, this.limits);
327
+ this.userPath = this.userController?.userPath ?? this.localUserPath;
328
+ this.userSourcePath = this.userController?.sourcePath ?? this.sourcePath;
329
+ this.initialize();
330
+ }
331
+ snapshot() {
332
+ const local = this.readSource();
333
+ if (this.userController === void 0) return this.snapshotUnlocked(local);
334
+ const global = this.userController.readSource();
335
+ return this.snapshotUnlocked({
336
+ version: 1,
337
+ entries: [...global.entries.filter((entry) => entry.target === "user"), ...local.entries.filter((entry) => entry.target === "memory")]
338
+ });
339
+ }
340
+ contextText(branch) {
341
+ return this.contextProjection(branch).text;
342
+ }
343
+ /**
344
+ * Read the exact Runtime revision and its prompt projection from each
345
+ * configured authority root.
346
+ * When a git branch is supplied, target=memory entries scoped to other
347
+ * branches are hidden from the projection; the on-disk Markdown files and
348
+ * the source JSON always remain complete.
349
+ */
350
+ contextProjection(branch) {
351
+ const branchScope = scopeBranch(branch);
352
+ const local = this.localContextProjection(branchScope);
353
+ const global = this.userController?.localContextProjection();
354
+ const user = global?.user ?? local.user;
355
+ const memory = local.memory;
356
+ const entries = global === void 0 ? local.snapshot.entries : [...global.snapshot.entries.filter((entry) => entry.target === "user"), ...local.snapshot.entries.filter((entry) => entry.target === "memory")];
357
+ const visibleMemory = local.snapshot.targets.memory;
358
+ const visibleUser = global?.snapshot.targets.user ?? local.snapshot.targets.user;
359
+ const snapshot = global === void 0 ? local.snapshot : {
360
+ directory: this.directory,
361
+ sourcePath: this.sourcePath,
362
+ revision: revision({
363
+ version: 1,
364
+ entries
365
+ }),
366
+ generatedAt: this.now().toISOString(),
367
+ entries,
368
+ targets: {
369
+ memory: {
370
+ ...visibleMemory,
371
+ markdownPath: this.memoryPath
372
+ },
373
+ user: {
374
+ ...visibleUser,
375
+ markdownPath: this.userPath
376
+ }
377
+ }
378
+ };
379
+ const storeMemory = this.targetView(entries, "memory");
380
+ const storeUser = this.targetView(entries, "user");
381
+ const visibleEntries = branchScope === void 0 ? entries : entries.filter((entry) => entry.target === "user" || entryMatchesBranch(entry, branchScope));
382
+ const branchLine = branchScope === void 0 ? "" : `\nGit branch: ${branchScope}${local.hidden > 0 ? ` (${local.hidden} branch-scoped entr${local.hidden === 1 ? "y" : "ies"} hidden)` : ""}`;
383
+ return {
384
+ revision: snapshot.revision,
385
+ entries: visibleEntries.map((entry) => ({
386
+ ...entry,
387
+ ...entry.branches === void 0 ? {} : { branches: [...entry.branches] }
388
+ })),
389
+ totalEntries: entries.length,
390
+ text: `MNEMON RUNTIME MEMORY SNAPSHOT
391
+ Revision: ${snapshot.revision}${branchLine}
392
+
393
+ Contents of USER.md (user profile; entries: ${visibleUser.entryCount}; UTF-8 bytes: ${storeUser.used}/${storeUser.limit})
394
+ <runtime-memory-file name="USER.md">
395
+ ${user || "(empty)"}
396
+ </runtime-memory-file>
397
+
398
+ Contents of MEMORY.md (working reference; entries: ${visibleMemory.entryCount}; UTF-8 bytes: ${storeMemory.used}/${storeMemory.limit})
399
+ <runtime-memory-file name="MEMORY.md">
400
+ ${memory || "(empty)"}
401
+ </runtime-memory-file>`
402
+ };
403
+ }
404
+ localContextProjection(branch) {
405
+ const branchScope = scopeBranch(branch);
406
+ return this.withLock(() => {
407
+ const file = this.readSource();
408
+ this.repairProjections(file);
409
+ const entries = file.entries.map((entry) => ({ ...entry }));
410
+ const visible = branchScope === void 0 ? entries : entries.filter((entry) => entry.target === "user" || entryMatchesBranch(entry, branchScope));
411
+ return {
412
+ snapshot: {
413
+ directory: this.directory,
414
+ sourcePath: this.sourcePath,
415
+ revision: revision(file),
416
+ generatedAt: this.now().toISOString(),
417
+ entries,
418
+ targets: {
419
+ memory: this.targetView(visible, "memory"),
420
+ user: this.targetView(visible, "user")
421
+ }
422
+ },
423
+ user: readFileSync(this.localUserPath, "utf8").trimEnd(),
424
+ memory: branchScope === void 0 ? readFileSync(this.memoryPath, "utf8").trimEnd() : markdown(visible, "memory").trimEnd(),
425
+ hidden: branchScope === void 0 ? 0 : entries.length - visible.length
426
+ };
427
+ });
428
+ }
429
+ mutate(request) {
430
+ if (request.target === "user" && this.userController !== void 0) return this.userController.mutate(request);
431
+ const operation = this.queue.then(() => this.withLock(() => this.mutateLocked(request)));
432
+ this.queue = operation.catch(() => void 0);
433
+ return operation;
434
+ }
435
+ /** Resolve exactly which committed entries survive a blocked mutation and are safe to compact. */
436
+ planMaintenance(request) {
437
+ if (request.target === "user" && this.userController !== void 0) return this.userController.planMaintenance(request);
438
+ const operation = this.queue.then(() => this.withLock(() => {
439
+ const file = this.readSource();
440
+ const prepared = prepareMutation(file.entries, request, this.now().toISOString());
441
+ const used = byteCount(file.entries, request.target);
442
+ const projected = byteCount(prepared.projectedEntries, request.target);
443
+ const limit = this.limits[request.target];
444
+ return {
445
+ revision: revision(file),
446
+ action: request.action,
447
+ target: request.target,
448
+ entries: prepared.compactableEntries.map((entry) => ({ ...entry })),
449
+ ...prepared.pendingEntry === void 0 ? {} : { pending: {
450
+ content: prepared.pendingEntry.content,
451
+ importance: prepared.pendingEntry.importance
452
+ } },
453
+ ...prepared.excludedEntry === void 0 ? {} : { excluded: { ...prepared.excludedEntry } },
454
+ used,
455
+ projected,
456
+ limit,
457
+ requiresMaintenance: prepared.changed && projected > limit
458
+ };
459
+ }));
460
+ this.queue = operation.catch(() => void 0);
461
+ return operation;
462
+ }
463
+ /** Commit semantic compaction and the original mutation together, or leave every local file unchanged. */
464
+ compactAndMutate(expectedRevision, request, compacted, maxCompactedBytes) {
465
+ if (request.target === "user" && this.userController !== void 0) return this.userController.compactAndMutate(expectedRevision, request, compacted, maxCompactedBytes);
466
+ const operation = this.queue.then(() => this.withLock(() => {
467
+ const file = this.readSource();
468
+ if (revision(file) !== expectedRevision) throw new RuntimeMemoryConflictError();
469
+ const now = this.now().toISOString();
470
+ const prepared = prepareMutation(file.entries, request, now);
471
+ const compactedByteBudget = maxCompactedBytes ?? this.limits[request.target];
472
+ if (!Number.isInteger(compactedByteBudget) || compactedByteBudget < 0 || compactedByteBudget > this.limits[request.target]) throw new Error("compaction byte budget is invalid");
473
+ if (!prepared.changed) return this.result(request.target, prepared.projectedEntries, prepared.fields);
474
+ const replacements = compactionCandidates(compacted, prepared.compactableEntries, request.target, now);
475
+ if (prepared.pendingEntry !== void 0 && replacements.some((entry) => entry.content === prepared.pendingEntry.content)) throw new Error("compacted runtime memory duplicates the pending mutation");
476
+ if (prepared.excludedEntry !== void 0 && replacements.some((entry) => entry.content === prepared.excludedEntry.content)) throw new Error("compacted runtime memory reintroduces the replaced or removed entry");
477
+ const targetEntries = [...packCompactionCandidates(replacements, request.target, compactedByteBudget), ...prepared.pendingEntry === void 0 ? [] : [prepared.pendingEntry]];
478
+ const entries = [...file.entries.filter((entry) => entry.target !== request.target), ...targetEntries];
479
+ const used = byteCount(entries, request.target);
480
+ const limit = this.limits[request.target];
481
+ if (used > limit) throw new RuntimeMemoryCapacityError(request.target, byteCount(file.entries, request.target), used, limit);
482
+ const next = {
483
+ version: 1,
484
+ entries
485
+ };
486
+ this.persist(next);
487
+ return this.result(request.target, entries, prepared.fields);
488
+ }));
489
+ this.queue = operation.catch(() => void 0);
490
+ return operation;
491
+ }
492
+ /** Apply an LLM-produced compaction only to the exact snapshot it reviewed. */
493
+ compactTarget(expectedRevision, target, compacted, maxBytes) {
494
+ if (target === "user" && this.userController !== void 0) return this.userController.compactTarget(expectedRevision, target, compacted, maxBytes).then(() => this.snapshot());
495
+ const operation = this.queue.then(() => this.withLock(() => {
496
+ const file = this.readSource();
497
+ if (revision(file) !== expectedRevision) throw new RuntimeMemoryConflictError();
498
+ const byteBudget = maxBytes ?? this.limits[target];
499
+ if (!Number.isInteger(byteBudget) || byteBudget < 0 || byteBudget > this.limits[target]) throw new Error("compaction byte budget is invalid");
500
+ const now = this.now().toISOString();
501
+ const fitted = packCompactionCandidates(compactionCandidates(compacted, file.entries.filter((entry) => entry.target === target), target, now), target, byteBudget);
502
+ const entries = [...file.entries.filter((entry) => entry.target !== target), ...fitted];
503
+ const used = byteCount(entries, target);
504
+ const limit = this.limits[target];
505
+ if (used > limit) throw new RuntimeMemoryCapacityError(target, byteCount(file.entries, target), used, limit);
506
+ this.persist({
507
+ version: 1,
508
+ entries
509
+ });
510
+ return this.snapshotUnlocked({
511
+ version: 1,
512
+ entries
513
+ });
514
+ }));
515
+ this.queue = operation.catch(() => void 0);
516
+ return this.userController === void 0 ? operation : operation.then(() => this.snapshot());
517
+ }
518
+ initialize() {
519
+ mkdirSync(this.directory, {
520
+ recursive: true,
521
+ mode: 448
522
+ });
523
+ this.withLock(() => {
524
+ const file = this.readSource();
525
+ this.persist(file);
526
+ });
527
+ }
528
+ mutateLocked(request) {
529
+ const file = this.readSource();
530
+ const prepared = prepareMutation(file.entries, request, this.now().toISOString());
531
+ if (!prepared.changed) return this.result(request.target, prepared.projectedEntries, prepared.fields);
532
+ const used = byteCount(prepared.projectedEntries, request.target);
533
+ const limit = this.limits[request.target];
534
+ if (used > limit) throw new RuntimeMemoryCapacityError(request.target, byteCount(file.entries, request.target), used, limit);
535
+ this.persist({
536
+ version: 1,
537
+ entries: prepared.projectedEntries
538
+ });
539
+ return this.result(request.target, prepared.projectedEntries, prepared.fields);
540
+ }
541
+ result(target, entries, fields) {
542
+ return {
543
+ success: true,
544
+ message: fields.message,
545
+ target,
546
+ entryCount: entries.filter((entry) => entry.target === target).length,
547
+ usage: {
548
+ used: byteCount(entries, target),
549
+ limit: this.limits[target]
550
+ },
551
+ ...fields.added === void 0 ? {} : { added: fields.added },
552
+ ...fields.replaced === void 0 ? {} : { replaced: fields.replaced },
553
+ ...fields.removed === void 0 ? {} : { removed: fields.removed }
554
+ };
555
+ }
556
+ targetView(entries, target) {
557
+ return {
558
+ target,
559
+ entryCount: entries.filter((entry) => entry.target === target).length,
560
+ used: byteCount(entries, target),
561
+ limit: this.limits[target],
562
+ markdownPath: target === "memory" ? this.memoryPath : this.userPath
563
+ };
564
+ }
565
+ snapshotUnlocked(file) {
566
+ const entries = file.entries.map((entry) => ({ ...entry }));
567
+ return {
568
+ directory: this.directory,
569
+ sourcePath: this.sourcePath,
570
+ revision: revision(file),
571
+ generatedAt: this.now().toISOString(),
572
+ entries,
573
+ targets: {
574
+ memory: this.targetView(entries, "memory"),
575
+ user: this.targetView(entries, "user")
576
+ }
577
+ };
578
+ }
579
+ readSource() {
580
+ if (!existsSync(this.sourcePath)) return {
581
+ version: 1,
582
+ entries: []
583
+ };
584
+ let parsed;
585
+ try {
586
+ parsed = JSON.parse(readFileSync(this.sourcePath, "utf8"));
587
+ } catch (error) {
588
+ throw new Error(`runtime memories.json is unreadable: ${error instanceof Error ? error.message : String(error)}`);
589
+ }
590
+ if (!isRecord(parsed) || parsed.version !== 1 || !Array.isArray(parsed.entries)) throw new Error(`runtime memories.json must use version 1`);
591
+ const entries = parsed.entries.map(parseEntry);
592
+ if (entries.some((entry) => entry === void 0)) throw new Error("runtime memories.json contains an invalid entry");
593
+ return {
594
+ version: 1,
595
+ entries
596
+ };
597
+ }
598
+ persist(file) {
599
+ mkdirSync(this.directory, {
600
+ recursive: true,
601
+ mode: 448
602
+ });
603
+ const nonce = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
604
+ const writes = [
605
+ [this.localUserPath, markdown(file.entries, "user")],
606
+ [this.memoryPath, markdown(file.entries, "memory")],
607
+ [this.sourcePath, `${JSON.stringify(file, null, 2)}\n`]
608
+ ];
609
+ const temporaries = writes.map(([path]) => join(this.directory, `.${basename(path)}.${nonce}.tmp`));
610
+ try {
611
+ writes.forEach(([, content], index) => writeFileSync(temporaries[index], content, {
612
+ encoding: "utf8",
613
+ mode: 384
614
+ }));
615
+ writes.forEach(([path], index) => renameSync(temporaries[index], path));
616
+ } finally {
617
+ for (const temporary of temporaries) rmSync(temporary, { force: true });
618
+ }
619
+ }
620
+ repairProjections(file) {
621
+ for (const [path, target] of [[this.localUserPath, "user"], [this.memoryPath, "memory"]]) {
622
+ const expected = markdown(file.entries, target);
623
+ let current;
624
+ try {
625
+ current = readFileSync(path, "utf8");
626
+ } catch {
627
+ current = void 0;
628
+ }
629
+ if (current === expected) continue;
630
+ const temporary = join(this.directory, `.${basename(path)}.${process.pid}.${Date.now()}.tmp`);
631
+ try {
632
+ writeFileSync(temporary, expected, {
633
+ encoding: "utf8",
634
+ mode: 384
635
+ });
636
+ renameSync(temporary, path);
637
+ } finally {
638
+ rmSync(temporary, { force: true });
639
+ }
640
+ }
641
+ }
642
+ withLock(callback) {
643
+ const started = Date.now();
644
+ let descriptor;
645
+ while (descriptor === void 0) try {
646
+ descriptor = openSync(this.lockPath, "wx", 384);
647
+ } catch (error) {
648
+ if ((isRecord(error) && typeof error.code === "string" ? error.code : void 0) !== "EEXIST") throw error;
649
+ try {
650
+ if (Date.now() - statSync(this.lockPath).mtimeMs > LOCK_STALE_MS) {
651
+ rmSync(this.lockPath, { force: true });
652
+ continue;
653
+ }
654
+ } catch {
655
+ continue;
656
+ }
657
+ if (Date.now() - started >= LOCK_TIMEOUT_MS) throw new Error("timed out waiting for the runtime memory controller lock");
658
+ sleepSync(LOCK_RETRY_MS);
659
+ }
660
+ try {
661
+ return callback();
662
+ } finally {
663
+ closeSync(descriptor);
664
+ rmSync(this.lockPath, { force: true });
665
+ }
666
+ }
667
+ };
668
+ //#endregion
669
+ //#region src/source.ts
670
+ const ACTIONS = /* @__PURE__ */ new Set([
671
+ "add",
672
+ "replace",
673
+ "remove"
674
+ ]);
675
+ const TARGETS = /* @__PURE__ */ new Set(["memory", "user"]);
676
+ const IMPORTANCE = /* @__PURE__ */ new Set([
677
+ "critical",
678
+ "normal",
679
+ "low"
680
+ ]);
681
+ function runtimeMutation(value) {
682
+ const input = memoryInputRecord(value, "Runtime Memory mutation");
683
+ const action = memoryInputText(input.action, "action", 20);
684
+ const target = memoryInputText(input.target, "target", 20);
685
+ if (!ACTIONS.has(action)) throw new Error(`unsupported Runtime Memory action: ${action}`);
686
+ if (!TARGETS.has(target)) throw new Error(`unsupported Runtime Memory target: ${target}`);
687
+ const importance = memoryInputText(input.importance, "importance", 20, false);
688
+ if (importance !== void 0 && !IMPORTANCE.has(importance)) throw new Error(`unsupported Runtime Memory importance: ${importance}`);
689
+ return {
690
+ action,
691
+ target,
692
+ ...memoryInputText(input.content, "content", 1e5, false) === void 0 ? {} : { content: memoryInputText(input.content, "content", 1e5, false) },
693
+ ...memoryInputText(input.oldText ?? input.old_text, "oldText", 1e5, false) === void 0 ? {} : { oldText: memoryInputText(input.oldText ?? input.old_text, "oldText", 1e5, false) },
694
+ ...importance === void 0 ? {} : { importance },
695
+ ...input.branches === void 0 ? {} : { branches: memoryInputStringArray(input.branches, "branches", 100) ?? [] }
696
+ };
697
+ }
698
+ function createRuntimeMemorySource(config = {}) {
699
+ const configured = Object.freeze({ ...config });
700
+ return defineMemorySource({
701
+ manifest: {
702
+ apiVersion: COMPOSABLE_MEMORY_API_VERSION,
703
+ kind: "source",
704
+ typeId: "runtime",
705
+ packageName: "dsh-mnemon-source-runtime",
706
+ role: "working-context",
707
+ capabilities: [
708
+ "status",
709
+ "project",
710
+ "write"
711
+ ],
712
+ consistency: "exact-snapshot",
713
+ actions: [{
714
+ id: "mutate",
715
+ description: "Add, replace, or remove an entry in Runtime Memory.",
716
+ capability: "write",
717
+ inputSchema: {
718
+ type: "object",
719
+ required: ["action", "target"],
720
+ additionalProperties: false,
721
+ properties: {
722
+ action: {
723
+ type: "string",
724
+ enum: [
725
+ "add",
726
+ "replace",
727
+ "remove"
728
+ ]
729
+ },
730
+ target: {
731
+ type: "string",
732
+ enum: ["memory", "user"]
733
+ },
734
+ content: { type: "string" },
735
+ oldText: { type: "string" },
736
+ importance: {
737
+ type: "string",
738
+ enum: [
739
+ "critical",
740
+ "normal",
741
+ "low"
742
+ ]
743
+ },
744
+ branches: { type: "array" }
745
+ }
746
+ }
747
+ }],
748
+ management: {
749
+ label: "Runtime Memory",
750
+ description: "Exact, bounded working context and user profile projection."
751
+ }
752
+ },
753
+ create(context) {
754
+ const effective = runtimeSourceConfig({
755
+ ...context.configuration,
756
+ ...configured
757
+ }, context.sourceInstanceKey);
758
+ const controller = new RuntimeMemoryController({ effectiveDataDir: () => effective.dataDir }, void 0, {
759
+ memory: effective.memoryLimitBytes,
760
+ user: effective.userLimitBytes
761
+ }, { effectiveDataDir: () => effective.userDataDir });
762
+ const projection = (workspaceId) => controller.contextProjection(resolveGitBranch(workspaceId));
763
+ const prepared = /* @__PURE__ */ new WeakMap();
764
+ return {
765
+ facts(request) {
766
+ if (request.scenario.startsWith("management.")) return {
767
+ sourceInstanceKey: context.sourceInstanceKey,
768
+ sourceTypeId: "runtime",
769
+ role: "working-context",
770
+ availability: "ready",
771
+ revision: controller.snapshot().revision,
772
+ capabilities: [
773
+ "status",
774
+ "project",
775
+ "write"
776
+ ],
777
+ routeIds: [],
778
+ actionIds: ["mutate"]
779
+ };
780
+ const current = projection(request.scope.workspaceId);
781
+ prepared.set(request.scope, current);
782
+ return {
783
+ sourceInstanceKey: context.sourceInstanceKey,
784
+ sourceTypeId: "runtime",
785
+ role: "working-context",
786
+ availability: "ready",
787
+ revision: current.revision,
788
+ capabilities: [
789
+ "status",
790
+ "project",
791
+ "write"
792
+ ],
793
+ routeIds: [],
794
+ actionIds: ["mutate"]
795
+ };
796
+ },
797
+ project(request) {
798
+ if (!request.includeProjection) return { fragments: [] };
799
+ const current = prepared.get(request.scope) ?? projection(request.scope.workspaceId);
800
+ prepared.delete(request.scope);
801
+ if (current.revision !== request.expectedRevision) throw new Error("Runtime projection revision changed during composition");
802
+ return {
803
+ fragments: [{
804
+ id: `${context.sourceInstanceKey}/projection`,
805
+ sourceInstanceKey: context.sourceInstanceKey,
806
+ mode: request.mode,
807
+ text: truncateMemoryText(current.text, request.maxCharacters),
808
+ revision: current.revision,
809
+ provenance: { sourceTypeId: "runtime" }
810
+ }],
811
+ presentation: {
812
+ visibleItems: current.entries.length,
813
+ totalItems: current.totalEntries,
814
+ items: current.entries.slice(0, 24).map((entry, index) => ({
815
+ id: `${entry.target}:${entry.created_at}:${index}`,
816
+ title: truncateMemoryText(entry.content, 160)
817
+ }))
818
+ }
819
+ };
820
+ },
821
+ async manage(request) {
822
+ const input = request.input === null ? {} : memoryInputRecord(request.input, "Runtime Memory management");
823
+ if (request.mode === "read") {
824
+ let value;
825
+ if (request.operation === "snapshot") value = controller.snapshot();
826
+ else if (request.operation === "maintenance-plan") value = await controller.planMaintenance(runtimeMutation(request.input));
827
+ else throw new Error("unsupported Runtime management read operation: " + request.operation);
828
+ return {
829
+ revision: controller.snapshot().revision,
830
+ value
831
+ };
832
+ }
833
+ if (!request.confirmed) throw new Error("Runtime management mutation requires explicit confirmation");
834
+ let result;
835
+ if (request.operation === "mutate") result = await controller.mutate(runtimeMutation(request.input));
836
+ else if (request.operation === "compact-and-mutate") {
837
+ if (!Array.isArray(input.compacted)) throw new Error("compacted must be an array");
838
+ const compacted = input.compacted.map((value) => {
839
+ const entry = memoryInputRecord(value, "compacted entry");
840
+ const importance = memoryInputText(entry.importance, "importance", 20);
841
+ if (!IMPORTANCE.has(importance)) throw new Error("invalid compacted importance");
842
+ return {
843
+ content: memoryInputText(entry.content, "content", 1e5),
844
+ importance,
845
+ ...entry.branches === void 0 ? {} : { branches: memoryInputStringArray(entry.branches, "branches", 100) ?? [] }
846
+ };
847
+ });
848
+ result = await controller.compactAndMutate(memoryInputText(input.revision, "revision", 300), runtimeMutation(input.mutation), compacted, typeof input.maxBytes === "number" ? input.maxBytes : void 0);
849
+ } else throw new Error("unsupported Runtime management mutation operation: " + request.operation);
850
+ return {
851
+ revision: controller.snapshot().revision,
852
+ value: result
853
+ };
854
+ },
855
+ async mutate(request) {
856
+ const result = await controller.mutate(runtimeMutation(request.input));
857
+ const revision = controller.snapshot().revision;
858
+ return createMemoryMutationReceipt(request.view.id, request.offer.id, context.sourceInstanceKey, revision, result, "committed");
859
+ }
860
+ };
861
+ }
862
+ });
863
+ }
864
+ const RUNTIME_MEMORY_SOURCE = createRuntimeMemorySource();
865
+ //#endregion
866
+ //#region src/index.ts
867
+ const name = "dsh-mnemon-source-runtime";
868
+ const inject = ["mnemonMemory"];
869
+ const memoryPlugin = defineMemoryPlugin({
870
+ packageName: name,
871
+ label: {
872
+ en: "Runtime memory",
873
+ "zh-CN": "运行时记忆"
874
+ },
875
+ description: {
876
+ en: "Working context for the current runtime and task.",
877
+ "zh-CN": "当前运行环境与任务使用的工作上下文。"
878
+ },
879
+ roles: ["source"],
880
+ provides: [{ id: "source" }, { id: "source.working-context" }]
881
+ });
882
+ function apply(ctx, config = {}) {
883
+ installMemory(ctx, {
884
+ plugin: memoryPlugin,
885
+ sources: [createRuntimeMemorySource(config)]
886
+ }, { effectiveDigest: memoryConfigurationDigest(config) });
887
+ }
888
+ //#endregion
889
+ export { Config, RUNTIME_MEMORY_SOURCE, apply, createRuntimeMemorySource, inject, memoryPlugin, name };