dsh-mnemon-source-runtime 0.5.12 → 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;
@@ -328,6 +338,9 @@ var RuntimeMemoryController = class RuntimeMemoryController {
328
338
  memoryPath;
329
339
  userPath;
330
340
  lockPath;
341
+ /** Where MEMORY.md entries go when compaction leaves them out and no Memory Space can take them. */
342
+ archivePath;
343
+ archiveSourcePath;
331
344
  limits;
332
345
  queue = Promise.resolve();
333
346
  localUserPath;
@@ -344,6 +357,8 @@ var RuntimeMemoryController = class RuntimeMemoryController {
344
357
  this.memoryPath = join(this.directory, "MEMORY.md");
345
358
  this.localUserPath = join(this.directory, "USER.md");
346
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");
347
362
  const userDirectory = userRunner === void 0 ? this.directory : join(userRunner.effectiveDataDir(), "runtime");
348
363
  this.userController = userDirectory === this.directory || userRunner === void 0 ? void 0 : new RuntimeMemoryController(userRunner, now, this.limits);
349
364
  this.userPath = this.userController?.userPath ?? this.localUserPath;
@@ -475,18 +490,24 @@ ${memory || "(empty)"}
475
490
  used,
476
491
  projected,
477
492
  limit,
478
- requiresMaintenance: prepared.changed && projected > limit
493
+ requiresMaintenance: prepared.changed && projected > limit,
494
+ localArchive: true
479
495
  };
480
496
  }));
481
497
  this.queue = operation.catch(() => void 0);
482
498
  return operation;
483
499
  }
484
- /** Commit semantic compaction and the original mutation together, or leave every local file unchanged. */
485
- 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 = {}) {
486
506
  if (request.target === "user" && this.userController !== void 0) return this.userController.compactAndMutate(expectedRevision, request, compacted, maxCompactedBytes);
487
507
  const operation = this.queue.then(() => this.withLock(() => {
488
508
  const file = this.readSource();
489
- if (revision(file) !== expectedRevision) throw new RuntimeMemoryConflictError();
509
+ const beforeRevision = revision(file);
510
+ if (beforeRevision !== expectedRevision) throw new RuntimeMemoryConflictError();
490
511
  const now = this.now().toISOString();
491
512
  const prepared = prepareMutation(file.entries, request, now);
492
513
  const compactedByteBudget = maxCompactedBytes ?? this.limits[request.target];
@@ -495,7 +516,8 @@ ${memory || "(empty)"}
495
516
  const replacements = compactionCandidates(compacted, prepared.compactableEntries, request.target, now);
496
517
  if (prepared.pendingEntry !== void 0 && replacements.some((entry) => entry.content === prepared.pendingEntry.content)) throw new Error("compacted runtime memory duplicates the pending mutation");
497
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");
498
- 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]];
499
521
  const entries = [...file.entries.filter((entry) => entry.target !== request.target), ...targetEntries];
500
522
  const used = byteCount(entries, request.target);
501
523
  const limit = this.limits[request.target];
@@ -504,12 +526,47 @@ ${memory || "(empty)"}
504
526
  version: 1,
505
527
  entries
506
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);
507
532
  this.persist(next);
508
- 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
+ };
509
540
  }));
510
541
  this.queue = operation.catch(() => void 0);
511
542
  return operation;
512
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
+ }
513
570
  initialize() {
514
571
  mkdirSync(this.directory, {
515
572
  recursive: true,
@@ -843,7 +900,8 @@ function createRuntimeMemorySource(config = {}) {
843
900
  ...entry.branches === void 0 ? {} : { branches: memoryInputStringArray(entry.branches, "branches", 100) ?? [] }
844
901
  };
845
902
  });
846
- 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" } : {});
847
905
  } else throw new Error("unsupported Runtime management mutation operation: " + request.operation);
848
906
  return {
849
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.12",
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.23",
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
  }