dsh-mnemon 0.5.36 → 0.5.38

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
@@ -1,5 +1,5 @@
1
1
  import { f as canonicalWorkspacePath, m as withMemoryStorageLock, n as memoryStrategyConfigurationValues, p as workspaceStorageId, r as readMemoryStrategyConfiguration, s as receipt } from "./strategy-configuration-CKP2kRss.js";
2
- import { a as validateMemoryPluginGraph, i as captureMemoryContributionSnapshot, n as isMemoryCompositionUnavailable, o as prepareMemoryContributions, r as MemoryCompositionGeneration, t as provideMemoryRuntime } from "./runtime-zuuBe8x1.js";
2
+ import { a as validateMemoryPluginGraph, i as captureMemoryContributionSnapshot, n as isMemoryCompositionUnavailable, o as prepareMemoryContributions, r as MemoryCompositionGeneration, t as provideMemoryRuntime } from "./runtime-0u6dOiF9.js";
3
3
  import { DEFAULT_MEMORY_VIEW_BUDGET, MEMORY_CAPABILITIES } from "./contracts.js";
4
4
  import { r as defineMemoryPlugin, t as canonicalMemoryJson } from "./definitions-DU7MLBhu.js";
5
5
  import { Context } from "@deepseek-ai/cordis";
@@ -9,7 +9,7 @@ import { resolveEmbedding, resolvePersistenceStrategy, resolveRecallQuality } fr
9
9
  import { CATEGORIES, DEFAULT_EMBEDDING_ENDPOINT, DEFAULT_EMBEDDING_MODEL, DEFAULT_EMBEDDING_PROTOCOL, EDGE_TYPES, MEMORY_PLACEMENT_CAPABILITIES, MEMORY_PROVIDER_REGISTRY_FILE, MEMORY_SPACES_DIRECTORY, MEMORY_SPACE_DATABASE_FILE, MEMORY_SPACE_ID_PATTERN, MEMORY_SPACE_NAME_MAX_LENGTH, MEMORY_SPACE_REGISTRY_FILE, MNEMON_ACTIVE_STORE_FILE, MNEMON_EMBEDDING_PROTOCOLS, SOURCES, parseMemorySpaceRegistry, summarizeMemoryProviderRegistry, summarizeMemorySpaceRegistry } from "dsh-mnemon-source-memory-spaces/contracts";
10
10
  import { RUNTIME_ARCHIVE_DIRECTORY, RUNTIME_DIRECTORY, RUNTIME_ENTRY_DELIMITER, RUNTIME_ENTRY_MAX_BYTES, RUNTIME_LOCK_FILE, RUNTIME_LOCK_STALE_MS, RUNTIME_MEMORIES_FILE, RUNTIME_MEMORY_LIMITS, RUNTIME_MEMORY_LIMIT_MAX_BYTES, RUNTIME_MEMORY_VERSION, RUNTIME_PROJECTION_FILES, parseRuntimeBranches, runtimeBytes, runtimeProjection, summarizeRuntimeMemories } from "dsh-mnemon-source-runtime/contracts";
11
11
  import { createHash, randomUUID } from "node:crypto";
12
- import { accessSync, chmodSync, closeSync, constants, copyFileSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
12
+ import fs, { accessSync, chmodSync, closeSync, constants, copyFileSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
13
13
  import { homedir, hostname } from "node:os";
14
14
  import { strToU8, unzipSync, zipSync } from "fflate";
15
15
  import { DOCUMENTS_ACTIVE_LIMIT_BYTES, DOCUMENTS_DIRECTORY, DOCUMENTS_INDEX_FILE, DOCUMENTS_LOCK_FILE, DOCUMENTS_LOCK_STALE_MS, DOCUMENT_FILENAME_PATTERN, parseDocumentIndex, summarizeDocumentIndex } from "dsh-mnemon-source-documents/contracts";
@@ -374,6 +374,7 @@ function isWorkspaceStorageScope(scope) {
374
374
  const DEFAULT_IDLE_REVIEW = {
375
375
  enabled: true,
376
376
  runtimeMemory: true,
377
+ persistWatermark: false,
377
378
  provider: "spawn",
378
379
  fallback: "spawn",
379
380
  agentTeams: "pause",
@@ -535,6 +536,7 @@ const RuntimeMemorySchema = z.object({
535
536
  const IdleReviewSchema = z.object({
536
537
  enabled: z.boolean().default(DEFAULT_IDLE_REVIEW.enabled),
537
538
  runtimeMemory: z.boolean().default(DEFAULT_IDLE_REVIEW.runtimeMemory),
539
+ persistWatermark: z.boolean().default(DEFAULT_IDLE_REVIEW.persistWatermark),
538
540
  provider: z.union(["spawn", "fork"]).default(DEFAULT_IDLE_REVIEW.provider),
539
541
  fallback: z.union(["spawn", "skip"]).default(DEFAULT_IDLE_REVIEW.fallback),
540
542
  agentTeams: z.union(["pause", "scoped"]).default(DEFAULT_IDLE_REVIEW.agentTeams),
@@ -1257,7 +1259,7 @@ const LOADED_VERSION = manifest$1(PACKAGE_MANIFEST_PATH)?.version;
1257
1259
  function pluginPackageVersion() {
1258
1260
  return LOADED_VERSION ?? "0.0.0";
1259
1261
  }
1260
- const CHECK_TIMEOUT_MS = 1e4;
1262
+ const CHECK_TIMEOUT_MS$1 = 1e4;
1261
1263
  const UPDATE_TIMEOUT_MS = 6e5;
1262
1264
  /** A check waits this long for an update that is finishing, within the browser's 15 s check deadline. */
1263
1265
  const IN_FLIGHT_WAIT_MS = 3e3;
@@ -1353,7 +1355,7 @@ async function fetchJson(url) {
1353
1355
  const controller = new AbortController();
1354
1356
  const timeout = setTimeout(() => {
1355
1357
  controller.abort();
1356
- }, CHECK_TIMEOUT_MS);
1358
+ }, CHECK_TIMEOUT_MS$1);
1357
1359
  try {
1358
1360
  const response = await fetch(url, {
1359
1361
  signal: controller.signal,
@@ -1621,7 +1623,7 @@ var VersionUpdateManager = class {
1621
1623
  let current;
1622
1624
  try {
1623
1625
  const invocation = resolveMnemonInvocation(command, ["--version"]);
1624
- current = versionFrom((await resultOrThrow(this.processRunner, invocation.command, invocation.args, CHECK_TIMEOUT_MS, { env: invocation.env })).stdout);
1626
+ current = versionFrom((await resultOrThrow(this.processRunner, invocation.command, invocation.args, CHECK_TIMEOUT_MS$1, { env: invocation.env })).stdout);
1625
1627
  } catch {
1626
1628
  return { install: {
1627
1629
  mode: launcher === void 0 ? "manual" : "npm",
@@ -1642,7 +1644,7 @@ var VersionUpdateManager = class {
1642
1644
  ...npm.args,
1643
1645
  "root",
1644
1646
  "--global"
1645
- ], CHECK_TIMEOUT_MS, { env: npm.env })).stdout.trim();
1647
+ ], CHECK_TIMEOUT_MS$1, { env: npm.env })).stdout.trim();
1646
1648
  if (isAbsolute(globalRoot) && samePath(dirname(dirname(launcher)), join(globalRoot, MNEMON_NPM_PACKAGE))) {
1647
1649
  install.hint = "npm";
1648
1650
  install.updateCommand = process.execPath;
@@ -1703,7 +1705,7 @@ var VersionUpdateManager = class {
1703
1705
  "GOARCH",
1704
1706
  "GOHOSTOS",
1705
1707
  "GOHOSTARCH"
1706
- ], CHECK_TIMEOUT_MS)).stdout);
1708
+ ], CHECK_TIMEOUT_MS$1)).stdout);
1707
1709
  const goPath = typeof environment.GOPATH === "string" ? environment.GOPATH.split(delimiter)[0] : void 0;
1708
1710
  const bin = typeof environment.GOBIN === "string" && environment.GOBIN !== "" ? environment.GOBIN : goPath === void 0 || goPath === "" ? void 0 : join(goPath, "bin");
1709
1711
  if (bin === void 0 || !isAbsolute(bin) || typeof environment.GOOS !== "string" || environment.GOOS !== environment.GOHOSTOS || typeof environment.GOARCH !== "string" || environment.GOARCH !== environment.GOHOSTARCH || realpathSync(join(bin, process.platform === "win32" ? "mnemon.exe" : "mnemon")) !== realCommand) return {
@@ -1717,7 +1719,7 @@ var VersionUpdateManager = class {
1717
1719
  "version",
1718
1720
  "-m",
1719
1721
  command
1720
- ], CHECK_TIMEOUT_MS)).stdout.split(/\r?\n/).map((line) => line.trim().split(/\s+/)).find((fields) => fields[0] === "path")?.[1] === MNEMON_MODULE) return {
1722
+ ], CHECK_TIMEOUT_MS$1)).stdout.split(/\r?\n/).map((line) => line.trim().split(/\s+/)).find((fields) => fields[0] === "path")?.[1] === MNEMON_MODULE) return {
1721
1723
  ...current === void 0 ? {} : { current },
1722
1724
  install: {
1723
1725
  mode: "go",
@@ -3994,7 +3996,7 @@ function shapeOf(answer) {
3994
3996
  const listed = keys.length > names.length ? [...names, String(keys.length - names.length) + " other"] : names;
3995
3997
  return "HTTP " + String(answer.status) + ", content-type " + (answer.type ?? "none") + ", fields: " + (listed.length === 0 ? "none" : listed.join(", "));
3996
3998
  }
3997
- function failure$2(action, status, body) {
3999
+ function failure$3(action, status, body) {
3998
4000
  const code = codeOf(body);
3999
4001
  const message = code === void 0 ? action + " failed with HTTP " + String(status) : action + " failed: " + code;
4000
4002
  return status === 401 ? new MnemonSyncError("github-unauthorized", message) : new Error(message);
@@ -4135,7 +4137,7 @@ var MnemonGitHubAuth = class {
4135
4137
  } catch (error) {
4136
4138
  throw new Error("the GitHub device code request could not reach GitHub: " + describeTransportError(error));
4137
4139
  }
4138
- if (answer.status !== 200) throw failure$2("the GitHub device code request", answer.status, answer.body);
4140
+ if (answer.status !== 200) throw failure$3("the GitHub device code request", answer.status, answer.body);
4139
4141
  const fields = record$6(answer.body);
4140
4142
  const deviceCode = text$1(fields?.device_code);
4141
4143
  const userCode = text$1(fields?.user_code);
@@ -4283,7 +4285,7 @@ var MnemonGitHubAuth = class {
4283
4285
  headers: this.authorization(token),
4284
4286
  ...signal === void 0 ? {} : { signal }
4285
4287
  }, "listing GitHub repositories");
4286
- if (status !== 200) throw failure$2("listing GitHub repositories", status, body);
4288
+ if (status !== 200) throw failure$3("listing GitHub repositories", status, body);
4287
4289
  if (!Array.isArray(body)) throw new Error("the GitHub repository answer was not a list");
4288
4290
  for (const entry of body) {
4289
4291
  const repository = repositoryOf(entry);
@@ -4314,7 +4316,7 @@ var MnemonGitHubAuth = class {
4314
4316
  }),
4315
4317
  ...signal === void 0 ? {} : { signal }
4316
4318
  }, "creating the GitHub repository");
4317
- if (status !== 201) throw failure$2("creating the GitHub repository", status, body);
4319
+ if (status !== 201) throw failure$3("creating the GitHub repository", status, body);
4318
4320
  const repository = repositoryOf(body);
4319
4321
  if (repository === void 0) throw new Error("the created GitHub repository could not be read back");
4320
4322
  return repository;
@@ -7490,6 +7492,7 @@ var LiveMnemonRuntime = class {
7490
7492
  githubAuth;
7491
7493
  autoBackupView;
7492
7494
  closed = false;
7495
+ changeListeners = /* @__PURE__ */ new Set();
7493
7496
  config;
7494
7497
  storage;
7495
7498
  packs;
@@ -7531,6 +7534,12 @@ var LiveMnemonRuntime = class {
7531
7534
  applyAutoBackup(graph) {
7532
7535
  if (this.autoBackupView !== void 0) graph.sync.useAutoBackup(this.autoBackupView);
7533
7536
  }
7537
+ onChanged(listener) {
7538
+ this.changeListeners.add(listener);
7539
+ return () => {
7540
+ this.changeListeners.delete(listener);
7541
+ };
7542
+ }
7534
7543
  swap(next) {
7535
7544
  if (this.closed) {
7536
7545
  next.dispose();
@@ -7544,6 +7553,7 @@ var LiveMnemonRuntime = class {
7544
7553
  this.retireGraph(previous);
7545
7554
  for (const graph of this.workspaceGraphs.values()) this.retireGraph(graph);
7546
7555
  this.workspaceGraphs.clear();
7556
+ for (const listener of this.changeListeners) listener();
7547
7557
  }
7548
7558
  snapshot() {
7549
7559
  this.assertOpen();
@@ -7568,6 +7578,7 @@ var LiveMnemonRuntime = class {
7568
7578
  dispose() {
7569
7579
  if (this.closed) return;
7570
7580
  this.executions.dispose();
7581
+ this.changeListeners.clear();
7571
7582
  this.closed = true;
7572
7583
  const graphs = /* @__PURE__ */ new Set([
7573
7584
  this.current,
@@ -10158,6 +10169,108 @@ async function assertAutomaticMemoryWrite(host, parent) {
10158
10169
  return host.writableSourceFor(parent, "memory-spaces", "remember");
10159
10170
  }
10160
10171
  //#endregion
10172
+ //#region src/host/review-admission.ts
10173
+ const RESULT_TOOL = "mnemon_subagent_result";
10174
+ const CHECK_TIMEOUT_MS = 5e3;
10175
+ /** A no-op challenge through the same mandatory completion channel as review.
10176
+ * Inventory alone cannot see execution-time guards. No errors are classified as
10177
+ * permission failures, and an unavailable checkpoint is never a permanent ban.
10178
+ */
10179
+ var AutomaticReviewAdmission = class {
10180
+ challenges = /* @__PURE__ */ new Map();
10181
+ controllers = /* @__PURE__ */ new Set();
10182
+ draining = /* @__PURE__ */ new WeakSet();
10183
+ disposed = false;
10184
+ /** Only an unexpired, host-issued challenge for this exact Agent can no-op. */
10185
+ acknowledge(args, execution) {
10186
+ if (typeof args !== "object" || args === null || !("requestId" in args) || typeof args.requestId !== "string") return false;
10187
+ const challenge = this.challenges.get(args.requestId);
10188
+ if (challenge === void 0 || challenge.agent !== execution.agent || challenge.execution !== void 0) return false;
10189
+ execution.signal.throwIfAborted();
10190
+ challenge.execution = execution;
10191
+ return true;
10192
+ }
10193
+ dispose() {
10194
+ this.disposed = true;
10195
+ this.challenges.clear();
10196
+ for (const controller of this.controllers) controller.abort();
10197
+ }
10198
+ async check(agent, signal) {
10199
+ signal.throwIfAborted();
10200
+ const tools = agent.ctx.tools;
10201
+ if (this.disposed || tools?.execute === void 0 || this.draining.has(agent)) return false;
10202
+ const controller = new AbortController();
10203
+ this.controllers.add(controller);
10204
+ const combined = AbortSignal.any([signal, controller.signal]);
10205
+ const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
10206
+ timeout.unref?.();
10207
+ let rejectAbort;
10208
+ const aborted = new Promise((_resolve, reject) => {
10209
+ rejectAbort = () => reject(combined.reason);
10210
+ combined.addEventListener("abort", rejectAbort, { once: true });
10211
+ });
10212
+ const attempt = async (codeMode) => {
10213
+ combined.throwIfAborted();
10214
+ const requestId = randomUUID();
10215
+ const callId = randomUUID();
10216
+ const challenge = {
10217
+ agent,
10218
+ accepted: false
10219
+ };
10220
+ this.challenges.set(requestId, challenge);
10221
+ const stops = [];
10222
+ let executing = false;
10223
+ const cleanup = () => {
10224
+ for (const stop of stops.reverse()) stop();
10225
+ this.draining.delete(agent);
10226
+ };
10227
+ try {
10228
+ stops.push(agent.ctx.on("tools/result", ((execution, result) => {
10229
+ if (execution === challenge.execution) challenge.accepted = result.isError === false;
10230
+ })));
10231
+ stops.push(agent.ctx.on("tools/pre-execute", (async (execution, next) => {
10232
+ const decision = await next();
10233
+ return execution.agent === agent && (execution.callId === callId || execution.rootCallId === callId) && decision.kind === "ask" ? { kind: "cancel" } : decision;
10234
+ }), { prepend: true }));
10235
+ const args = {
10236
+ requestId,
10237
+ result: {}
10238
+ };
10239
+ const pending = tools.execute({
10240
+ callId,
10241
+ agent,
10242
+ signal: combined,
10243
+ name: codeMode ? "run_code" : RESULT_TOOL,
10244
+ arguments: codeMode ? {
10245
+ description: "Check automatic memory review availability",
10246
+ code: `await tools.mnemon_subagent_result(${JSON.stringify(args)})`
10247
+ } : args
10248
+ });
10249
+ executing = true;
10250
+ this.draining.add(agent);
10251
+ const settled = pending.finally(cleanup);
10252
+ const outcome = await Promise.race([settled, aborted]);
10253
+ combined.throwIfAborted();
10254
+ return challenge.execution !== void 0 && challenge.accepted && outcome.isError === false;
10255
+ } finally {
10256
+ this.challenges.delete(requestId);
10257
+ if (!executing) cleanup();
10258
+ }
10259
+ };
10260
+ try {
10261
+ if (await attempt(false)) return true;
10262
+ return tools.get?.("run_code", agent) !== void 0 && await attempt(true);
10263
+ } catch {
10264
+ signal.throwIfAborted();
10265
+ return false;
10266
+ } finally {
10267
+ clearTimeout(timeout);
10268
+ if (rejectAbort !== void 0) combined.removeEventListener("abort", rejectAbort);
10269
+ this.controllers.delete(controller);
10270
+ }
10271
+ }
10272
+ };
10273
+ //#endregion
10161
10274
  //#region src/host/subagent.ts
10162
10275
  function evidenceInsights(evidence) {
10163
10276
  return evidence.items.map((item) => {
@@ -10343,6 +10456,7 @@ var MnemonSubagentCoordinator = class {
10343
10456
  /** Delegated children whose run has ended, newest last, at most FINISHED_CHILDREN. */
10344
10457
  finishedChildren = /* @__PURE__ */ new Set();
10345
10458
  disposed = false;
10459
+ reviewAdmission = new AutomaticReviewAdmission();
10346
10460
  constructor(subagents, runtimeSource, resultRuntime, taskAgentModelResolver, runtimeMaintenanceMaxTokensResolver, runtimeMaintenanceTaskRunner) {
10347
10461
  this.subagents = subagents;
10348
10462
  this.runtimeSource = runtimeSource;
@@ -10363,6 +10477,7 @@ var MnemonSubagentCoordinator = class {
10363
10477
  }]
10364
10478
  },
10365
10479
  execute: async (args, execution) => {
10480
+ if (this.reviewAdmission.acknowledge(args, execution)) return { recorded: true };
10366
10481
  if (!isSubagent(execution.agent)) throw new Error("Mnemon subagent result tools are restricted to delegated children");
10367
10482
  execution.signal.throwIfAborted();
10368
10483
  assertDshOutputValue(RESULT_TOOL_INPUT_SCHEMA, args);
@@ -10377,6 +10492,7 @@ var MnemonSubagentCoordinator = class {
10377
10492
  }
10378
10493
  dispose() {
10379
10494
  this.disposed = true;
10495
+ this.reviewAdmission.dispose();
10380
10496
  this.resultRequests.clear();
10381
10497
  const resumeGuard = this.disposeResumeGuard;
10382
10498
  this.disposeResumeGuard = void 0;
@@ -10841,8 +10957,8 @@ var MnemonSubagentCoordinator = class {
10841
10957
  * One tool-less structured run for skill evolution: a review, a draft, a
10842
10958
  * backtest step or a case derivation. The child sees only the prompt.
10843
10959
  */
10844
- async skillTask(parent, label, prompt, schema, persona, signal) {
10845
- const { provider, runId, result } = await this.delegate(parent, "skill", label, prompt, [], schema, signal, "spawn", persona);
10960
+ async skillTask(parent, label, prompt, schema, persona, signal, route) {
10961
+ const { provider, runId, result } = await this.delegate(parent, "skill", label, prompt, [], schema, signal, "spawn", persona, void 0, void 0, route);
10846
10962
  return {
10847
10963
  provider,
10848
10964
  runId,
@@ -10922,7 +11038,28 @@ ${naturalRequest(request, target === void 0 ? REQUEST_LABELS : TARGETED_WRITE_LA
10922
11038
  throw new Error("The Memory Space directory could not be read, so nothing was saved: " + (error instanceof Error ? error.message : String(error)), { cause: error });
10923
11039
  }
10924
11040
  }
10925
- async review(parent, signal) {
11041
+ /** Cheap scheduler admission: no model task when every review write target is unavailable. */
11042
+ canReview(parent) {
11043
+ return this.reviewTools(parent).some((tool) => tool !== "mnemon_document_search");
11044
+ }
11045
+ /** Automatic review admission; explicit user-requested reviews are unchanged. */
11046
+ checkReviewAccess(parent, signal) {
11047
+ return this.reviewAdmission.check(parent, signal);
11048
+ }
11049
+ availableTools(parent, tools) {
11050
+ const registry = parent.ctx?.tools;
11051
+ return typeof registry?.get === "function" ? tools.filter((name) => registry.get(name, parent) !== void 0) : [...tools];
11052
+ }
11053
+ reviewTools(parent) {
11054
+ const config = this.runtimeSource.config;
11055
+ if (!config.writeEnabled) return [];
11056
+ return this.availableTools(parent, REVIEW_TOOLS).filter((tool) => {
11057
+ if (tool === "mnemon_document_search") return true;
11058
+ const type = memoryTool(tool).sourceTypeId;
11059
+ return (tool !== "mnemon_runtime_memory" || config.idleReview.runtimeMemory) && allowsParticipation(config, type, "write", "automatic");
11060
+ });
11061
+ }
11062
+ async review(parent, signal, admission) {
10926
11063
  const config = this.runtimeSource.config.idleReview;
10927
11064
  if (idleReviewBlockReason(parent, config.agentTeams) !== void 0) return {
10928
11065
  delegated: false,
@@ -10930,6 +11067,13 @@ ${naturalRequest(request, target === void 0 ? REQUEST_LABELS : TARGETED_WRITE_LA
10930
11067
  summary: "Idle review is paused by the Agent Teams compatibility setting.",
10931
11068
  documentIds: []
10932
11069
  };
11070
+ const skipped = {
11071
+ delegated: false,
11072
+ action: "skipped",
11073
+ summary: "Idle review has no applicable writable Source target.",
11074
+ documentIds: []
11075
+ };
11076
+ if (!this.canReview(parent)) return skipped;
10933
11077
  let preferred = config.provider;
10934
11078
  if (preferred === "fork") try {
10935
11079
  this.provider("fork");
@@ -10942,24 +11086,40 @@ ${naturalRequest(request, target === void 0 ? REQUEST_LABELS : TARGETED_WRITE_LA
10942
11086
  };
10943
11087
  preferred = "spawn";
10944
11088
  }
10945
- const prompt = preferred === "fork" ? "Review the inherited completed checkpoint now." : reviewCheckpoint(parent.session, config.maxContextChars);
10946
- const base = reviewPersona(config.runtimeMemory);
10947
- const persona = preferred === "fork" ? base : base.replaceAll("inherited", "supplied bounded");
10948
- const tools = config.runtimeMemory ? REVIEW_TOOLS : REVIEW_TOOLS.filter((tool) => tool !== "mnemon_runtime_memory");
10949
- const writers = tools.filter((tool) => tool !== "mnemon_document_search");
10950
- const { provider, runId, result } = await this.delegate(parent, "review", "Mnemon idle checkpoint review", prompt, tools, WRITE_SCHEMA, signal, preferred, persona, { terminalTools: writers });
10951
- const value = object$6(result.structured);
10952
- return {
10953
- delegated: true,
10954
- runId,
10955
- provider,
10956
- summary: typeof value.summary === "string" ? value.summary : "",
10957
- action: typeof value.action === "string" ? value.action : "failed",
10958
- memoryBodyIds: strings(value.memoryBodyIds),
10959
- documentIds: strings(value.documentIds)
10960
- };
11089
+ const execution = await this.runtimeSource.executions.workflow(parent, "review", signal);
11090
+ try {
11091
+ if (admission !== void 0) {
11092
+ execution.signal.throwIfAborted();
11093
+ admission();
11094
+ }
11095
+ const view = execution.context.view;
11096
+ const instances = execution.graph.memoryComposition.generation(view.runtimeGeneration).sourceInstances();
11097
+ const tools = this.reviewTools(parent).filter((tool) => {
11098
+ if (tool === "mnemon_document_search") return true;
11099
+ const entry = memoryTool(tool);
11100
+ return view.actionOffers.some((offer) => offer.authority === void 0 && offer.sourceActionId === entry.operation && instances.some((instance) => instance.sourceInstanceKey === offer.sourceInstanceKey && instance.sourceTypeId === entry.sourceTypeId));
11101
+ });
11102
+ const writers = tools.filter((tool) => tool !== "mnemon_document_search");
11103
+ if (writers.length === 0) return skipped;
11104
+ const prompt = preferred === "fork" ? "Review the inherited completed checkpoint now." : reviewCheckpoint(parent.session, config.maxContextChars);
11105
+ const base = reviewPersona(tools.includes("mnemon_runtime_memory"));
11106
+ const persona = preferred === "fork" ? base : base.replaceAll("inherited", "supplied bounded");
11107
+ const { provider, runId, result } = await this.delegate(parent, "review", "Mnemon idle checkpoint review", prompt, tools, WRITE_SCHEMA, execution.signal, preferred, persona, { terminalTools: writers }, void 0, void 0, admission);
11108
+ const value = object$6(result.structured);
11109
+ return {
11110
+ delegated: true,
11111
+ runId,
11112
+ provider,
11113
+ summary: typeof value.summary === "string" ? value.summary : "",
11114
+ action: typeof value.action === "string" ? value.action : "failed",
11115
+ memoryBodyIds: strings(value.memoryBodyIds),
11116
+ documentIds: strings(value.documentIds)
11117
+ };
11118
+ } finally {
11119
+ execution.release();
11120
+ }
10961
11121
  }
10962
- async delegate(parent, operation, label, prompt, tools, outputSchema, signal, preferredProvider = "spawn", persona = WRITE_PERSONA, recovery, guard) {
11122
+ async delegate(parent, operation, label, prompt, tools, outputSchema, signal, preferredProvider = "spawn", persona = WRITE_PERSONA, recovery, guard, route, admission) {
10963
11123
  if (this.disposed) throw new Error("dsh-mnemon subagent coordinator is disposed");
10964
11124
  const provider = this.provider(preferredProvider);
10965
11125
  assertDshOutputSchema(outputSchema);
@@ -11068,7 +11228,7 @@ Completion protocol: call \`${resultToolName}\` exactly once with requestId \`${
11068
11228
  ${JSON.stringify(outputSchema)}
11069
11229
  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.`;
11070
11230
  const perOpMaxTokens = operation === "migration" || operation === "compaction" || operation === "reconcile" ? this.runtimeMaintenanceMaxTokensResolver?.() ?? 8192 : operation === "document-archive" ? 8192 : operation === "metadata-maintenance" ? 4096 : operation === "review" ? this.runtimeSource.config.idleReview.maxTokens : operation === "skill" ? 16384 : void 0;
11071
- const fixed = this.taskAgentModelResolver?.();
11231
+ const fixed = route ?? this.taskAgentModelResolver?.();
11072
11232
  const baseAgentOptions = perOpMaxTokens === void 0 ? void 0 : { maxTokens: perOpMaxTokens };
11073
11233
  const resolvedAgentOptions = fixed === void 0 ? baseAgentOptions : {
11074
11234
  ...baseAgentOptions ?? {},
@@ -11076,19 +11236,25 @@ This is the only completion channel for this run. Do not finish with a plain-tex
11076
11236
  model: fixed.model
11077
11237
  };
11078
11238
  const host = this.resultRuntime;
11079
- const start = () => startWithContinuationTurns(host, parent, () => startSubagent(this.subagents, host.agents, provider, {
11080
- label,
11081
- prompt: [{
11082
- type: "text",
11083
- text: `${delegatedTaskLine(operation)}\n${prompt}`
11084
- }],
11085
- parent,
11086
- signal,
11087
- ...resolvedAgentOptions === void 0 ? {} : { agentOptions: resolvedAgentOptions },
11088
- maxDepth: 1,
11089
- toolFilter: { allow: [...tools, resultToolName] },
11090
- persona: completionPersona
11091
- }, host));
11239
+ const start = () => startWithContinuationTurns(host, parent, () => {
11240
+ if (operation === "review") {
11241
+ signal.throwIfAborted();
11242
+ admission?.();
11243
+ }
11244
+ return startSubagent(this.subagents, host.agents, provider, {
11245
+ label,
11246
+ prompt: [{
11247
+ type: "text",
11248
+ text: `${delegatedTaskLine(operation)}\n${prompt}`
11249
+ }],
11250
+ parent,
11251
+ signal,
11252
+ ...resolvedAgentOptions === void 0 ? {} : { agentOptions: resolvedAgentOptions },
11253
+ maxDepth: 1,
11254
+ toolFilter: { allow: [...this.availableTools(parent, tools), resultToolName] },
11255
+ persona: completionPersona
11256
+ }, host);
11257
+ });
11092
11258
  run = operation === "review" ? await startGuardedReview(this.resultRuntime, parent, [...tools, resultToolName], start, (child) => {
11093
11259
  reviewChildId = child.id;
11094
11260
  }, reviewLayerPolicy()) : guard !== void 0 ? await startGuardedChild(this.resultRuntime, parent, start, guard, {
@@ -11765,6 +11931,157 @@ var AgentMemoryTurn = class {
11765
11931
  }
11766
11932
  };
11767
11933
  //#endregion
11934
+ //#region src/host/idle-review-watermark.ts
11935
+ const MAX_STATE_BYTES = 16384;
11936
+ const MAX_TIMESTAMP = 864e13;
11937
+ function validAttempts(value) {
11938
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
11939
+ }
11940
+ function validTimestamp(value) {
11941
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= MAX_TIMESTAMP;
11942
+ }
11943
+ /** Messages deliberately omit filesystem paths, session ids and raw input. */
11944
+ function failure$2(error) {
11945
+ const code = error?.code;
11946
+ const detail = code === "EBUSY" ? " (busy)" : typeof code === "string" && /^[A-Z_]+$/u.test(code) ? ` (${code})` : "";
11947
+ return new Error(`Idle review watermark unavailable${detail}. Automatic review is paused. Stop Hosts using this storage root, repair this session's state or lock under state/idle-review, then reopen the session.`, { cause: error });
11948
+ }
11949
+ /** Host-local admission state, deliberately outside Packs and Git sync. */
11950
+ var IdleReviewWatermarkStore = class {
11951
+ sessionId;
11952
+ directory;
11953
+ path;
11954
+ constructor(directory, sessionId) {
11955
+ this.sessionId = sessionId;
11956
+ if (!sessionId || Buffer.byteLength(sessionId) > 4096 || Buffer.byteLength(JSON.stringify(sessionId)) > 16128) throw failure$2(void 0);
11957
+ this.directory = join(resolve(directory), "state", "idle-review");
11958
+ this.path = join(this.directory, createHash("sha256").update(sessionId).digest("hex") + ".json");
11959
+ }
11960
+ load() {
11961
+ let descriptor;
11962
+ try {
11963
+ try {
11964
+ descriptor = fs.openSync(this.path, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
11965
+ } catch (error) {
11966
+ if (error.code === "ENOENT") return {
11967
+ version: 1,
11968
+ sessionId: this.sessionId,
11969
+ attempts: 0,
11970
+ lastReviewAttemptAt: null
11971
+ };
11972
+ throw error;
11973
+ }
11974
+ const info = fs.fstatSync(descriptor);
11975
+ if (!info.isFile() || info.size > MAX_STATE_BYTES) throw new Error("invalid state file");
11976
+ const bytes = Buffer.alloc(16385);
11977
+ let length = 0;
11978
+ while (length < bytes.length) {
11979
+ const count = fs.readSync(descriptor, bytes, length, bytes.length - length, null);
11980
+ if (count === 0) break;
11981
+ length += count;
11982
+ }
11983
+ if (length > MAX_STATE_BYTES) throw new Error("oversized state");
11984
+ const value = JSON.parse(bytes.subarray(0, length).toString("utf8"));
11985
+ if (value === null || typeof value !== "object") throw new Error("invalid state");
11986
+ const state = value;
11987
+ if (state.version !== 1 || state.sessionId !== this.sessionId || !validAttempts(state.attempts) || (state.attempts === 0 ? state.lastReviewAttemptAt !== null : !validTimestamp(state.lastReviewAttemptAt))) throw new Error("invalid state");
11988
+ return {
11989
+ version: 1,
11990
+ sessionId: this.sessionId,
11991
+ attempts: state.attempts,
11992
+ lastReviewAttemptAt: state.lastReviewAttemptAt
11993
+ };
11994
+ } catch (error) {
11995
+ throw failure$2(error);
11996
+ } finally {
11997
+ if (descriptor !== void 0) try {
11998
+ fs.closeSync(descriptor);
11999
+ } catch (error) {
12000
+ throw failure$2(error);
12001
+ }
12002
+ }
12003
+ }
12004
+ /** Preserve already-used in-process quota on opt-in without admitting work. */
12005
+ restore(floor) {
12006
+ if (floor === void 0 || floor.attempts === 0 && floor.lastReviewAttemptAt === null) return this.load();
12007
+ return this.withLock(() => {
12008
+ const saved = this.load();
12009
+ const merged = this.merge(saved, floor);
12010
+ if (merged.attempts !== saved.attempts || merged.lastReviewAttemptAt !== saved.lastReviewAttemptAt) this.write(merged);
12011
+ return merged;
12012
+ });
12013
+ }
12014
+ reserve(policy) {
12015
+ if (!validTimestamp(policy.now) || !validAttempts(policy.maxPerSession) || !validAttempts(policy.minIntervalMs)) throw failure$2(void 0);
12016
+ return this.withLock(() => {
12017
+ const saved = this.merge(this.load(), policy.floor);
12018
+ if (saved.attempts >= policy.maxPerSession || policy.now - (saved.lastReviewAttemptAt ?? -Infinity) < policy.minIntervalMs) return void 0;
12019
+ const next = {
12020
+ version: 1,
12021
+ sessionId: this.sessionId,
12022
+ attempts: saved.attempts + 1,
12023
+ lastReviewAttemptAt: policy.now
12024
+ };
12025
+ this.write(next);
12026
+ return next;
12027
+ });
12028
+ }
12029
+ merge(saved, floor) {
12030
+ if (floor === void 0) return saved;
12031
+ if (!validAttempts(floor.attempts) || (floor.attempts === 0 ? floor.lastReviewAttemptAt !== null : !validTimestamp(floor.lastReviewAttemptAt))) throw failure$2(void 0);
12032
+ const previous = Math.max(saved.lastReviewAttemptAt ?? -Infinity, floor.lastReviewAttemptAt ?? -Infinity);
12033
+ return {
12034
+ version: 1,
12035
+ sessionId: this.sessionId,
12036
+ attempts: Math.max(saved.attempts, floor.attempts),
12037
+ lastReviewAttemptAt: previous === -Infinity ? null : previous
12038
+ };
12039
+ }
12040
+ write(state) {
12041
+ const temporary = this.path + "." + randomUUID() + ".tmp";
12042
+ try {
12043
+ fs.writeFileSync(temporary, JSON.stringify(state) + "\n", {
12044
+ mode: 384,
12045
+ flag: "wx"
12046
+ });
12047
+ fs.renameSync(temporary, this.path);
12048
+ } finally {
12049
+ fs.rmSync(temporary, { force: true });
12050
+ }
12051
+ }
12052
+ /**
12053
+ * One bounded synchronous read/check/write critical section. Exclusive mkdir
12054
+ * protects independent Hosts too; contention fails closed without stale-lock
12055
+ * takeover. Never hold this lock while starting or awaiting model work.
12056
+ */
12057
+ withLock(operation) {
12058
+ const lock = this.path + ".lock";
12059
+ let held = false;
12060
+ try {
12061
+ fs.mkdirSync(this.directory, {
12062
+ recursive: true,
12063
+ mode: 448
12064
+ });
12065
+ try {
12066
+ fs.mkdirSync(lock, { mode: 448 });
12067
+ } catch (error) {
12068
+ if (error.code === "EEXIST") throw Object.assign(/* @__PURE__ */ new Error("busy"), { code: "EBUSY" });
12069
+ throw error;
12070
+ }
12071
+ held = true;
12072
+ return operation();
12073
+ } catch (error) {
12074
+ throw failure$2(error);
12075
+ } finally {
12076
+ if (held) try {
12077
+ fs.rmdirSync(lock);
12078
+ } catch (error) {
12079
+ throw failure$2(error);
12080
+ }
12081
+ }
12082
+ }
12083
+ };
12084
+ //#endregion
11768
12085
  //#region src/host/lifecycle.ts
11769
12086
  function modelService(value) {
11770
12087
  if (typeof value !== "object" || value === null || !("currentSelection" in value) || typeof value.currentSelection !== "function") return void 0;
@@ -11858,6 +12175,7 @@ var MnemonAgentLifecycle = class {
11858
12175
  config;
11859
12176
  counters;
11860
12177
  memoryTurn;
12178
+ runtimeDirectory;
11861
12179
  primePending = true;
11862
12180
  startSource;
11863
12181
  guidedTurns = /* @__PURE__ */ new Set();
@@ -11874,10 +12192,21 @@ var MnemonAgentLifecycle = class {
11874
12192
  idleReviewTimer;
11875
12193
  reviewController;
11876
12194
  reviewRunning = false;
12195
+ reviewAccessBlocked = false;
11877
12196
  lastReviewAt;
11878
12197
  lastReviewAction;
11879
12198
  lastReviewScore;
11880
12199
  lastReviewDocumentIds;
12200
+ disposed = false;
12201
+ reviewEpoch = 0;
12202
+ observedReviewPolicy;
12203
+ watermarkEnabled = false;
12204
+ /** Only this Agent's observed usage, attributed to the graph that composed each checkpoint. */
12205
+ reviewUsage = /* @__PURE__ */ new Map();
12206
+ watermarkKey;
12207
+ watermarkStore;
12208
+ watermarkError;
12209
+ restoredAttempts = 0;
11881
12210
  idleReviewAttempts = 0;
11882
12211
  lastReviewAttemptAt;
11883
12212
  lastReviewFailure;
@@ -11885,15 +12214,18 @@ var MnemonAgentLifecycle = class {
11885
12214
  lastAt;
11886
12215
  lastError;
11887
12216
  composedTurn;
11888
- constructor(agent, coordinator, config, counters, source, memoryTurn) {
12217
+ constructor(agent, coordinator, config, counters, source, memoryTurn, runtimeDirectory) {
11889
12218
  this.agent = agent;
11890
12219
  this.coordinator = coordinator;
11891
12220
  this.config = config;
11892
12221
  this.counters = counters;
11893
12222
  this.memoryTurn = memoryTurn;
12223
+ this.runtimeDirectory = runtimeDirectory;
11894
12224
  this.startSource = source;
11895
12225
  }
11896
12226
  start() {
12227
+ this.restoreWatermark();
12228
+ this.observedReviewPolicy = this.reviewPolicy();
11897
12229
  const disposers = [
11898
12230
  this.agent.ctx.on("session/event", ((session, event) => this.sessionEvent(session, event))),
11899
12231
  this.agent.ctx.on("system-prompt/assemble", ((assembly, context, next) => this.assemblePrompt(assembly, context, next))),
@@ -11901,12 +12233,14 @@ var MnemonAgentLifecycle = class {
11901
12233
  this.agent.ctx.on("agent/turn-stopping", ((payload) => this.finishTurn(payload)))
11902
12234
  ];
11903
12235
  return () => {
12236
+ this.disposed = true;
11904
12237
  this.releaseView();
11905
12238
  this.cancelIdleReview(true);
11906
12239
  for (const dispose of disposers.reverse()) dispose();
11907
12240
  };
11908
12241
  }
11909
12242
  snapshot() {
12243
+ this.restoreWatermark();
11910
12244
  return {
11911
12245
  sessionId: this.agent.id,
11912
12246
  status: this.agent.status,
@@ -11918,8 +12252,14 @@ var MnemonAgentLifecycle = class {
11918
12252
  reviewRunning: this.reviewRunning,
11919
12253
  reviewActivity: this.reviewActivity(),
11920
12254
  idleReviewAttempts: this.idleReviewAttempts,
11921
- ...idleReviewBlockReason(this.agent, this.config.idleReview.agentTeams) === void 0 ? {} : { idleReviewBlocked: "agent-team" },
11922
- ...this.lastReviewAttemptAt === void 0 ? {} : { nextReviewAt: new Date(this.lastReviewAttemptAt + this.config.idleReview.minIntervalMs).toISOString() },
12255
+ ...this.config.idleReview.persistWatermark ? { idleReviewWatermark: {
12256
+ state: this.watermarkError === void 0 ? "ready" : "error",
12257
+ restoredAttempts: this.restoredAttempts,
12258
+ maxPerSession: this.config.idleReview.maxPerSession,
12259
+ ...this.watermarkError === void 0 ? {} : { error: this.watermarkError }
12260
+ } } : {},
12261
+ ...idleReviewBlockReason(this.agent, this.config.idleReview.agentTeams) !== void 0 ? { idleReviewBlocked: "agent-team" } : this.reviewAccessBlocked ? { idleReviewBlocked: "tool-access" } : {},
12262
+ ...this.lastReviewAttemptAt === void 0 ? {} : { nextReviewAt: new Date(Math.min(864e13, this.lastReviewAttemptAt + this.config.idleReview.minIntervalMs)).toISOString() },
11923
12263
  ...this.lastReviewFailure === void 0 ? {} : { lastReviewFailure: this.lastReviewFailure },
11924
12264
  lastPhase: this.lastPhase,
11925
12265
  ...this.lastReviewAt === void 0 ? {} : { lastReviewAt: this.lastReviewAt },
@@ -12042,15 +12382,17 @@ var MnemonAgentLifecycle = class {
12042
12382
  scheduleIdleReview(turn) {
12043
12383
  this.cancelIdleReview(true);
12044
12384
  this.recordComposedTurn(turn);
12045
- if (!this.idleReviewAllowed()) return;
12385
+ if (!this.restoreWatermark() || !this.idleReviewAllowed()) return;
12046
12386
  const activity = this.ensureTurnActivity(turn);
12047
12387
  const tools = completedToolActivity(this.agent.session.snapshotEvents(), turn);
12048
12388
  activity.toolCallCount = tools.count;
12049
12389
  activity.toolNames = tools.names;
12050
12390
  if (!this.reviewActivity().eligible || !this.reviewAdmitted(turn)) return;
12391
+ const epoch = this.reviewEpoch;
12392
+ const policy = this.reviewPolicy();
12051
12393
  this.idleReviewTimer = setTimeout(() => {
12052
12394
  this.idleReviewTimer = void 0;
12053
- if (!this.idleReviewAllowed() || this.reviewRunning) return;
12395
+ if (epoch !== this.reviewEpoch || policy !== this.reviewPolicy() || !this.idleReviewAllowed() || this.reviewRunning) return;
12054
12396
  if (this.lastReviewAttemptAt !== void 0 && Date.now() < this.lastReviewAttemptAt + this.config.idleReview.minIntervalMs) {
12055
12397
  this.scheduleIdleReview(turn);
12056
12398
  return;
@@ -12058,7 +12400,73 @@ var MnemonAgentLifecycle = class {
12058
12400
  if (this.agent.status !== "idle") return;
12059
12401
  if (!this.agent.session.snapshotEvents().some((event) => event.type === "turn/end" && eventTurn(event) === turn) || !this.reviewActivity().eligible || !this.reviewAdmitted(turn)) return;
12060
12402
  this.runIdleReview();
12061
- }, Math.max(this.config.idleReviewMs, (this.lastReviewAttemptAt ?? -Infinity) + this.config.idleReview.minIntervalMs - Date.now()));
12403
+ }, Math.min(2147483647, Math.max(this.config.idleReviewMs, (this.lastReviewAttemptAt ?? -Infinity) + this.config.idleReview.minIntervalMs - Date.now())));
12404
+ }
12405
+ /** Resolve only the graph actually serving this parent, never a selected View or startup cwd. */
12406
+ restoreWatermark() {
12407
+ if (!this.config.idleReview.persistWatermark) {
12408
+ this.watermarkEnabled = false;
12409
+ return true;
12410
+ }
12411
+ try {
12412
+ const directory = this.runtimeDirectory?.();
12413
+ if (directory === void 0 || directory.trim() === "") throw new Error("runtime storage unavailable");
12414
+ const key = JSON.stringify([resolve(directory), this.agent.id]);
12415
+ if (this.watermarkKey === key && this.watermarkEnabled) return this.watermarkError === void 0;
12416
+ const sameRoot = this.watermarkKey === key;
12417
+ this.watermarkEnabled = true;
12418
+ this.watermarkKey = key;
12419
+ this.watermarkStore = new IdleReviewWatermarkStore(directory, this.agent.id);
12420
+ this.watermarkError = void 0;
12421
+ this.restoredAttempts = 0;
12422
+ const floor = this.reviewUsage.get(resolve(directory));
12423
+ if (!sameRoot) {
12424
+ this.idleReviewAttempts = floor?.attempts ?? 0;
12425
+ this.lastReviewAttemptAt = floor?.lastReviewAttemptAt ?? void 0;
12426
+ }
12427
+ const state = this.watermarkStore.restore(floor);
12428
+ this.restoredAttempts = state.attempts;
12429
+ this.idleReviewAttempts = state.attempts;
12430
+ this.lastReviewAttemptAt = state.lastReviewAttemptAt ?? void 0;
12431
+ this.reviewUsage.set(resolve(directory), state);
12432
+ return true;
12433
+ } catch (error) {
12434
+ this.pauseWatermark(error);
12435
+ return false;
12436
+ }
12437
+ }
12438
+ configurationChanged() {
12439
+ const policy = this.reviewPolicy();
12440
+ if (policy !== this.observedReviewPolicy) {
12441
+ this.cancelIdleReview(true);
12442
+ this.reviewAccessBlocked = false;
12443
+ }
12444
+ this.observedReviewPolicy = policy;
12445
+ this.restoreWatermark();
12446
+ }
12447
+ pauseWatermark(error) {
12448
+ const message = error instanceof Error && error.message.startsWith("Idle review watermark unavailable") ? error.message : "Idle review watermark unavailable. Automatic review is paused. Stop Hosts using this storage root, repair this session's state or lock under state/idle-review, then reopen the session.";
12449
+ this.watermarkError = message;
12450
+ }
12451
+ /** Policy/root changes invalidate an already queued checkpoint instead of catching up. */
12452
+ reviewPolicy() {
12453
+ try {
12454
+ return JSON.stringify([
12455
+ this.config.idleReview.persistWatermark ? this.runtimeDirectory?.() : void 0,
12456
+ this.config.lifecycleEnabled,
12457
+ this.config.writeEnabled,
12458
+ this.config.storageScope,
12459
+ this.config.dataDir,
12460
+ this.config.writebackMode,
12461
+ this.config.idleReviewMs,
12462
+ this.config.idleReview,
12463
+ this.config.taskAgentModel,
12464
+ mainStrategyTypeId(this.config)
12465
+ ]);
12466
+ } catch (error) {
12467
+ if (this.config.idleReview.persistWatermark) this.pauseWatermark(error);
12468
+ return;
12469
+ }
12062
12470
  }
12063
12471
  /** Which Strategy composed the turn, while its View is still pinned, and whether it declares idle review. */
12064
12472
  recordComposedTurn(turn) {
@@ -12069,7 +12477,8 @@ var MnemonAgentLifecycle = class {
12069
12477
  this.composedTurn = {
12070
12478
  turn,
12071
12479
  typeId: strategy.typeId,
12072
- idleReview: strategyDrives(strategy, "idle-review")
12480
+ idleReview: strategyDrives(strategy, "idle-review"),
12481
+ directory: pinned.graph.directory
12073
12482
  };
12074
12483
  } else if (this.composedTurn?.turn !== turn) this.composedTurn = void 0;
12075
12484
  }
@@ -12086,19 +12495,63 @@ var MnemonAgentLifecycle = class {
12086
12495
  if (this.memoryTurn === void 0) return strategyDrives({ typeId }, "idle-review");
12087
12496
  return this.composedTurn?.typeId === typeId && this.composedTurn.idleReview;
12088
12497
  }
12089
- idleReviewAllowed() {
12090
- return this.config.lifecycleEnabled && this.config.writeEnabled && this.config.writebackMode === "guided" && this.config.idleReview.enabled && this.idleReviewAttempts < this.config.idleReview.maxPerSession && idleReviewBlockReason(this.agent, this.config.idleReview.agentTeams) === void 0 && this.strategyDrivesIdleReview();
12498
+ idleReviewAllowed(reserved = false) {
12499
+ return !this.disposed && (!this.config.idleReview.persistWatermark || this.watermarkError === void 0) && this.config.lifecycleEnabled && this.config.writeEnabled && this.config.writebackMode === "guided" && this.config.idleReview.enabled && (reserved || this.idleReviewAttempts < this.config.idleReview.maxPerSession) && idleReviewBlockReason(this.agent, this.config.idleReview.agentTeams) === void 0 && this.strategyDrivesIdleReview() && this.coordinator.canReview(this.agent);
12091
12500
  }
12092
12501
  async runIdleReview() {
12093
12502
  const controller = new AbortController();
12094
12503
  const triggeredScore = this.reviewActivity().score;
12095
- this.idleReviewAttempts += 1;
12096
- this.lastReviewAttemptAt = Date.now();
12504
+ const epoch = this.reviewEpoch;
12505
+ const policy = this.reviewPolicy();
12097
12506
  this.reviewRunning = true;
12098
12507
  this.reviewController = controller;
12099
- this.mark("review");
12100
12508
  try {
12101
- const result = await this.coordinator.review(this.agent, controller.signal);
12509
+ const available = await this.coordinator.checkReviewAccess(this.agent, controller.signal);
12510
+ if (epoch !== this.reviewEpoch || policy !== this.reviewPolicy() || !this.idleReviewAllowed()) controller.abort(/* @__PURE__ */ new Error("Automatic idle review admission changed"));
12511
+ controller.signal.throwIfAborted();
12512
+ this.reviewAccessBlocked = !available;
12513
+ if (!available) return;
12514
+ if (this.config.idleReview.persistWatermark) try {
12515
+ if (!this.restoreWatermark() || this.watermarkStore === void 0 || !this.idleReviewAllowed()) return;
12516
+ const reserved = this.watermarkStore.reserve({
12517
+ ...this.config.idleReview,
12518
+ now: Date.now(),
12519
+ floor: {
12520
+ attempts: this.idleReviewAttempts,
12521
+ lastReviewAttemptAt: this.lastReviewAttemptAt ?? null
12522
+ }
12523
+ });
12524
+ const state = reserved ?? this.watermarkStore.load();
12525
+ this.idleReviewAttempts = Math.max(this.idleReviewAttempts, state.attempts);
12526
+ this.lastReviewAttemptAt = Math.max(this.lastReviewAttemptAt ?? -Infinity, state.lastReviewAttemptAt ?? -Infinity);
12527
+ if (this.lastReviewAttemptAt === -Infinity) this.lastReviewAttemptAt = void 0;
12528
+ const directory = this.runtimeDirectory?.();
12529
+ if (directory !== void 0) this.reviewUsage.set(resolve(directory), {
12530
+ attempts: this.idleReviewAttempts,
12531
+ lastReviewAttemptAt: this.lastReviewAttemptAt ?? null
12532
+ });
12533
+ if (reserved === void 0) return;
12534
+ } catch (error) {
12535
+ this.pauseWatermark(error);
12536
+ return;
12537
+ }
12538
+ else {
12539
+ this.idleReviewAttempts += 1;
12540
+ this.lastReviewAttemptAt = Date.now();
12541
+ const directory = this.composedTurn?.directory;
12542
+ if (directory !== void 0) {
12543
+ const key = resolve(directory);
12544
+ this.reviewUsage.set(key, {
12545
+ attempts: (this.reviewUsage.get(key)?.attempts ?? 0) + 1,
12546
+ lastReviewAttemptAt: this.lastReviewAttemptAt
12547
+ });
12548
+ }
12549
+ }
12550
+ this.mark("review");
12551
+ const result = await this.coordinator.review(this.agent, controller.signal, () => {
12552
+ if (epoch !== this.reviewEpoch || policy !== this.reviewPolicy() || !this.idleReviewAllowed(true)) controller.abort(/* @__PURE__ */ new Error("Automatic idle review admission changed"));
12553
+ controller.signal.throwIfAborted();
12554
+ });
12102
12555
  if (controller.signal.aborted) return;
12103
12556
  this.lastReviewAt = (/* @__PURE__ */ new Date()).toISOString();
12104
12557
  this.lastReviewAction = result.action;
@@ -12119,6 +12572,7 @@ var MnemonAgentLifecycle = class {
12119
12572
  }
12120
12573
  }
12121
12574
  cancelIdleReview(abortRunning) {
12575
+ this.reviewEpoch += 1;
12122
12576
  if (this.idleReviewTimer !== void 0) clearTimeout(this.idleReviewTimer);
12123
12577
  this.idleReviewTimer = void 0;
12124
12578
  if (abortRunning) this.reviewController?.abort();
@@ -12199,6 +12653,16 @@ var MnemonAgentLifecycle = class {
12199
12653
  function isDirectory(path) {
12200
12654
  return stat(path).then((info) => info.isDirectory(), () => false);
12201
12655
  }
12656
+ /** Explicit model route carried by the task Agent a skill run created. */
12657
+ const TASK_ROUTE = Symbol.for("dsh-mnemon.taskRoute");
12658
+ /**
12659
+ * The route a task Agent was created with, when its caller picked one. Skill
12660
+ * work started under that Agent hands the same pick to its own children, so a
12661
+ * run never falls back to the profile default halfway through.
12662
+ */
12663
+ function taskRoute(agent) {
12664
+ return agent[TASK_ROUTE];
12665
+ }
12202
12666
  /** DSH-native owner for per-agent Mnemon lifecycle hooks and UI-triggered LLM work. */
12203
12667
  var MnemonLifecycle = class {
12204
12668
  ctx;
@@ -12234,8 +12698,8 @@ var MnemonLifecycle = class {
12234
12698
  return this.skillEvolution;
12235
12699
  }
12236
12700
  /** Model work for skill evolution under a fresh task Agent, outside any conversation. */
12237
- runSkillTask(workspaceRoot, signal, operation) {
12238
- return this.runTaskAgent("", workspaceRoot, signal, operation);
12701
+ runSkillTask(workspaceRoot, signal, operation, route) {
12702
+ return this.runTaskAgent("", workspaceRoot, signal, operation, route);
12239
12703
  }
12240
12704
  /**
12241
12705
  * What one conversation did with skills, for the bars under its turns. A
@@ -12272,11 +12736,15 @@ var MnemonLifecycle = class {
12272
12736
  return this.skillEvolution.reviewNow(this.liveAgent(sessionId));
12273
12737
  }
12274
12738
  start() {
12739
+ const stopChanged = this.runtimeSource?.onChanged?.(() => {
12740
+ for (const { lifecycle } of this.owners.values()) lifecycle.configurationChanged();
12741
+ });
12275
12742
  const stopCreated = this.ctx.on("agent/created", (({ agent, source }) => {
12276
12743
  this.install(agent, source);
12277
12744
  }));
12278
12745
  for (const agent of this.ctx.agents.roots()) this.install(agent, "adopted");
12279
12746
  return () => {
12747
+ stopChanged?.();
12280
12748
  stopCreated();
12281
12749
  for (const dispose of [...this.children.values()].reverse()) dispose();
12282
12750
  for (const owner of [...this.owners.values()].reverse()) owner.dispose();
@@ -12582,18 +13050,21 @@ var MnemonLifecycle = class {
12582
13050
  * is the explicit Web workbench scope, so LiveMnemonRuntime resolves the same
12583
13051
  * workspace graph without borrowing conversation history or ownership.
12584
13052
  */
12585
- async runTaskAgent(fallbackSessionId, workspaceRoot, signal, operation) {
13053
+ async runTaskAgent(fallbackSessionId, workspaceRoot, signal, operation, route) {
12586
13054
  const sessionId = randomUUID();
12587
13055
  this.taskAgentIds.add(sessionId);
12588
13056
  let handle;
12589
13057
  let failure;
12590
13058
  try {
12591
- const creation = await this.taskAgentCreation(fallbackSessionId, workspaceRoot, signal);
13059
+ const creation = await this.taskAgentCreation(fallbackSessionId, workspaceRoot, signal, route);
12592
13060
  handle = await this.ctx.agents.create({
12593
13061
  sessionId,
12594
13062
  ...creation,
12595
13063
  signal
12596
13064
  });
13065
+ if (route !== void 0) try {
13066
+ handle.agent[TASK_ROUTE] = route;
13067
+ } catch {}
12597
13068
  return await operation(handle.agent);
12598
13069
  } catch (error) {
12599
13070
  failure = error;
@@ -12618,8 +13089,15 @@ var MnemonLifecycle = class {
12618
13089
  * or `delegationDepth`: the task belongs to no conversation, and its children stay
12619
13090
  * one level deep.
12620
13091
  */
12621
- async taskAgentCreation(fallbackSessionId, workspaceRoot, signal) {
12622
- const agentOptions = this.taskAgentModelOptions(fallbackSessionId, workspaceRoot);
13092
+ async taskAgentCreation(fallbackSessionId, workspaceRoot, signal, route) {
13093
+ const baseOptions = this.taskAgentModelOptions(fallbackSessionId, workspaceRoot);
13094
+ const { reasoningEffort: _inheritedEffort, ...baseRest } = baseOptions ?? {};
13095
+ const agentOptions = route === void 0 ? baseOptions : {
13096
+ ...baseRest,
13097
+ provider: route.provider,
13098
+ model: route.model,
13099
+ ...route.reasoningEffort === void 0 ? {} : { reasoningEffort: route.reasoningEffort }
13100
+ };
12623
13101
  if (agentOptions === void 0) throw new Error("no default provider/model is available for a clean task Agent");
12624
13102
  const { cwd, workFrom } = await this.taskAgentDirectories(workspaceRoot);
12625
13103
  const presets = presetService(this.ctx.get("agentPresets"));
@@ -12771,7 +13249,7 @@ var MnemonLifecycle = class {
12771
13249
  }
12772
13250
  if (!this.ctx.agents.roots().includes(agent)) return;
12773
13251
  const memoryTurn = this.runtimeSource === void 0 ? void 0 : new AgentMemoryTurn(agent, this.runtimeSource);
12774
- const lifecycle = new MnemonAgentLifecycle(agent, this.coordinator, this.config, this.counters, source, memoryTurn);
13252
+ const lifecycle = new MnemonAgentLifecycle(agent, this.coordinator, this.config, this.counters, source, memoryTurn, () => this.runtimeSource?.forAgent(agent).directory);
12775
13253
  let dispose;
12776
13254
  dispose = agent.ctx.effect(() => {
12777
13255
  const stop = lifecycle.start();
@@ -14008,7 +14486,7 @@ var SkillEvolution = class {
14008
14486
  let prompt = skillDraftPrompt(pack, guidance, targetRoot);
14009
14487
  let draft;
14010
14488
  for (let attempt = 0; draft === void 0; attempt++) {
14011
- const parsed = parseSkillDraft((await this.host.runTask(workspace, signal, (task) => this.host.skillTask(task, "Mnemon skill draft", prompt, SKILL_DRAFT_SCHEMA, SKILL_DRAFT_PERSONA, signal))).structured);
14489
+ const parsed = parseSkillDraft((await this.host.runTask(workspace, signal, (task) => this.host.skillTask(task, "Mnemon skill draft", prompt, SKILL_DRAFT_SCHEMA, SKILL_DRAFT_PERSONA, signal, input.route), input.route)).structured);
14012
14490
  try {
14013
14491
  draft = await source.mutate("draft-record", {
14014
14492
  ...input.suggestionId === void 0 ? {} : { suggestionId: input.suggestionId },
@@ -14030,16 +14508,18 @@ var SkillEvolution = class {
14030
14508
  target: {
14031
14509
  kind: "draft",
14032
14510
  draftId: draft.id
14033
- }
14511
+ },
14512
+ ...input.route === void 0 ? {} : { route: input.route }
14034
14513
  });
14035
- if (pack.action === "update" && pack.lastRun !== void 0 && pack.lastRun.target.hash === pack.skill?.hash) await source.mutate("run-link", {
14514
+ if (input.route === void 0 && pack.action === "update" && pack.lastRun !== void 0 && pack.lastRun.target.hash === pack.skill?.hash) await source.mutate("run-link", {
14036
14515
  draftId: draft.id,
14037
14516
  runId: pack.lastRun.id
14038
14517
  });
14039
14518
  else {
14040
14519
  const baseline = await this.backtest(source, workspace, {
14041
14520
  skillName: pack.skillName,
14042
- target: { kind: pack.action === "update" ? "current" : "none" }
14521
+ target: { kind: pack.action === "update" ? "current" : "none" },
14522
+ ...input.route === void 0 ? {} : { route: input.route }
14043
14523
  });
14044
14524
  await source.mutate("run-link", {
14045
14525
  draftId: draft.id,
@@ -14096,7 +14576,7 @@ var SkillEvolution = class {
14096
14576
  }
14097
14577
  };
14098
14578
  await Promise.all(Array.from({ length: Math.min(BACKTEST_CONCURRENCY, queue.length) }, worker));
14099
- });
14579
+ }, input.route);
14100
14580
  await source.mutate("run-finish", {
14101
14581
  id: run.id,
14102
14582
  status: "completed"
@@ -14538,6 +15018,20 @@ function optionalString(value, label) {
14538
15018
  if (typeof value !== "string") throw new Error(label + " must be a string");
14539
15019
  return value.trim() === "" ? void 0 : value.trim();
14540
15020
  }
15021
+ /**
15022
+ * The `provider` + `model` pair a page may send with model work. Both or
15023
+ * neither: a half route cannot run, and an absent one keeps the profile's own.
15024
+ */
15025
+ function modelRoute(input, missing = "a draft model needs both provider and model") {
15026
+ const provider = optionalString(input.provider, "provider");
15027
+ const model = optionalString(input.model, "model");
15028
+ if (provider === void 0 && model === void 0) return void 0;
15029
+ if (provider === void 0 || model === void 0) throw new Error(missing);
15030
+ return {
15031
+ provider,
15032
+ model
15033
+ };
15034
+ }
14541
15035
  /** Skill evolution's model work: drafts and backtests run in the background, cases and reviews answer here. */
14542
15036
  async function skillAssistance(runtime, lifecycle, operation, input, signal) {
14543
15037
  const skills = lifecycle.skills;
@@ -14558,11 +15052,13 @@ async function skillAssistance(runtime, lifecycle, operation, input, signal) {
14558
15052
  if (suggestionId === void 0 && skill === void 0) throw new Error("a draft needs a suggestion or a skill");
14559
15053
  const guidance = optionalString(input.guidance, "guidance");
14560
15054
  if (guidance !== void 0 && guidance.length > 4e3) throw new Error("guidance is too long (max 4000 characters)");
15055
+ const draftRoute = modelRoute(input);
14561
15056
  return skills.draft(source, workspaceRoot, {
14562
15057
  ...suggestionId === void 0 ? {} : { suggestionId },
14563
15058
  ...skill === void 0 ? {} : { skill },
14564
15059
  ...guidance === void 0 ? {} : { guidance },
14565
- ...root === void 0 ? {} : { targetRoot: root }
15060
+ ...root === void 0 ? {} : { targetRoot: root },
15061
+ ...draftRoute === void 0 ? {} : { route: draftRoute }
14566
15062
  });
14567
15063
  }
14568
15064
  case "skill-backtest": {
@@ -14577,10 +15073,12 @@ async function skillAssistance(runtime, lifecycle, operation, input, signal) {
14577
15073
  ].includes(String(target.kind))) throw new Error("target kind must be current, draft, version or none");
14578
15074
  const caseIds = input.caseIds === void 0 ? void 0 : input.caseIds;
14579
15075
  if (caseIds !== void 0 && (!Array.isArray(caseIds) || caseIds.some((id) => typeof id !== "string"))) throw new Error("caseIds must be a string array");
15076
+ const runRoute = modelRoute(input, "a backtest model needs both provider and model");
14580
15077
  return skills.backtest(source, workspaceRoot, {
14581
15078
  skillName,
14582
15079
  target,
14583
- ...caseIds === void 0 ? {} : { caseIds }
15080
+ ...caseIds === void 0 ? {} : { caseIds },
15081
+ ...runRoute === void 0 ? {} : { route: runRoute }
14584
15082
  });
14585
15083
  }
14586
15084
  case "skill-derive-cases": {
@@ -15293,16 +15791,59 @@ function documentActionReceipt(receipt) {
15293
15791
  }
15294
15792
  /**
15295
15793
  * Named product tools are views over the same Source Route/Action protocol. Each family registers its tools in the
15296
- * order the model has always been given them, which puts Memory Spaces' reads before the Documents tools and its
15297
- * writes after the runtime memory tool.
15794
+ * historical registry order, which puts Memory Spaces' reads before the Documents tools and its writes after
15795
+ * the runtime memory tool. DSH's prompt assembler independently orders wire schemas.
15298
15796
  */
15299
- function registerTools(ctx, runtimeSource, coordinator) {
15797
+ function registerTools(ctx, runtimeSource, coordinator, contributions, initialReady) {
15798
+ const definitions = [];
15799
+ const catalog = { tools: { register: (tool) => {
15800
+ definitions.push(tool);
15801
+ } } };
15300
15802
  const helpers = toolHelpers(runtimeSource);
15301
- registerViewTools(ctx, helpers, coordinator);
15302
- registerMemorySpaceReadTools(ctx, helpers, coordinator);
15303
- registerDocumentTools(ctx, helpers, coordinator);
15304
- registerRuntimeTools(ctx, helpers, coordinator);
15305
- registerMemorySpaceWriteTools(ctx, helpers, coordinator);
15803
+ registerViewTools(catalog, helpers, coordinator);
15804
+ registerMemorySpaceReadTools(catalog, helpers, coordinator);
15805
+ registerDocumentTools(catalog, helpers, coordinator);
15806
+ registerRuntimeTools(catalog, helpers, coordinator);
15807
+ registerMemorySpaceWriteTools(catalog, helpers, coordinator);
15808
+ const registered = /* @__PURE__ */ new Map();
15809
+ let active = true;
15810
+ let ready = initialReady === void 0;
15811
+ const reconcile = (snapshot) => {
15812
+ const types = new Set(snapshot.sources.map((source) => source.definition.manifest.typeId));
15813
+ for (const tool of definitions) {
15814
+ const type = memoryTool(tool.name)?.sourceTypeId;
15815
+ if (!(type === void 0 || ready && types.has(type))) {
15816
+ registered.get(tool.name)?.();
15817
+ registered.delete(tool.name);
15818
+ } else if (!registered.has(tool.name)) {
15819
+ const dispose = ctx.tools.register(tool);
15820
+ if (typeof dispose !== "function") throw new Error("dsh-mnemon tool registration did not return a disposer: " + tool.name);
15821
+ registered.set(tool.name, dispose);
15822
+ }
15823
+ }
15824
+ };
15825
+ const unsubscribe = contributions.onCommittedContributionsChanged(reconcile);
15826
+ const dispose = () => {
15827
+ if (!active) return;
15828
+ active = false;
15829
+ unsubscribe();
15830
+ for (const release of registered.values()) release();
15831
+ registered.clear();
15832
+ };
15833
+ try {
15834
+ reconcile(contributions.committedContributionSnapshot());
15835
+ } catch (error) {
15836
+ dispose();
15837
+ throw error;
15838
+ }
15839
+ initialReady?.then(() => {
15840
+ if (!active) return;
15841
+ ready = true;
15842
+ reconcile(contributions.committedContributionSnapshot());
15843
+ }).catch((error) => {
15844
+ if (active) console.warn("dsh-mnemon: could not initialize Source tool families", error);
15845
+ });
15846
+ return dispose;
15306
15847
  }
15307
15848
  /** What the handlers of every family share: the calling Agent's runtime graph and Sources, its checks, and a Source Action call. */
15308
15849
  function toolHelpers(runtimeSource) {
@@ -18215,8 +18756,8 @@ function apply(rawContext, rawConfig = {}) {
18215
18756
  const skills = new SkillEvolution({
18216
18757
  settings: () => skillSettings.get(),
18217
18758
  source: (agent) => skillsSourceFor(runtime, agent),
18218
- runTask: (workspaceRoot, signal, operation) => lifecycle.runSkillTask(workspaceRoot, signal, operation),
18219
- skillTask: (parent, label, prompt, schema, persona, signal) => coordinator.skillTask(parent, label, prompt, schema, persona, signal)
18759
+ runTask: (workspaceRoot, signal, operation, route) => lifecycle.runSkillTask(workspaceRoot, signal, operation, route),
18760
+ skillTask: (parent, label, prompt, schema, persona, signal) => coordinator.skillTask(parent, label, prompt, schema, persona, signal, taskRoute(parent))
18220
18761
  });
18221
18762
  lifecycle.useSkillEvolution(skills);
18222
18763
  ctx.effect(() => {
@@ -18234,7 +18775,10 @@ function apply(rawContext, rawConfig = {}) {
18234
18775
  runtime.dispose();
18235
18776
  };
18236
18777
  }, "dsh-mnemon.lifecycle-root()");
18237
- registerTools(ctx, runtime, coordinator);
18778
+ ctx.effect(() => {
18779
+ const loader = ctx.get("loader");
18780
+ return registerTools(ctx, runtime, coordinator, extensions, loader?.await?.());
18781
+ }, "dsh-mnemon: Source tool lifecycle");
18238
18782
  registerCommands(ctx.commands, runtime, coordinator);
18239
18783
  registerGuidance(ctx, resolved);
18240
18784
  ctx.inject(["connection", "webServer"], (webContext) => {