dsh-mnemon 0.3.3 → 0.3.5

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/index.js CHANGED
@@ -11,6 +11,7 @@ import { createHash, randomUUID } from "node:crypto";
11
11
  import { accessSync, closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
12
12
  import { homedir, tmpdir } from "node:os";
13
13
  import { strToU8, unzipSync, zipSync } from "fflate";
14
+ import { execFileSync } from "node:child_process";
14
15
  import { fileURLToPath } from "node:url";
15
16
  import { z as z$1 } from "zod";
16
17
  //#region src/shared/contracts.ts
@@ -54,6 +55,11 @@ const DEFAULT_IDLE_REVIEW_MS = 3e4;
54
55
  const DEFAULT_RECALL_QUALITY_POLICY = "strict-v1";
55
56
  const DEFAULT_RECALL_LOW_SCORE_THRESHOLD = .25;
56
57
  const DEFAULT_RECALL_HIGH_SCORE_THRESHOLD = .6;
58
+ const DEFAULT_RUNTIME_MEMORY_LIMIT_BYTES = 10240;
59
+ const DEFAULT_RUNTIME_USER_LIMIT_BYTES = 4096;
60
+ const DEFAULT_RUNTIME_MAINTENANCE_MAX_TOKENS = 8192;
61
+ const MAX_RUNTIME_MEMORY_LIMIT_BYTES = 1048576;
62
+ const MAX_RUNTIME_MAINTENANCE_MAX_TOKENS = 1e6;
57
63
  //#endregion
58
64
  //#region src/config.ts
59
65
  const InteractionConfig = z.object({
@@ -118,6 +124,11 @@ const RecallQualitySchema = z.object({
118
124
  maxMediumResults: z.number().step(1).min(0).max(50).default(4),
119
125
  maxUnknownResults: z.number().step(1).min(0).max(50).default(2)
120
126
  });
127
+ const RuntimeMemorySchema = z.object({
128
+ memoryLimitBytes: z.number().step(1).min(1).max(MAX_RUNTIME_MEMORY_LIMIT_BYTES).default(DEFAULT_RUNTIME_MEMORY_LIMIT_BYTES),
129
+ userLimitBytes: z.number().step(1).min(1).max(MAX_RUNTIME_MEMORY_LIMIT_BYTES).default(DEFAULT_RUNTIME_USER_LIMIT_BYTES),
130
+ maintenanceMaxTokens: z.number().step(1).min(1).max(MAX_RUNTIME_MAINTENANCE_MAX_TOKENS).default(DEFAULT_RUNTIME_MAINTENANCE_MAX_TOKENS)
131
+ });
121
132
  const MemoryParticipationModeSchema = z.union([
122
133
  "off",
123
134
  "manual",
@@ -144,6 +155,7 @@ const Config = z.object({
144
155
  "workspace",
145
156
  "custom"
146
157
  ]),
158
+ runtimeUserScope: z.union(["storage", "global"]).default("storage"),
147
159
  cliPath: z.string(),
148
160
  dataDir: z.string(),
149
161
  customPackId: z.string(),
@@ -155,6 +167,11 @@ const Config = z.object({
155
167
  store: z.string(),
156
168
  timeoutMs: z.number().step(1).min(100).max(12e4).default(DEFAULT_TIMEOUT_MS),
157
169
  defaultRecallLimit: z.number().step(1).min(1).max(50).default(10),
170
+ runtimeMemory: RuntimeMemorySchema.default({
171
+ memoryLimitBytes: DEFAULT_RUNTIME_MEMORY_LIMIT_BYTES,
172
+ userLimitBytes: DEFAULT_RUNTIME_USER_LIMIT_BYTES,
173
+ maintenanceMaxTokens: DEFAULT_RUNTIME_MAINTENANCE_MAX_TOKENS
174
+ }),
158
175
  embedding: MnemonEmbeddingSchema.default({
159
176
  enabled: false,
160
177
  endpoint: DEFAULT_EMBEDDING_ENDPOINT,
@@ -337,6 +354,19 @@ function resolveRecallQuality(value) {
337
354
  maxUnknownResults
338
355
  };
339
356
  }
357
+ function resolveRuntimeMemory(value) {
358
+ const memoryLimitBytes = value?.memoryLimitBytes ?? 10240;
359
+ const userLimitBytes = value?.userLimitBytes ?? 4096;
360
+ const maintenanceMaxTokens = value?.maintenanceMaxTokens ?? 8192;
361
+ if (!Number.isInteger(memoryLimitBytes) || memoryLimitBytes < 1 || memoryLimitBytes > 1048576) throw new Error(`dsh-mnemon: Runtime MEMORY.md limit must be an integer within 1..${MAX_RUNTIME_MEMORY_LIMIT_BYTES} bytes`);
362
+ if (!Number.isInteger(userLimitBytes) || userLimitBytes < 1 || userLimitBytes > 1048576) throw new Error(`dsh-mnemon: Runtime USER.md limit must be an integer within 1..${MAX_RUNTIME_MEMORY_LIMIT_BYTES} bytes`);
363
+ if (!Number.isInteger(maintenanceMaxTokens) || maintenanceMaxTokens < 1 || maintenanceMaxTokens > 1e6) throw new Error(`dsh-mnemon: Runtime maintenance maxTokens must be an integer within 1..${MAX_RUNTIME_MAINTENANCE_MAX_TOKENS}`);
364
+ return {
365
+ memoryLimitBytes,
366
+ userLimitBytes,
367
+ maintenanceMaxTokens
368
+ };
369
+ }
340
370
  const MEMORY_COMPONENT_ID = /^[a-z][a-z0-9-]{0,127}$/u;
341
371
  const MEMORY_PARTICIPATION_MODES = /* @__PURE__ */ new Set([
342
372
  "off",
@@ -387,6 +417,8 @@ function resolveConfig(config = {}) {
387
417
  if (requestedPackId !== void 0 && !CUSTOM_PACK_ID.test(requestedPackId)) throw new Error("dsh-mnemon: customPackId is invalid");
388
418
  const store = optionalText$1(config.store);
389
419
  const storageScope = config.storageScope ?? (legacyDataDir === void 0 && legacyPacks.length === 0 ? "global" : "custom");
420
+ const runtimeUserScope = config.runtimeUserScope ?? "storage";
421
+ if (runtimeUserScope !== "storage" && runtimeUserScope !== "global") throw new Error(`dsh-mnemon: unsupported Runtime USER.md scope: ${String(runtimeUserScope)}`);
390
422
  const selectedPack = requestedPackId === void 0 ? legacyPacks.find((pack) => pack.dataDir === legacyDataDir) ?? (legacyPacks.length === 1 ? legacyPacks[0] : void 0) : legacyPacks.find((pack) => pack.id === requestedPackId);
391
423
  if (requestedPackId !== void 0 && selectedPack === void 0) throw new Error(`dsh-mnemon: unknown custom Pack: ${requestedPackId}`);
392
424
  const dataDir = selectedPack?.dataDir ?? legacyDataDir;
@@ -394,11 +426,13 @@ function resolveConfig(config = {}) {
394
426
  if (store !== void 0 && !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(store)) throw new Error("dsh-mnemon: store must match [a-zA-Z0-9][a-zA-Z0-9_-]*");
395
427
  return {
396
428
  storageScope,
429
+ runtimeUserScope,
397
430
  ...cliPath === void 0 ? {} : { cliPath },
398
431
  ...dataDir === void 0 ? {} : { dataDir },
399
432
  ...store === void 0 ? {} : { store },
400
433
  timeoutMs: config.timeoutMs ?? 1e4,
401
434
  defaultRecallLimit: config.defaultRecallLimit ?? 10,
435
+ runtimeMemory: resolveRuntimeMemory(config.runtimeMemory),
402
436
  embedding: resolveEmbedding(config.embedding),
403
437
  memoryTopology: resolveMemoryTopology(config.memoryTopology),
404
438
  recallQuality: resolveRecallQuality(config.recallQuality),
@@ -1163,8 +1197,8 @@ var DocumentManager = class {
1163
1197
  };
1164
1198
  const RUNTIME_ENTRY_DELIMITER = "\n§\n";
1165
1199
  const RUNTIME_MEMORY_LIMITS = {
1166
- memory: 10240,
1167
- user: 4096
1200
+ memory: DEFAULT_RUNTIME_MEMORY_LIMIT_BYTES,
1201
+ user: DEFAULT_RUNTIME_USER_LIMIT_BYTES
1168
1202
  };
1169
1203
  const RUNTIME_MEMORY_PROTOCOL = `MNEMON RUNTIME MEMORY PROTOCOL
1170
1204
  Runtime Memory keeps compact hot memory available for every turn. The latest MNEMON RUNTIME MEMORY SNAPSHOT in runtime context is a complete projection of USER.md and MEMORY.md and supersedes earlier Runtime Memory snapshots. Apply applicable entries silently and never recite them merely to prove they were read.
@@ -1184,6 +1218,7 @@ WRITE PROTOCOL
1184
1218
  - Choose target="user" only for the user profile and target="memory" only for project/environment knowledge. Use importance="critical" for explicit must/always/never rules or strong preferences, "low" for transient or one-time facts that are still worth keeping, and "normal" otherwise.
1185
1219
  - Entries are separated by a standalone §. old_text must uniquely identify one entry. Tool receipts are sufficient; do not echo either complete file after a successful mutation.
1186
1220
  - If USER.md reaches capacity, the tool conservatively consolidates the local profile without sending preferences to Mnemon Memory Spaces. If MEMORY.md reaches capacity, the tool archives committed working memories into one or more semantically appropriate Memory Spaces, then atomically applies compaction and the pending mutation only when the reviewed revision is still current. Never evade either limit with direct file edits.
1221
+ - Branch scoping (target=memory only): pass the optional branches parameter (a list of git branch names) to project an entry only in sessions on those branches; omit it for cross-branch facts. Use it for branch-specific architecture decisions and experiments, and tag new branch-scoped entries with the git branch reported in the snapshot header. On replace, provide branches to change the scope, an empty list to clear it, or omit it to keep the current scope. Non-git workspaces and detached HEAD project every entry regardless of scope.
1187
1222
 
1188
1223
  IMPORTANT: Runtime Memory is always relevant when applicable, after the current request. Use mnemon_runtime_memory only when the criteria above are met; otherwise do not mutate memory.`;
1189
1224
  const LOCK_TIMEOUT_MS$1 = 5e3;
@@ -1219,6 +1254,41 @@ function isTarget(value) {
1219
1254
  function isImportance(value) {
1220
1255
  return value === "critical" || value === "normal" || value === "low";
1221
1256
  }
1257
+ const RUNTIME_BRANCH_NAME_RE = /^[A-Za-z0-9._/-]+$/u;
1258
+ /** Validate a model-supplied branch scope. Returns the normalized list; an empty list means "no scope". */
1259
+ function normalizeRuntimeBranches(value, field = "branches") {
1260
+ if (!Array.isArray(value)) throw new Error(`${field} must be an array of git branch names`);
1261
+ const branches = [];
1262
+ const seen = /* @__PURE__ */ new Set();
1263
+ for (const item of value) {
1264
+ if (typeof item !== "string") throw new Error(`${field} must contain only git branch names`);
1265
+ const branch = item.trim();
1266
+ 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)`);
1267
+ if (seen.has(branch)) throw new Error(`${field} must not repeat a branch name`);
1268
+ seen.add(branch);
1269
+ branches.push(branch);
1270
+ }
1271
+ if (branches.length > 16) throw new Error(`${field} supports at most 16 branch names`);
1272
+ return branches;
1273
+ }
1274
+ /** Parse a stored branch scope. Returns undefined for absent or invalid data. */
1275
+ function parseRuntimeBranches(value) {
1276
+ if (value === void 0) return void 0;
1277
+ if (!Array.isArray(value)) return void 0;
1278
+ const branches = [];
1279
+ for (const item of value) {
1280
+ if (typeof item !== "string" || item === "" || item.length > 128 || !RUNTIME_BRANCH_NAME_RE.test(item)) return void 0;
1281
+ branches.push(item);
1282
+ }
1283
+ return branches;
1284
+ }
1285
+ function entryMatchesBranch(entry, branch) {
1286
+ return entry.branches === void 0 || entry.branches.length === 0 || entry.branches.includes(branch);
1287
+ }
1288
+ function scopeBranch(branch) {
1289
+ const value = branch?.trim();
1290
+ return value === void 0 || value === "" ? void 0 : value;
1291
+ }
1222
1292
  function normalizeContent(value, field) {
1223
1293
  const content = value?.trim().replace(/\s+/gu, " ") ?? "";
1224
1294
  if (content === "") throw new Error(`${field} is required`);
@@ -1232,12 +1302,15 @@ function parseEntry(value) {
1232
1302
  if (typeof value.created_at !== "string" || typeof value.updated_at !== "string") return void 0;
1233
1303
  const content = value.content.trim().replace(/\s+/gu, " ");
1234
1304
  if (content === "" || content.includes("§")) return void 0;
1305
+ if (value.target === "user" && value.branches !== void 0) return void 0;
1306
+ const branches = parseRuntimeBranches(value.branches);
1235
1307
  return {
1236
1308
  content,
1237
1309
  created_at: value.created_at,
1238
1310
  updated_at: value.updated_at,
1239
1311
  target: value.target,
1240
- importance: value.importance
1312
+ importance: value.importance,
1313
+ ...branches === void 0 ? {} : { branches }
1241
1314
  };
1242
1315
  }
1243
1316
  function byteCount(entries, target) {
@@ -1259,6 +1332,8 @@ function prepareMutation(before, request, now) {
1259
1332
  "remove"
1260
1333
  ].includes(request.action)) throw new Error("action must be add, replace, or remove");
1261
1334
  if (request.importance !== void 0 && !isImportance(request.importance)) throw new Error("importance must be critical, normal, or low");
1335
+ if (request.target === "user" && request.branches !== void 0) throw new Error("branches applies to target=memory only");
1336
+ const branchScope = request.branches === void 0 ? void 0 : normalizeRuntimeBranches(request.branches);
1262
1337
  const entries = before.map((entry) => ({ ...entry }));
1263
1338
  if (request.action === "add") {
1264
1339
  const content = normalizeContent(request.content, "content");
@@ -1277,7 +1352,8 @@ function prepareMutation(before, request, now) {
1277
1352
  created_at: now,
1278
1353
  updated_at: now,
1279
1354
  target: request.target,
1280
- importance: request.importance ?? "normal"
1355
+ importance: request.importance ?? "normal",
1356
+ ...branchScope !== void 0 && branchScope.length > 0 ? { branches: branchScope } : {}
1281
1357
  };
1282
1358
  return {
1283
1359
  changed: true,
@@ -1305,6 +1381,10 @@ function prepareMutation(before, request, now) {
1305
1381
  updated_at: now,
1306
1382
  importance: request.importance ?? previous.importance
1307
1383
  };
1384
+ if (branchScope !== void 0) {
1385
+ if (branchScope.length === 0) delete pendingEntry.branches;
1386
+ else pendingEntry.branches = branchScope;
1387
+ }
1308
1388
  entries[index] = pendingEntry;
1309
1389
  return {
1310
1390
  changed: true,
@@ -1340,12 +1420,14 @@ function compactionCandidates(compacted, existing, target, now) {
1340
1420
  if (seen.has(content)) throw new Error("compacted runtime memory contains duplicate entries");
1341
1421
  seen.add(content);
1342
1422
  const unchanged = existing.find((current) => current.content === content);
1423
+ const inheritedBranches = entry.branches ?? unchanged?.branches;
1343
1424
  return {
1344
1425
  content,
1345
1426
  created_at: unchanged?.created_at ?? now,
1346
1427
  updated_at: unchanged?.updated_at ?? now,
1347
1428
  target,
1348
- importance: entry.importance
1429
+ importance: entry.importance,
1430
+ ...inheritedBranches === void 0 ? {} : { branches: inheritedBranches }
1349
1431
  };
1350
1432
  });
1351
1433
  }
@@ -1376,49 +1458,112 @@ function sleepSync$1(milliseconds) {
1376
1458
  * Single authority for hot memory. JSON is the durable source of truth;
1377
1459
  * Markdown files are deterministic projections consumed by prompt assembly.
1378
1460
  */
1379
- var RuntimeMemoryController = class {
1461
+ var RuntimeMemoryController = class RuntimeMemoryController {
1380
1462
  now;
1381
1463
  recordCommit;
1382
1464
  directory;
1383
1465
  sourcePath;
1466
+ userSourcePath;
1384
1467
  memoryPath;
1385
1468
  userPath;
1386
1469
  lockPath;
1470
+ limits;
1387
1471
  queue = Promise.resolve();
1388
- constructor(runner, now = () => /* @__PURE__ */ new Date(), recordCommit) {
1472
+ localUserPath;
1473
+ userController;
1474
+ constructor(runner, now = () => /* @__PURE__ */ new Date(), recordCommit, limits = RUNTIME_MEMORY_LIMITS, userRunner) {
1389
1475
  this.now = now;
1390
1476
  this.recordCommit = recordCommit;
1477
+ for (const [target, limit] of Object.entries(limits)) if (!Number.isInteger(limit) || limit < 1 || limit > 1048576) throw new Error(`runtime ${target} memory limit must be an integer within 1..${MAX_RUNTIME_MEMORY_LIMIT_BYTES} bytes`);
1478
+ this.limits = {
1479
+ memory: limits.memory,
1480
+ user: limits.user
1481
+ };
1391
1482
  this.directory = join(runner.effectiveDataDir(), "runtime");
1392
1483
  this.sourcePath = join(this.directory, "memories.json");
1393
1484
  this.memoryPath = join(this.directory, "MEMORY.md");
1394
- this.userPath = join(this.directory, "USER.md");
1485
+ this.localUserPath = join(this.directory, "USER.md");
1395
1486
  this.lockPath = join(this.directory, ".memories.lock");
1487
+ const userDirectory = userRunner === void 0 ? this.directory : join(userRunner.effectiveDataDir(), "runtime");
1488
+ this.userController = userDirectory === this.directory || userRunner === void 0 ? void 0 : new RuntimeMemoryController(userRunner, now, recordCommit, this.limits);
1489
+ this.userPath = this.userController?.userPath ?? this.localUserPath;
1490
+ this.userSourcePath = this.userController?.sourcePath ?? this.sourcePath;
1396
1491
  this.initialize();
1397
1492
  }
1398
1493
  snapshot() {
1399
- const file = this.readSource();
1400
- const entries = file.entries.map((entry) => ({ ...entry }));
1401
- return {
1494
+ const local = this.readSource();
1495
+ if (this.userController === void 0) return this.snapshotUnlocked(local);
1496
+ const global = this.userController.readSource();
1497
+ return this.snapshotUnlocked({
1498
+ version: 1,
1499
+ entries: [...global.entries.filter((entry) => entry.target === "user"), ...local.entries.filter((entry) => entry.target === "memory")]
1500
+ });
1501
+ }
1502
+ contextText(branch) {
1503
+ return this.contextProjection(branch).text;
1504
+ }
1505
+ /**
1506
+ * Read the exact Runtime revision and its prompt projection from each
1507
+ * configured authority root.
1508
+ * When a git branch is supplied, target=memory entries scoped to other
1509
+ * branches are hidden from the projection; the on-disk Markdown files and
1510
+ * the source JSON always remain complete.
1511
+ */
1512
+ contextProjection(branch) {
1513
+ const branchScope = scopeBranch(branch);
1514
+ const local = this.localContextProjection(branchScope);
1515
+ const global = this.userController?.localContextProjection();
1516
+ const user = global?.user ?? local.user;
1517
+ const memory = local.memory;
1518
+ const entries = global === void 0 ? local.snapshot.entries : [...global.snapshot.entries.filter((entry) => entry.target === "user"), ...local.snapshot.entries.filter((entry) => entry.target === "memory")];
1519
+ const visibleMemory = local.snapshot.targets.memory;
1520
+ const visibleUser = global?.snapshot.targets.user ?? local.snapshot.targets.user;
1521
+ const snapshot = global === void 0 ? local.snapshot : {
1402
1522
  directory: this.directory,
1403
1523
  sourcePath: this.sourcePath,
1404
- revision: revision(file),
1524
+ revision: revision({
1525
+ version: 1,
1526
+ entries
1527
+ }),
1405
1528
  generatedAt: this.now().toISOString(),
1406
1529
  entries,
1407
1530
  targets: {
1408
- memory: this.targetView(entries, "memory"),
1409
- user: this.targetView(entries, "user")
1531
+ memory: {
1532
+ ...visibleMemory,
1533
+ markdownPath: this.memoryPath
1534
+ },
1535
+ user: {
1536
+ ...visibleUser,
1537
+ markdownPath: this.userPath
1538
+ }
1410
1539
  }
1411
1540
  };
1541
+ const storeMemory = this.targetView(entries, "memory");
1542
+ const storeUser = this.targetView(entries, "user");
1543
+ const branchLine = branchScope === void 0 ? "" : `\nGit branch: ${branchScope}${local.hidden > 0 ? ` (${local.hidden} branch-scoped entr${local.hidden === 1 ? "y" : "ies"} hidden)` : ""}`;
1544
+ return {
1545
+ revision: snapshot.revision,
1546
+ text: `MNEMON RUNTIME MEMORY SNAPSHOT
1547
+ Revision: ${snapshot.revision}${branchLine}
1548
+
1549
+ Contents of USER.md (user profile; entries: ${visibleUser.entryCount}; UTF-8 bytes: ${storeUser.used}/${storeUser.limit})
1550
+ <runtime-memory-file name="USER.md">
1551
+ ${user || "(empty)"}
1552
+ </runtime-memory-file>
1553
+
1554
+ Contents of MEMORY.md (working reference; entries: ${visibleMemory.entryCount}; UTF-8 bytes: ${storeMemory.used}/${storeMemory.limit})
1555
+ <runtime-memory-file name="MEMORY.md">
1556
+ ${memory || "(empty)"}
1557
+ </runtime-memory-file>`
1558
+ };
1412
1559
  }
1413
- contextText() {
1414
- return this.contextProjection().text;
1415
- }
1416
- /** Read the exact Runtime revision and its prompt projection under one lock. */
1417
- contextProjection() {
1418
- const { snapshot, user, memory } = this.withLock(() => {
1560
+ localContextProjection(branch) {
1561
+ const branchScope = scopeBranch(branch);
1562
+ return this.withLock(() => {
1419
1563
  const file = this.readSource();
1420
1564
  this.repairProjections(file);
1421
1565
  const entries = file.entries.map((entry) => ({ ...entry }));
1566
+ const visible = branchScope === void 0 ? entries : entries.filter((entry) => entry.target === "user" || entryMatchesBranch(entry, branchScope));
1422
1567
  return {
1423
1568
  snapshot: {
1424
1569
  directory: this.directory,
@@ -1427,33 +1572,18 @@ var RuntimeMemoryController = class {
1427
1572
  generatedAt: this.now().toISOString(),
1428
1573
  entries,
1429
1574
  targets: {
1430
- memory: this.targetView(entries, "memory"),
1431
- user: this.targetView(entries, "user")
1575
+ memory: this.targetView(visible, "memory"),
1576
+ user: this.targetView(visible, "user")
1432
1577
  }
1433
1578
  },
1434
- user: readFileSync(this.userPath, "utf8").trimEnd(),
1435
- memory: readFileSync(this.memoryPath, "utf8").trimEnd()
1579
+ user: readFileSync(this.localUserPath, "utf8").trimEnd(),
1580
+ memory: branchScope === void 0 ? readFileSync(this.memoryPath, "utf8").trimEnd() : markdown(visible, "memory").trimEnd(),
1581
+ hidden: branchScope === void 0 ? 0 : entries.length - visible.length
1436
1582
  };
1437
1583
  });
1438
- const userUsage = snapshot.targets.user;
1439
- const memoryUsage = snapshot.targets.memory;
1440
- return {
1441
- revision: snapshot.revision,
1442
- text: `MNEMON RUNTIME MEMORY SNAPSHOT
1443
- Revision: ${snapshot.revision}
1444
-
1445
- Contents of USER.md (user profile; entries: ${userUsage.entryCount}; UTF-8 bytes: ${userUsage.used}/${userUsage.limit})
1446
- <runtime-memory-file name="USER.md">
1447
- ${user || "(empty)"}
1448
- </runtime-memory-file>
1449
-
1450
- Contents of MEMORY.md (working reference; entries: ${memoryUsage.entryCount}; UTF-8 bytes: ${memoryUsage.used}/${memoryUsage.limit})
1451
- <runtime-memory-file name="MEMORY.md">
1452
- ${memory || "(empty)"}
1453
- </runtime-memory-file>`
1454
- };
1455
1584
  }
1456
1585
  mutate(request) {
1586
+ if (request.target === "user" && this.userController !== void 0) return this.userController.mutate(request);
1457
1587
  const operation = this.queue.then(() => this.withLock(() => {
1458
1588
  const beforeRevision = revision(this.readSource());
1459
1589
  return {
@@ -1479,12 +1609,13 @@ ${memory || "(empty)"}
1479
1609
  }
1480
1610
  /** Resolve exactly which committed entries survive a blocked mutation and are safe to compact. */
1481
1611
  planMaintenance(request) {
1612
+ if (request.target === "user" && this.userController !== void 0) return this.userController.planMaintenance(request);
1482
1613
  const operation = this.queue.then(() => this.withLock(() => {
1483
1614
  const file = this.readSource();
1484
1615
  const prepared = prepareMutation(file.entries, request, this.now().toISOString());
1485
1616
  const used = byteCount(file.entries, request.target);
1486
1617
  const projected = byteCount(prepared.projectedEntries, request.target);
1487
- const limit = RUNTIME_MEMORY_LIMITS[request.target];
1618
+ const limit = this.limits[request.target];
1488
1619
  return {
1489
1620
  revision: revision(file),
1490
1621
  action: request.action,
@@ -1505,14 +1636,16 @@ ${memory || "(empty)"}
1505
1636
  return operation;
1506
1637
  }
1507
1638
  /** Commit semantic compaction and the original mutation together, or leave every local file unchanged. */
1508
- compactAndMutate(expectedRevision, request, compacted, maxCompactedBytes = RUNTIME_MEMORY_LIMITS[request.target], lineage = []) {
1639
+ compactAndMutate(expectedRevision, request, compacted, maxCompactedBytes, lineage = []) {
1640
+ if (request.target === "user" && this.userController !== void 0) return this.userController.compactAndMutate(expectedRevision, request, compacted, maxCompactedBytes, lineage);
1509
1641
  const operation = this.queue.then(() => this.withLock(() => {
1510
1642
  const file = this.readSource();
1511
1643
  const beforeRevision = revision(file);
1512
1644
  if (beforeRevision !== expectedRevision) throw new RuntimeMemoryConflictError();
1513
1645
  const now = this.now().toISOString();
1514
1646
  const prepared = prepareMutation(file.entries, request, now);
1515
- if (!Number.isInteger(maxCompactedBytes) || maxCompactedBytes < 0 || maxCompactedBytes > RUNTIME_MEMORY_LIMITS[request.target]) throw new Error("compaction byte budget is invalid");
1647
+ const compactedByteBudget = maxCompactedBytes ?? this.limits[request.target];
1648
+ if (!Number.isInteger(compactedByteBudget) || compactedByteBudget < 0 || compactedByteBudget > this.limits[request.target]) throw new Error("compaction byte budget is invalid");
1516
1649
  if (!prepared.changed) return {
1517
1650
  result: this.result(request.target, prepared.projectedEntries, prepared.fields),
1518
1651
  beforeRevision,
@@ -1521,10 +1654,10 @@ ${memory || "(empty)"}
1521
1654
  const replacements = compactionCandidates(compacted, prepared.compactableEntries, request.target, now);
1522
1655
  if (prepared.pendingEntry !== void 0 && replacements.some((entry) => entry.content === prepared.pendingEntry.content)) throw new Error("compacted runtime memory duplicates the pending mutation");
1523
1656
  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");
1524
- const targetEntries = [...packCompactionCandidates(replacements, request.target, maxCompactedBytes), ...prepared.pendingEntry === void 0 ? [] : [prepared.pendingEntry]];
1657
+ const targetEntries = [...packCompactionCandidates(replacements, request.target, compactedByteBudget), ...prepared.pendingEntry === void 0 ? [] : [prepared.pendingEntry]];
1525
1658
  const entries = [...file.entries.filter((entry) => entry.target !== request.target), ...targetEntries];
1526
1659
  const used = byteCount(entries, request.target);
1527
- const limit = RUNTIME_MEMORY_LIMITS[request.target];
1660
+ const limit = this.limits[request.target];
1528
1661
  if (used > limit) throw new RuntimeMemoryCapacityError(request.target, byteCount(file.entries, request.target), used, limit);
1529
1662
  const next = {
1530
1663
  version: 1,
@@ -1560,17 +1693,19 @@ ${memory || "(empty)"}
1560
1693
  return operation;
1561
1694
  }
1562
1695
  /** Apply an LLM-produced compaction only to the exact snapshot it reviewed. */
1563
- compactTarget(expectedRevision, target, compacted, maxBytes = RUNTIME_MEMORY_LIMITS[target], lineage = []) {
1696
+ compactTarget(expectedRevision, target, compacted, maxBytes, lineage = []) {
1697
+ if (target === "user" && this.userController !== void 0) return this.userController.compactTarget(expectedRevision, target, compacted, maxBytes, lineage).then(() => this.snapshot());
1564
1698
  const operation = this.queue.then(() => this.withLock(() => {
1565
1699
  const file = this.readSource();
1566
1700
  const beforeRevision = revision(file);
1567
1701
  if (beforeRevision !== expectedRevision) throw new RuntimeMemoryConflictError();
1568
- if (!Number.isInteger(maxBytes) || maxBytes < 0 || maxBytes > RUNTIME_MEMORY_LIMITS[target]) throw new Error("compaction byte budget is invalid");
1702
+ const byteBudget = maxBytes ?? this.limits[target];
1703
+ if (!Number.isInteger(byteBudget) || byteBudget < 0 || byteBudget > this.limits[target]) throw new Error("compaction byte budget is invalid");
1569
1704
  const now = this.now().toISOString();
1570
- const fitted = packCompactionCandidates(compactionCandidates(compacted, file.entries.filter((entry) => entry.target === target), target, now), target, maxBytes);
1705
+ const fitted = packCompactionCandidates(compactionCandidates(compacted, file.entries.filter((entry) => entry.target === target), target, now), target, byteBudget);
1571
1706
  const entries = [...file.entries.filter((entry) => entry.target !== target), ...fitted];
1572
1707
  const used = byteCount(entries, target);
1573
- const limit = RUNTIME_MEMORY_LIMITS[target];
1708
+ const limit = this.limits[target];
1574
1709
  if (used > limit) throw new RuntimeMemoryCapacityError(target, byteCount(file.entries, target), used, limit);
1575
1710
  this.persist({
1576
1711
  version: 1,
@@ -1601,7 +1736,7 @@ ${memory || "(empty)"}
1601
1736
  return snapshot;
1602
1737
  });
1603
1738
  this.queue = operation.catch(() => void 0);
1604
- return operation;
1739
+ return this.userController === void 0 ? operation : operation.then(() => this.snapshot());
1605
1740
  }
1606
1741
  initialize() {
1607
1742
  mkdirSync(this.directory, {
@@ -1618,7 +1753,7 @@ ${memory || "(empty)"}
1618
1753
  const prepared = prepareMutation(file.entries, request, this.now().toISOString());
1619
1754
  if (!prepared.changed) return this.result(request.target, prepared.projectedEntries, prepared.fields);
1620
1755
  const used = byteCount(prepared.projectedEntries, request.target);
1621
- const limit = RUNTIME_MEMORY_LIMITS[request.target];
1756
+ const limit = this.limits[request.target];
1622
1757
  if (used > limit) throw new RuntimeMemoryCapacityError(request.target, byteCount(file.entries, request.target), used, limit);
1623
1758
  this.persist({
1624
1759
  version: 1,
@@ -1634,7 +1769,7 @@ ${memory || "(empty)"}
1634
1769
  entryCount: entries.filter((entry) => entry.target === target).length,
1635
1770
  usage: {
1636
1771
  used: byteCount(entries, target),
1637
- limit: RUNTIME_MEMORY_LIMITS[target]
1772
+ limit: this.limits[target]
1638
1773
  },
1639
1774
  ...fields.added === void 0 ? {} : { added: fields.added },
1640
1775
  ...fields.replaced === void 0 ? {} : { replaced: fields.replaced },
@@ -1646,7 +1781,7 @@ ${memory || "(empty)"}
1646
1781
  target,
1647
1782
  entryCount: entries.filter((entry) => entry.target === target).length,
1648
1783
  used: byteCount(entries, target),
1649
- limit: RUNTIME_MEMORY_LIMITS[target],
1784
+ limit: this.limits[target],
1650
1785
  markdownPath: target === "memory" ? this.memoryPath : this.userPath
1651
1786
  };
1652
1787
  }
@@ -1690,7 +1825,7 @@ ${memory || "(empty)"}
1690
1825
  });
1691
1826
  const nonce = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
1692
1827
  const writes = [
1693
- [this.userPath, markdown(file.entries, "user")],
1828
+ [this.localUserPath, markdown(file.entries, "user")],
1694
1829
  [this.memoryPath, markdown(file.entries, "memory")],
1695
1830
  [this.sourcePath, `${JSON.stringify(file, null, 2)}\n`]
1696
1831
  ];
@@ -1706,7 +1841,7 @@ ${memory || "(empty)"}
1706
1841
  }
1707
1842
  }
1708
1843
  repairProjections(file) {
1709
- for (const [path, target] of [[this.userPath, "user"], [this.memoryPath, "memory"]]) {
1844
+ for (const [path, target] of [[this.localUserPath, "user"], [this.memoryPath, "memory"]]) {
1710
1845
  const expected = markdown(file.entries, target);
1711
1846
  let current;
1712
1847
  try {
@@ -1951,7 +2086,7 @@ function decodeArchive(base64) {
1951
2086
  if (bytes.length === 0 || bytes.length > 50331648) throw new Error("Mnemon Pack archive is empty or too large");
1952
2087
  return bytes;
1953
2088
  }
1954
- function parseArchive(base64) {
2089
+ function parseArchive(base64, runtimeLimits = RUNTIME_MEMORY_LIMITS) {
1955
2090
  const archive = decodeArchive(base64);
1956
2091
  let count = 0;
1957
2092
  let expandedBytes = 0;
@@ -1982,8 +2117,8 @@ function parseArchive(base64) {
1982
2117
  const expected = checksumFiles[path];
1983
2118
  if (typeof expected !== "string" || expected !== sha256$1(files[path])) throw new Error(`Mnemon Pack checksum mismatch: ${path}`);
1984
2119
  }
1985
- validatePackPayload(files, manifest.components);
1986
- const actualSummary = summaryFor(manifest.components, files);
2120
+ validatePackPayload(files, manifest.components, runtimeLimits);
2121
+ const actualSummary = summaryFor(manifest.components, files, runtimeLimits);
1987
2122
  return {
1988
2123
  archiveBytes: archive.length,
1989
2124
  expandedBytes,
@@ -1994,7 +2129,7 @@ function parseArchive(base64) {
1994
2129
  }
1995
2130
  };
1996
2131
  }
1997
- function parseRuntime(value) {
2132
+ function parseRuntime(value, limits = RUNTIME_MEMORY_LIMITS) {
1998
2133
  const source = record$2(value);
1999
2134
  if (source?.version !== 1 || !Array.isArray(source.entries)) throw new Error("runtime memories.json is invalid");
2000
2135
  const entries = source.entries.map((raw) => {
@@ -2015,7 +2150,7 @@ function parseRuntime(value) {
2015
2150
  updated_at: entry.updated_at
2016
2151
  };
2017
2152
  });
2018
- for (const target of ["user", "memory"]) if (runtimeBytes(entries, target) > RUNTIME_MEMORY_LIMITS[target]) throw new Error(`runtime ${target} memory exceeds its ${RUNTIME_MEMORY_LIMITS[target]} byte limit`);
2153
+ for (const target of ["user", "memory"]) if (runtimeBytes(entries, target) > limits[target]) throw new Error(`runtime ${target} memory exceeds its ${limits[target]} byte limit`);
2019
2154
  return {
2020
2155
  version: 1,
2021
2156
  entries
@@ -2090,8 +2225,8 @@ function parseRegistry(value) {
2090
2225
  function validDatabase(bytes, path) {
2091
2226
  if (bytes.length < 100 || !Buffer.from(bytes.subarray(0, SQLITE_HEADER.length)).equals(SQLITE_HEADER)) throw new Error(`${path} is not a SQLite mnemon.db`);
2092
2227
  }
2093
- function validatePackPayload(files, components) {
2094
- if (components.includes("runtime")) parseRuntime(json(files["payload/runtime/memories.json"] ?? /* @__PURE__ */ new Uint8Array(), "payload/runtime/memories.json"));
2228
+ function validatePackPayload(files, components, runtimeLimits) {
2229
+ if (components.includes("runtime")) parseRuntime(json(files["payload/runtime/memories.json"] ?? /* @__PURE__ */ new Uint8Array(), "payload/runtime/memories.json"), runtimeLimits);
2095
2230
  if (components.includes("documents")) {
2096
2231
  const index = parseDocumentIndex(json(files["payload/documents/index.json"] ?? /* @__PURE__ */ new Uint8Array(), "payload/documents/index.json"));
2097
2232
  for (const document of index.documents) {
@@ -2172,9 +2307,9 @@ function emptyRuntime() {
2172
2307
  entries: []
2173
2308
  };
2174
2309
  }
2175
- function readCurrentRuntime(root) {
2310
+ function readCurrentRuntime(root, limits = RUNTIME_MEMORY_LIMITS) {
2176
2311
  const path = join(root, "runtime", "memories.json");
2177
- return existsSync(path) ? parseRuntime(JSON.parse(readFileSync(path, "utf8"))) : emptyRuntime();
2312
+ return existsSync(path) ? parseRuntime(JSON.parse(readFileSync(path, "utf8")), limits) : emptyRuntime();
2178
2313
  }
2179
2314
  function writeRuntime(directory, file) {
2180
2315
  mkdirSync(directory, {
@@ -2306,12 +2441,12 @@ function writeRegistry(directory, registry, databases) {
2306
2441
  }
2307
2442
  writeFileSync(join(directory, ".dsh-memory-bodies.json"), `${JSON.stringify(registry, null, 2)}\n`, { mode: 384 });
2308
2443
  }
2309
- function componentItems(component, files) {
2310
- if (component === "runtime") return parseRuntime(json(files["payload/runtime/memories.json"], "payload/runtime/memories.json")).entries.length;
2444
+ function componentItems(component, files, runtimeLimits = RUNTIME_MEMORY_LIMITS) {
2445
+ if (component === "runtime") return parseRuntime(json(files["payload/runtime/memories.json"], "payload/runtime/memories.json"), runtimeLimits).entries.length;
2311
2446
  if (component === "documents") return parseDocumentIndex(json(files["payload/documents/index.json"], "payload/documents/index.json")).documents.length;
2312
2447
  return parseRegistry(json(files["payload/data/.dsh-memory-bodies.json"], "payload/data/.dsh-memory-bodies.json")).bodies.length;
2313
2448
  }
2314
- function summaryFor(components, files) {
2449
+ function summaryFor(components, files, runtimeLimits = RUNTIME_MEMORY_LIMITS) {
2315
2450
  return components.map((component) => {
2316
2451
  const prefix = `payload/${COMPONENT_DIRECTORIES[component]}/`;
2317
2452
  const entries = Object.entries(files).filter(([path]) => path.startsWith(prefix));
@@ -2319,14 +2454,14 @@ function summaryFor(components, files) {
2319
2454
  component,
2320
2455
  files: entries.length,
2321
2456
  bytes: entries.reduce((sum, [, value]) => sum + value.length, 0),
2322
- items: componentItems(component, files)
2457
+ items: componentItems(component, files, runtimeLimits)
2323
2458
  };
2324
2459
  });
2325
2460
  }
2326
- function collectExport(root, components) {
2461
+ function collectExport(root, components, runtimeLimits = RUNTIME_MEMORY_LIMITS) {
2327
2462
  const files = {};
2328
2463
  if (components.includes("runtime")) {
2329
- const runtime = readCurrentRuntime(root);
2464
+ const runtime = readCurrentRuntime(root, runtimeLimits);
2330
2465
  files["payload/runtime/memories.json"] = strToU8(`${JSON.stringify(runtime, null, 2)}\n`);
2331
2466
  files["payload/runtime/USER.md"] = strToU8(runtimeProjection(runtime.entries, "user"));
2332
2467
  files["payload/runtime/MEMORY.md"] = strToU8(runtimeProjection(runtime.entries, "memory"));
@@ -2343,9 +2478,9 @@ function collectExport(root, components) {
2343
2478
  }
2344
2479
  return files;
2345
2480
  }
2346
- function mergeRuntime(root, pack) {
2347
- const current = readCurrentRuntime(root);
2348
- const incoming = parseRuntime(json(pack.files["payload/runtime/memories.json"], "payload/runtime/memories.json"));
2481
+ function mergeRuntime(root, pack, runtimeLimits) {
2482
+ const current = readCurrentRuntime(root, runtimeLimits);
2483
+ const incoming = parseRuntime(json(pack.files["payload/runtime/memories.json"], "payload/runtime/memories.json"), runtimeLimits);
2349
2484
  const keys = new Set(current.entries.map((entry) => `${entry.target}\0${entry.content}`));
2350
2485
  const entries = [...current.entries];
2351
2486
  for (const entry of incoming.entries) {
@@ -2358,7 +2493,7 @@ function mergeRuntime(root, pack) {
2358
2493
  return parseRuntime({
2359
2494
  version: 1,
2360
2495
  entries
2361
- });
2496
+ }, runtimeLimits);
2362
2497
  }
2363
2498
  function mergeDocuments(root, pack) {
2364
2499
  const current = readCurrentDocuments(root);
@@ -2424,7 +2559,7 @@ function reconcilePersistedStore(root) {
2424
2559
  rmSync(temporary, { force: true });
2425
2560
  }
2426
2561
  }
2427
- function stageImport(root, pack, components, mode) {
2562
+ function stageImport(root, pack, components, mode, runtimeLimits) {
2428
2563
  const staging = join(root, `.dsh-pack-stage-${randomUUID()}`);
2429
2564
  mkdirSync(staging, {
2430
2565
  recursive: true,
@@ -2432,7 +2567,7 @@ function stageImport(root, pack, components, mode) {
2432
2567
  });
2433
2568
  try {
2434
2569
  if (components.includes("runtime")) {
2435
- const runtime = mode === "merge" ? mergeRuntime(root, pack) : parseRuntime(json(pack.files["payload/runtime/memories.json"], "payload/runtime/memories.json"));
2570
+ const runtime = mode === "merge" ? mergeRuntime(root, pack, runtimeLimits) : parseRuntime(json(pack.files["payload/runtime/memories.json"], "payload/runtime/memories.json"), runtimeLimits);
2436
2571
  writeRuntime(join(staging, "runtime"), runtime);
2437
2572
  }
2438
2573
  if (components.includes("documents")) {
@@ -2531,12 +2666,17 @@ var MnemonPackManager = class {
2531
2666
  afterImport;
2532
2667
  now;
2533
2668
  root;
2669
+ runtimeLimits;
2534
2670
  constructor(runner, config, afterImport = () => {}, now = () => /* @__PURE__ */ new Date()) {
2535
2671
  this.runner = runner;
2536
2672
  this.config = config;
2537
2673
  this.afterImport = afterImport;
2538
2674
  this.now = now;
2539
2675
  this.root = resolve(runner.effectiveDataDir());
2676
+ this.runtimeLimits = {
2677
+ memory: config.runtimeMemory.memoryLimitBytes,
2678
+ user: config.runtimeMemory.userLimitBytes
2679
+ };
2540
2680
  }
2541
2681
  target() {
2542
2682
  return {
@@ -2549,9 +2689,9 @@ var MnemonPackManager = class {
2549
2689
  return this.runner.withExclusive(async () => {
2550
2690
  await new Promise((resolveReady) => setImmediate(resolveReady));
2551
2691
  return withLocks(this.root, components, () => {
2552
- const payload = collectExport(this.root, components);
2692
+ const payload = collectExport(this.root, components, this.runtimeLimits);
2553
2693
  const exportedAt = this.now().toISOString();
2554
- const summary = summaryFor(components, payload);
2694
+ const summary = summaryFor(components, payload, this.runtimeLimits);
2555
2695
  const manifest = {
2556
2696
  format: MNEMON_PACK_FORMAT,
2557
2697
  version: 1,
@@ -2590,7 +2730,7 @@ var MnemonPackManager = class {
2590
2730
  });
2591
2731
  }
2592
2732
  inspectPack(base64, fileName) {
2593
- const pack = parseArchive(base64);
2733
+ const pack = parseArchive(base64, this.runtimeLimits);
2594
2734
  const sanitizedName = safeName(fileName);
2595
2735
  return {
2596
2736
  ...sanitizedName === void 0 ? {} : { fileName: sanitizedName },
@@ -2603,7 +2743,7 @@ var MnemonPackManager = class {
2603
2743
  };
2604
2744
  }
2605
2745
  async importPack(base64, options) {
2606
- const pack = parseArchive(base64);
2746
+ const pack = parseArchive(base64, this.runtimeLimits);
2607
2747
  if (options.mode !== "merge" && options.mode !== "replace") throw new Error("Pack import mode must be merge or replace");
2608
2748
  if (options.components !== void 0 && (new Set(options.components).size !== options.components.length || options.components.some((component) => !COMPONENT_ORDER.includes(component)))) throw new Error("requested import components are invalid");
2609
2749
  const components = options.components === void 0 ? pack.manifest.components : COMPONENT_ORDER.filter((component) => options.components.includes(component));
@@ -2615,7 +2755,7 @@ var MnemonPackManager = class {
2615
2755
  mode: 448
2616
2756
  });
2617
2757
  return withLocks(this.root, components, () => {
2618
- const staging = stageImport(this.root, pack, components, options.mode);
2758
+ const staging = stageImport(this.root, pack, components, options.mode, this.runtimeLimits);
2619
2759
  commitStaging(this.root, staging, components);
2620
2760
  this.afterImport(components);
2621
2761
  return {
@@ -5482,6 +5622,37 @@ function registerBuiltinMemoryAdapters(catalog) {
5482
5622
  };
5483
5623
  }
5484
5624
  //#endregion
5625
+ //#region src/git-branch.ts
5626
+ const GIT_BRANCH_TIMEOUT_MS = 2e3;
5627
+ /**
5628
+ * Resolve the current git branch of a workspace root. Returns undefined when
5629
+ * the directory is not a git working tree, HEAD is detached, or the probe
5630
+ * fails or times out, so callers fall back to the unfiltered view.
5631
+ */
5632
+ function resolveGitBranch(cwd) {
5633
+ const root = cwd?.trim();
5634
+ if (root === void 0 || root === "") return void 0;
5635
+ try {
5636
+ const branch = execFileSync("git", [
5637
+ "-C",
5638
+ root,
5639
+ "branch",
5640
+ "--show-current"
5641
+ ], {
5642
+ encoding: "utf8",
5643
+ timeout: GIT_BRANCH_TIMEOUT_MS,
5644
+ stdio: [
5645
+ "ignore",
5646
+ "pipe",
5647
+ "ignore"
5648
+ ]
5649
+ }).trim();
5650
+ return branch === "" ? void 0 : branch;
5651
+ } catch {
5652
+ return;
5653
+ }
5654
+ }
5655
+ //#endregion
5485
5656
  //#region src/memory-view.ts
5486
5657
  function hash(value) {
5487
5658
  return createHash("sha256").update(value).digest("hex");
@@ -5490,12 +5661,12 @@ function workspace(scope) {
5490
5661
  const value = scope?.workspaceId?.trim();
5491
5662
  return value === void 0 || value === "" ? void 0 : value;
5492
5663
  }
5493
- function runtimeSource(runtimeMemory) {
5664
+ function createRuntimeMemorySource(runtimeMemory, resolveBranch = resolveGitBranch) {
5494
5665
  return {
5495
5666
  layerId: "runtime",
5496
5667
  mode: "eager",
5497
- snapshot: () => {
5498
- const projection = runtimeMemory.contextProjection();
5668
+ snapshot: (context) => {
5669
+ const projection = runtimeMemory.contextProjection(resolveBranch(workspace(context.scope)));
5499
5670
  return {
5500
5671
  revision: projection.revision,
5501
5672
  wake: projection.text
@@ -5556,9 +5727,9 @@ function memorySpacesSource(service) {
5556
5727
  }
5557
5728
  };
5558
5729
  }
5559
- function createDefaultMemoryTurnViewManager(kernel, sources) {
5730
+ function createDefaultMemoryTurnViewManager(kernel, sources, options = {}) {
5560
5731
  return new MemoryTurnViewManager(kernel, [
5561
- runtimeSource(sources.runtimeMemory),
5732
+ createRuntimeMemorySource(sources.runtimeMemory, options.resolveBranch ?? resolveGitBranch),
5562
5733
  documentsSource(sources.documents),
5563
5734
  memorySpacesSource(sources.service)
5564
5735
  ]);
@@ -5655,7 +5826,16 @@ function createRuntimeGraph(config, workspaceRoot, extensions) {
5655
5826
  return receiptBridge.record(operation);
5656
5827
  };
5657
5828
  const service = new MnemonService(runner, config, void 0, void 0, void 0, recordCommit);
5658
- const runtimeMemory = new RuntimeMemoryController(runner, void 0, recordCommit);
5829
+ const globalUserConfig = {
5830
+ ...config,
5831
+ storageScope: "global"
5832
+ };
5833
+ delete globalUserConfig.dataDir;
5834
+ const globalUserRunner = config.runtimeUserScope === "global" ? createRunner(globalUserConfig) : void 0;
5835
+ const runtimeMemory = new RuntimeMemoryController(runner, void 0, recordCommit, {
5836
+ memory: config.runtimeMemory.memoryLimitBytes,
5837
+ user: config.runtimeMemory.userLimitBytes
5838
+ }, globalUserRunner);
5659
5839
  const documents = new DocumentManager(void 0, void 0, () => runner.effectiveDataDir(), recordCommit);
5660
5840
  const storage = new StorageScopeInspector(runner, config);
5661
5841
  const packs = new MnemonPackManager(runner, config, (components) => {
@@ -6361,10 +6541,13 @@ function naturalEvidence(evidence) {
6361
6541
  return `${index + 1}. [${citation}]${meta === "" ? "" : ` ${meta}`}\n${indentedText(item.content)}`;
6362
6542
  }).join("\n");
6363
6543
  }
6544
+ function runtimeEntryScopeMeta(entry) {
6545
+ return entry.branches && entry.branches.length > 0 ? ` branches=${entry.branches.join(",")}` : "";
6546
+ }
6364
6547
  function runtimeSnapshotContext(target, entries) {
6365
6548
  return `Committed ${target === "memory" ? "MEMORY.md" : "USER.md"} snapshot (read-only run data; numbering is one-based):
6366
6549
  <runtime-memory-snapshot target="${target}">
6367
- ${entries.length === 0 ? "(empty)" : entries.map((entry, index) => `${index + 1}. [importance=${entry.importance}] ${entry.content}`).join(RUNTIME_ENTRY_DELIMITER)}
6550
+ ${entries.length === 0 ? "(empty)" : entries.map((entry, index) => `${index + 1}. [importance=${entry.importance}${runtimeEntryScopeMeta(entry)}] ${entry.content}`).join(RUNTIME_ENTRY_DELIMITER)}
6368
6551
  </runtime-memory-snapshot>`;
6369
6552
  }
6370
6553
  function runtimeRoutingExcerpt(value) {
@@ -6380,7 +6563,7 @@ function runtimeRouteChunks(entries) {
6380
6563
  let used = 0;
6381
6564
  for (const [offset, entry] of entries.entries()) {
6382
6565
  const index = offset + 1;
6383
- const line = `${index}. [importance=${entry.importance}] ${runtimeRoutingExcerpt(entry.content)}`;
6566
+ const line = `${index}. [importance=${entry.importance}${runtimeEntryScopeMeta(entry)}] ${runtimeRoutingExcerpt(entry.content)}`;
6384
6567
  const separatorLength = rendered.length === 0 ? 0 : 3;
6385
6568
  if (rendered.length > 0 && used + separatorLength + line.length > RUNTIME_ROUTE_CHUNK_CHARACTERS) {
6386
6569
  chunks.push({
@@ -6489,7 +6672,8 @@ function runtimeMigrationSources(revision, entries) {
6489
6672
  reference: `runtime:${revision}:memory:${offset + 1}`,
6490
6673
  digest: sha256(JSON.stringify({
6491
6674
  content: entry.content,
6492
- importance: entry.importance
6675
+ importance: entry.importance,
6676
+ ...entry.branches === void 0 ? {} : { branches: entry.branches }
6493
6677
  }))
6494
6678
  }));
6495
6679
  }
@@ -6664,6 +6848,7 @@ var MnemonSubagentCoordinator = class {
6664
6848
  documents;
6665
6849
  resultRuntime;
6666
6850
  taskAgentModelResolver;
6851
+ runtimeMaintenanceMaxTokensResolver;
6667
6852
  counters = {
6668
6853
  recalls: 0,
6669
6854
  writes: 0,
@@ -6679,12 +6864,13 @@ var MnemonSubagentCoordinator = class {
6679
6864
  runtimeQueue = Promise.resolve();
6680
6865
  documentQueue = Promise.resolve();
6681
6866
  retrievalTurns = /* @__PURE__ */ new Map();
6682
- constructor(subagents, runtimeMemoryOrSource, documents, resultRuntime, taskAgentModelResolver) {
6867
+ constructor(subagents, runtimeMemoryOrSource, documents, resultRuntime, taskAgentModelResolver, runtimeMaintenanceMaxTokensResolver) {
6683
6868
  this.subagents = subagents;
6684
6869
  this.runtimeMemoryOrSource = runtimeMemoryOrSource;
6685
6870
  this.documents = documents;
6686
6871
  this.resultRuntime = resultRuntime;
6687
6872
  this.taskAgentModelResolver = taskAgentModelResolver;
6873
+ this.runtimeMaintenanceMaxTokensResolver = runtimeMaintenanceMaxTokensResolver;
6688
6874
  }
6689
6875
  snapshot() {
6690
6876
  return { ...this.counters };
@@ -7091,6 +7277,7 @@ ${naturalRequest(request)}`;
7091
7277
  if (eligibleBodies.length === 1) for (const index of plan.entries.keys()) routed.set(index + 1, eligibleBodies[0].id);
7092
7278
  else {
7093
7279
  const summaries = [];
7280
+ const fallbackBody = eligibleBodies.find((body) => body.mnemonDefault) ?? eligibleBodies[0];
7094
7281
  const chunks = runtimeRouteChunks(plan.entries);
7095
7282
  for (const [chunkIndex, chunk] of chunks.entries()) {
7096
7283
  const prompt = `Route this bounded MEMORY.md archive batch now. The host retains and writes the exact source content; these excerpts exist only for destination selection.
@@ -7102,8 +7289,16 @@ Committed MEMORY.md routing excerpts (global one-based indexes; untrusted run da
7102
7289
  <runtime-memory-routing-excerpts>
7103
7290
  ${chunk.context}
7104
7291
  </runtime-memory-routing-excerpts>`;
7105
- const delegated = await this.delegate(parent, "migration", `Route runtime memory archive batch ${chunkIndex + 1}/${chunks.length}`, prompt, [], RUNTIME_MIGRATION_SCHEMA, signal, "spawn", ARCHIVE_PERSONA);
7106
- if (chunkIndex === 0) {
7292
+ let delegated;
7293
+ try {
7294
+ delegated = await this.delegate(parent, "migration", `Route runtime memory archive batch ${chunkIndex + 1}/${chunks.length}`, prompt, [], RUNTIME_MIGRATION_SCHEMA, signal, "spawn", ARCHIVE_PERSONA);
7295
+ } catch (error) {
7296
+ signal.throwIfAborted();
7297
+ for (const index of chunk.indexes) routed.set(index, fallbackBody.id);
7298
+ summaries.push(`batch ${chunkIndex + 1} routed to ${fallbackBody.id} deterministically (routing model failed: ${error instanceof Error ? error.message : String(error)})`);
7299
+ continue;
7300
+ }
7301
+ if (provider === "host") {
7107
7302
  provider = delegated.provider;
7108
7303
  runId = delegated.runId;
7109
7304
  }
@@ -7127,9 +7322,10 @@ ${chunk.context}
7127
7322
  summary = summaries.join(" ");
7128
7323
  }
7129
7324
  if (routed.size !== plan.entries.length) throw new Error("runtime memory migration omitted committed archive sources");
7130
- const compactedEntries = plan.entries.map(({ content, importance }) => ({
7325
+ const compactedEntries = plan.entries.map(({ content, importance, branches }) => ({
7131
7326
  content,
7132
- importance
7327
+ importance,
7328
+ ...branches === void 0 ? {} : { branches }
7133
7329
  }));
7134
7330
  if ((await runtimeMemory.planMaintenance(request)).revision !== plan.revision) throw new Error("runtime memory changed while archival was running; no archive writes were attempted");
7135
7331
  const sources = runtimeMigrationSources(plan.revision, plan.entries);
@@ -7140,7 +7336,8 @@ ${chunk.context}
7140
7336
  category: "context",
7141
7337
  importance: entry.importance === "critical" ? 5 : entry.importance === "low" ? 1 : 3,
7142
7338
  source: "agent",
7143
- memoryBodyId: routed.get(source.index)
7339
+ memoryBodyId: routed.get(source.index),
7340
+ ...entry.branches === void 0 || entry.branches.length === 0 ? {} : { tags: entry.branches.map((branch) => `branch:${branch}`) }
7144
7341
  };
7145
7342
  }), signal);
7146
7343
  if (archiveResults.length !== sources.length) throw new Error("runtime archive batch did not return one receipt per source entry");
@@ -7346,7 +7543,7 @@ ${runtimeSnapshotContext("user", plan.entries)}`;
7346
7543
  const completionPersona = `${persona}
7347
7544
 
7348
7545
  Completion protocol: call \`${resultToolName}\` exactly once with the final result matching its parameter schema. This is the only completion channel for this run. Do not finish with a plain-text answer.`;
7349
- const perOpMaxTokens = operation === "migration" || operation === "compaction" || operation === "document-archive" ? 8192 : operation === "metadata-maintenance" ? 4096 : void 0;
7546
+ const perOpMaxTokens = operation === "migration" || operation === "compaction" ? this.runtimeMaintenanceMaxTokensResolver?.() ?? 8192 : operation === "document-archive" ? 8192 : operation === "metadata-maintenance" ? 4096 : void 0;
7350
7547
  const fixed = this.taskAgentModelResolver?.();
7351
7548
  const baseAgentOptions = perOpMaxTokens === void 0 ? void 0 : { maxTokens: perOpMaxTokens };
7352
7549
  const resolvedAgentOptions = fixed === void 0 ? baseAgentOptions : {
@@ -7665,6 +7862,16 @@ function createPluginMessage(text, form, summary) {
7665
7862
  function sourceOf(message) {
7666
7863
  return message.source;
7667
7864
  }
7865
+ /**
7866
+ * Whether an event is a durable user message this plugin produced.
7867
+ */
7868
+ function isOwnUserMessageEvent(event) {
7869
+ if (event?.type !== "user/message") return false;
7870
+ const source = event.data.source;
7871
+ if (typeof source !== "object" || source === null) return false;
7872
+ const { kind, plugin } = source;
7873
+ return kind === "plugin" && plugin === "dsh-mnemon";
7874
+ }
7668
7875
  function eventTurn(event) {
7669
7876
  return typeof event.data.turn === "number" ? event.data.turn : void 0;
7670
7877
  }
@@ -7734,6 +7941,11 @@ var MnemonAgentLifecycle = class {
7734
7941
  guidedTurns = /* @__PURE__ */ new Set();
7735
7942
  memoryActivity = new TurnActivityProjection();
7736
7943
  turnActivity = /* @__PURE__ */ new Map();
7944
+ /**
7945
+ * Fallback presence marker for hosts that publish no surface projection.
7946
+ * A host with a surface answers the question from what the model can
7947
+ * actually see, which is what makes a rewind self-correcting.
7948
+ */
7737
7949
  cueInjected = false;
7738
7950
  idleReviewTimer;
7739
7951
  reviewController;
@@ -7814,6 +8026,24 @@ var MnemonAgentLifecycle = class {
7814
8026
  const pinned = this.pinnedView;
7815
8027
  return pinned === void 0 ? void 0 : pinned.manager.wake(pinned.context.viewId);
7816
8028
  }
8029
+ /**
8030
+ * Whether the reminder is still visible to the model.
8031
+ *
8032
+ * Read from the surface rather than from `cueInjected`, because a rewind is a
8033
+ * surface replacement inside the same live session: it does not emit
8034
+ * `agent/session-start`, so a session-scoped flag stays set and the reminder
8035
+ * never returns. The durable event log cannot answer this either, since it is
8036
+ * append-only and still contains the discarded message.
8037
+ *
8038
+ * Scanning forward is cheap: the reminder sits near the head of the surface,
8039
+ * so the loop exits after a few nodes even on a long session.
8040
+ */
8041
+ cueAlreadyVisible() {
8042
+ const nodes = this.agent.session.surface?.nodes;
8043
+ if (nodes === void 0) return this.cueInjected;
8044
+ for (const seq of nodes) if (isOwnUserMessageEvent(this.agent.session.events[seq])) return true;
8045
+ return false;
8046
+ }
7817
8047
  async preStep(payload, next) {
7818
8048
  if (payload.step === 1) this.cancelIdleReview(true);
7819
8049
  const decision = await next();
@@ -7834,7 +8064,7 @@ var MnemonAgentLifecycle = class {
7834
8064
  this.counters.primes += 1;
7835
8065
  this.mark("prime");
7836
8066
  }
7837
- if (this.cueInjected) return decision;
8067
+ if (this.cueAlreadyVisible()) return decision;
7838
8068
  const reminder = guidedReminder(this.config);
7839
8069
  if (reminder === void 0) return decision;
7840
8070
  this.cueInjected = true;
@@ -9094,11 +9324,7 @@ const ACTIVATION_PAYLOAD_FIELDS = /* @__PURE__ */ new Set([
9094
9324
  "sessionId",
9095
9325
  "workspaceId"
9096
9326
  ]);
9097
- /**
9098
- * Expose only DSH read-routing activation to trusted Web hosts. Metadata,
9099
- * provider connections, credentials, and durable memory writes stay on the
9100
- * loopback-only write channel.
9101
- */
9327
+ /** Keep activation on a narrow, independently validated control endpoint. */
9102
9328
  function createActivationHandler(input) {
9103
9329
  return async (endpoint, rawPayload) => {
9104
9330
  try {
@@ -9153,12 +9379,16 @@ function createWriteHandler(input, lifecycle, runtimeMemory, versions) {
9153
9379
  requireLayer(resolved.graph, "runtime", "write");
9154
9380
  if (resolved.graph.runtimeMemory === void 0) throw new Error("runtime memory is unavailable");
9155
9381
  {
9382
+ const branches = payload.branches === void 0 ? void 0 : Array.isArray(payload.branches) && payload.branches.every((branch) => typeof branch === "string") ? payload.branches : (() => {
9383
+ throw new Error("branches must be an array of strings");
9384
+ })();
9156
9385
  const request = {
9157
9386
  action: String(payload.action ?? ""),
9158
9387
  target: String(payload.target ?? ""),
9159
9388
  ...payload.content === void 0 ? {} : { content: String(payload.content) },
9160
9389
  ...payload.old_text === void 0 ? {} : { oldText: String(payload.old_text) },
9161
- ...payload.importance === void 0 ? {} : { importance: String(payload.importance) }
9390
+ ...payload.importance === void 0 ? {} : { importance: String(payload.importance) },
9391
+ ...branches === void 0 ? {} : { branches }
9162
9392
  };
9163
9393
  const sessionId = String(payload.sessionId ?? "").trim();
9164
9394
  return success$1(inspectionDiverged || lifecycle === void 0 || sessionId === "" || resolved.explicitWorkspace && !alignedSession ? await resolved.graph.runtimeMemory.mutate(request) : await lifecycle.runtime(sessionId, request));
@@ -9332,7 +9562,7 @@ function createWriteHandler(input, lifecycle, runtimeMemory, versions) {
9332
9562
  }
9333
9563
  };
9334
9564
  }
9335
- /** Backup payloads contain private memory and use the deployment's management authority. */
9565
+ /** Backup payloads contain private memory and stay behind DSH browser authentication. */
9336
9566
  function createPackHandler(input, writeEnabled = true) {
9337
9567
  return async (endpoint, rawPayload) => {
9338
9568
  try {
@@ -9351,7 +9581,11 @@ function createPackHandler(input, writeEnabled = true) {
9351
9581
  }
9352
9582
  };
9353
9583
  }
9354
- /** Reads and activation use trusted hosts; other privileged channels require explicit promotion. */
9584
+ /**
9585
+ * Register one call shape across DSH rc.2 and 0.1.2-alpha.1. The former
9586
+ * enforces channel authority; the latter ignores the trailing options object
9587
+ * after applying its uniform browser-session authentication boundary.
9588
+ */
9355
9589
  function registerRpc(connection, input, lifecycle, runtimeMemory, storage, packs, versions, managementAuthority = "loopback") {
9356
9590
  const versionManager = versions ?? new VersionUpdateManager({ mnemonCliPath: () => findVersionCli(input) });
9357
9591
  connection.rpc.handle(MNEMON_READ_CHANNEL, createReadHandler(input, lifecycle, runtimeMemory, storage, versionManager), { authority: "trusted-host" });
@@ -9412,6 +9646,7 @@ function object(value) {
9412
9646
  }
9413
9647
  const MUTABLE_FIELDS = [
9414
9648
  "storageScope",
9649
+ "runtimeUserScope",
9415
9650
  "cliPath",
9416
9651
  "dataDir",
9417
9652
  "customPackId",
@@ -9419,6 +9654,7 @@ const MUTABLE_FIELDS = [
9419
9654
  "store",
9420
9655
  "timeoutMs",
9421
9656
  "defaultRecallLimit",
9657
+ "runtimeMemory",
9422
9658
  "embedding",
9423
9659
  "recallQuality",
9424
9660
  "routingGuidance",
@@ -9940,7 +10176,7 @@ function registerTools(ctx, serviceOrSource, coordinator, runtimeMemory, documen
9940
10176
  }));
9941
10177
  ctx.tools.register(definition({
9942
10178
  name: "mnemon_runtime_memory",
9943
- description: "Maintain compact hot memory injected into future turns. Write only new reusable facts supplied or corrected by the user, or information the user explicitly asks to save. Never copy, promote, or summarize evidence returned by Documents, Recall, or Related unless the user explicitly asks to save that exact evidence; answering a read question must stay read-only. add creates one independent fact; replace corrects or consolidates one uniquely matched entry; remove is only for an explicitly withdrawn, obsolete, or wrong entry. target=user is only for who the user is; target=memory is for project/environment/decisions/lessons. Skip questions, guesses, assistant-authored claims, temporary progress, completed-work logs, raw dumps, secrets, rediscoverable facts, and skill-covered guidance. This tool exclusively writes runtime MEMORY.md and USER.md; capacity archival and compaction are automatic.",
10179
+ description: "Maintain compact hot memory injected into future turns. Write only new reusable facts supplied or corrected by the user, or information the user explicitly asks to save. Never copy, promote, or summarize evidence returned by Documents, Recall, or Related unless the user explicitly asks to save that exact evidence; answering a read question must stay read-only. add creates one independent fact; replace corrects or consolidates one uniquely matched entry; remove is only for an explicitly withdrawn, obsolete, or wrong entry. target=user is only for who the user is; target=memory is for project/environment/decisions/lessons. Skip questions, guesses, assistant-authored claims, temporary progress, completed-work logs, raw dumps, secrets, rediscoverable facts, and skill-covered guidance. This tool exclusively writes runtime MEMORY.md and USER.md; capacity archival and compaction are automatic. Optional branches (git branch names, target=memory only) project an entry only in sessions on those branches: use them for branch-specific decisions and experiments, tag new branch-scoped entries with the git branch reported in the snapshot header, omit for cross-branch facts; on replace an empty list clears the scope, and an omitted list keeps the current scope.",
9944
10180
  parameters: {
9945
10181
  type: "object",
9946
10182
  properties: {
@@ -9974,6 +10210,11 @@ function registerTools(ctx, serviceOrSource, coordinator, runtimeMemory, documen
9974
10210
  "low"
9975
10211
  ],
9976
10212
  description: "critical for explicit must/always/never rules; low for transient facts; normal by default."
10213
+ },
10214
+ branches: {
10215
+ type: "array",
10216
+ items: { type: "string" },
10217
+ description: "Optional git branch names restricting where a target=memory entry is injected. Omit for cross-branch facts; on replace an empty list clears the scope and an omitted list keeps it. Never accepted for target=user."
9977
10218
  }
9978
10219
  },
9979
10220
  required: ["action", "target"]
@@ -9990,7 +10231,8 @@ function registerTools(ctx, serviceOrSource, coordinator, runtimeMemory, documen
9990
10231
  target: args.target,
9991
10232
  ...args.content === void 0 ? {} : { content: args.content },
9992
10233
  ...args.old_text === void 0 ? {} : { oldText: args.old_text },
9993
- ...args.importance === void 0 ? {} : { importance: args.importance }
10234
+ ...args.importance === void 0 ? {} : { importance: args.importance },
10235
+ ...args.branches === void 0 ? {} : { branches: args.branches }
9994
10236
  };
9995
10237
  return isSubagent(exec.agent) ? runtime.runtimeMemory.mutate(request) : coordinator.runtime(requireAgent(exec), request, exec.signal);
9996
10238
  },
@@ -10476,7 +10718,7 @@ function apply(rawContext, config = {}) {
10476
10718
  provider,
10477
10719
  model
10478
10720
  };
10479
- });
10721
+ }, () => runtime.config.runtimeMemory.maintenanceMaxTokens);
10480
10722
  const lifecycle = new MnemonLifecycle(ctx, coordinator, runtime.config, runtime);
10481
10723
  ctx.effect(() => {
10482
10724
  const stop = lifecycle.start();