dsh-mnemon 0.3.2 → 0.3.4

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,7 +11,9 @@ 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";
16
+ import { z as z$1 } from "zod";
15
17
  //#region src/shared/contracts.ts
16
18
  const MNEMON_READ_CHANNEL = "/dsh-mnemon-read";
17
19
  const MNEMON_ACTIVATION_CHANNEL = "/dsh-mnemon-activation";
@@ -53,6 +55,11 @@ const DEFAULT_IDLE_REVIEW_MS = 3e4;
53
55
  const DEFAULT_RECALL_QUALITY_POLICY = "strict-v1";
54
56
  const DEFAULT_RECALL_LOW_SCORE_THRESHOLD = .25;
55
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;
56
63
  //#endregion
57
64
  //#region src/config.ts
58
65
  const InteractionConfig = z.object({
@@ -117,6 +124,11 @@ const RecallQualitySchema = z.object({
117
124
  maxMediumResults: z.number().step(1).min(0).max(50).default(4),
118
125
  maxUnknownResults: z.number().step(1).min(0).max(50).default(2)
119
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
+ });
120
132
  const MemoryParticipationModeSchema = z.union([
121
133
  "off",
122
134
  "manual",
@@ -143,6 +155,7 @@ const Config = z.object({
143
155
  "workspace",
144
156
  "custom"
145
157
  ]),
158
+ runtimeUserScope: z.union(["storage", "global"]).default("storage"),
146
159
  cliPath: z.string(),
147
160
  dataDir: z.string(),
148
161
  customPackId: z.string(),
@@ -154,6 +167,11 @@ const Config = z.object({
154
167
  store: z.string(),
155
168
  timeoutMs: z.number().step(1).min(100).max(12e4).default(DEFAULT_TIMEOUT_MS),
156
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
+ }),
157
175
  embedding: MnemonEmbeddingSchema.default({
158
176
  enabled: false,
159
177
  endpoint: DEFAULT_EMBEDDING_ENDPOINT,
@@ -336,6 +354,19 @@ function resolveRecallQuality(value) {
336
354
  maxUnknownResults
337
355
  };
338
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
+ }
339
370
  const MEMORY_COMPONENT_ID = /^[a-z][a-z0-9-]{0,127}$/u;
340
371
  const MEMORY_PARTICIPATION_MODES = /* @__PURE__ */ new Set([
341
372
  "off",
@@ -386,6 +417,8 @@ function resolveConfig(config = {}) {
386
417
  if (requestedPackId !== void 0 && !CUSTOM_PACK_ID.test(requestedPackId)) throw new Error("dsh-mnemon: customPackId is invalid");
387
418
  const store = optionalText$1(config.store);
388
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)}`);
389
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);
390
423
  if (requestedPackId !== void 0 && selectedPack === void 0) throw new Error(`dsh-mnemon: unknown custom Pack: ${requestedPackId}`);
391
424
  const dataDir = selectedPack?.dataDir ?? legacyDataDir;
@@ -393,11 +426,13 @@ function resolveConfig(config = {}) {
393
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_-]*");
394
427
  return {
395
428
  storageScope,
429
+ runtimeUserScope,
396
430
  ...cliPath === void 0 ? {} : { cliPath },
397
431
  ...dataDir === void 0 ? {} : { dataDir },
398
432
  ...store === void 0 ? {} : { store },
399
433
  timeoutMs: config.timeoutMs ?? 1e4,
400
434
  defaultRecallLimit: config.defaultRecallLimit ?? 10,
435
+ runtimeMemory: resolveRuntimeMemory(config.runtimeMemory),
401
436
  embedding: resolveEmbedding(config.embedding),
402
437
  memoryTopology: resolveMemoryTopology(config.memoryTopology),
403
438
  recallQuality: resolveRecallQuality(config.recallQuality),
@@ -1162,8 +1197,8 @@ var DocumentManager = class {
1162
1197
  };
1163
1198
  const RUNTIME_ENTRY_DELIMITER = "\n§\n";
1164
1199
  const RUNTIME_MEMORY_LIMITS = {
1165
- memory: 10240,
1166
- user: 4096
1200
+ memory: DEFAULT_RUNTIME_MEMORY_LIMIT_BYTES,
1201
+ user: DEFAULT_RUNTIME_USER_LIMIT_BYTES
1167
1202
  };
1168
1203
  const RUNTIME_MEMORY_PROTOCOL = `MNEMON RUNTIME MEMORY PROTOCOL
1169
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.
@@ -1183,6 +1218,7 @@ WRITE PROTOCOL
1183
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.
1184
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.
1185
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.
1186
1222
 
1187
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.`;
1188
1224
  const LOCK_TIMEOUT_MS$1 = 5e3;
@@ -1218,6 +1254,41 @@ function isTarget(value) {
1218
1254
  function isImportance(value) {
1219
1255
  return value === "critical" || value === "normal" || value === "low";
1220
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
+ }
1221
1292
  function normalizeContent(value, field) {
1222
1293
  const content = value?.trim().replace(/\s+/gu, " ") ?? "";
1223
1294
  if (content === "") throw new Error(`${field} is required`);
@@ -1231,12 +1302,15 @@ function parseEntry(value) {
1231
1302
  if (typeof value.created_at !== "string" || typeof value.updated_at !== "string") return void 0;
1232
1303
  const content = value.content.trim().replace(/\s+/gu, " ");
1233
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);
1234
1307
  return {
1235
1308
  content,
1236
1309
  created_at: value.created_at,
1237
1310
  updated_at: value.updated_at,
1238
1311
  target: value.target,
1239
- importance: value.importance
1312
+ importance: value.importance,
1313
+ ...branches === void 0 ? {} : { branches }
1240
1314
  };
1241
1315
  }
1242
1316
  function byteCount(entries, target) {
@@ -1258,6 +1332,8 @@ function prepareMutation(before, request, now) {
1258
1332
  "remove"
1259
1333
  ].includes(request.action)) throw new Error("action must be add, replace, or remove");
1260
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);
1261
1337
  const entries = before.map((entry) => ({ ...entry }));
1262
1338
  if (request.action === "add") {
1263
1339
  const content = normalizeContent(request.content, "content");
@@ -1276,7 +1352,8 @@ function prepareMutation(before, request, now) {
1276
1352
  created_at: now,
1277
1353
  updated_at: now,
1278
1354
  target: request.target,
1279
- importance: request.importance ?? "normal"
1355
+ importance: request.importance ?? "normal",
1356
+ ...branchScope !== void 0 && branchScope.length > 0 ? { branches: branchScope } : {}
1280
1357
  };
1281
1358
  return {
1282
1359
  changed: true,
@@ -1304,6 +1381,10 @@ function prepareMutation(before, request, now) {
1304
1381
  updated_at: now,
1305
1382
  importance: request.importance ?? previous.importance
1306
1383
  };
1384
+ if (branchScope !== void 0) {
1385
+ if (branchScope.length === 0) delete pendingEntry.branches;
1386
+ else pendingEntry.branches = branchScope;
1387
+ }
1307
1388
  entries[index] = pendingEntry;
1308
1389
  return {
1309
1390
  changed: true,
@@ -1339,12 +1420,14 @@ function compactionCandidates(compacted, existing, target, now) {
1339
1420
  if (seen.has(content)) throw new Error("compacted runtime memory contains duplicate entries");
1340
1421
  seen.add(content);
1341
1422
  const unchanged = existing.find((current) => current.content === content);
1423
+ const inheritedBranches = entry.branches ?? unchanged?.branches;
1342
1424
  return {
1343
1425
  content,
1344
1426
  created_at: unchanged?.created_at ?? now,
1345
1427
  updated_at: unchanged?.updated_at ?? now,
1346
1428
  target,
1347
- importance: entry.importance
1429
+ importance: entry.importance,
1430
+ ...inheritedBranches === void 0 ? {} : { branches: inheritedBranches }
1348
1431
  };
1349
1432
  });
1350
1433
  }
@@ -1375,49 +1458,112 @@ function sleepSync$1(milliseconds) {
1375
1458
  * Single authority for hot memory. JSON is the durable source of truth;
1376
1459
  * Markdown files are deterministic projections consumed by prompt assembly.
1377
1460
  */
1378
- var RuntimeMemoryController = class {
1461
+ var RuntimeMemoryController = class RuntimeMemoryController {
1379
1462
  now;
1380
1463
  recordCommit;
1381
1464
  directory;
1382
1465
  sourcePath;
1466
+ userSourcePath;
1383
1467
  memoryPath;
1384
1468
  userPath;
1385
1469
  lockPath;
1470
+ limits;
1386
1471
  queue = Promise.resolve();
1387
- constructor(runner, now = () => /* @__PURE__ */ new Date(), recordCommit) {
1472
+ localUserPath;
1473
+ userController;
1474
+ constructor(runner, now = () => /* @__PURE__ */ new Date(), recordCommit, limits = RUNTIME_MEMORY_LIMITS, userRunner) {
1388
1475
  this.now = now;
1389
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
+ };
1390
1482
  this.directory = join(runner.effectiveDataDir(), "runtime");
1391
1483
  this.sourcePath = join(this.directory, "memories.json");
1392
1484
  this.memoryPath = join(this.directory, "MEMORY.md");
1393
- this.userPath = join(this.directory, "USER.md");
1485
+ this.localUserPath = join(this.directory, "USER.md");
1394
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;
1395
1491
  this.initialize();
1396
1492
  }
1397
1493
  snapshot() {
1398
- const file = this.readSource();
1399
- const entries = file.entries.map((entry) => ({ ...entry }));
1400
- 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 : {
1401
1522
  directory: this.directory,
1402
1523
  sourcePath: this.sourcePath,
1403
- revision: revision(file),
1524
+ revision: revision({
1525
+ version: 1,
1526
+ entries
1527
+ }),
1404
1528
  generatedAt: this.now().toISOString(),
1405
1529
  entries,
1406
1530
  targets: {
1407
- memory: this.targetView(entries, "memory"),
1408
- user: this.targetView(entries, "user")
1531
+ memory: {
1532
+ ...visibleMemory,
1533
+ markdownPath: this.memoryPath
1534
+ },
1535
+ user: {
1536
+ ...visibleUser,
1537
+ markdownPath: this.userPath
1538
+ }
1409
1539
  }
1410
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
+ };
1411
1559
  }
1412
- contextText() {
1413
- return this.contextProjection().text;
1414
- }
1415
- /** Read the exact Runtime revision and its prompt projection under one lock. */
1416
- contextProjection() {
1417
- const { snapshot, user, memory } = this.withLock(() => {
1560
+ localContextProjection(branch) {
1561
+ const branchScope = scopeBranch(branch);
1562
+ return this.withLock(() => {
1418
1563
  const file = this.readSource();
1419
1564
  this.repairProjections(file);
1420
1565
  const entries = file.entries.map((entry) => ({ ...entry }));
1566
+ const visible = branchScope === void 0 ? entries : entries.filter((entry) => entry.target === "user" || entryMatchesBranch(entry, branchScope));
1421
1567
  return {
1422
1568
  snapshot: {
1423
1569
  directory: this.directory,
@@ -1426,33 +1572,18 @@ var RuntimeMemoryController = class {
1426
1572
  generatedAt: this.now().toISOString(),
1427
1573
  entries,
1428
1574
  targets: {
1429
- memory: this.targetView(entries, "memory"),
1430
- user: this.targetView(entries, "user")
1575
+ memory: this.targetView(visible, "memory"),
1576
+ user: this.targetView(visible, "user")
1431
1577
  }
1432
1578
  },
1433
- user: readFileSync(this.userPath, "utf8").trimEnd(),
1434
- 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
1435
1582
  };
1436
1583
  });
1437
- const userUsage = snapshot.targets.user;
1438
- const memoryUsage = snapshot.targets.memory;
1439
- return {
1440
- revision: snapshot.revision,
1441
- text: `MNEMON RUNTIME MEMORY SNAPSHOT
1442
- Revision: ${snapshot.revision}
1443
-
1444
- Contents of USER.md (user profile; entries: ${userUsage.entryCount}; UTF-8 bytes: ${userUsage.used}/${userUsage.limit})
1445
- <runtime-memory-file name="USER.md">
1446
- ${user || "(empty)"}
1447
- </runtime-memory-file>
1448
-
1449
- Contents of MEMORY.md (working reference; entries: ${memoryUsage.entryCount}; UTF-8 bytes: ${memoryUsage.used}/${memoryUsage.limit})
1450
- <runtime-memory-file name="MEMORY.md">
1451
- ${memory || "(empty)"}
1452
- </runtime-memory-file>`
1453
- };
1454
1584
  }
1455
1585
  mutate(request) {
1586
+ if (request.target === "user" && this.userController !== void 0) return this.userController.mutate(request);
1456
1587
  const operation = this.queue.then(() => this.withLock(() => {
1457
1588
  const beforeRevision = revision(this.readSource());
1458
1589
  return {
@@ -1478,12 +1609,13 @@ ${memory || "(empty)"}
1478
1609
  }
1479
1610
  /** Resolve exactly which committed entries survive a blocked mutation and are safe to compact. */
1480
1611
  planMaintenance(request) {
1612
+ if (request.target === "user" && this.userController !== void 0) return this.userController.planMaintenance(request);
1481
1613
  const operation = this.queue.then(() => this.withLock(() => {
1482
1614
  const file = this.readSource();
1483
1615
  const prepared = prepareMutation(file.entries, request, this.now().toISOString());
1484
1616
  const used = byteCount(file.entries, request.target);
1485
1617
  const projected = byteCount(prepared.projectedEntries, request.target);
1486
- const limit = RUNTIME_MEMORY_LIMITS[request.target];
1618
+ const limit = this.limits[request.target];
1487
1619
  return {
1488
1620
  revision: revision(file),
1489
1621
  action: request.action,
@@ -1504,14 +1636,16 @@ ${memory || "(empty)"}
1504
1636
  return operation;
1505
1637
  }
1506
1638
  /** Commit semantic compaction and the original mutation together, or leave every local file unchanged. */
1507
- 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);
1508
1641
  const operation = this.queue.then(() => this.withLock(() => {
1509
1642
  const file = this.readSource();
1510
1643
  const beforeRevision = revision(file);
1511
1644
  if (beforeRevision !== expectedRevision) throw new RuntimeMemoryConflictError();
1512
1645
  const now = this.now().toISOString();
1513
1646
  const prepared = prepareMutation(file.entries, request, now);
1514
- 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");
1515
1649
  if (!prepared.changed) return {
1516
1650
  result: this.result(request.target, prepared.projectedEntries, prepared.fields),
1517
1651
  beforeRevision,
@@ -1520,10 +1654,10 @@ ${memory || "(empty)"}
1520
1654
  const replacements = compactionCandidates(compacted, prepared.compactableEntries, request.target, now);
1521
1655
  if (prepared.pendingEntry !== void 0 && replacements.some((entry) => entry.content === prepared.pendingEntry.content)) throw new Error("compacted runtime memory duplicates the pending mutation");
1522
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");
1523
- 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]];
1524
1658
  const entries = [...file.entries.filter((entry) => entry.target !== request.target), ...targetEntries];
1525
1659
  const used = byteCount(entries, request.target);
1526
- const limit = RUNTIME_MEMORY_LIMITS[request.target];
1660
+ const limit = this.limits[request.target];
1527
1661
  if (used > limit) throw new RuntimeMemoryCapacityError(request.target, byteCount(file.entries, request.target), used, limit);
1528
1662
  const next = {
1529
1663
  version: 1,
@@ -1559,17 +1693,19 @@ ${memory || "(empty)"}
1559
1693
  return operation;
1560
1694
  }
1561
1695
  /** Apply an LLM-produced compaction only to the exact snapshot it reviewed. */
1562
- 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());
1563
1698
  const operation = this.queue.then(() => this.withLock(() => {
1564
1699
  const file = this.readSource();
1565
1700
  const beforeRevision = revision(file);
1566
1701
  if (beforeRevision !== expectedRevision) throw new RuntimeMemoryConflictError();
1567
- 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");
1568
1704
  const now = this.now().toISOString();
1569
- 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);
1570
1706
  const entries = [...file.entries.filter((entry) => entry.target !== target), ...fitted];
1571
1707
  const used = byteCount(entries, target);
1572
- const limit = RUNTIME_MEMORY_LIMITS[target];
1708
+ const limit = this.limits[target];
1573
1709
  if (used > limit) throw new RuntimeMemoryCapacityError(target, byteCount(file.entries, target), used, limit);
1574
1710
  this.persist({
1575
1711
  version: 1,
@@ -1600,7 +1736,7 @@ ${memory || "(empty)"}
1600
1736
  return snapshot;
1601
1737
  });
1602
1738
  this.queue = operation.catch(() => void 0);
1603
- return operation;
1739
+ return this.userController === void 0 ? operation : operation.then(() => this.snapshot());
1604
1740
  }
1605
1741
  initialize() {
1606
1742
  mkdirSync(this.directory, {
@@ -1617,7 +1753,7 @@ ${memory || "(empty)"}
1617
1753
  const prepared = prepareMutation(file.entries, request, this.now().toISOString());
1618
1754
  if (!prepared.changed) return this.result(request.target, prepared.projectedEntries, prepared.fields);
1619
1755
  const used = byteCount(prepared.projectedEntries, request.target);
1620
- const limit = RUNTIME_MEMORY_LIMITS[request.target];
1756
+ const limit = this.limits[request.target];
1621
1757
  if (used > limit) throw new RuntimeMemoryCapacityError(request.target, byteCount(file.entries, request.target), used, limit);
1622
1758
  this.persist({
1623
1759
  version: 1,
@@ -1633,7 +1769,7 @@ ${memory || "(empty)"}
1633
1769
  entryCount: entries.filter((entry) => entry.target === target).length,
1634
1770
  usage: {
1635
1771
  used: byteCount(entries, target),
1636
- limit: RUNTIME_MEMORY_LIMITS[target]
1772
+ limit: this.limits[target]
1637
1773
  },
1638
1774
  ...fields.added === void 0 ? {} : { added: fields.added },
1639
1775
  ...fields.replaced === void 0 ? {} : { replaced: fields.replaced },
@@ -1645,7 +1781,7 @@ ${memory || "(empty)"}
1645
1781
  target,
1646
1782
  entryCount: entries.filter((entry) => entry.target === target).length,
1647
1783
  used: byteCount(entries, target),
1648
- limit: RUNTIME_MEMORY_LIMITS[target],
1784
+ limit: this.limits[target],
1649
1785
  markdownPath: target === "memory" ? this.memoryPath : this.userPath
1650
1786
  };
1651
1787
  }
@@ -1689,7 +1825,7 @@ ${memory || "(empty)"}
1689
1825
  });
1690
1826
  const nonce = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
1691
1827
  const writes = [
1692
- [this.userPath, markdown(file.entries, "user")],
1828
+ [this.localUserPath, markdown(file.entries, "user")],
1693
1829
  [this.memoryPath, markdown(file.entries, "memory")],
1694
1830
  [this.sourcePath, `${JSON.stringify(file, null, 2)}\n`]
1695
1831
  ];
@@ -1705,7 +1841,7 @@ ${memory || "(empty)"}
1705
1841
  }
1706
1842
  }
1707
1843
  repairProjections(file) {
1708
- for (const [path, target] of [[this.userPath, "user"], [this.memoryPath, "memory"]]) {
1844
+ for (const [path, target] of [[this.localUserPath, "user"], [this.memoryPath, "memory"]]) {
1709
1845
  const expected = markdown(file.entries, target);
1710
1846
  let current;
1711
1847
  try {
@@ -1950,7 +2086,7 @@ function decodeArchive(base64) {
1950
2086
  if (bytes.length === 0 || bytes.length > 50331648) throw new Error("Mnemon Pack archive is empty or too large");
1951
2087
  return bytes;
1952
2088
  }
1953
- function parseArchive(base64) {
2089
+ function parseArchive(base64, runtimeLimits = RUNTIME_MEMORY_LIMITS) {
1954
2090
  const archive = decodeArchive(base64);
1955
2091
  let count = 0;
1956
2092
  let expandedBytes = 0;
@@ -1981,8 +2117,8 @@ function parseArchive(base64) {
1981
2117
  const expected = checksumFiles[path];
1982
2118
  if (typeof expected !== "string" || expected !== sha256$1(files[path])) throw new Error(`Mnemon Pack checksum mismatch: ${path}`);
1983
2119
  }
1984
- validatePackPayload(files, manifest.components);
1985
- const actualSummary = summaryFor(manifest.components, files);
2120
+ validatePackPayload(files, manifest.components, runtimeLimits);
2121
+ const actualSummary = summaryFor(manifest.components, files, runtimeLimits);
1986
2122
  return {
1987
2123
  archiveBytes: archive.length,
1988
2124
  expandedBytes,
@@ -1993,7 +2129,7 @@ function parseArchive(base64) {
1993
2129
  }
1994
2130
  };
1995
2131
  }
1996
- function parseRuntime(value) {
2132
+ function parseRuntime(value, limits = RUNTIME_MEMORY_LIMITS) {
1997
2133
  const source = record$2(value);
1998
2134
  if (source?.version !== 1 || !Array.isArray(source.entries)) throw new Error("runtime memories.json is invalid");
1999
2135
  const entries = source.entries.map((raw) => {
@@ -2014,7 +2150,7 @@ function parseRuntime(value) {
2014
2150
  updated_at: entry.updated_at
2015
2151
  };
2016
2152
  });
2017
- 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`);
2018
2154
  return {
2019
2155
  version: 1,
2020
2156
  entries
@@ -2089,8 +2225,8 @@ function parseRegistry(value) {
2089
2225
  function validDatabase(bytes, path) {
2090
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`);
2091
2227
  }
2092
- function validatePackPayload(files, components) {
2093
- 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);
2094
2230
  if (components.includes("documents")) {
2095
2231
  const index = parseDocumentIndex(json(files["payload/documents/index.json"] ?? /* @__PURE__ */ new Uint8Array(), "payload/documents/index.json"));
2096
2232
  for (const document of index.documents) {
@@ -2171,9 +2307,9 @@ function emptyRuntime() {
2171
2307
  entries: []
2172
2308
  };
2173
2309
  }
2174
- function readCurrentRuntime(root) {
2310
+ function readCurrentRuntime(root, limits = RUNTIME_MEMORY_LIMITS) {
2175
2311
  const path = join(root, "runtime", "memories.json");
2176
- return existsSync(path) ? parseRuntime(JSON.parse(readFileSync(path, "utf8"))) : emptyRuntime();
2312
+ return existsSync(path) ? parseRuntime(JSON.parse(readFileSync(path, "utf8")), limits) : emptyRuntime();
2177
2313
  }
2178
2314
  function writeRuntime(directory, file) {
2179
2315
  mkdirSync(directory, {
@@ -2305,12 +2441,12 @@ function writeRegistry(directory, registry, databases) {
2305
2441
  }
2306
2442
  writeFileSync(join(directory, ".dsh-memory-bodies.json"), `${JSON.stringify(registry, null, 2)}\n`, { mode: 384 });
2307
2443
  }
2308
- function componentItems(component, files) {
2309
- 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;
2310
2446
  if (component === "documents") return parseDocumentIndex(json(files["payload/documents/index.json"], "payload/documents/index.json")).documents.length;
2311
2447
  return parseRegistry(json(files["payload/data/.dsh-memory-bodies.json"], "payload/data/.dsh-memory-bodies.json")).bodies.length;
2312
2448
  }
2313
- function summaryFor(components, files) {
2449
+ function summaryFor(components, files, runtimeLimits = RUNTIME_MEMORY_LIMITS) {
2314
2450
  return components.map((component) => {
2315
2451
  const prefix = `payload/${COMPONENT_DIRECTORIES[component]}/`;
2316
2452
  const entries = Object.entries(files).filter(([path]) => path.startsWith(prefix));
@@ -2318,14 +2454,14 @@ function summaryFor(components, files) {
2318
2454
  component,
2319
2455
  files: entries.length,
2320
2456
  bytes: entries.reduce((sum, [, value]) => sum + value.length, 0),
2321
- items: componentItems(component, files)
2457
+ items: componentItems(component, files, runtimeLimits)
2322
2458
  };
2323
2459
  });
2324
2460
  }
2325
- function collectExport(root, components) {
2461
+ function collectExport(root, components, runtimeLimits = RUNTIME_MEMORY_LIMITS) {
2326
2462
  const files = {};
2327
2463
  if (components.includes("runtime")) {
2328
- const runtime = readCurrentRuntime(root);
2464
+ const runtime = readCurrentRuntime(root, runtimeLimits);
2329
2465
  files["payload/runtime/memories.json"] = strToU8(`${JSON.stringify(runtime, null, 2)}\n`);
2330
2466
  files["payload/runtime/USER.md"] = strToU8(runtimeProjection(runtime.entries, "user"));
2331
2467
  files["payload/runtime/MEMORY.md"] = strToU8(runtimeProjection(runtime.entries, "memory"));
@@ -2342,9 +2478,9 @@ function collectExport(root, components) {
2342
2478
  }
2343
2479
  return files;
2344
2480
  }
2345
- function mergeRuntime(root, pack) {
2346
- const current = readCurrentRuntime(root);
2347
- 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);
2348
2484
  const keys = new Set(current.entries.map((entry) => `${entry.target}\0${entry.content}`));
2349
2485
  const entries = [...current.entries];
2350
2486
  for (const entry of incoming.entries) {
@@ -2357,7 +2493,7 @@ function mergeRuntime(root, pack) {
2357
2493
  return parseRuntime({
2358
2494
  version: 1,
2359
2495
  entries
2360
- });
2496
+ }, runtimeLimits);
2361
2497
  }
2362
2498
  function mergeDocuments(root, pack) {
2363
2499
  const current = readCurrentDocuments(root);
@@ -2423,7 +2559,7 @@ function reconcilePersistedStore(root) {
2423
2559
  rmSync(temporary, { force: true });
2424
2560
  }
2425
2561
  }
2426
- function stageImport(root, pack, components, mode) {
2562
+ function stageImport(root, pack, components, mode, runtimeLimits) {
2427
2563
  const staging = join(root, `.dsh-pack-stage-${randomUUID()}`);
2428
2564
  mkdirSync(staging, {
2429
2565
  recursive: true,
@@ -2431,7 +2567,7 @@ function stageImport(root, pack, components, mode) {
2431
2567
  });
2432
2568
  try {
2433
2569
  if (components.includes("runtime")) {
2434
- 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);
2435
2571
  writeRuntime(join(staging, "runtime"), runtime);
2436
2572
  }
2437
2573
  if (components.includes("documents")) {
@@ -2530,12 +2666,17 @@ var MnemonPackManager = class {
2530
2666
  afterImport;
2531
2667
  now;
2532
2668
  root;
2669
+ runtimeLimits;
2533
2670
  constructor(runner, config, afterImport = () => {}, now = () => /* @__PURE__ */ new Date()) {
2534
2671
  this.runner = runner;
2535
2672
  this.config = config;
2536
2673
  this.afterImport = afterImport;
2537
2674
  this.now = now;
2538
2675
  this.root = resolve(runner.effectiveDataDir());
2676
+ this.runtimeLimits = {
2677
+ memory: config.runtimeMemory.memoryLimitBytes,
2678
+ user: config.runtimeMemory.userLimitBytes
2679
+ };
2539
2680
  }
2540
2681
  target() {
2541
2682
  return {
@@ -2548,9 +2689,9 @@ var MnemonPackManager = class {
2548
2689
  return this.runner.withExclusive(async () => {
2549
2690
  await new Promise((resolveReady) => setImmediate(resolveReady));
2550
2691
  return withLocks(this.root, components, () => {
2551
- const payload = collectExport(this.root, components);
2692
+ const payload = collectExport(this.root, components, this.runtimeLimits);
2552
2693
  const exportedAt = this.now().toISOString();
2553
- const summary = summaryFor(components, payload);
2694
+ const summary = summaryFor(components, payload, this.runtimeLimits);
2554
2695
  const manifest = {
2555
2696
  format: MNEMON_PACK_FORMAT,
2556
2697
  version: 1,
@@ -2589,7 +2730,7 @@ var MnemonPackManager = class {
2589
2730
  });
2590
2731
  }
2591
2732
  inspectPack(base64, fileName) {
2592
- const pack = parseArchive(base64);
2733
+ const pack = parseArchive(base64, this.runtimeLimits);
2593
2734
  const sanitizedName = safeName(fileName);
2594
2735
  return {
2595
2736
  ...sanitizedName === void 0 ? {} : { fileName: sanitizedName },
@@ -2602,7 +2743,7 @@ var MnemonPackManager = class {
2602
2743
  };
2603
2744
  }
2604
2745
  async importPack(base64, options) {
2605
- const pack = parseArchive(base64);
2746
+ const pack = parseArchive(base64, this.runtimeLimits);
2606
2747
  if (options.mode !== "merge" && options.mode !== "replace") throw new Error("Pack import mode must be merge or replace");
2607
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");
2608
2749
  const components = options.components === void 0 ? pack.manifest.components : COMPONENT_ORDER.filter((component) => options.components.includes(component));
@@ -2614,7 +2755,7 @@ var MnemonPackManager = class {
2614
2755
  mode: 448
2615
2756
  });
2616
2757
  return withLocks(this.root, components, () => {
2617
- const staging = stageImport(this.root, pack, components, options.mode);
2758
+ const staging = stageImport(this.root, pack, components, options.mode, this.runtimeLimits);
2618
2759
  commitStaging(this.root, staging, components);
2619
2760
  this.afterImport(components);
2620
2761
  return {
@@ -5481,6 +5622,37 @@ function registerBuiltinMemoryAdapters(catalog) {
5481
5622
  };
5482
5623
  }
5483
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
5484
5656
  //#region src/memory-view.ts
5485
5657
  function hash(value) {
5486
5658
  return createHash("sha256").update(value).digest("hex");
@@ -5489,12 +5661,12 @@ function workspace(scope) {
5489
5661
  const value = scope?.workspaceId?.trim();
5490
5662
  return value === void 0 || value === "" ? void 0 : value;
5491
5663
  }
5492
- function runtimeSource(runtimeMemory) {
5664
+ function createRuntimeMemorySource(runtimeMemory, resolveBranch = resolveGitBranch) {
5493
5665
  return {
5494
5666
  layerId: "runtime",
5495
5667
  mode: "eager",
5496
- snapshot: () => {
5497
- const projection = runtimeMemory.contextProjection();
5668
+ snapshot: (context) => {
5669
+ const projection = runtimeMemory.contextProjection(resolveBranch(workspace(context.scope)));
5498
5670
  return {
5499
5671
  revision: projection.revision,
5500
5672
  wake: projection.text
@@ -5555,9 +5727,9 @@ function memorySpacesSource(service) {
5555
5727
  }
5556
5728
  };
5557
5729
  }
5558
- function createDefaultMemoryTurnViewManager(kernel, sources) {
5730
+ function createDefaultMemoryTurnViewManager(kernel, sources, options = {}) {
5559
5731
  return new MemoryTurnViewManager(kernel, [
5560
- runtimeSource(sources.runtimeMemory),
5732
+ createRuntimeMemorySource(sources.runtimeMemory, options.resolveBranch ?? resolveGitBranch),
5561
5733
  documentsSource(sources.documents),
5562
5734
  memorySpacesSource(sources.service)
5563
5735
  ]);
@@ -5654,7 +5826,16 @@ function createRuntimeGraph(config, workspaceRoot, extensions) {
5654
5826
  return receiptBridge.record(operation);
5655
5827
  };
5656
5828
  const service = new MnemonService(runner, config, void 0, void 0, void 0, recordCommit);
5657
- 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);
5658
5839
  const documents = new DocumentManager(void 0, void 0, () => runner.effectiveDataDir(), recordCommit);
5659
5840
  const storage = new StorageScopeInspector(runner, config);
5660
5841
  const packs = new MnemonPackManager(runner, config, (components) => {
@@ -6360,10 +6541,13 @@ function naturalEvidence(evidence) {
6360
6541
  return `${index + 1}. [${citation}]${meta === "" ? "" : ` ${meta}`}\n${indentedText(item.content)}`;
6361
6542
  }).join("\n");
6362
6543
  }
6544
+ function runtimeEntryScopeMeta(entry) {
6545
+ return entry.branches && entry.branches.length > 0 ? ` branches=${entry.branches.join(",")}` : "";
6546
+ }
6363
6547
  function runtimeSnapshotContext(target, entries) {
6364
6548
  return `Committed ${target === "memory" ? "MEMORY.md" : "USER.md"} snapshot (read-only run data; numbering is one-based):
6365
6549
  <runtime-memory-snapshot target="${target}">
6366
- ${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)}
6367
6551
  </runtime-memory-snapshot>`;
6368
6552
  }
6369
6553
  function runtimeRoutingExcerpt(value) {
@@ -6379,7 +6563,7 @@ function runtimeRouteChunks(entries) {
6379
6563
  let used = 0;
6380
6564
  for (const [offset, entry] of entries.entries()) {
6381
6565
  const index = offset + 1;
6382
- const line = `${index}. [importance=${entry.importance}] ${runtimeRoutingExcerpt(entry.content)}`;
6566
+ const line = `${index}. [importance=${entry.importance}${runtimeEntryScopeMeta(entry)}] ${runtimeRoutingExcerpt(entry.content)}`;
6383
6567
  const separatorLength = rendered.length === 0 ? 0 : 3;
6384
6568
  if (rendered.length > 0 && used + separatorLength + line.length > RUNTIME_ROUTE_CHUNK_CHARACTERS) {
6385
6569
  chunks.push({
@@ -6488,7 +6672,8 @@ function runtimeMigrationSources(revision, entries) {
6488
6672
  reference: `runtime:${revision}:memory:${offset + 1}`,
6489
6673
  digest: sha256(JSON.stringify({
6490
6674
  content: entry.content,
6491
- importance: entry.importance
6675
+ importance: entry.importance,
6676
+ ...entry.branches === void 0 ? {} : { branches: entry.branches }
6492
6677
  }))
6493
6678
  }));
6494
6679
  }
@@ -6663,6 +6848,7 @@ var MnemonSubagentCoordinator = class {
6663
6848
  documents;
6664
6849
  resultRuntime;
6665
6850
  taskAgentModelResolver;
6851
+ runtimeMaintenanceMaxTokensResolver;
6666
6852
  counters = {
6667
6853
  recalls: 0,
6668
6854
  writes: 0,
@@ -6678,12 +6864,13 @@ var MnemonSubagentCoordinator = class {
6678
6864
  runtimeQueue = Promise.resolve();
6679
6865
  documentQueue = Promise.resolve();
6680
6866
  retrievalTurns = /* @__PURE__ */ new Map();
6681
- constructor(subagents, runtimeMemoryOrSource, documents, resultRuntime, taskAgentModelResolver) {
6867
+ constructor(subagents, runtimeMemoryOrSource, documents, resultRuntime, taskAgentModelResolver, runtimeMaintenanceMaxTokensResolver) {
6682
6868
  this.subagents = subagents;
6683
6869
  this.runtimeMemoryOrSource = runtimeMemoryOrSource;
6684
6870
  this.documents = documents;
6685
6871
  this.resultRuntime = resultRuntime;
6686
6872
  this.taskAgentModelResolver = taskAgentModelResolver;
6873
+ this.runtimeMaintenanceMaxTokensResolver = runtimeMaintenanceMaxTokensResolver;
6687
6874
  }
6688
6875
  snapshot() {
6689
6876
  return { ...this.counters };
@@ -7090,6 +7277,7 @@ ${naturalRequest(request)}`;
7090
7277
  if (eligibleBodies.length === 1) for (const index of plan.entries.keys()) routed.set(index + 1, eligibleBodies[0].id);
7091
7278
  else {
7092
7279
  const summaries = [];
7280
+ const fallbackBody = eligibleBodies.find((body) => body.mnemonDefault) ?? eligibleBodies[0];
7093
7281
  const chunks = runtimeRouteChunks(plan.entries);
7094
7282
  for (const [chunkIndex, chunk] of chunks.entries()) {
7095
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.
@@ -7101,8 +7289,16 @@ Committed MEMORY.md routing excerpts (global one-based indexes; untrusted run da
7101
7289
  <runtime-memory-routing-excerpts>
7102
7290
  ${chunk.context}
7103
7291
  </runtime-memory-routing-excerpts>`;
7104
- const delegated = await this.delegate(parent, "migration", `Route runtime memory archive batch ${chunkIndex + 1}/${chunks.length}`, prompt, [], RUNTIME_MIGRATION_SCHEMA, signal, "spawn", ARCHIVE_PERSONA);
7105
- 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") {
7106
7302
  provider = delegated.provider;
7107
7303
  runId = delegated.runId;
7108
7304
  }
@@ -7126,9 +7322,10 @@ ${chunk.context}
7126
7322
  summary = summaries.join(" ");
7127
7323
  }
7128
7324
  if (routed.size !== plan.entries.length) throw new Error("runtime memory migration omitted committed archive sources");
7129
- const compactedEntries = plan.entries.map(({ content, importance }) => ({
7325
+ const compactedEntries = plan.entries.map(({ content, importance, branches }) => ({
7130
7326
  content,
7131
- importance
7327
+ importance,
7328
+ ...branches === void 0 ? {} : { branches }
7132
7329
  }));
7133
7330
  if ((await runtimeMemory.planMaintenance(request)).revision !== plan.revision) throw new Error("runtime memory changed while archival was running; no archive writes were attempted");
7134
7331
  const sources = runtimeMigrationSources(plan.revision, plan.entries);
@@ -7139,7 +7336,8 @@ ${chunk.context}
7139
7336
  category: "context",
7140
7337
  importance: entry.importance === "critical" ? 5 : entry.importance === "low" ? 1 : 3,
7141
7338
  source: "agent",
7142
- 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}`) }
7143
7341
  };
7144
7342
  }), signal);
7145
7343
  if (archiveResults.length !== sources.length) throw new Error("runtime archive batch did not return one receipt per source entry");
@@ -7345,7 +7543,7 @@ ${runtimeSnapshotContext("user", plan.entries)}`;
7345
7543
  const completionPersona = `${persona}
7346
7544
 
7347
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.`;
7348
- 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;
7349
7547
  const fixed = this.taskAgentModelResolver?.();
7350
7548
  const baseAgentOptions = perOpMaxTokens === void 0 ? void 0 : { maxTokens: perOpMaxTokens };
7351
7549
  const resolvedAgentOptions = fixed === void 0 ? baseAgentOptions : {
@@ -7664,6 +7862,16 @@ function createPluginMessage(text, form, summary) {
7664
7862
  function sourceOf(message) {
7665
7863
  return message.source;
7666
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
+ }
7667
7875
  function eventTurn(event) {
7668
7876
  return typeof event.data.turn === "number" ? event.data.turn : void 0;
7669
7877
  }
@@ -7733,6 +7941,11 @@ var MnemonAgentLifecycle = class {
7733
7941
  guidedTurns = /* @__PURE__ */ new Set();
7734
7942
  memoryActivity = new TurnActivityProjection();
7735
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
+ */
7736
7949
  cueInjected = false;
7737
7950
  idleReviewTimer;
7738
7951
  reviewController;
@@ -7813,6 +8026,24 @@ var MnemonAgentLifecycle = class {
7813
8026
  const pinned = this.pinnedView;
7814
8027
  return pinned === void 0 ? void 0 : pinned.manager.wake(pinned.context.viewId);
7815
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
+ }
7816
8047
  async preStep(payload, next) {
7817
8048
  if (payload.step === 1) this.cancelIdleReview(true);
7818
8049
  const decision = await next();
@@ -7833,7 +8064,7 @@ var MnemonAgentLifecycle = class {
7833
8064
  this.counters.primes += 1;
7834
8065
  this.mark("prime");
7835
8066
  }
7836
- if (this.cueInjected) return decision;
8067
+ if (this.cueAlreadyVisible()) return decision;
7837
8068
  const reminder = guidedReminder(this.config);
7838
8069
  if (reminder === void 0) return decision;
7839
8070
  this.cueInjected = true;
@@ -9152,12 +9383,16 @@ function createWriteHandler(input, lifecycle, runtimeMemory, versions) {
9152
9383
  requireLayer(resolved.graph, "runtime", "write");
9153
9384
  if (resolved.graph.runtimeMemory === void 0) throw new Error("runtime memory is unavailable");
9154
9385
  {
9386
+ const branches = payload.branches === void 0 ? void 0 : Array.isArray(payload.branches) && payload.branches.every((branch) => typeof branch === "string") ? payload.branches : (() => {
9387
+ throw new Error("branches must be an array of strings");
9388
+ })();
9155
9389
  const request = {
9156
9390
  action: String(payload.action ?? ""),
9157
9391
  target: String(payload.target ?? ""),
9158
9392
  ...payload.content === void 0 ? {} : { content: String(payload.content) },
9159
9393
  ...payload.old_text === void 0 ? {} : { oldText: String(payload.old_text) },
9160
- ...payload.importance === void 0 ? {} : { importance: String(payload.importance) }
9394
+ ...payload.importance === void 0 ? {} : { importance: String(payload.importance) },
9395
+ ...branches === void 0 ? {} : { branches }
9161
9396
  };
9162
9397
  const sessionId = String(payload.sessionId ?? "").trim();
9163
9398
  return success$1(inspectionDiverged || lifecycle === void 0 || sessionId === "" || resolved.explicitWorkspace && !alignedSession ? await resolved.graph.runtimeMemory.mutate(request) : await lifecycle.runtime(sessionId, request));
@@ -9411,6 +9646,7 @@ function object(value) {
9411
9646
  }
9412
9647
  const MUTABLE_FIELDS = [
9413
9648
  "storageScope",
9649
+ "runtimeUserScope",
9414
9650
  "cliPath",
9415
9651
  "dataDir",
9416
9652
  "customPackId",
@@ -9418,6 +9654,7 @@ const MUTABLE_FIELDS = [
9418
9654
  "store",
9419
9655
  "timeoutMs",
9420
9656
  "defaultRecallLimit",
9657
+ "runtimeMemory",
9421
9658
  "embedding",
9422
9659
  "recallQuality",
9423
9660
  "routingGuidance",
@@ -9939,7 +10176,7 @@ function registerTools(ctx, serviceOrSource, coordinator, runtimeMemory, documen
9939
10176
  }));
9940
10177
  ctx.tools.register(definition({
9941
10178
  name: "mnemon_runtime_memory",
9942
- 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.",
9943
10180
  parameters: {
9944
10181
  type: "object",
9945
10182
  properties: {
@@ -9973,6 +10210,11 @@ function registerTools(ctx, serviceOrSource, coordinator, runtimeMemory, documen
9973
10210
  "low"
9974
10211
  ],
9975
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."
9976
10218
  }
9977
10219
  },
9978
10220
  required: ["action", "target"]
@@ -9989,7 +10231,8 @@ function registerTools(ctx, serviceOrSource, coordinator, runtimeMemory, documen
9989
10231
  target: args.target,
9990
10232
  ...args.content === void 0 ? {} : { content: args.content },
9991
10233
  ...args.old_text === void 0 ? {} : { oldText: args.old_text },
9992
- ...args.importance === void 0 ? {} : { importance: args.importance }
10234
+ ...args.importance === void 0 ? {} : { importance: args.importance },
10235
+ ...args.branches === void 0 ? {} : { branches: args.branches }
9993
10236
  };
9994
10237
  return isSubagent(exec.agent) ? runtime.runtimeMemory.mutate(request) : coordinator.runtime(requireAgent(exec), request, exec.signal);
9995
10238
  },
@@ -10280,6 +10523,130 @@ function registerTools(ctx, serviceOrSource, coordinator, runtimeMemory, documen
10280
10523
  }));
10281
10524
  }
10282
10525
  //#endregion
10526
+ //#region src/subagent-token-usage.ts
10527
+ /**
10528
+ * Mnemon-owned projection for provider usage after a subagent descriptor.
10529
+ *
10530
+ * Fork-backed subagents inherit the parent's durable event prefix. DSH's
10531
+ * generic `tokenUsage` projection intentionally totals that complete log, so
10532
+ * it cannot also represent the cost of the child shown in the subagent
10533
+ * catalog. This independent projection keeps the generic metric untouched
10534
+ * and resets at every durable descriptor.
10535
+ */
10536
+ const tokenCountSchema = z$1.number().int().nonnegative();
10537
+ const tokenUsageSchema = z$1.object({
10538
+ uncachedInputTokens: tokenCountSchema,
10539
+ outputTokens: tokenCountSchema,
10540
+ cacheReadTokens: tokenCountSchema,
10541
+ cacheWriteTokens: tokenCountSchema
10542
+ }).strict();
10543
+ const tokenUsageStateSchema = z$1.object({
10544
+ descriptorSeen: z$1.boolean(),
10545
+ totals: tokenUsageSchema,
10546
+ last: z$1.object({
10547
+ turn: tokenCountSchema,
10548
+ step: tokenCountSchema,
10549
+ buckets: tokenUsageSchema
10550
+ }).strict().nullable()
10551
+ }).strict();
10552
+ const emptyTokenUsage = () => ({
10553
+ uncachedInputTokens: 0,
10554
+ outputTokens: 0,
10555
+ cacheReadTokens: 0,
10556
+ cacheWriteTokens: 0
10557
+ });
10558
+ function nonnegativeInteger(value) {
10559
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
10560
+ }
10561
+ function usageBuckets(value) {
10562
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
10563
+ const usage = value;
10564
+ const inputTokens = nonnegativeInteger(usage.inputTokens);
10565
+ const outputTokens = nonnegativeInteger(usage.outputTokens);
10566
+ const cacheReadTokens = usage.cacheReadTokens === void 0 ? 0 : nonnegativeInteger(usage.cacheReadTokens);
10567
+ const cacheWriteTokens = usage.cacheWriteTokens === void 0 ? 0 : nonnegativeInteger(usage.cacheWriteTokens);
10568
+ if (inputTokens === void 0 || outputTokens === void 0 || cacheReadTokens === void 0 || cacheWriteTokens === void 0) return void 0;
10569
+ return {
10570
+ uncachedInputTokens: inputTokens,
10571
+ outputTokens,
10572
+ cacheReadTokens,
10573
+ cacheWriteTokens
10574
+ };
10575
+ }
10576
+ function usageSample(event) {
10577
+ const turn = nonnegativeInteger(event.data.turn);
10578
+ const step = nonnegativeInteger(event.data.step);
10579
+ if (turn === void 0 || step === void 0) return void 0;
10580
+ let rawUsage;
10581
+ if (event.type === "assistant/chunk") {
10582
+ const chunk = event.data.chunk;
10583
+ if (typeof chunk !== "object" || chunk === null || Array.isArray(chunk)) return void 0;
10584
+ const record = chunk;
10585
+ if (record.type !== "usage") return void 0;
10586
+ rawUsage = record.usage;
10587
+ } else if (event.type === "assistant/message") rawUsage = event.data.usage;
10588
+ else return;
10589
+ const buckets = usageBuckets(rawUsage);
10590
+ return buckets === void 0 ? void 0 : {
10591
+ turn,
10592
+ step,
10593
+ buckets
10594
+ };
10595
+ }
10596
+ function equalTokenUsage(left, right) {
10597
+ return left.uncachedInputTokens === right.uncachedInputTokens && left.outputTokens === right.outputTokens && left.cacheReadTokens === right.cacheReadTokens && left.cacheWriteTokens === right.cacheWriteTokens;
10598
+ }
10599
+ function replaceTokenUsage(totals, previous, next) {
10600
+ return {
10601
+ uncachedInputTokens: totals.uncachedInputTokens - (previous?.uncachedInputTokens ?? 0) + next.uncachedInputTokens,
10602
+ outputTokens: totals.outputTokens - (previous?.outputTokens ?? 0) + next.outputTokens,
10603
+ cacheReadTokens: totals.cacheReadTokens - (previous?.cacheReadTokens ?? 0) + next.cacheReadTokens,
10604
+ cacheWriteTokens: totals.cacheWriteTokens - (previous?.cacheWriteTokens ?? 0) + next.cacheWriteTokens
10605
+ };
10606
+ }
10607
+ /**
10608
+ * Fold provider usage after the latest subagent descriptor. A fork may carry
10609
+ * an ancestor descriptor as part of its seed, so every descriptor resets the
10610
+ * fold and the child's own descriptor becomes the final authoritative origin.
10611
+ */
10612
+ const mnemonSubagentTokenUsageProjectionDefinition = {
10613
+ key: "mnemonSubagentTokenUsage",
10614
+ stateVersion: 1,
10615
+ stateSchema: tokenUsageStateSchema,
10616
+ init: () => ({
10617
+ descriptorSeen: false,
10618
+ totals: emptyTokenUsage(),
10619
+ last: null
10620
+ }),
10621
+ apply: (state, event) => {
10622
+ if (event.type === "subagent/descriptor") return {
10623
+ descriptorSeen: true,
10624
+ totals: emptyTokenUsage(),
10625
+ last: null
10626
+ };
10627
+ if (!state.descriptorSeen) return state;
10628
+ const sample = usageSample(event);
10629
+ if (sample === void 0) return state;
10630
+ const previous = state.last !== null && state.last.turn === sample.turn && state.last.step === sample.step ? state.last.buckets : void 0;
10631
+ if (previous !== void 0 && equalTokenUsage(previous, sample.buckets)) return state;
10632
+ return {
10633
+ descriptorSeen: true,
10634
+ totals: replaceTokenUsage(state.totals, previous, sample.buckets),
10635
+ last: sample
10636
+ };
10637
+ },
10638
+ wire: {
10639
+ viewSchema: tokenUsageSchema.nullable(),
10640
+ view: (state) => state.descriptorSeen ? state.totals : null
10641
+ }
10642
+ };
10643
+ /** Register lazily when the optional DSH projection service is present. */
10644
+ function registerMnemonSubagentTokenUsageProjection(ctx) {
10645
+ ctx.inject(["sessionProjections"], (rawContext) => {
10646
+ rawContext.sessionProjections?.register(mnemonSubagentTokenUsageProjectionDefinition);
10647
+ });
10648
+ }
10649
+ //#endregion
10283
10650
  //#region src/index.ts
10284
10651
  const name = "dsh-mnemon";
10285
10652
  const provide = ["mnemonMemory"];
@@ -10301,6 +10668,7 @@ function optionalWorkspaceRegistry(ctx) {
10301
10668
  /** Mount native model tools on every DSH surface and UI RPC only when Web connection exists. */
10302
10669
  function apply(rawContext, config = {}) {
10303
10670
  const ctx = rawContext;
10671
+ registerMnemonSubagentTokenUsageProjection(ctx);
10304
10672
  const extensions = memoryBoot;
10305
10673
  ctx.provide?.("mnemonMemory", extensions);
10306
10674
  const prepared = /* @__PURE__ */ new Map();
@@ -10350,7 +10718,7 @@ function apply(rawContext, config = {}) {
10350
10718
  provider,
10351
10719
  model
10352
10720
  };
10353
- });
10721
+ }, () => runtime.config.runtimeMemory.maintenanceMaxTokens);
10354
10722
  const lifecycle = new MnemonLifecycle(ctx, coordinator, runtime.config, runtime);
10355
10723
  ctx.effect(() => {
10356
10724
  const stop = lifecycle.start();