dsh-mnemon 0.5.8 → 0.5.10

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
@@ -20,6 +20,11 @@ import { spawn } from "node:child_process";
20
20
  import { StringDecoder } from "node:string_decoder";
21
21
  import { z as z$1 } from "zod";
22
22
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
23
+ //#region src/host/view-protocol.ts
24
+ const MNEMON_VIEW_CHANNEL = "/dsh-mnemon-view";
25
+ const MNEMON_VIEW_WRITE_CHANNEL = "/dsh-mnemon-view-settings";
26
+ const MNEMON_VIEW_SETTINGS_NAMESPACE = "mnemon-view";
27
+ //#endregion
23
28
  //#region src/host/display-mode.ts
24
29
  /** Keep the historical misspelling at input boundaries, never in UI or runtime state. */
25
30
  function normalizeDisplayMode(value) {
@@ -28,11 +33,6 @@ function normalizeDisplayMode(value) {
28
33
  throw new Error("dsh-mnemon: displayMode must be sidebar or builtin (legacy buildin is accepted)");
29
34
  }
30
35
  //#endregion
31
- //#region src/host/view-protocol.ts
32
- const MNEMON_VIEW_CHANNEL = "/dsh-mnemon-view";
33
- const MNEMON_VIEW_WRITE_CHANNEL = "/dsh-mnemon-view-settings";
34
- const MNEMON_VIEW_SETTINGS_NAMESPACE = "mnemon-view";
35
- //#endregion
36
36
  //#region src/host/protocol.ts
37
37
  /** Starter Entry ids remain reserved when DSH nests them under an include. */
38
38
  function isDefaultSourceInstance(instanceKey, sourceTypeId) {
@@ -48,6 +48,15 @@ const MNEMON_SETTINGS_NAMESPACE = "mnemon";
48
48
  function isWorkspaceStorageScope(scope) {
49
49
  return scope === "workspace" || scope === "workspaces";
50
50
  }
51
+ const DEFAULT_IDLE_REVIEW = {
52
+ enabled: true,
53
+ provider: "spawn",
54
+ fallback: "spawn",
55
+ minIntervalMs: 3e5,
56
+ maxPerSession: 20,
57
+ maxContextChars: 24e3,
58
+ maxTokens: 4096
59
+ };
51
60
  const CATEGORIES = [
52
61
  "preference",
53
62
  "decision",
@@ -151,6 +160,15 @@ const RuntimeMemorySchema = z.object({
151
160
  userLimitBytes: z.number().step(1).min(1).max(MAX_RUNTIME_MEMORY_LIMIT_BYTES).default(DEFAULT_RUNTIME_USER_LIMIT_BYTES),
152
161
  maintenanceMaxTokens: z.number().step(1).min(1).max(MAX_RUNTIME_MAINTENANCE_MAX_TOKENS).default(DEFAULT_RUNTIME_MAINTENANCE_MAX_TOKENS)
153
162
  });
163
+ const IdleReviewSchema = z.object({
164
+ enabled: z.boolean().default(DEFAULT_IDLE_REVIEW.enabled),
165
+ provider: z.union(["spawn", "fork"]).default(DEFAULT_IDLE_REVIEW.provider),
166
+ fallback: z.union(["spawn", "skip"]).default(DEFAULT_IDLE_REVIEW.fallback),
167
+ minIntervalMs: z.number().step(1).min(5e3).max(864e5).default(DEFAULT_IDLE_REVIEW.minIntervalMs),
168
+ maxPerSession: z.number().step(1).min(0).max(200).default(DEFAULT_IDLE_REVIEW.maxPerSession),
169
+ maxContextChars: z.number().step(1).min(1e3).max(1e6).default(DEFAULT_IDLE_REVIEW.maxContextChars),
170
+ maxTokens: z.number().step(1).min(128).max(131072).default(DEFAULT_IDLE_REVIEW.maxTokens)
171
+ });
154
172
  const MemoryParticipationModeSchema = z.union([
155
173
  "off",
156
174
  "manual",
@@ -224,6 +242,7 @@ const Config = z.object({
224
242
  recallMode: z.union(["guided", "off"]).default("guided"),
225
243
  writebackMode: z.union(["guided", "off"]).default("guided"),
226
244
  idleReviewMs: z.number().step(1).min(5e3).max(6e5).default(DEFAULT_IDLE_REVIEW_MS),
245
+ idleReview: IdleReviewSchema.default(DEFAULT_IDLE_REVIEW),
227
246
  conversationInteraction: z.object({
228
247
  toolviews: z.boolean().default(false),
229
248
  turnBar: z.boolean().default(true),
@@ -394,6 +413,7 @@ function resolveConfig(config = {}) {
394
413
  recallMode: config.recallMode ?? "guided",
395
414
  writebackMode: config.writebackMode ?? "guided",
396
415
  idleReviewMs: config.idleReviewMs ?? 3e4,
416
+ idleReview: IdleReviewSchema(config.idleReview ?? {}),
397
417
  conversationInteraction: {
398
418
  toolviews: config.conversationInteraction?.toolviews ?? false,
399
419
  turnBar: config.conversationInteraction?.turnBar ?? true,
@@ -424,7 +444,7 @@ const COMPONENT_ORDER = [
424
444
  ];
425
445
  const BODY_ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
426
446
  const SQLITE_HEADER = Buffer.from("SQLite format 3\0", "binary");
427
- function record$2(value) {
447
+ function record$3(value) {
428
448
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
429
449
  }
430
450
  function utf8(bytes, label) {
@@ -467,7 +487,7 @@ function componentsForScope(scope) {
467
487
  return [scope];
468
488
  }
469
489
  function parseManifest(value) {
470
- const manifest = record$2(value);
490
+ const manifest = record$3(value);
471
491
  if (manifest?.format !== "mnemonpack" || manifest.version !== 1) throw new Error("unsupported Mnemon Pack format or version");
472
492
  if (manifest.scope !== "full" && !COMPONENT_ORDER.includes(manifest.scope)) throw new Error("Mnemon Pack scope is invalid");
473
493
  if (typeof manifest.exportedAt !== "string") throw new Error("Mnemon Pack exportedAt is invalid");
@@ -476,11 +496,11 @@ function parseManifest(value) {
476
496
  if (components.length === 0 || new Set(components).size !== components.length || components.some((component) => !COMPONENT_ORDER.includes(component))) throw new Error("Mnemon Pack components are invalid");
477
497
  const expected = componentsForScope(manifest.scope);
478
498
  if (components.length !== expected.length || expected.some((component) => !components.includes(component))) throw new Error("Mnemon Pack scope does not match its components");
479
- const source = record$2(manifest.source);
499
+ const source = record$3(manifest.source);
480
500
  if (source?.plugin !== "dsh-mnemon" || typeof source.pluginVersion !== "string") throw new Error("Mnemon Pack source is invalid");
481
501
  if (!Array.isArray(manifest.summary)) throw new Error("Mnemon Pack summary is invalid");
482
502
  const summary = manifest.summary.map((entry) => {
483
- const item = record$2(entry);
503
+ const item = record$3(entry);
484
504
  const component = item?.component;
485
505
  if (!COMPONENT_ORDER.includes(component) || !Number.isSafeInteger(item?.files) || !Number.isSafeInteger(item?.bytes) || !Number.isSafeInteger(item?.items) || Number(item?.files) < 0 || Number(item?.bytes) < 0 || Number(item?.items) < 0) throw new Error("Mnemon Pack summary entry is invalid");
486
506
  return {
@@ -530,8 +550,8 @@ function parseArchive(base64, runtimeLimits = RUNTIME_MEMORY_LIMITS) {
530
550
  if (path !== "manifest.json" && path !== "checksums.json" && !allowedPayloadPath(path)) throw new Error(`unsupported Mnemon Pack entry: ${path}`);
531
551
  }
532
552
  const manifest = parseManifest(json$1(files["manifest.json"], "manifest.json"));
533
- const checksumValue = record$2(json$1(files["checksums.json"], "checksums.json"));
534
- const checksumFiles = record$2(checksumValue?.files);
553
+ const checksumValue = record$3(json$1(files["checksums.json"], "checksums.json"));
554
+ const checksumFiles = record$3(checksumValue?.files);
535
555
  if (checksumValue?.algorithm !== "sha256" || checksumFiles === void 0) throw new Error("Mnemon Pack checksums are invalid");
536
556
  const payloadNames = names.filter((path) => path.startsWith("payload/")).sort();
537
557
  if (Object.keys(checksumFiles).length !== payloadNames.length) throw new Error("Mnemon Pack checksum inventory does not match the payload");
@@ -554,10 +574,10 @@ function parseArchive(base64, runtimeLimits = RUNTIME_MEMORY_LIMITS) {
554
574
  };
555
575
  }
556
576
  function parseRuntime(value, limits = RUNTIME_MEMORY_LIMITS) {
557
- const source = record$2(value);
577
+ const source = record$3(value);
558
578
  if (source?.version !== RUNTIME_MEMORY_VERSION || !Array.isArray(source.entries)) throw new Error("runtime memories.json is invalid");
559
579
  const entries = source.entries.map((raw) => {
560
- const entry = record$2(raw);
580
+ const entry = record$3(raw);
561
581
  if (typeof entry?.content !== "string" || entry.target !== "memory" && entry.target !== "user" || ![
562
582
  "critical",
563
583
  "normal",
@@ -588,13 +608,13 @@ function runtimeProjection(entries, target) {
588
608
  return content === "" ? "" : `${content}\n`;
589
609
  }
590
610
  function parseDocumentIndex(value) {
591
- const index = record$2(value);
611
+ const index = record$3(value);
592
612
  if (index?.version !== DOCUMENTS_VERSION || !Array.isArray(index.documents)) throw new Error("Documents index.json is invalid");
593
613
  const ids = /* @__PURE__ */ new Set();
594
614
  return {
595
615
  version: 1,
596
616
  documents: index.documents.map((raw) => {
597
- const item = record$2(raw);
617
+ const item = record$3(raw);
598
618
  if (typeof item?.id !== "string" || item.id.trim() === "" || ids.has(item.id)) throw new Error("Documents index contains an invalid or duplicate id");
599
619
  if (typeof item.title !== "string" || typeof item.description !== "string" || item.status !== "active" && item.status !== "archived") throw new Error("Documents index contains an invalid record");
600
620
  if (typeof item.filename !== "string" || basename(item.filename) !== item.filename || !/^[a-zA-Z0-9._-]+\.md$/u.test(item.filename)) throw new Error("Documents index contains an unsafe filename");
@@ -624,13 +644,13 @@ function parseDocumentIndex(value) {
624
644
  };
625
645
  }
626
646
  function parseRegistry(value) {
627
- const registry = record$2(value);
647
+ const registry = record$3(value);
628
648
  if (registry?.version !== 1 || !Array.isArray(registry.bodies)) throw new Error("Memory Space registry is invalid");
629
649
  const ids = /* @__PURE__ */ new Set();
630
650
  return {
631
651
  version: 1,
632
652
  bodies: registry.bodies.map((raw) => {
633
- const body = record$2(raw);
653
+ const body = record$3(raw);
634
654
  if (typeof body?.id !== "string" || !BODY_ID.test(body.id) || ids.has(body.id)) throw new Error("Memory Space registry contains an invalid or duplicate id");
635
655
  if (typeof body.name !== "string" || body.name.trim() === "" || body.name.length > 100 || typeof body.description !== "string" || body.description.length > 1e3) throw new Error("Memory Space registry contains invalid metadata");
636
656
  if (typeof body.createdAt !== "string" || typeof body.updatedAt !== "string") throw new Error("Memory Space registry contains invalid timestamps");
@@ -1263,7 +1283,7 @@ function safeBytes(path) {
1263
1283
  function readJson(path) {
1264
1284
  return JSON.parse(readFileSync(path, "utf8"));
1265
1285
  }
1266
- function record$1(value) {
1286
+ function record$2(value) {
1267
1287
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1268
1288
  }
1269
1289
  function missing(kind, path) {
@@ -1281,9 +1301,9 @@ function runtimeArea(root) {
1281
1301
  const source = join(path, "memories.json");
1282
1302
  if (!existsSync(source)) return missing("runtime", path);
1283
1303
  try {
1284
- const file = record$1(readJson(source));
1304
+ const file = record$2(readJson(source));
1285
1305
  if (file === void 0 || !Array.isArray(file.entries)) throw new Error("memories.json is not a valid runtime-memory source");
1286
- const entries = file.entries.map(record$1).filter((entry) => entry !== void 0);
1306
+ const entries = file.entries.map(record$2).filter((entry) => entry !== void 0);
1287
1307
  const userEntries = entries.filter((entry) => entry.target === "user").length;
1288
1308
  const memoryEntries = entries.filter((entry) => entry.target === "memory").length;
1289
1309
  const projectionsHealthy = existsSync(join(path, "USER.md")) && existsSync(join(path, "MEMORY.md"));
@@ -1318,8 +1338,8 @@ function memorySpacesArea(root) {
1318
1338
  if (!existsSync(path)) return missing("memory-bodies", path);
1319
1339
  try {
1320
1340
  const registryPath = join(path, ".dsh-memory-bodies.json");
1321
- const registry = existsSync(registryPath) ? record$1(readJson(registryPath)) : void 0;
1322
- const bodies = Array.isArray(registry?.bodies) ? registry.bodies.map(record$1).filter((body) => body !== void 0) : [];
1341
+ const registry = existsSync(registryPath) ? record$2(readJson(registryPath)) : void 0;
1342
+ const bodies = Array.isArray(registry?.bodies) ? registry.bodies.map(record$2).filter((body) => body !== void 0) : [];
1323
1343
  const databaseCount = readdirSync(path, { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync(join(path, entry.name, "mnemon.db"))).length;
1324
1344
  const activeCount = bodies.filter((body) => body.active === true).length;
1325
1345
  const invalidRegistry = existsSync(registryPath) && (registry?.version !== 1 || !Array.isArray(registry.bodies));
@@ -1354,9 +1374,9 @@ function documentsArea(root) {
1354
1374
  const indexPath = join(path, "index.json");
1355
1375
  if (!existsSync(indexPath)) return missing("documents", path);
1356
1376
  try {
1357
- const index = record$1(readJson(indexPath));
1377
+ const index = record$2(readJson(indexPath));
1358
1378
  if (index === void 0 || !Array.isArray(index.documents)) throw new Error("index.json is not a valid Documents index");
1359
- const documents = index.documents.map(record$1).filter((document) => document !== void 0);
1379
+ const documents = index.documents.map(record$2).filter((document) => document !== void 0);
1360
1380
  const active = documents.filter((document) => document.status === "active").length;
1361
1381
  const archived = documents.filter((document) => document.status === "archived").length;
1362
1382
  return {
@@ -1392,9 +1412,9 @@ function stateArea(root) {
1392
1412
  let providerConnections = 0;
1393
1413
  let providerServices = 0;
1394
1414
  if (existsSync(providerRegistry)) try {
1395
- const registry = record$1(readJson(providerRegistry));
1415
+ const registry = record$2(readJson(providerRegistry));
1396
1416
  providerConnections = Array.isArray(registry?.bodies) ? registry.bodies.length : 0;
1397
- providerServices = record$1(registry?.services) === void 0 ? 0 : Object.keys(record$1(registry?.services)).length;
1417
+ providerServices = record$2(registry?.services) === void 0 ? 0 : Object.keys(record$2(registry?.services)).length;
1398
1418
  } catch {}
1399
1419
  return {
1400
1420
  kind: "state",
@@ -2615,12 +2635,21 @@ function hostSessionEventAt(session, seq) {
2615
2635
  //#region src/host/review-tools.ts
2616
2636
  const startingReview = new AsyncLocalStorage();
2617
2637
  /**
2638
+ * The published Agent Teams alpha.2 tool suite installs its policy before a
2639
+ * provider publishes a child descriptor. Both rc.2 in-process providers then
2640
+ * lose membership on step two. Inspect public services/capabilities only and
2641
+ * skip before creating a child; do not remove another plugin's policy.
2642
+ */
2643
+ function idleReviewBlockReason(parent) {
2644
+ return parent.ctx?.get?.("agentTeams") !== void 0 && parent.ctx.tools?.get?.("spawn_teammate", parent) !== void 0 ? "agent-team" : void 0;
2645
+ }
2646
+ /**
2618
2647
  * DSH restrict() filters inherited capabilities, leaving own-scope plugin tools
2619
2648
  * visible. Attach its monotonic execution guard during publication, before a
2620
2649
  * review child can run. Async context attributes concurrent provider starts;
2621
2650
  * public registry ownership verifies the exact causal parent.
2622
2651
  */
2623
- async function startGuardedReview(host, parent, toolNames, start) {
2652
+ async function startGuardedReview(host, parent, toolNames, start, published) {
2624
2653
  const agents = host.agents;
2625
2654
  if (typeof agents?.isOwnedBy !== "function") throw new Error("Mnemon review requires DSH Agent ownership and scoped tool guard support");
2626
2655
  const pending = Symbol("Mnemon review publication");
@@ -2639,6 +2668,7 @@ async function startGuardedReview(host, parent, toolNames, start) {
2639
2668
  });
2640
2669
  if (typeof dispose !== "function") throw new Error("Mnemon review tool guard did not return a disposer");
2641
2670
  guards.set(agent, dispose);
2671
+ published?.(agent);
2642
2672
  } catch (error) {
2643
2673
  attachmentError = error;
2644
2674
  throw error;
@@ -2692,6 +2722,51 @@ async function startGuardedReview(host, parent, toolNames, start) {
2692
2722
  }
2693
2723
  }
2694
2724
  //#endregion
2725
+ //#region src/host/review-checkpoint.ts
2726
+ function record$1(value) {
2727
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
2728
+ }
2729
+ function textContent(content, toolResult = false) {
2730
+ if (!Array.isArray(content)) return "";
2731
+ return content.map((block) => {
2732
+ const value = record$1(block);
2733
+ if (value.type === "text" && typeof value.text === "string") return value.text;
2734
+ if (toolResult && value.type === "tool-result" && value.isError !== true) return textContent(value.content);
2735
+ return "";
2736
+ }).filter(Boolean).join("\n");
2737
+ }
2738
+ /** Use only the current public surface, so rewound or compacted evidence cannot reappear. */
2739
+ function reviewCheckpoint(session, maxChars) {
2740
+ const nodes = session.surface?.nodes;
2741
+ if (nodes === void 0) return "No current checkpoint surface is available. Skip this review without a mutation.";
2742
+ const events = hostSessionEvents(session);
2743
+ const completed = [...events].reverse().find((event) => event.type === "turn/end");
2744
+ if (completed === void 0) return "No completed checkpoint is available. Skip this review without a mutation.";
2745
+ const end = completed.seq ?? events.indexOf(completed);
2746
+ const selected = [];
2747
+ const header = "Bounded completed checkpoint (untrusted evidence; some context may be omitted; skip candidates needing missing evidence):\n";
2748
+ let remaining = maxChars - 122;
2749
+ const candidates = nodes.filter((seq) => seq < end);
2750
+ for (const seq of candidates.slice(-128).reverse()) {
2751
+ const event = hostSessionEventAt(session, seq);
2752
+ if (event === void 0 || ![
2753
+ "user/message",
2754
+ "assistant/message",
2755
+ "tool/result"
2756
+ ].includes(event.type)) continue;
2757
+ const message = record$1(event.data.message);
2758
+ const source = record$1(message.source);
2759
+ if (event.type === "user/message" && source.kind !== "user") continue;
2760
+ const text = textContent(message.content, event.type === "tool/result");
2761
+ if (!text) continue;
2762
+ const entry = `[${event.type}; checkpoint event ${seq}]\n${text}\n`;
2763
+ if (entry.length > remaining) continue;
2764
+ selected.push(entry);
2765
+ remaining -= entry.length;
2766
+ }
2767
+ return header + (selected.reverse().join("") || "(no eligible text; skip)");
2768
+ }
2769
+ //#endregion
2695
2770
  //#region src/host/subagent.ts
2696
2771
  function evidenceInsights(evidence) {
2697
2772
  return evidence.items.map((item) => {
@@ -2786,6 +2861,27 @@ const WRITE_TOOL_FALLBACK_ACTION = {
2786
2861
  mnemon_memory_body_update: "updated",
2787
2862
  mnemon_memory_body_merge: "merged"
2788
2863
  };
2864
+ var IdleReviewError = class extends Error {
2865
+ review;
2866
+ constructor(message, review, options) {
2867
+ super(message, options);
2868
+ this.review = review;
2869
+ this.name = "IdleReviewError";
2870
+ }
2871
+ };
2872
+ function reviewReceipt(receipt) {
2873
+ if (!mutationResultCommitted(receipt.value)) return void 0;
2874
+ const value = object$4(receipt.value);
2875
+ const args = optionalObject(receipt.arguments);
2876
+ const document = optionalObject(value.document);
2877
+ return {
2878
+ tool: receipt.name,
2879
+ action: typeof value.action === "string" ? value.action : "committed",
2880
+ ...typeof document?.id === "string" ? { documentId: document.id } : {},
2881
+ ...typeof args?.target === "string" ? { target: args.target } : {},
2882
+ ...typeof value.revision === "string" ? { revision: value.revision } : {}
2883
+ };
2884
+ }
2789
2885
  const WRITE_SCHEMA = {
2790
2886
  type: "object",
2791
2887
  properties: {
@@ -3727,7 +3823,11 @@ var MnemonSubagentCoordinator = class {
3727
3823
  return {
3728
3824
  source,
3729
3825
  cleanup,
3730
- memoryBodyIds: new Set(strings(object$4(grant.value).memoryBodyIds))
3826
+ memoryBodyIds: new Set(strings(object$4(grant.value).memoryBodyIds)),
3827
+ writeScope: {
3828
+ viewId: view.id,
3829
+ grant
3830
+ }
3731
3831
  };
3732
3832
  }
3733
3833
  runtimeModel(scope, parent, signal, operation, label, prompt, schema, persona) {
@@ -3781,7 +3881,28 @@ ${naturalRequest(request)}`;
3781
3881
  };
3782
3882
  }
3783
3883
  async review(parent, signal) {
3784
- const { provider, runId, result } = await this.delegate(parent, "review", "Mnemon idle checkpoint review", "Review the inherited completed checkpoint now.", REVIEW_TOOLS, WRITE_SCHEMA, signal, "fork", REVIEW_PERSONA);
3884
+ if (idleReviewBlockReason(parent) !== void 0) return {
3885
+ delegated: false,
3886
+ action: "skipped",
3887
+ summary: "Idle review is paused while Agent Teams tools are active because the published child policy is incompatible.",
3888
+ documentIds: []
3889
+ };
3890
+ const config = this.runtimeSource.config.idleReview;
3891
+ let preferred = config.provider;
3892
+ if (preferred === "fork") try {
3893
+ this.provider("fork");
3894
+ } catch (error) {
3895
+ if (config.fallback === "skip") return {
3896
+ delegated: false,
3897
+ action: "skipped",
3898
+ summary: "Fork provider unavailable; idle review skipped.",
3899
+ documentIds: []
3900
+ };
3901
+ preferred = "spawn";
3902
+ }
3903
+ const prompt = preferred === "fork" ? "Review the inherited completed checkpoint now." : reviewCheckpoint(parent.session, config.maxContextChars);
3904
+ const persona = preferred === "fork" ? REVIEW_PERSONA : REVIEW_PERSONA.replaceAll("inherited", "supplied bounded");
3905
+ const { provider, runId, result } = await this.delegate(parent, "review", "Mnemon idle checkpoint review", prompt, REVIEW_TOOLS, WRITE_SCHEMA, signal, preferred, persona, { terminalTools: ["mnemon_runtime_memory", "mnemon_document_create"] });
3785
3906
  const value = object$4(result.structured);
3786
3907
  return {
3787
3908
  delegated: true,
@@ -3973,13 +4094,27 @@ ${naturalRequest(request)}`;
3973
4094
  if (request.target === "user") return this.compactUserAndCommit(context, request, plan, signal);
3974
4095
  const archive = await context.memorySpaces();
3975
4096
  const memoryService = archive.source;
3976
- const writable = (body) => body.active && body.providerEnabled !== false && body.provider.capabilities.remember === true && (archive.memoryBodyIds === void 0 || archive.memoryBodyIds.has(body.id));
4097
+ let allowed = archive.memoryBodyIds;
4098
+ const readDirectory = async () => {
4099
+ const catalog = await memoryService.read("body-directory", archive.writeScope === void 0 ? null : { writeScope: archive.writeScope }, signal);
4100
+ allowed = archive.memoryBodyIds;
4101
+ if (catalog.writeScope !== void 0) {
4102
+ const scope = catalog.writeScope;
4103
+ if (archive.writeScope === void 0 || scope === null || scope.viewId !== archive.writeScope.viewId || scope.sourceInstanceKey !== archive.writeScope.grant.sourceInstanceKey || !Array.isArray(scope.memoryBodyIds) || scope.memoryBodyIds.length > 1e4 || scope.memoryBodyIds.some((id) => typeof id !== "string" || id.trim() === "" || id !== id.trim())) throw new Error("Runtime archival received an invalid Memory Spaces write scope; pending write was not committed; retry the same request in a new turn");
4104
+ allowed = new Set(scope.memoryBodyIds);
4105
+ }
4106
+ return catalog;
4107
+ };
4108
+ const catalog = await readDirectory();
4109
+ const writable = (body) => body.active && body.providerEnabled !== false && body.provider.capabilities.remember === true && (allowed === void 0 || allowed.has(body.id));
3977
4110
  const eligible = (body) => writable(body) && body.provider.capabilities.writeMode === "exact" && body.provider.capabilities.forget === true;
3978
- const catalog = await memoryService.read("body-directory", null, signal);
3979
4111
  const eligibleBodies = catalog.items.filter(eligible);
3980
4112
  if (eligibleBodies.length === 0) {
3981
- const unsupported = catalog.items.filter(writable).map((body) => `${body.id} (provider=${body.provider.id}, writeMode=${body.provider.capabilities.writeMode}, forget=${body.provider.capabilities.forget})`).join(", ");
3982
- throw new Error(`runtime memory archival requires an existing active writable Memory Space with exact writes and safe forget; activate a supported Memory Space or increase runtimeMemory.memoryLimitBytes${unsupported === "" ? "" : `; unsupported destinations: ${unsupported}`}`);
4113
+ const writableBodies = catalog.items.filter(writable);
4114
+ const unsupported = writableBodies.map((body) => `${body.id} (provider=${body.provider.id}, writeMode=${body.provider.capabilities.writeMode}, forget=${body.provider.capabilities.forget})`).join(", ");
4115
+ const reason = catalog.items.length === 0 ? "Memory Space body-directory is empty" : allowed?.size === 0 ? "current View write scope is empty" : allowed !== void 0 && catalog.items.some((body) => body.active && body.providerEnabled !== false && body.provider.capabilities.remember && !allowed?.has(body.id)) && writableBodies.length === 0 ? "current View write scope excludes the active writable Memory Spaces" : writableBodies.length === 0 ? "no active writable Memory Space in the authorized scope" : "authorized destinations lack exact writes or safe forget";
4116
+ const scope = archive.writeScope === void 0 ? "management" : `${archive.writeScope.grant.sourceInstanceKey}/${catalog.writeScope === void 0 ? "pinned-read" : "source-write"}`;
4117
+ throw new Error(`runtime memory archival requires an existing active writable Memory Space with exact writes and safe forget; ${reason}; scope=${scope}, catalog=${catalog.items.length}, authorized=${allowed?.size ?? "management"}, writable=${writableBodies.length}; pending write was not committed and existing runtime entries are unchanged; retry the same request in a new turn after activating a supported authorized Memory Space, or increase runtimeMemory.memoryLimitBytes${unsupported === "" ? "" : `; unsupported destinations: ${unsupported}`}`);
3983
4118
  }
3984
4119
  const eligibleById = new Map(eligibleBodies.map((body) => [body.id, body]));
3985
4120
  const budget = compactedBudget(plan);
@@ -4043,7 +4178,7 @@ ${chunk.context}
4043
4178
  ...branches === void 0 ? {} : { branches }
4044
4179
  }));
4045
4180
  if ((await runtimeMemory.read("maintenance-plan", request, signal)).revision !== plan.revision) throw new Error("runtime memory changed while archival was running; no archive writes were attempted");
4046
- const destinations = await memoryService.read("body-directory", null, signal);
4181
+ const destinations = await readDirectory();
4047
4182
  for (const memoryBodyId of new Set(routed.values())) if (!destinations.items.some((body) => body.id === memoryBodyId && eligible(body))) throw new Error(`runtime archive Memory Space ${memoryBodyId} is no longer eligible; no archive writes were attempted`);
4048
4183
  signal.throwIfAborted();
4049
4184
  context.assertWritable?.();
@@ -4067,6 +4202,7 @@ ${chunk.context}
4067
4202
  for (const memoryBodyId of new Set(routed.values())) {
4068
4203
  signal.throwIfAborted();
4069
4204
  context.assertWritable?.();
4205
+ if (archive.writeScope !== void 0 && !(await readDirectory()).items.some((body) => body.id === memoryBodyId && eligible(body))) throw new Error(`runtime archive Memory Space ${memoryBodyId} is no longer authorized or eligible; pending write was not committed`);
4070
4206
  const batch = sources.filter((source) => routed.get(source.index) === memoryBodyId);
4071
4207
  const written = await memoryService.mutate("remember-many", { requests: batch.map((source) => requests[source.index - 1]) }, signal);
4072
4208
  for (const [offset, result] of written.entries()) {
@@ -4252,8 +4388,11 @@ ${runtimeSnapshotContext("user", plan.entries)}`;
4252
4388
  const staged = /* @__PURE__ */ new WeakMap();
4253
4389
  const recoverableTools = new Set(recovery?.terminalTools ?? []);
4254
4390
  const committedReceipts = [];
4391
+ const reviewReceipts = [];
4392
+ let reviewChildId;
4255
4393
  const stagedReceipts = /* @__PURE__ */ new Map();
4256
4394
  let run;
4395
+ let completed;
4257
4396
  let failure;
4258
4397
  let disposeResultObserver;
4259
4398
  let releaseWorkflow;
@@ -4265,7 +4404,14 @@ ${runtimeSnapshotContext("user", plan.entries)}`;
4265
4404
  }
4266
4405
  if (this.disposed) throw new Error("dsh-mnemon subagent coordinator is disposed");
4267
4406
  const observer = this.resultRuntime.on("tools/result", ((execution, result) => {
4268
- if (signal.aborted || !this.resultRequests.has(requestId)) return;
4407
+ if (!this.resultRequests.has(requestId)) return;
4408
+ if (operation === "review" && execution.agent?.id === reviewChildId && execution.name !== void 0 && recoverableTools.has(execution.name) && result.isError !== true && Object.hasOwn(result, "value")) reviewReceipts.push({
4409
+ agentId: execution.agent.id,
4410
+ name: execution.name,
4411
+ arguments: execution.arguments,
4412
+ value: result.value
4413
+ });
4414
+ if (signal.aborted) return;
4269
4415
  if (execution.token !== void 0) {
4270
4416
  const entries = stagedReceipts.get(execution.token);
4271
4417
  if (entries !== void 0) {
@@ -4330,7 +4476,7 @@ ${runtimeSnapshotContext("user", plan.entries)}`;
4330
4476
  Completion protocol: call \`${resultToolName}\` exactly once with requestId \`${requestId}\` and result matching this JSON schema:
4331
4477
  ${JSON.stringify(outputSchema)}
4332
4478
  This is the only completion channel for this run. Do not finish with a plain-text answer. The requestId expires when this run finishes or is cancelled.`;
4333
- const perOpMaxTokens = operation === "migration" || operation === "compaction" ? this.runtimeMaintenanceMaxTokensResolver?.() ?? 8192 : operation === "document-archive" ? 8192 : operation === "metadata-maintenance" ? 4096 : void 0;
4479
+ const perOpMaxTokens = operation === "migration" || operation === "compaction" ? this.runtimeMaintenanceMaxTokensResolver?.() ?? 8192 : operation === "document-archive" ? 8192 : operation === "metadata-maintenance" ? 4096 : operation === "review" ? this.runtimeSource.config.idleReview.maxTokens : void 0;
4334
4480
  const fixed = this.taskAgentModelResolver?.();
4335
4481
  const baseAgentOptions = perOpMaxTokens === void 0 ? void 0 : { maxTokens: perOpMaxTokens };
4336
4482
  const resolvedAgentOptions = fixed === void 0 ? baseAgentOptions : {
@@ -4351,7 +4497,9 @@ This is the only completion channel for this run. Do not finish with a plain-tex
4351
4497
  toolFilter: { allow: [...tools, resultToolName] },
4352
4498
  persona: completionPersona
4353
4499
  });
4354
- run = operation === "review" ? await startGuardedReview(this.resultRuntime, parent, [...tools, resultToolName], start) : await start();
4500
+ run = operation === "review" ? await startGuardedReview(this.resultRuntime, parent, [...tools, resultToolName], start, (child) => {
4501
+ reviewChildId = child.id;
4502
+ }) : await start();
4355
4503
  const activeRun = run;
4356
4504
  const result = await activeRun.result;
4357
4505
  if (captured !== void 0 && captured.agentId !== activeRun.id) throw new Error("Mnemon subagent result was recorded by a different child");
@@ -4363,11 +4511,12 @@ This is the only completion channel for this run. Do not finish with a plain-tex
4363
4511
  throw new Error(`memory subagent stopped with ${result.stopReason}${detail === void 0 ? "" : `: ${detail}`}`);
4364
4512
  }
4365
4513
  if (structured === void 0) throw new Error("memory subagent completed without recording its result");
4366
- this.counters[operation === "write" ? "writes" : operation === "review" ? "reviews" : operation === "placement" ? "placements" : operation === "migration" ? "migrations" : operation === "compaction" ? "compactions" : operation === "document-archive" ? "documentArchives" : operation === "metadata-maintenance" ? "metadataMaintenances" : "answers"] += 1;
4367
- this.counters.lastRunId = activeRun.id;
4368
- if (operation !== "answer") this.counters.lastOperation = operation;
4369
- this.counters.lastAt = (/* @__PURE__ */ new Date()).toISOString();
4370
- return {
4514
+ if (operation === "review" && [
4515
+ "failed",
4516
+ "partial",
4517
+ "unknown"
4518
+ ].includes(String(object$4(structured).action))) throw new Error(`memory review reported ${object$4(structured).action}`);
4519
+ completed = {
4371
4520
  provider,
4372
4521
  runId: activeRun.id,
4373
4522
  result: {
@@ -4378,8 +4527,14 @@ This is the only completion channel for this run. Do not finish with a plain-tex
4378
4527
  };
4379
4528
  } catch (error) {
4380
4529
  this.counters.failures += 1;
4381
- failure = error;
4382
- throw error;
4530
+ const receipts = reviewReceipts.map(reviewReceipt).filter((receipt) => receipt !== void 0);
4531
+ failure = operation === "review" ? new IdleReviewError(error instanceof Error ? error.message : String(error), {
4532
+ status: receipts.length > 0 ? "partial" : "failed",
4533
+ provider,
4534
+ ...reviewChildId === void 0 ? {} : { runId: reviewChildId },
4535
+ receipts
4536
+ }, { cause: error }) : error;
4537
+ throw failure;
4383
4538
  } finally {
4384
4539
  this.resultRequests.delete(requestId);
4385
4540
  let cleanupFailure;
@@ -4394,8 +4549,24 @@ This is the only completion channel for this run. Do not finish with a plain-tex
4394
4549
  if (failure === void 0 && cleanupFailure === void 0) cleanupFailure = error;
4395
4550
  }
4396
4551
  releaseWorkflow?.();
4397
- if (cleanupFailure !== void 0) throw cleanupFailure;
4552
+ if (cleanupFailure !== void 0) {
4553
+ this.counters.failures += 1;
4554
+ if (operation !== "review") throw cleanupFailure;
4555
+ const receipts = reviewReceipts.map(reviewReceipt).filter((receipt) => receipt !== void 0);
4556
+ throw new IdleReviewError(cleanupFailure instanceof Error ? cleanupFailure.message : String(cleanupFailure), {
4557
+ status: receipts.length > 0 ? "partial" : "failed",
4558
+ provider,
4559
+ ...reviewChildId === void 0 ? {} : { runId: reviewChildId },
4560
+ receipts
4561
+ }, { cause: cleanupFailure });
4562
+ }
4398
4563
  }
4564
+ if (completed === void 0) throw new Error("memory subagent did not complete");
4565
+ this.counters[operation === "write" ? "writes" : operation === "review" ? "reviews" : operation === "placement" ? "placements" : operation === "migration" ? "migrations" : operation === "compaction" ? "compactions" : operation === "document-archive" ? "documentArchives" : operation === "metadata-maintenance" ? "metadataMaintenances" : "answers"] += 1;
4566
+ this.counters.lastRunId = completed.runId;
4567
+ if (operation !== "answer") this.counters.lastOperation = operation;
4568
+ this.counters.lastAt = (/* @__PURE__ */ new Date()).toISOString();
4569
+ return completed;
4399
4570
  }
4400
4571
  provider(preferred) {
4401
4572
  const names = this.subagents.list();
@@ -5098,6 +5269,9 @@ var MnemonAgentLifecycle = class {
5098
5269
  lastReviewAction;
5099
5270
  lastReviewScore;
5100
5271
  lastReviewDocumentIds;
5272
+ idleReviewAttempts = 0;
5273
+ lastReviewAttemptAt;
5274
+ lastReviewFailure;
5101
5275
  lastPhase = "idle";
5102
5276
  lastAt;
5103
5277
  lastError;
@@ -5147,6 +5321,10 @@ var MnemonAgentLifecycle = class {
5147
5321
  idleReviewPending: this.idleReviewTimer !== void 0,
5148
5322
  reviewRunning: this.reviewRunning,
5149
5323
  reviewActivity: this.reviewActivity(),
5324
+ idleReviewAttempts: this.idleReviewAttempts,
5325
+ ...idleReviewBlockReason(this.agent) === void 0 ? {} : { idleReviewBlocked: "agent-team" },
5326
+ ...this.lastReviewAttemptAt === void 0 ? {} : { nextReviewAt: new Date(this.lastReviewAttemptAt + this.config.idleReview.minIntervalMs).toISOString() },
5327
+ ...this.lastReviewFailure === void 0 ? {} : { lastReviewFailure: this.lastReviewFailure },
5150
5328
  lastPhase: this.lastPhase,
5151
5329
  ...this.lastReviewAt === void 0 ? {} : { lastReviewAt: this.lastReviewAt },
5152
5330
  ...this.lastReviewAction === void 0 ? {} : { lastReviewAction: this.lastReviewAction },
@@ -5272,9 +5450,9 @@ var MnemonAgentLifecycle = class {
5272
5450
  this.releaseView(turn);
5273
5451
  }
5274
5452
  scheduleIdleReview(turn) {
5275
- if (!this.config.lifecycleEnabled || !this.config.writeEnabled || this.config.writebackMode !== "guided") return;
5276
- if (this.config.memoryTopology.strategyId !== "default-three-tier") return;
5277
5453
  this.cancelIdleReview(true);
5454
+ if (!this.idleReviewAllowed()) return;
5455
+ if (this.config.memoryTopology.strategyId !== "default-three-tier") return;
5278
5456
  const activity = this.ensureTurnActivity(turn);
5279
5457
  const tools = completedToolActivity(hostSessionEvents(this.agent.session), turn);
5280
5458
  activity.toolCallCount = tools.count;
@@ -5282,15 +5460,24 @@ var MnemonAgentLifecycle = class {
5282
5460
  if (!this.reviewActivity().eligible || !this.reviewAdmitted(turn)) return;
5283
5461
  this.idleReviewTimer = setTimeout(() => {
5284
5462
  this.idleReviewTimer = void 0;
5285
- if (this.config.memoryTopology.strategyId !== "default-three-tier") return;
5463
+ if (!this.idleReviewAllowed() || this.reviewRunning) return;
5464
+ if (this.lastReviewAttemptAt !== void 0 && Date.now() < this.lastReviewAttemptAt + this.config.idleReview.minIntervalMs) {
5465
+ this.scheduleIdleReview(turn);
5466
+ return;
5467
+ }
5286
5468
  if (this.agent.status !== "idle") return;
5287
5469
  if (!hostSessionEvents(this.agent.session).some((event) => event.type === "turn/end" && eventTurn(event) === turn) || !this.reviewActivity().eligible || !this.reviewAdmitted(turn)) return;
5288
5470
  this.runIdleReview();
5289
- }, this.config.idleReviewMs);
5471
+ }, Math.max(this.config.idleReviewMs, (this.lastReviewAttemptAt ?? -Infinity) + this.config.idleReview.minIntervalMs - Date.now()));
5472
+ }
5473
+ idleReviewAllowed() {
5474
+ return this.config.lifecycleEnabled && this.config.writeEnabled && this.config.writebackMode === "guided" && this.config.idleReview.enabled && this.idleReviewAttempts < this.config.idleReview.maxPerSession && idleReviewBlockReason(this.agent) === void 0 && this.config.memoryTopology.strategyId === "default-three-tier";
5290
5475
  }
5291
5476
  async runIdleReview() {
5292
5477
  const controller = new AbortController();
5293
5478
  const triggeredScore = this.reviewActivity().score;
5479
+ this.idleReviewAttempts += 1;
5480
+ this.lastReviewAttemptAt = Date.now();
5294
5481
  this.reviewRunning = true;
5295
5482
  this.reviewController = controller;
5296
5483
  this.mark("review");
@@ -5301,11 +5488,13 @@ var MnemonAgentLifecycle = class {
5301
5488
  this.lastReviewAction = result.action;
5302
5489
  this.lastReviewScore = triggeredScore;
5303
5490
  this.lastReviewDocumentIds = result.documentIds;
5491
+ this.lastReviewFailure = void 0;
5304
5492
  this.turnActivity.clear();
5305
5493
  this.lastError = void 0;
5306
5494
  this.mark("review");
5307
5495
  } catch (error) {
5308
- if (!controller.signal.aborted) this.fail(error);
5496
+ if (error instanceof IdleReviewError) this.lastReviewFailure = error.review;
5497
+ if (!controller.signal.aborted || error instanceof IdleReviewError && error.review.status === "partial") this.fail(error);
5309
5498
  } finally {
5310
5499
  if (this.reviewController === controller) {
5311
5500
  this.reviewRunning = false;
@@ -7099,6 +7288,7 @@ const MUTABLE_FIELDS = [
7099
7288
  "recallMode",
7100
7289
  "writebackMode",
7101
7290
  "idleReviewMs",
7291
+ "idleReview",
7102
7292
  "displayMode",
7103
7293
  "tabEnabled",
7104
7294
  "writeEnabled",
@@ -7655,7 +7845,7 @@ function registerTools(ctx, runtimeSource, coordinator) {
7655
7845
  },
7656
7846
  old_text: {
7657
7847
  type: "string",
7658
- description: "Unique substring of the existing entry. Required for replace and remove."
7848
+ description: "Full content of the existing entry, or a unique substring. A unique full-content match takes precedence within the selected target. Required for replace and remove."
7659
7849
  },
7660
7850
  importance: {
7661
7851
  type: "string",
@@ -7705,7 +7895,7 @@ function registerTools(ctx, runtimeSource, coordinator) {
7705
7895
  }));
7706
7896
  ctx.tools.register(definition({
7707
7897
  name: "mnemon_remember",
7708
- description: "Archive one durable insight in a selected provider-backed Memory Space. Ordinary new hot memory belongs in mnemon_runtime_memory; use direct archival only for explicit long-term persistence or runtime capacity migration. Choose the narrowest existing space, search it first, verify capabilities.remember=true, and wait for the provider receipt. OpenViking writes are asynchronous semantic extraction and may truthfully return skipped. Do not dump transcripts, temporary progress, routine observations, or repository-obvious facts.",
7898
+ description: "Archive one durable insight in a selected provider-backed Memory Space. Ordinary new hot memory belongs in mnemon_runtime_memory; use direct archival only for explicit long-term persistence or runtime capacity migration. Choose the narrowest existing space, search it first, verify capabilities.remember=true, and wait for the provider receipt. Exact writes require confirmed persistence; providers using semantic extraction may truthfully return skipped. Do not dump transcripts, temporary progress, routine observations, or repository-obvious facts.",
7709
7899
  parameters: {
7710
7900
  type: "object",
7711
7901
  properties: {