dsh-mnemon-source-runtime 0.5.12 → 0.5.14

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,
@@ -0,0 +1,32 @@
1
+ //#region src/defaults.ts
2
+ const DEFAULT_RUNTIME_MEMORY_LIMIT_BYTES = 10240;
3
+ const DEFAULT_RUNTIME_USER_LIMIT_BYTES = 4096;
4
+ const MAX_RUNTIME_MEMORY_LIMIT_BYTES = 1048576;
5
+ //#endregion
6
+ //#region src/contracts.ts
7
+ const RUNTIME_MEMORY_VERSION = 1;
8
+ const RUNTIME_ENTRY_DELIMITER = "\n§\n";
9
+ const RUNTIME_MEMORY_LIMITS = {
10
+ memory: DEFAULT_RUNTIME_MEMORY_LIMIT_BYTES,
11
+ user: DEFAULT_RUNTIME_USER_LIMIT_BYTES
12
+ };
13
+ /** The longest git branch name a scope may hold. */
14
+ const RUNTIME_BRANCH_NAME_MAX = 128;
15
+ /** The characters a git branch name in a scope may use. */
16
+ const RUNTIME_BRANCH_NAME_PATTERN = /^[A-Za-z0-9._/-]+$/u;
17
+ /**
18
+ * Read a stored branch scope. Absent or malformed data reads as no scope, so an entry that
19
+ * a hand edit or another installation left malformed is projected on every branch instead
20
+ * of breaking the projection that reads it.
21
+ */
22
+ function parseRuntimeBranches(value) {
23
+ if (!Array.isArray(value)) return void 0;
24
+ const branches = [];
25
+ for (const item of value) {
26
+ if (typeof item !== "string" || item === "" || item.length > 128 || !RUNTIME_BRANCH_NAME_PATTERN.test(item)) return void 0;
27
+ branches.push(item);
28
+ }
29
+ return branches;
30
+ }
31
+ //#endregion
32
+ export { RUNTIME_MEMORY_VERSION as a, DEFAULT_RUNTIME_USER_LIMIT_BYTES as c, RUNTIME_MEMORY_LIMITS as i, MAX_RUNTIME_MEMORY_LIMIT_BYTES as l, RUNTIME_BRANCH_NAME_PATTERN as n, parseRuntimeBranches as o, RUNTIME_ENTRY_DELIMITER as r, DEFAULT_RUNTIME_MEMORY_LIMIT_BYTES as s, RUNTIME_BRANCH_NAME_MAX as t };
@@ -2,6 +2,12 @@
2
2
  type RuntimeMemoryTarget = 'memory' | 'user';
3
3
  type RuntimeMemoryImportance = 'critical' | 'normal' | 'low';
4
4
  type RuntimeMemoryAction = 'add' | 'replace' | 'remove';
5
+ /** Provenance a DSH Host stamps when an entry crosses machines; absent for local writes. */
6
+ interface RuntimeMemoryEntryOrigin {
7
+ machine: string;
8
+ label: string;
9
+ at: string;
10
+ }
5
11
  interface RuntimeMemoryEntry {
6
12
  content: string;
7
13
  created_at: string;
@@ -10,6 +16,8 @@ interface RuntimeMemoryEntry {
10
16
  importance: RuntimeMemoryImportance;
11
17
  /** Optional git branch names that limit where this entry is projected. Absent means every branch. */
12
18
  branches?: string[];
19
+ /** Which installation wrote this entry, so a merge can tell two machines apart. */
20
+ origin?: RuntimeMemoryEntryOrigin;
13
21
  }
14
22
  interface RuntimeMemoryUsage {
15
23
  used: number;
@@ -55,8 +63,13 @@ type RuntimeMemoryMutationResult = {
55
63
  to: string;
56
64
  };
57
65
  removed?: string;
66
+ /** MEMORY.md entries a compaction moved to the local archive, when it ran with `archive: 'local'`. */
67
+ archived?: {
68
+ entries: number;
69
+ path: string;
70
+ };
58
71
  maintenance?: {
59
- kind: 'local-compaction' | 'mnemon-archive';
72
+ kind: 'local-compaction' | 'mnemon-archive' | 'local-archive';
60
73
  runId: string;
61
74
  provider: string;
62
75
  summary: string;
@@ -74,6 +87,8 @@ interface RuntimeMemoryMaintenancePlan {
74
87
  projected: number;
75
88
  limit: number;
76
89
  requiresMaintenance: boolean;
90
+ /** This Runtime Source moves the entries compaction leaves out to its local archive when asked (#336). */
91
+ localArchive?: true;
77
92
  }
78
93
  declare const RUNTIME_MEMORY_VERSION = 1;
79
94
  declare const RUNTIME_ENTRY_DELIMITER = "\n§\n";
@@ -82,5 +97,15 @@ interface RuntimeMemoryLimits {
82
97
  readonly user: number;
83
98
  }
84
99
  declare const RUNTIME_MEMORY_LIMITS: RuntimeMemoryLimits;
100
+ /** The longest git branch name a scope may hold. */
101
+ declare const RUNTIME_BRANCH_NAME_MAX = 128;
102
+ /** The characters a git branch name in a scope may use. */
103
+ declare const RUNTIME_BRANCH_NAME_PATTERN: RegExp;
104
+ /**
105
+ * Read a stored branch scope. Absent or malformed data reads as no scope, so an entry that
106
+ * a hand edit or another installation left malformed is projected on every branch instead
107
+ * of breaking the projection that reads it.
108
+ */
109
+ declare function parseRuntimeBranches(value: unknown): string[] | undefined;
85
110
  //#endregion
86
- export { RUNTIME_ENTRY_DELIMITER, RUNTIME_MEMORY_LIMITS, RUNTIME_MEMORY_VERSION, RuntimeMemoryAction, RuntimeMemoryCompactedEntry, RuntimeMemoryEntry, RuntimeMemoryImportance, RuntimeMemoryLimits, RuntimeMemoryMaintenancePlan, RuntimeMemoryMutation, RuntimeMemoryMutationResult, RuntimeMemorySnapshot, RuntimeMemoryTarget, RuntimeMemoryTargetView, RuntimeMemoryUsage };
111
+ export { RUNTIME_BRANCH_NAME_MAX, RUNTIME_BRANCH_NAME_PATTERN, RUNTIME_ENTRY_DELIMITER, RUNTIME_MEMORY_LIMITS, RUNTIME_MEMORY_VERSION, RuntimeMemoryAction, RuntimeMemoryCompactedEntry, RuntimeMemoryEntry, RuntimeMemoryEntryOrigin, RuntimeMemoryImportance, RuntimeMemoryLimits, RuntimeMemoryMaintenancePlan, RuntimeMemoryMutation, RuntimeMemoryMutationResult, RuntimeMemorySnapshot, RuntimeMemoryTarget, RuntimeMemoryTargetView, RuntimeMemoryUsage, parseRuntimeBranches };
package/lib/contracts.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as RUNTIME_MEMORY_LIMITS, r as RUNTIME_MEMORY_VERSION, t as RUNTIME_ENTRY_DELIMITER } from "./contracts-BqaPCrGD.js";
2
- export { RUNTIME_ENTRY_DELIMITER, RUNTIME_MEMORY_LIMITS, RUNTIME_MEMORY_VERSION };
1
+ import { a as RUNTIME_MEMORY_VERSION, i as RUNTIME_MEMORY_LIMITS, n as RUNTIME_BRANCH_NAME_PATTERN, o as parseRuntimeBranches, r as RUNTIME_ENTRY_DELIMITER, t as RUNTIME_BRANCH_NAME_MAX } from "./contracts-B6vRCw3C.js";
2
+ export { RUNTIME_BRANCH_NAME_MAX, RUNTIME_BRANCH_NAME_PATTERN, RUNTIME_ENTRY_DELIMITER, RUNTIME_MEMORY_LIMITS, RUNTIME_MEMORY_VERSION, parseRuntimeBranches };
package/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { RUNTIME_ENTRY_DELIMITER, RUNTIME_MEMORY_LIMITS, RUNTIME_MEMORY_VERSION, RuntimeMemoryAction, RuntimeMemoryCompactedEntry, RuntimeMemoryEntry, RuntimeMemoryImportance, RuntimeMemoryLimits, RuntimeMemoryMaintenancePlan, RuntimeMemoryMutation, RuntimeMemoryMutationResult, RuntimeMemorySnapshot, RuntimeMemoryTarget, RuntimeMemoryTargetView, RuntimeMemoryUsage } from "./contracts.js";
1
+ import { RUNTIME_BRANCH_NAME_MAX, RUNTIME_BRANCH_NAME_PATTERN, RUNTIME_ENTRY_DELIMITER, RUNTIME_MEMORY_LIMITS, RUNTIME_MEMORY_VERSION, RuntimeMemoryAction, RuntimeMemoryCompactedEntry, RuntimeMemoryEntry, RuntimeMemoryEntryOrigin, RuntimeMemoryImportance, RuntimeMemoryLimits, RuntimeMemoryMaintenancePlan, RuntimeMemoryMutation, RuntimeMemoryMutationResult, RuntimeMemorySnapshot, RuntimeMemoryTarget, RuntimeMemoryTargetView, RuntimeMemoryUsage, parseRuntimeBranches } from "./contracts.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { MemorySourceDefinition } from "dsh-mnemon/contracts";
4
4
  import { Context } from "@deepseek-ai/cordis";
@@ -21,4 +21,4 @@ declare const inject: string[];
21
21
  declare const memoryPlugin: import("dsh-mnemon/extension-sdk").MemoryPluginDescriptor;
22
22
  declare function apply(ctx: Context, config?: Config): void;
23
23
  //#endregion
24
- export { Config, type RUNTIME_ENTRY_DELIMITER, type RUNTIME_MEMORY_LIMITS, RUNTIME_MEMORY_SOURCE, type RUNTIME_MEMORY_VERSION, type RuntimeMemoryAction, type RuntimeMemoryCompactedEntry, type RuntimeMemoryEntry, type RuntimeMemoryImportance, type RuntimeMemoryLimits, type RuntimeMemoryMaintenancePlan, type RuntimeMemoryMutation, type RuntimeMemoryMutationResult, type RuntimeMemorySnapshot, type RuntimeMemoryTarget, type RuntimeMemoryTargetView, type RuntimeMemoryUsage, apply, createRuntimeMemorySource, inject, memoryPlugin, name };
24
+ export { Config, type RUNTIME_BRANCH_NAME_MAX, type RUNTIME_BRANCH_NAME_PATTERN, type RUNTIME_ENTRY_DELIMITER, type RUNTIME_MEMORY_LIMITS, RUNTIME_MEMORY_SOURCE, type RUNTIME_MEMORY_VERSION, type RuntimeMemoryAction, type RuntimeMemoryCompactedEntry, type RuntimeMemoryEntry, type RuntimeMemoryEntryOrigin, type RuntimeMemoryImportance, type RuntimeMemoryLimits, type RuntimeMemoryMaintenancePlan, type RuntimeMemoryMutation, type RuntimeMemoryMutationResult, type RuntimeMemorySnapshot, type RuntimeMemoryTarget, type RuntimeMemoryTargetView, type RuntimeMemoryUsage, apply, createRuntimeMemorySource, inject, memoryPlugin, name, type parseRuntimeBranches };
package/lib/index.js CHANGED
@@ -1,11 +1,11 @@
1
- import { n as RUNTIME_MEMORY_LIMITS, o as MAX_RUNTIME_MEMORY_LIMIT_BYTES, t as RUNTIME_ENTRY_DELIMITER } from "./contracts-BqaPCrGD.js";
1
+ import { i as RUNTIME_MEMORY_LIMITS, l as MAX_RUNTIME_MEMORY_LIMIT_BYTES, n as RUNTIME_BRANCH_NAME_PATTERN, o as parseRuntimeBranches, r as RUNTIME_ENTRY_DELIMITER } from "./contracts-B6vRCw3C.js";
2
2
  import { createMemoryMutationReceipt, defineMemoryPlugin, defineMemorySource, installMemory, memoryConfigurationDigest, memoryInputRecord, memoryInputStringArray, memoryInputText, truncateMemoryText } from "dsh-mnemon/extension-sdk";
3
3
  import { homedir } from "node:os";
4
4
  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({
@@ -34,6 +34,19 @@ const GIT_BRANCH_TIMEOUT_MS = 2e3;
34
34
  * the directory is not a git working tree, HEAD is detached, or the probe
35
35
  * fails or times out, so callers fall back to the unfiltered view.
36
36
  */
37
+ /**
38
+ * Git takes the repository location from the environment, so a launcher that
39
+ * exports `GIT_DIR` for its own reasons would point this probe at a directory
40
+ * that is not the workspace. The probe runs with those variables removed.
41
+ */
42
+ function gitEnvironment(environment = process.env) {
43
+ const sanitized = {};
44
+ for (const [name, value] of Object.entries(environment)) {
45
+ if (value === void 0 || name.toUpperCase().startsWith("GIT_")) continue;
46
+ sanitized[name] = value;
47
+ }
48
+ return sanitized;
49
+ }
37
50
  function resolveGitBranch(cwd) {
38
51
  const root = cwd?.trim();
39
52
  if (root === void 0 || root === "") return void 0;
@@ -51,7 +64,8 @@ function resolveGitBranch(cwd) {
51
64
  "pipe",
52
65
  "ignore"
53
66
  ],
54
- windowsHide: true
67
+ windowsHide: true,
68
+ env: gitEnvironment()
55
69
  }).trim();
56
70
  return branch === "" ? void 0 : branch;
57
71
  } catch {
@@ -64,6 +78,16 @@ const LOCK_TIMEOUT_MS = 5e3;
64
78
  const LOCK_STALE_MS = 3e4;
65
79
  const LOCK_RETRY_MS = 20;
66
80
  const MAX_ENTRY_BYTES = 8192;
81
+ /** Append to a file that is not a symbolic link; the open fails on one. */
82
+ function appendWithoutLinks(path, text) {
83
+ const descriptor = openSync(path, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | (constants.O_NOFOLLOW ?? 0), 384);
84
+ try {
85
+ const bytes = Buffer.from(text, "utf8");
86
+ for (let offset = 0; offset < bytes.length;) offset += writeSync(descriptor, bytes, offset, bytes.length - offset);
87
+ } finally {
88
+ closeSync(descriptor);
89
+ }
90
+ }
67
91
  var RuntimeMemoryCapacityError = class extends Error {
68
92
  target;
69
93
  used;
@@ -95,7 +119,6 @@ function isTarget(value) {
95
119
  function isImportance(value) {
96
120
  return value === "critical" || value === "normal" || value === "low";
97
121
  }
98
- const RUNTIME_BRANCH_NAME_RE = /^[A-Za-z0-9._/-]+$/u;
99
122
  /** Validate a model-supplied branch scope. Returns the normalized list; an empty list means "no scope". */
100
123
  function normalizeRuntimeBranches(value, field = "branches") {
101
124
  if (!Array.isArray(value)) throw new Error(`${field} must be an array of git branch names`);
@@ -104,7 +127,7 @@ function normalizeRuntimeBranches(value, field = "branches") {
104
127
  for (const item of value) {
105
128
  if (typeof item !== "string") throw new Error(`${field} must contain only git branch names`);
106
129
  const branch = item.trim();
107
- 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)`);
130
+ if (branch === "" || branch.length > 128 || !RUNTIME_BRANCH_NAME_PATTERN.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)`);
108
131
  if (seen.has(branch)) throw new Error(`${field} must not repeat a branch name`);
109
132
  seen.add(branch);
110
133
  branches.push(branch);
@@ -112,17 +135,6 @@ function normalizeRuntimeBranches(value, field = "branches") {
112
135
  if (branches.length > 16) throw new Error(`${field} supports at most 16 branch names`);
113
136
  return branches;
114
137
  }
115
- /** Parse a stored branch scope. Returns undefined for absent or invalid data. */
116
- function parseRuntimeBranches(value) {
117
- if (value === void 0) return void 0;
118
- if (!Array.isArray(value)) return void 0;
119
- const branches = [];
120
- for (const item of value) {
121
- if (typeof item !== "string" || item === "" || item.length > 128 || !RUNTIME_BRANCH_NAME_RE.test(item)) return void 0;
122
- branches.push(item);
123
- }
124
- return branches;
125
- }
126
138
  function entryMatchesBranch(entry, branch) {
127
139
  return entry.branches === void 0 || entry.branches.length === 0 || entry.branches.includes(branch);
128
140
  }
@@ -138,6 +150,15 @@ function normalizeContent(value, field) {
138
150
  if (bytes > MAX_ENTRY_BYTES) throw new Error(`${field} is too large (${bytes} bytes; max ${MAX_ENTRY_BYTES})`);
139
151
  return content;
140
152
  }
153
+ function parseOrigin(value) {
154
+ if (!isRecord(value)) return void 0;
155
+ if (typeof value.machine !== "string" || typeof value.label !== "string" || typeof value.at !== "string") return void 0;
156
+ return {
157
+ machine: value.machine,
158
+ label: value.label,
159
+ at: value.at
160
+ };
161
+ }
141
162
  function parseEntry(value) {
142
163
  if (!isRecord(value) || typeof value.content !== "string" || !isTarget(value.target) || !isImportance(value.importance)) return void 0;
143
164
  if (typeof value.created_at !== "string" || typeof value.updated_at !== "string") return void 0;
@@ -145,13 +166,15 @@ function parseEntry(value) {
145
166
  if (content === "" || content.includes("§")) return void 0;
146
167
  if (value.target === "user" && value.branches !== void 0) return void 0;
147
168
  const branches = parseRuntimeBranches(value.branches);
169
+ const origin = parseOrigin(value.origin);
148
170
  return {
149
171
  content,
150
172
  created_at: value.created_at,
151
173
  updated_at: value.updated_at,
152
174
  target: value.target,
153
175
  importance: value.importance,
154
- ...branches === void 0 ? {} : { branches }
176
+ ...branches === void 0 ? {} : { branches },
177
+ ...origin === void 0 ? {} : { origin }
155
178
  };
156
179
  }
157
180
  function byteCount(entries, target) {
@@ -328,6 +351,9 @@ var RuntimeMemoryController = class RuntimeMemoryController {
328
351
  memoryPath;
329
352
  userPath;
330
353
  lockPath;
354
+ /** Where MEMORY.md entries go when compaction leaves them out and no Memory Space can take them. */
355
+ archivePath;
356
+ archiveSourcePath;
331
357
  limits;
332
358
  queue = Promise.resolve();
333
359
  localUserPath;
@@ -344,6 +370,8 @@ var RuntimeMemoryController = class RuntimeMemoryController {
344
370
  this.memoryPath = join(this.directory, "MEMORY.md");
345
371
  this.localUserPath = join(this.directory, "USER.md");
346
372
  this.lockPath = join(this.directory, ".memories.lock");
373
+ this.archivePath = join(this.directory, "archived", "MEMORY.md");
374
+ this.archiveSourcePath = join(this.directory, "archived", "memories.jsonl");
347
375
  const userDirectory = userRunner === void 0 ? this.directory : join(userRunner.effectiveDataDir(), "runtime");
348
376
  this.userController = userDirectory === this.directory || userRunner === void 0 ? void 0 : new RuntimeMemoryController(userRunner, now, this.limits);
349
377
  this.userPath = this.userController?.userPath ?? this.localUserPath;
@@ -475,18 +503,24 @@ ${memory || "(empty)"}
475
503
  used,
476
504
  projected,
477
505
  limit,
478
- requiresMaintenance: prepared.changed && projected > limit
506
+ requiresMaintenance: prepared.changed && projected > limit,
507
+ localArchive: true
479
508
  };
480
509
  }));
481
510
  this.queue = operation.catch(() => void 0);
482
511
  return operation;
483
512
  }
484
- /** Commit semantic compaction and the original mutation together, or leave every local file unchanged. */
485
- compactAndMutate(expectedRevision, request, compacted, maxCompactedBytes) {
513
+ /**
514
+ * Commit semantic compaction and the original mutation together, or leave every local file unchanged.
515
+ * With `archive: 'local'`, committed MEMORY.md entries that compaction leaves out are appended to the
516
+ * local archive first, so a limit with no Memory Space to archive into never loses memory.
517
+ */
518
+ compactAndMutate(expectedRevision, request, compacted, maxCompactedBytes, options = {}) {
486
519
  if (request.target === "user" && this.userController !== void 0) return this.userController.compactAndMutate(expectedRevision, request, compacted, maxCompactedBytes);
487
520
  const operation = this.queue.then(() => this.withLock(() => {
488
521
  const file = this.readSource();
489
- if (revision(file) !== expectedRevision) throw new RuntimeMemoryConflictError();
522
+ const beforeRevision = revision(file);
523
+ if (beforeRevision !== expectedRevision) throw new RuntimeMemoryConflictError();
490
524
  const now = this.now().toISOString();
491
525
  const prepared = prepareMutation(file.entries, request, now);
492
526
  const compactedByteBudget = maxCompactedBytes ?? this.limits[request.target];
@@ -495,7 +529,8 @@ ${memory || "(empty)"}
495
529
  const replacements = compactionCandidates(compacted, prepared.compactableEntries, request.target, now);
496
530
  if (prepared.pendingEntry !== void 0 && replacements.some((entry) => entry.content === prepared.pendingEntry.content)) throw new Error("compacted runtime memory duplicates the pending mutation");
497
531
  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]];
532
+ const fitted = packCompactionCandidates(replacements, request.target, compactedByteBudget);
533
+ const targetEntries = [...fitted, ...prepared.pendingEntry === void 0 ? [] : [prepared.pendingEntry]];
499
534
  const entries = [...file.entries.filter((entry) => entry.target !== request.target), ...targetEntries];
500
535
  const used = byteCount(entries, request.target);
501
536
  const limit = this.limits[request.target];
@@ -504,12 +539,47 @@ ${memory || "(empty)"}
504
539
  version: 1,
505
540
  entries
506
541
  };
542
+ const kept = new Set(fitted.map((entry) => entry.content));
543
+ const archived = options.archive === "local" && request.target === "memory" ? prepared.compactableEntries.filter((entry) => !kept.has(entry.content)) : void 0;
544
+ if (archived !== void 0 && archived.length > 0) this.archiveLocally(archived, beforeRevision, now);
507
545
  this.persist(next);
508
- return this.result(request.target, entries, prepared.fields);
546
+ return {
547
+ ...this.result(request.target, entries, prepared.fields),
548
+ ...archived === void 0 ? {} : { archived: {
549
+ entries: archived.length,
550
+ path: this.archivePath
551
+ } }
552
+ };
509
553
  }));
510
554
  this.queue = operation.catch(() => void 0);
511
555
  return operation;
512
556
  }
557
+ /** Append entries to the local archive: JSON Lines to restore from, and Markdown to read. */
558
+ archiveLocally(entries, fromRevision, archivedAt) {
559
+ const directory = join(this.directory, "archived");
560
+ mkdirSync(directory, {
561
+ recursive: true,
562
+ mode: 448
563
+ });
564
+ const stat = lstatSync(directory);
565
+ if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`Runtime Memory archive is not a directory: ${directory}`);
566
+ for (const path of [this.archiveSourcePath, this.archivePath]) {
567
+ let file;
568
+ try {
569
+ file = lstatSync(path);
570
+ } catch {
571
+ continue;
572
+ }
573
+ if (!file.isFile()) throw new Error(`Runtime Memory archive file is not a regular file: ${path}`);
574
+ }
575
+ appendWithoutLinks(this.archiveSourcePath, entries.map((entry) => `${JSON.stringify({
576
+ archivedAt,
577
+ fromRevision,
578
+ ...entry
579
+ })}\n`).join(""));
580
+ 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";
581
+ appendWithoutLinks(this.archivePath, `${header}\n## ${archivedAt}\n\n${entries.map((entry) => entry.content).join(RUNTIME_ENTRY_DELIMITER)}\n`);
582
+ }
513
583
  initialize() {
514
584
  mkdirSync(this.directory, {
515
585
  recursive: true,
@@ -843,7 +913,8 @@ function createRuntimeMemorySource(config = {}) {
843
913
  ...entry.branches === void 0 ? {} : { branches: memoryInputStringArray(entry.branches, "branches", 100) ?? [] }
844
914
  };
845
915
  });
846
- result = await controller.compactAndMutate(memoryInputText(input.revision, "revision", 300), runtimeMutation(input.mutation), compacted, typeof input.maxBytes === "number" ? input.maxBytes : void 0);
916
+ if (input.archive !== void 0 && input.archive !== "local") throw new Error("archive must be \"local\" when given");
917
+ 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
918
  } else throw new Error("unsupported Runtime management mutation operation: " + request.operation);
848
919
  return {
849
920
  revision: controller.snapshot().revision,
@@ -1,6 +1,12 @@
1
1
  export type RuntimeMemoryTarget = 'memory' | 'user';
2
2
  export type RuntimeMemoryImportance = 'critical' | 'normal' | 'low';
3
3
  export type RuntimeMemoryAction = 'add' | 'replace' | 'remove';
4
+ /** Provenance a DSH Host stamps when an entry crosses machines; absent for local writes. */
5
+ export interface RuntimeMemoryEntryOrigin {
6
+ machine: string;
7
+ label: string;
8
+ at: string;
9
+ }
4
10
  export interface RuntimeMemoryEntry {
5
11
  content: string;
6
12
  created_at: string;
@@ -9,6 +15,8 @@ export interface RuntimeMemoryEntry {
9
15
  importance: RuntimeMemoryImportance;
10
16
  /** Optional git branch names that limit where this entry is projected. Absent means every branch. */
11
17
  branches?: string[];
18
+ /** Which installation wrote this entry, so a merge can tell two machines apart. */
19
+ origin?: RuntimeMemoryEntryOrigin;
12
20
  }
13
21
  export interface RuntimeMemoryUsage {
14
22
  used: number;
@@ -54,8 +62,13 @@ export type RuntimeMemoryMutationResult = {
54
62
  to: string;
55
63
  };
56
64
  removed?: string;
65
+ /** MEMORY.md entries a compaction moved to the local archive, when it ran with `archive: 'local'`. */
66
+ archived?: {
67
+ entries: number;
68
+ path: string;
69
+ };
57
70
  maintenance?: {
58
- kind: 'local-compaction' | 'mnemon-archive';
71
+ kind: 'local-compaction' | 'mnemon-archive' | 'local-archive';
59
72
  runId: string;
60
73
  provider: string;
61
74
  summary: string;
@@ -73,6 +86,8 @@ export interface RuntimeMemoryMaintenancePlan {
73
86
  projected: number;
74
87
  limit: number;
75
88
  requiresMaintenance: boolean;
89
+ /** This Runtime Source moves the entries compaction leaves out to its local archive when asked (#336). */
90
+ localArchive?: true;
76
91
  }
77
92
  export declare const RUNTIME_MEMORY_VERSION = 1;
78
93
  export declare const RUNTIME_ENTRY_DELIMITER = "\n\u00A7\n";
@@ -81,3 +96,13 @@ export interface RuntimeMemoryLimits {
81
96
  readonly user: number;
82
97
  }
83
98
  export declare const RUNTIME_MEMORY_LIMITS: RuntimeMemoryLimits;
99
+ /** The longest git branch name a scope may hold. */
100
+ export declare const RUNTIME_BRANCH_NAME_MAX = 128;
101
+ /** The characters a git branch name in a scope may use. */
102
+ export declare const RUNTIME_BRANCH_NAME_PATTERN: RegExp;
103
+ /**
104
+ * Read a stored branch scope. Absent or malformed data reads as no scope, so an entry that
105
+ * a hand edit or another installation left malformed is projected on every branch instead
106
+ * of breaking the projection that reads it.
107
+ */
108
+ export declare function parseRuntimeBranches(value: unknown): string[] | undefined;
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.14",
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.26",
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
  }
@@ -1,14 +0,0 @@
1
- //#region src/defaults.ts
2
- const DEFAULT_RUNTIME_MEMORY_LIMIT_BYTES = 10240;
3
- const DEFAULT_RUNTIME_USER_LIMIT_BYTES = 4096;
4
- const MAX_RUNTIME_MEMORY_LIMIT_BYTES = 1048576;
5
- //#endregion
6
- //#region src/contracts.ts
7
- const RUNTIME_MEMORY_VERSION = 1;
8
- const RUNTIME_ENTRY_DELIMITER = "\n§\n";
9
- const RUNTIME_MEMORY_LIMITS = {
10
- memory: DEFAULT_RUNTIME_MEMORY_LIMIT_BYTES,
11
- user: DEFAULT_RUNTIME_USER_LIMIT_BYTES
12
- };
13
- //#endregion
14
- export { DEFAULT_RUNTIME_USER_LIMIT_BYTES as a, DEFAULT_RUNTIME_MEMORY_LIMIT_BYTES as i, RUNTIME_MEMORY_LIMITS as n, MAX_RUNTIME_MEMORY_LIMIT_BYTES as o, RUNTIME_MEMORY_VERSION as r, RUNTIME_ENTRY_DELIMITER as t };