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 +6 -0
- package/lib/contracts.d.ts +8 -1
- package/lib/index.js +89 -22
- package/lib/types/contracts.d.ts +8 -1
- package/package.json +2 -2
- package/presentation/locales.json +2 -0
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,
|
package/lib/contracts.d.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
289
|
-
critical:
|
|
290
|
-
normal:
|
|
291
|
-
low:
|
|
300
|
+
const ranked = {
|
|
301
|
+
critical: [],
|
|
302
|
+
normal: [],
|
|
303
|
+
low: []
|
|
292
304
|
};
|
|
293
|
-
|
|
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
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
/**
|
|
476
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
package/lib/types/contracts.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
}
|