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 +6 -0
- package/lib/contracts.d.ts +8 -1
- package/lib/index.js +66 -8
- 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;
|
|
@@ -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
|
-
/**
|
|
485
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
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
|
}
|