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 +6 -0
- package/lib/contracts-B6vRCw3C.js +32 -0
- package/lib/contracts.d.ts +27 -2
- package/lib/contracts.js +2 -2
- package/lib/index.d.ts +2 -2
- package/lib/index.js +95 -24
- package/lib/types/contracts.d.ts +26 -1
- package/package.json +2 -2
- package/presentation/locales.json +2 -0
- package/lib/contracts-BqaPCrGD.js +0 -14
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 };
|
package/lib/contracts.d.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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 || !
|
|
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
|
-
/**
|
|
485
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
package/lib/types/contracts.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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 };
|