dsh-mnemon-source-runtime 0.5.11 → 0.5.13

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/client.js CHANGED
@@ -51,6 +51,7 @@ window.__ModuleLoader__.load({
51
51
  "runtime.result.remove": "已从{target}移除 · 当前 {count} 条",
52
52
  "runtime.result.maintenance": "容量整理完成:已先归档到记忆空间 {spaces},再更新{target} · 当前 {count} 条",
53
53
  "runtime.result.localCompaction": "本地画像整理完成:未写入记忆空间,已更新{target} · 当前 {count} 条",
54
+ "runtime.result.localArchive": "容量整理完成:没有可写的记忆空间,已先把 {archived} 条移入本地归档,再更新{target} · 当前 {count} 条",
54
55
  "runtime.readOnly": "当前为只读模式;运行时记忆仍会进入上下文,但不能在此修改。",
55
56
  "runtime.footnote": "memories.json 是唯一事实源;两个 Markdown 文件由控制层生成,不应直接编辑。"
56
57
  };
@@ -97,6 +98,7 @@ window.__ModuleLoader__.load({
97
98
  "runtime.result.remove": "Removed from {target} · {count} entries",
98
99
  "runtime.result.maintenance": "Capacity maintenance complete: archived to {spaces}, then updated {target} · {count} entries",
99
100
  "runtime.result.localCompaction": "Local profile compaction complete: no Memory Space write; updated {target} · {count} entries",
101
+ "runtime.result.localArchive": "Capacity maintenance complete: with no Memory Space to write to, moved {archived} entries to the local archive, then updated {target} · {count} entries",
100
102
  "runtime.readOnly": "Read only here: runtime memory still enters context but cannot be changed on this page.",
101
103
  "runtime.footnote": "memories.json is the only source of truth. The control plane generates both Markdown files; do not edit them directly."
102
104
  };
@@ -251,6 +253,10 @@ window.__ModuleLoader__.load({
251
253
  }) : result.maintenance.kind === "local-compaction" ? t("runtime.result.localCompaction", {
252
254
  target: t(`runtime.target.${request.target}`),
253
255
  count: result.entryCount
256
+ }) : result.maintenance.kind === "local-archive" ? t("runtime.result.localArchive", {
257
+ target: t(`runtime.target.${request.target}`),
258
+ count: result.entryCount,
259
+ archived: result.archived?.entries ?? 0
254
260
  }) : t("runtime.result.maintenance", {
255
261
  target: t(`runtime.target.${request.target}`),
256
262
  count: result.entryCount,
@@ -55,8 +55,13 @@ type RuntimeMemoryMutationResult = {
55
55
  to: string;
56
56
  };
57
57
  removed?: string;
58
+ /** MEMORY.md entries a compaction moved to the local archive, when it ran with `archive: 'local'`. */
59
+ archived?: {
60
+ entries: number;
61
+ path: string;
62
+ };
58
63
  maintenance?: {
59
- kind: 'local-compaction' | 'mnemon-archive';
64
+ kind: 'local-compaction' | 'mnemon-archive' | 'local-archive';
60
65
  runId: string;
61
66
  provider: string;
62
67
  summary: string;
@@ -74,6 +79,8 @@ interface RuntimeMemoryMaintenancePlan {
74
79
  projected: number;
75
80
  limit: number;
76
81
  requiresMaintenance: boolean;
82
+ /** This Runtime Source moves the entries compaction leaves out to its local archive when asked (#336). */
83
+ localArchive?: true;
77
84
  }
78
85
  declare const RUNTIME_MEMORY_VERSION = 1;
79
86
  declare const RUNTIME_ENTRY_DELIMITER = "\n§\n";
package/lib/index.js CHANGED
@@ -5,7 +5,7 @@ import { basename, isAbsolute, join } from "node:path";
5
5
  import z from "@deepseek-ai/schemastery";
6
6
  import { COMPOSABLE_MEMORY_API_VERSION } from "dsh-mnemon/contracts";
7
7
  import { execFileSync } from "node:child_process";
8
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
8
+ import { closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
9
9
  import { createHash } from "node:crypto";
10
10
  //#region src/config.ts
11
11
  const Config = z.object({
@@ -64,6 +64,16 @@ const LOCK_TIMEOUT_MS = 5e3;
64
64
  const LOCK_STALE_MS = 3e4;
65
65
  const LOCK_RETRY_MS = 20;
66
66
  const MAX_ENTRY_BYTES = 8192;
67
+ /** Append to a file that is not a symbolic link; the open fails on one. */
68
+ function appendWithoutLinks(path, text) {
69
+ const descriptor = openSync(path, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | (constants.O_NOFOLLOW ?? 0), 384);
70
+ try {
71
+ const bytes = Buffer.from(text, "utf8");
72
+ for (let offset = 0; offset < bytes.length;) offset += writeSync(descriptor, bytes, offset, bytes.length - offset);
73
+ } finally {
74
+ closeSync(descriptor);
75
+ }
76
+ }
67
77
  var RuntimeMemoryCapacityError = class extends Error {
68
78
  target;
69
79
  used;
@@ -267,12 +277,14 @@ function prepareMutation(before, request, now) {
267
277
  }
268
278
  function compactionCandidates(compacted, existing, target, now) {
269
279
  const seen = /* @__PURE__ */ new Set();
280
+ const existingByContent = /* @__PURE__ */ new Map();
281
+ for (const entry of existing) if (!existingByContent.has(entry.content)) existingByContent.set(entry.content, entry);
270
282
  return compacted.map((entry) => {
271
283
  const content = normalizeContent(entry.content, "compacted content");
272
284
  if (!isImportance(entry.importance)) throw new Error("compacted importance must be critical, normal, or low");
273
285
  if (seen.has(content)) throw new Error("compacted runtime memory contains duplicate entries");
274
286
  seen.add(content);
275
- const unchanged = existing.find((current) => current.content === content);
287
+ const unchanged = existingByContent.get(content);
276
288
  const inheritedBranches = entry.branches ?? unchanged?.branches;
277
289
  return {
278
290
  content,
@@ -285,21 +297,28 @@ function compactionCandidates(compacted, existing, target, now) {
285
297
  });
286
298
  }
287
299
  function packCompactionCandidates(replacements, target, maxBytes) {
288
- const priority = {
289
- critical: 0,
290
- normal: 1,
291
- low: 2
300
+ const ranked = {
301
+ critical: [],
302
+ normal: [],
303
+ low: []
292
304
  };
293
- const ranked = replacements.map((entry, index) => ({
294
- entry,
295
- index
296
- })).sort((left, right) => priority[left.entry.importance] - priority[right.entry.importance] || left.index - right.index);
305
+ replacements.forEach((entry, index) => ranked[entry.importance].push(index));
297
306
  const selected = /* @__PURE__ */ new Set();
298
- const packed = [];
299
- for (const candidate of ranked) {
300
- if (byteCount([...packed, candidate.entry], target) > maxBytes) continue;
301
- packed.push(candidate.entry);
302
- selected.add(candidate.index);
307
+ const delimiterBytes = Buffer.byteLength(RUNTIME_ENTRY_DELIMITER, "utf8");
308
+ let used = 0;
309
+ let count = 0;
310
+ for (const importance of [
311
+ "critical",
312
+ "normal",
313
+ "low"
314
+ ]) for (const index of ranked[importance]) {
315
+ const entry = replacements[index];
316
+ const matchesTarget = entry.target === target;
317
+ const added = matchesTarget ? Buffer.byteLength(entry.content, "utf8") + (count === 0 ? 0 : delimiterBytes) : 0;
318
+ if (used + added > maxBytes) continue;
319
+ used += added;
320
+ if (matchesTarget) count += 1;
321
+ selected.add(index);
303
322
  }
304
323
  return replacements.filter((_, index) => selected.has(index));
305
324
  }
@@ -319,6 +338,9 @@ var RuntimeMemoryController = class RuntimeMemoryController {
319
338
  memoryPath;
320
339
  userPath;
321
340
  lockPath;
341
+ /** Where MEMORY.md entries go when compaction leaves them out and no Memory Space can take them. */
342
+ archivePath;
343
+ archiveSourcePath;
322
344
  limits;
323
345
  queue = Promise.resolve();
324
346
  localUserPath;
@@ -335,6 +357,8 @@ var RuntimeMemoryController = class RuntimeMemoryController {
335
357
  this.memoryPath = join(this.directory, "MEMORY.md");
336
358
  this.localUserPath = join(this.directory, "USER.md");
337
359
  this.lockPath = join(this.directory, ".memories.lock");
360
+ this.archivePath = join(this.directory, "archived", "MEMORY.md");
361
+ this.archiveSourcePath = join(this.directory, "archived", "memories.jsonl");
338
362
  const userDirectory = userRunner === void 0 ? this.directory : join(userRunner.effectiveDataDir(), "runtime");
339
363
  this.userController = userDirectory === this.directory || userRunner === void 0 ? void 0 : new RuntimeMemoryController(userRunner, now, this.limits);
340
364
  this.userPath = this.userController?.userPath ?? this.localUserPath;
@@ -466,18 +490,24 @@ ${memory || "(empty)"}
466
490
  used,
467
491
  projected,
468
492
  limit,
469
- requiresMaintenance: prepared.changed && projected > limit
493
+ requiresMaintenance: prepared.changed && projected > limit,
494
+ localArchive: true
470
495
  };
471
496
  }));
472
497
  this.queue = operation.catch(() => void 0);
473
498
  return operation;
474
499
  }
475
- /** Commit semantic compaction and the original mutation together, or leave every local file unchanged. */
476
- compactAndMutate(expectedRevision, request, compacted, maxCompactedBytes) {
500
+ /**
501
+ * Commit semantic compaction and the original mutation together, or leave every local file unchanged.
502
+ * With `archive: 'local'`, committed MEMORY.md entries that compaction leaves out are appended to the
503
+ * local archive first, so a limit with no Memory Space to archive into never loses memory.
504
+ */
505
+ compactAndMutate(expectedRevision, request, compacted, maxCompactedBytes, options = {}) {
477
506
  if (request.target === "user" && this.userController !== void 0) return this.userController.compactAndMutate(expectedRevision, request, compacted, maxCompactedBytes);
478
507
  const operation = this.queue.then(() => this.withLock(() => {
479
508
  const file = this.readSource();
480
- if (revision(file) !== expectedRevision) throw new RuntimeMemoryConflictError();
509
+ const beforeRevision = revision(file);
510
+ if (beforeRevision !== expectedRevision) throw new RuntimeMemoryConflictError();
481
511
  const now = this.now().toISOString();
482
512
  const prepared = prepareMutation(file.entries, request, now);
483
513
  const compactedByteBudget = maxCompactedBytes ?? this.limits[request.target];
@@ -486,7 +516,8 @@ ${memory || "(empty)"}
486
516
  const replacements = compactionCandidates(compacted, prepared.compactableEntries, request.target, now);
487
517
  if (prepared.pendingEntry !== void 0 && replacements.some((entry) => entry.content === prepared.pendingEntry.content)) throw new Error("compacted runtime memory duplicates the pending mutation");
488
518
  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");
489
- const targetEntries = [...packCompactionCandidates(replacements, request.target, compactedByteBudget), ...prepared.pendingEntry === void 0 ? [] : [prepared.pendingEntry]];
519
+ const fitted = packCompactionCandidates(replacements, request.target, compactedByteBudget);
520
+ const targetEntries = [...fitted, ...prepared.pendingEntry === void 0 ? [] : [prepared.pendingEntry]];
490
521
  const entries = [...file.entries.filter((entry) => entry.target !== request.target), ...targetEntries];
491
522
  const used = byteCount(entries, request.target);
492
523
  const limit = this.limits[request.target];
@@ -495,12 +526,47 @@ ${memory || "(empty)"}
495
526
  version: 1,
496
527
  entries
497
528
  };
529
+ const kept = new Set(fitted.map((entry) => entry.content));
530
+ const archived = options.archive === "local" && request.target === "memory" ? prepared.compactableEntries.filter((entry) => !kept.has(entry.content)) : void 0;
531
+ if (archived !== void 0 && archived.length > 0) this.archiveLocally(archived, beforeRevision, now);
498
532
  this.persist(next);
499
- return this.result(request.target, entries, prepared.fields);
533
+ return {
534
+ ...this.result(request.target, entries, prepared.fields),
535
+ ...archived === void 0 ? {} : { archived: {
536
+ entries: archived.length,
537
+ path: this.archivePath
538
+ } }
539
+ };
500
540
  }));
501
541
  this.queue = operation.catch(() => void 0);
502
542
  return operation;
503
543
  }
544
+ /** Append entries to the local archive: JSON Lines to restore from, and Markdown to read. */
545
+ archiveLocally(entries, fromRevision, archivedAt) {
546
+ const directory = join(this.directory, "archived");
547
+ mkdirSync(directory, {
548
+ recursive: true,
549
+ mode: 448
550
+ });
551
+ const stat = lstatSync(directory);
552
+ if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`Runtime Memory archive is not a directory: ${directory}`);
553
+ for (const path of [this.archiveSourcePath, this.archivePath]) {
554
+ let file;
555
+ try {
556
+ file = lstatSync(path);
557
+ } catch {
558
+ continue;
559
+ }
560
+ if (!file.isFile()) throw new Error(`Runtime Memory archive file is not a regular file: ${path}`);
561
+ }
562
+ appendWithoutLinks(this.archiveSourcePath, entries.map((entry) => `${JSON.stringify({
563
+ archivedAt,
564
+ fromRevision,
565
+ ...entry
566
+ })}\n`).join(""));
567
+ const header = existsSync(this.archivePath) ? "" : "# MEMORY.md archive\n\nEntries that left MEMORY.md at its limit while no Memory Space could take them, oldest first.\n";
568
+ appendWithoutLinks(this.archivePath, `${header}\n## ${archivedAt}\n\n${entries.map((entry) => entry.content).join(RUNTIME_ENTRY_DELIMITER)}\n`);
569
+ }
504
570
  initialize() {
505
571
  mkdirSync(this.directory, {
506
572
  recursive: true,
@@ -834,7 +900,8 @@ function createRuntimeMemorySource(config = {}) {
834
900
  ...entry.branches === void 0 ? {} : { branches: memoryInputStringArray(entry.branches, "branches", 100) ?? [] }
835
901
  };
836
902
  });
837
- result = await controller.compactAndMutate(memoryInputText(input.revision, "revision", 300), runtimeMutation(input.mutation), compacted, typeof input.maxBytes === "number" ? input.maxBytes : void 0);
903
+ if (input.archive !== void 0 && input.archive !== "local") throw new Error("archive must be \"local\" when given");
904
+ result = await controller.compactAndMutate(memoryInputText(input.revision, "revision", 300), runtimeMutation(input.mutation), compacted, typeof input.maxBytes === "number" ? input.maxBytes : void 0, input.archive === "local" ? { archive: "local" } : {});
838
905
  } else throw new Error("unsupported Runtime management mutation operation: " + request.operation);
839
906
  return {
840
907
  revision: controller.snapshot().revision,
@@ -54,8 +54,13 @@ export type RuntimeMemoryMutationResult = {
54
54
  to: string;
55
55
  };
56
56
  removed?: string;
57
+ /** MEMORY.md entries a compaction moved to the local archive, when it ran with `archive: 'local'`. */
58
+ archived?: {
59
+ entries: number;
60
+ path: string;
61
+ };
57
62
  maintenance?: {
58
- kind: 'local-compaction' | 'mnemon-archive';
63
+ kind: 'local-compaction' | 'mnemon-archive' | 'local-archive';
59
64
  runId: string;
60
65
  provider: string;
61
66
  summary: string;
@@ -73,6 +78,8 @@ export interface RuntimeMemoryMaintenancePlan {
73
78
  projected: number;
74
79
  limit: number;
75
80
  requiresMaintenance: boolean;
81
+ /** This Runtime Source moves the entries compaction leaves out to its local archive when asked (#336). */
82
+ localArchive?: true;
76
83
  }
77
84
  export declare const RUNTIME_MEMORY_VERSION = 1;
78
85
  export declare const RUNTIME_ENTRY_DELIMITER = "\n\u00A7\n";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mnemon-source-runtime",
3
- "version": "0.5.11",
3
+ "version": "0.5.13",
4
4
  "description": "Self-contained runtime Source for Composable View Memory.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -60,7 +60,7 @@
60
60
  "@types/node": "^22.0.0",
61
61
  "@types/react": "^18.3.0",
62
62
  "@types/react-dom": "^18.3.0",
63
- "dsh-mnemon": "^0.5.19",
63
+ "dsh-mnemon": "^0.5.25",
64
64
  "jsdom": "^26.0.0",
65
65
  "react": "^18.2.0",
66
66
  "react-dom": "^18.2.0",
@@ -42,6 +42,7 @@
42
42
  "runtime.result.remove": "已从{target}移除 · 当前 {count} 条",
43
43
  "runtime.result.maintenance": "容量整理完成:已先归档到记忆空间 {spaces},再更新{target} · 当前 {count} 条",
44
44
  "runtime.result.localCompaction": "本地画像整理完成:未写入记忆空间,已更新{target} · 当前 {count} 条",
45
+ "runtime.result.localArchive": "容量整理完成:没有可写的记忆空间,已先把 {archived} 条移入本地归档,再更新{target} · 当前 {count} 条",
45
46
  "runtime.readOnly": "当前为只读模式;运行时记忆仍会进入上下文,但不能在此修改。",
46
47
  "runtime.footnote": "memories.json 是唯一事实源;两个 Markdown 文件由控制层生成,不应直接编辑。"
47
48
  },
@@ -88,6 +89,7 @@
88
89
  "runtime.result.remove": "Removed from {target} · {count} entries",
89
90
  "runtime.result.maintenance": "Capacity maintenance complete: archived to {spaces}, then updated {target} · {count} entries",
90
91
  "runtime.result.localCompaction": "Local profile compaction complete: no Memory Space write; updated {target} · {count} entries",
92
+ "runtime.result.localArchive": "Capacity maintenance complete: with no Memory Space to write to, moved {archived} entries to the local archive, then updated {target} · {count} entries",
91
93
  "runtime.readOnly": "Read only here: runtime memory still enters context but cannot be changed on this page.",
92
94
  "runtime.footnote": "memories.json is the only source of truth. The control plane generates both Markdown files; do not edit them directly."
93
95
  }