dsh-mnemon 0.5.37 → 0.5.39

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
- 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";
1
+ import { d as truncate, 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-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";
@@ -259,7 +259,7 @@ const MNEMON_SYNC_CHANNEL = "/dsh-mnemon-sync";
259
259
  const MNEMON_SYNC_TOKEN_ENV = "MNEMON_SYNC_GIT_TOKEN";
260
260
  /**
261
261
  * What a storage root publishes before anyone configures it: the branch, the
262
- * directory inside it, and the commit identity used when no author is set. They
262
+ * directory inside it, and the fallback when no author or Git identity is set. They
263
263
  * are declared with the wire constants because the settings form shows the same
264
264
  * defaults the Host would apply to an empty field.
265
265
  */
@@ -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",
@@ -386,6 +387,7 @@ const DEFAULT_SKILLS_CONFIG = Object.freeze({
386
387
  evolution: false,
387
388
  allowCreate: true,
388
389
  allowUpdate: true,
390
+ targetScope: "user",
389
391
  autoBacktest: true,
390
392
  hints: true,
391
393
  sidebar: false,
@@ -473,6 +475,11 @@ const SkillsConfig = z.object({
473
475
  evolution: z.boolean().default(DEFAULT_SKILLS_CONFIG.evolution),
474
476
  allowCreate: z.boolean().default(DEFAULT_SKILLS_CONFIG.allowCreate),
475
477
  allowUpdate: z.boolean().default(DEFAULT_SKILLS_CONFIG.allowUpdate),
478
+ targetScope: z.union([
479
+ "storage",
480
+ "user",
481
+ "project"
482
+ ]).default(DEFAULT_SKILLS_CONFIG.targetScope),
476
483
  autoBacktest: z.boolean().default(DEFAULT_SKILLS_CONFIG.autoBacktest),
477
484
  hints: z.boolean().default(DEFAULT_SKILLS_CONFIG.hints),
478
485
  sidebar: z.boolean().default(DEFAULT_SKILLS_CONFIG.sidebar),
@@ -535,6 +542,7 @@ const RuntimeMemorySchema = z.object({
535
542
  const IdleReviewSchema = z.object({
536
543
  enabled: z.boolean().default(DEFAULT_IDLE_REVIEW.enabled),
537
544
  runtimeMemory: z.boolean().default(DEFAULT_IDLE_REVIEW.runtimeMemory),
545
+ persistWatermark: z.boolean().default(DEFAULT_IDLE_REVIEW.persistWatermark),
538
546
  provider: z.union(["spawn", "fork"]).default(DEFAULT_IDLE_REVIEW.provider),
539
547
  fallback: z.union(["spawn", "skip"]).default(DEFAULT_IDLE_REVIEW.fallback),
540
548
  agentTeams: z.union(["pause", "scoped"]).default(DEFAULT_IDLE_REVIEW.agentTeams),
@@ -1257,7 +1265,7 @@ const LOADED_VERSION = manifest$1(PACKAGE_MANIFEST_PATH)?.version;
1257
1265
  function pluginPackageVersion() {
1258
1266
  return LOADED_VERSION ?? "0.0.0";
1259
1267
  }
1260
- const CHECK_TIMEOUT_MS = 1e4;
1268
+ const CHECK_TIMEOUT_MS$1 = 1e4;
1261
1269
  const UPDATE_TIMEOUT_MS = 6e5;
1262
1270
  /** A check waits this long for an update that is finishing, within the browser's 15 s check deadline. */
1263
1271
  const IN_FLIGHT_WAIT_MS = 3e3;
@@ -1353,7 +1361,7 @@ async function fetchJson(url) {
1353
1361
  const controller = new AbortController();
1354
1362
  const timeout = setTimeout(() => {
1355
1363
  controller.abort();
1356
- }, CHECK_TIMEOUT_MS);
1364
+ }, CHECK_TIMEOUT_MS$1);
1357
1365
  try {
1358
1366
  const response = await fetch(url, {
1359
1367
  signal: controller.signal,
@@ -1621,7 +1629,7 @@ var VersionUpdateManager = class {
1621
1629
  let current;
1622
1630
  try {
1623
1631
  const invocation = resolveMnemonInvocation(command, ["--version"]);
1624
- current = versionFrom((await resultOrThrow(this.processRunner, invocation.command, invocation.args, CHECK_TIMEOUT_MS, { env: invocation.env })).stdout);
1632
+ current = versionFrom((await resultOrThrow(this.processRunner, invocation.command, invocation.args, CHECK_TIMEOUT_MS$1, { env: invocation.env })).stdout);
1625
1633
  } catch {
1626
1634
  return { install: {
1627
1635
  mode: launcher === void 0 ? "manual" : "npm",
@@ -1642,7 +1650,7 @@ var VersionUpdateManager = class {
1642
1650
  ...npm.args,
1643
1651
  "root",
1644
1652
  "--global"
1645
- ], CHECK_TIMEOUT_MS, { env: npm.env })).stdout.trim();
1653
+ ], CHECK_TIMEOUT_MS$1, { env: npm.env })).stdout.trim();
1646
1654
  if (isAbsolute(globalRoot) && samePath(dirname(dirname(launcher)), join(globalRoot, MNEMON_NPM_PACKAGE))) {
1647
1655
  install.hint = "npm";
1648
1656
  install.updateCommand = process.execPath;
@@ -1703,7 +1711,7 @@ var VersionUpdateManager = class {
1703
1711
  "GOARCH",
1704
1712
  "GOHOSTOS",
1705
1713
  "GOHOSTARCH"
1706
- ], CHECK_TIMEOUT_MS)).stdout);
1714
+ ], CHECK_TIMEOUT_MS$1)).stdout);
1707
1715
  const goPath = typeof environment.GOPATH === "string" ? environment.GOPATH.split(delimiter)[0] : void 0;
1708
1716
  const bin = typeof environment.GOBIN === "string" && environment.GOBIN !== "" ? environment.GOBIN : goPath === void 0 || goPath === "" ? void 0 : join(goPath, "bin");
1709
1717
  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 +1725,7 @@ var VersionUpdateManager = class {
1717
1725
  "version",
1718
1726
  "-m",
1719
1727
  command
1720
- ], CHECK_TIMEOUT_MS)).stdout.split(/\r?\n/).map((line) => line.trim().split(/\s+/)).find((fields) => fields[0] === "path")?.[1] === MNEMON_MODULE) return {
1728
+ ], CHECK_TIMEOUT_MS$1)).stdout.split(/\r?\n/).map((line) => line.trim().split(/\s+/)).find((fields) => fields[0] === "path")?.[1] === MNEMON_MODULE) return {
1721
1729
  ...current === void 0 ? {} : { current },
1722
1730
  install: {
1723
1731
  mode: "go",
@@ -3994,7 +4002,7 @@ function shapeOf(answer) {
3994
4002
  const listed = keys.length > names.length ? [...names, String(keys.length - names.length) + " other"] : names;
3995
4003
  return "HTTP " + String(answer.status) + ", content-type " + (answer.type ?? "none") + ", fields: " + (listed.length === 0 ? "none" : listed.join(", "));
3996
4004
  }
3997
- function failure$2(action, status, body) {
4005
+ function failure$3(action, status, body) {
3998
4006
  const code = codeOf(body);
3999
4007
  const message = code === void 0 ? action + " failed with HTTP " + String(status) : action + " failed: " + code;
4000
4008
  return status === 401 ? new MnemonSyncError("github-unauthorized", message) : new Error(message);
@@ -4135,7 +4143,7 @@ var MnemonGitHubAuth = class {
4135
4143
  } catch (error) {
4136
4144
  throw new Error("the GitHub device code request could not reach GitHub: " + describeTransportError(error));
4137
4145
  }
4138
- if (answer.status !== 200) throw failure$2("the GitHub device code request", answer.status, answer.body);
4146
+ if (answer.status !== 200) throw failure$3("the GitHub device code request", answer.status, answer.body);
4139
4147
  const fields = record$6(answer.body);
4140
4148
  const deviceCode = text$1(fields?.device_code);
4141
4149
  const userCode = text$1(fields?.user_code);
@@ -4283,7 +4291,7 @@ var MnemonGitHubAuth = class {
4283
4291
  headers: this.authorization(token),
4284
4292
  ...signal === void 0 ? {} : { signal }
4285
4293
  }, "listing GitHub repositories");
4286
- if (status !== 200) throw failure$2("listing GitHub repositories", status, body);
4294
+ if (status !== 200) throw failure$3("listing GitHub repositories", status, body);
4287
4295
  if (!Array.isArray(body)) throw new Error("the GitHub repository answer was not a list");
4288
4296
  for (const entry of body) {
4289
4297
  const repository = repositoryOf(entry);
@@ -4314,7 +4322,7 @@ var MnemonGitHubAuth = class {
4314
4322
  }),
4315
4323
  ...signal === void 0 ? {} : { signal }
4316
4324
  }, "creating the GitHub repository");
4317
- if (status !== 201) throw failure$2("creating the GitHub repository", status, body);
4325
+ if (status !== 201) throw failure$3("creating the GitHub repository", status, body);
4318
4326
  const repository = repositoryOf(body);
4319
4327
  if (repository === void 0) throw new Error("the created GitHub repository could not be read back");
4320
4328
  return repository;
@@ -5329,7 +5337,7 @@ var MnemonGitSync = class {
5329
5337
  if (committed) {
5330
5338
  const identity = await this.commitIdentity(settings, signal);
5331
5339
  const result = await this.git([
5332
- ...identity,
5340
+ ...identity.args,
5333
5341
  "commit",
5334
5342
  "--quiet",
5335
5343
  "-m",
@@ -5338,7 +5346,8 @@ var MnemonGitSync = class {
5338
5346
  settings.subdir
5339
5347
  ], {
5340
5348
  cwd: this.store.mirror(),
5341
- signal
5349
+ signal,
5350
+ env: identity.env
5342
5351
  });
5343
5352
  if (result.exitCode !== 0) throw new Error("git commit failed: " + tail(result.stderr || result.stdout));
5344
5353
  }
@@ -5371,20 +5380,35 @@ var MnemonGitSync = class {
5371
5380
  };
5372
5381
  }
5373
5382
  /**
5374
- * The identity a sync commit is recorded under: the one configured here, else the
5375
- * Git identity this machine has. A machine without one gets the channel's own
5376
- * rather than a failed commit, and rather than the account and host names Git
5377
- * would otherwise guess and publish.
5383
+ * The saved default pair also appears in older settings that never selected an
5384
+ * author. Like blank fields, it defers to Git in the mirror's directory; only a
5385
+ * machine without a configured identity uses the channel's placeholder. Any
5386
+ * other pair keeps its explicit fields, even when one matches a default value.
5378
5387
  */
5379
5388
  async commitIdentity(settings, signal) {
5380
- const name = settings.authorName !== "" ? settings.authorName : await this.configured("user.name", signal) ?? "dsh-mnemon sync";
5381
- const email = settings.authorEmail !== "" ? settings.authorEmail : await this.configured("user.email", signal) ?? "mnemon@localhost";
5382
- return [
5383
- "-c",
5384
- "user.name=" + name,
5385
- "-c",
5386
- "user.email=" + email
5387
- ];
5389
+ const defaults = settings.authorName === "dsh-mnemon sync" && settings.authorEmail === "mnemon@localhost";
5390
+ const authorName = defaults ? "" : settings.authorName;
5391
+ const authorEmail = defaults ? "" : settings.authorEmail;
5392
+ const name = authorName !== "" ? authorName : await this.configured("user.name", signal) ?? "dsh-mnemon sync";
5393
+ const email = authorEmail !== "" ? authorEmail : await this.configured("user.email", signal) ?? "mnemon@localhost";
5394
+ const env = {};
5395
+ if (authorName !== "") {
5396
+ env.GIT_AUTHOR_NAME = authorName;
5397
+ env.GIT_COMMITTER_NAME = authorName;
5398
+ }
5399
+ if (authorEmail !== "") {
5400
+ env.GIT_AUTHOR_EMAIL = authorEmail;
5401
+ env.GIT_COMMITTER_EMAIL = authorEmail;
5402
+ }
5403
+ return {
5404
+ args: [
5405
+ "-c",
5406
+ "user.name=" + name,
5407
+ "-c",
5408
+ "user.email=" + email
5409
+ ],
5410
+ env
5411
+ };
5388
5412
  }
5389
5413
  /** One Git setting as this machine resolves it, or nothing when no config sets it. */
5390
5414
  async configured(key, signal) {
@@ -6115,7 +6139,7 @@ var MnemonGitSync = class {
6115
6139
  const failure = rejectedPush(output) ? "push-rejected" : REMOTE_FAILURE[remoteIssueOf(output)];
6116
6140
  return {
6117
6141
  pushed: false,
6118
- reason: credential.token === void 0 && failure !== "push-rejected" ? "the commit stays local because no token is available: " + reason : reason,
6142
+ reason: /\[remote rejected\]/iu.test(output) ? "the remote rejected this push; check its policy and the commit author in Sync > Advanced, then sync again: " + reason : credential.token === void 0 && failure !== "push-rejected" ? "the commit stays local because no token is available: " + reason : reason,
6119
6143
  ...failure === void 0 ? {} : { failure }
6120
6144
  };
6121
6145
  }
@@ -6277,7 +6301,10 @@ var MnemonGitSync = class {
6277
6301
  try {
6278
6302
  return await this.run(GIT_LABEL, prepared.args, {
6279
6303
  timeoutMs: options.timeoutMs ?? 12e4,
6280
- env: gitEnvironment(),
6304
+ env: gitEnvironment({
6305
+ ...process.env,
6306
+ ...options.env
6307
+ }),
6281
6308
  ...options.cwd === void 0 ? {} : { cwd: options.cwd },
6282
6309
  ...options.signal === void 0 ? {} : { signal: options.signal },
6283
6310
  ...options.maxOutputBytes === void 0 ? {} : { maxOutputBytes: options.maxOutputBytes },
@@ -7490,6 +7517,7 @@ var LiveMnemonRuntime = class {
7490
7517
  githubAuth;
7491
7518
  autoBackupView;
7492
7519
  closed = false;
7520
+ changeListeners = /* @__PURE__ */ new Set();
7493
7521
  config;
7494
7522
  storage;
7495
7523
  packs;
@@ -7531,6 +7559,12 @@ var LiveMnemonRuntime = class {
7531
7559
  applyAutoBackup(graph) {
7532
7560
  if (this.autoBackupView !== void 0) graph.sync.useAutoBackup(this.autoBackupView);
7533
7561
  }
7562
+ onChanged(listener) {
7563
+ this.changeListeners.add(listener);
7564
+ return () => {
7565
+ this.changeListeners.delete(listener);
7566
+ };
7567
+ }
7534
7568
  swap(next) {
7535
7569
  if (this.closed) {
7536
7570
  next.dispose();
@@ -7544,6 +7578,7 @@ var LiveMnemonRuntime = class {
7544
7578
  this.retireGraph(previous);
7545
7579
  for (const graph of this.workspaceGraphs.values()) this.retireGraph(graph);
7546
7580
  this.workspaceGraphs.clear();
7581
+ for (const listener of this.changeListeners) listener();
7547
7582
  }
7548
7583
  snapshot() {
7549
7584
  this.assertOpen();
@@ -7568,6 +7603,7 @@ var LiveMnemonRuntime = class {
7568
7603
  dispose() {
7569
7604
  if (this.closed) return;
7570
7605
  this.executions.dispose();
7606
+ this.changeListeners.clear();
7571
7607
  this.closed = true;
7572
7608
  const graphs = /* @__PURE__ */ new Set([
7573
7609
  this.current,
@@ -10158,148 +10194,535 @@ async function assertAutomaticMemoryWrite(host, parent) {
10158
10194
  return host.writableSourceFor(parent, "memory-spaces", "remember");
10159
10195
  }
10160
10196
  //#endregion
10161
- //#region src/host/subagent.ts
10162
- function evidenceInsights(evidence) {
10163
- return evidence.items.map((item) => {
10164
- return {
10165
- ...optionalObject(item.provenance) ?? {},
10166
- id: item.id,
10167
- content: item.text,
10168
- ...item.score === void 0 ? {} : { score: item.score },
10169
- ...item.revision === void 0 ? {} : { revision: item.revision }
10170
- };
10171
- });
10172
- }
10197
+ //#region src/host/view-presentation.ts
10173
10198
  /**
10174
- * Every task dsh-mnemon delegates to a child Agent starts with one line of its own, such as
10175
- * `[dsh-mnemon:delegated review]` (#393). DSH records the task as the child's prompt from a
10176
- * person, so a plugin that reads sessions, such as one that collects a workspace's work, can
10177
- * rely on this line, documented as stable, to leave Mnemon's own children out.
10199
+ * The named tools bound to each Source type's Routes or Actions, from the tool table: these are
10200
+ * existing DSH product-tool bindings, not Core operation names. Tools that share one operation are
10201
+ * listed together, in table order, as the View's tool list names them.
10178
10202
  */
10179
- const DELEGATED_TASK_PREFIX = "[dsh-mnemon:delegated ";
10180
- function delegatedTaskLine(operation) {
10181
- return `${DELEGATED_TASK_PREFIX}${operation}]`;
10182
- }
10183
- const WRITE_TOOLS = [
10184
- ...[
10185
- "mnemon_memory_bodies",
10186
- "mnemon_recall",
10187
- "mnemon_related"
10188
- ],
10189
- "mnemon_remember",
10190
- "mnemon_link",
10191
- "mnemon_forget",
10192
- "mnemon_memory_body_create",
10193
- "mnemon_memory_body_update",
10194
- "mnemon_memory_body_merge"
10195
- ];
10196
- const AUTONOMOUS_WRITE_TOOLS = WRITE_TOOLS.filter((tool) => tool !== "mnemon_forget");
10197
- const EXPLICIT_WRITE_TOOLS = WRITE_TOOLS;
10198
- const REVIEW_TOOLS = [
10199
- ...["mnemon_document_search"],
10200
- "mnemon_runtime_memory",
10201
- "mnemon_document_create"
10202
- ];
10203
- const RESULT_TOOL_NAME = "mnemon_subagent_result";
10204
- /** How many finished delegated children stay refused memory tools if DSH resumes one. */
10205
- const FINISHED_CHILDREN = 1e3;
10206
- const FINISHED_CHILD_REFUSAL = "This Mnemon task has finished, so it no longer reads or changes memory. Reply without memory tools.";
10207
- const RESULT_TOOL_INPUT_SCHEMA = {
10208
- type: "object",
10209
- properties: {
10210
- requestId: {
10211
- type: "string",
10212
- minLength: 1
10213
- },
10214
- result: {
10215
- type: "object",
10216
- additionalProperties: true
10217
- }
10218
- },
10219
- required: ["requestId", "result"],
10220
- additionalProperties: false
10221
- };
10222
- const RESULT_TOOL_OUTPUT_SCHEMA = {
10223
- type: "object",
10224
- properties: { recorded: {
10225
- type: "boolean",
10226
- const: true
10227
- } },
10228
- required: ["recorded"],
10229
- additionalProperties: false
10230
- };
10231
- const WRITE_OPERATION_RESULT_TOOL = {
10232
- remember: "mnemon_remember",
10233
- "supervised-writeback": "mnemon_remember",
10234
- link: "mnemon_link",
10235
- forget: "mnemon_forget",
10236
- "create-memory-body": "mnemon_memory_body_create",
10237
- "update-memory-body": "mnemon_memory_body_update",
10238
- "merge-memory-bodies": "mnemon_memory_body_merge"
10239
- };
10240
- var IdleReviewError = class extends Error {
10241
- review;
10242
- constructor(message, review, options) {
10243
- super(message, options);
10244
- this.review = review;
10245
- this.name = "IdleReviewError";
10203
+ function namedBindings(kind) {
10204
+ const bindings = {};
10205
+ for (const [name, tool] of memoryTools()) {
10206
+ if (tool.sourceTypeId === void 0 || tool.operation === void 0 || tool.kind === "write" !== (kind === "action")) continue;
10207
+ const operations = bindings[tool.sourceTypeId] ??= {};
10208
+ const listed = operations[tool.operation];
10209
+ operations[tool.operation] = listed === void 0 ? name : `${listed} / ${name}`;
10246
10210
  }
10247
- };
10248
- /** What a memory saved without a vector means for recall, and how its vector comes. */
10249
- const UNEMBEDDED_HINT = "Saved without a vector, because embedding did not succeed: semantic matching misses it until its vector is filled in, while keyword, link and recency recall still reach it. Once the embedding service answers, the user fills it in from Test status in the Mnemon Native embedding settings, which runs through the storage root and embedding settings DSH uses; a terminal mnemon command may reach neither.";
10250
- /** The memories a write saved without a vector: Mnemon Native says so with `embedded: false` (#388). */
10251
- function withoutVector(receipts) {
10252
- return receipts.filter((receipt) => isTool(receipt.name, "mnemon_remember")).flatMap((receipt) => {
10253
- const value = optionalObject(receipt.value);
10254
- if (value?.embedded !== false) return [];
10255
- return [{
10256
- ...typeof value.memoryBodyId === "string" ? { memoryBodyId: value.memoryBodyId } : {},
10257
- ...typeof value.id === "string" ? { id: value.id } : {}
10258
- }];
10259
- });
10211
+ return bindings;
10212
+ }
10213
+ const ROUTE_TOOLS = namedBindings("route");
10214
+ const ACTION_TOOLS = namedBindings("action");
10215
+ const NAMED_SOURCE_TYPES = [.../* @__PURE__ */ new Set([...Object.keys(ROUTE_TOOLS), ...Object.keys(ACTION_TOOLS)])];
10216
+ /** Use a short availability list only where the Host has the matching named binding.
10217
+ * Unbound external operations retain exact ids and schemas in the generic envelope. */
10218
+ function modelMemoryWake(graph, turn) {
10219
+ const generation = graph.memoryComposition.generation(turn.view.runtimeGeneration);
10220
+ if (generation === void 0) throw new Error("Cannot present an unpinned Memory generation");
10221
+ return graph.composableTurns.memoryWake(turn.view.id, memoryWakeBindings(generation, turn.view));
10222
+ }
10223
+ function presentMemoryWake(generation, view) {
10224
+ return createMemoryWake(view, memoryWakeBindings(generation, view));
10260
10225
  }
10261
- /** A Memory Space write tool: each switches on the space it goes to. */
10262
- const SPACE_WRITE_TOOLS = /* @__PURE__ */ new Set(["mnemon_remember", "mnemon_link"]);
10263
10226
  /**
10264
- * Holds a delegated write to the Memory Space its caller named, and keeps a writer
10265
- * from switching a space back on: a write or a link switches on the space it goes to,
10266
- * so a space that was inactive when the run started takes one only when the caller
10267
- * named it (#387). A space the writer creates is not in `inactive` and takes its first
10268
- * write. Saving a memory never merges spaces: a merge switches spaces on and off, and
10269
- * is a request of its own.
10227
+ * Why a generic View tool names no id this turn's View offers, told apart (#390); undefined when
10228
+ * the id is offered. It names only what the MNEMON VIEW ROUTES envelope shows: an operation bound
10229
+ * to a named tool is offered through that tool, and the envelope does not list its id.
10270
10230
  */
10271
- function spaceWriteGuard(target, inactive) {
10272
- return (execution) => {
10273
- if (isTool(execution.name, "mnemon_memory_body_merge")) return "Saving a memory never merges Memory Spaces; a merge is a request of its own. Write the memory, or finish without writing.";
10274
- const input = optionalObject(execution.arguments) ?? {};
10275
- const space = typeof input.memoryBodyId === "string" ? input.memoryBodyId.trim() : void 0;
10276
- if (target !== void 0) {
10277
- if (execution.name !== void 0 && SPACE_WRITE_TOOLS.has(execution.name) && space !== target) return `This write was requested for Memory Space ${JSON.stringify(target)}. Call ${execution.name} with memoryBodyId ${JSON.stringify(target)}, or finish without writing and say why it does not fit.`;
10278
- if (isTool(execution.name, "mnemon_memory_body_create", "mnemon_memory_body_update")) return `This write was requested for Memory Space ${JSON.stringify(target)}, so it creates or changes no space. Write there, or finish without writing and say why it does not fit.`;
10279
- return;
10231
+ function unofferedViewId(graph, turn, kind, id) {
10232
+ const view = turn.view;
10233
+ const offered = kind === "route" ? view.routes.map((route) => ({
10234
+ id: route.id,
10235
+ operation: route.sourceRouteId
10236
+ })) : view.actionOffers.map((offer) => ({
10237
+ id: offer.id,
10238
+ operation: offer.sourceActionId
10239
+ }));
10240
+ const label = kind === "route" ? "memory View Route" : "memory ActionOffer";
10241
+ if (typeof id !== "string" || id === "") return `${label} needs ${kind === "route" ? "routeId" : "offerId"}, the exact id of one it offers`;
10242
+ if (offered.some((entry) => entry.id === id)) return void 0;
10243
+ const generation = graph.memoryComposition.generation(view.runtimeGeneration);
10244
+ const bindings = generation === void 0 ? {} : memoryWakeBindings(generation, view);
10245
+ const named = (kind === "route" ? bindings.routes : bindings.actions) ?? {};
10246
+ const short = id.split("/").at(-1);
10247
+ const tools = [...new Set(offered.filter((entry) => named[entry.id] !== void 0 && (entry.operation === short || entry.id.split("/").at(-1) === short)).map((entry) => named[entry.id]))];
10248
+ if (tools.length > 0) return `${label} is unavailable: ${id}; this View offers it as ${tools.join(" and ")}, so call that tool`;
10249
+ const shown = offered.filter((entry) => named[entry.id] === void 0).map((entry) => entry.id);
10250
+ if (shown.length === 0) return `${label} is unavailable: ${id}; MNEMON VIEW ROUTES offers none by id this turn, only the named tools in MNEMON VIEW TOOLS`;
10251
+ const near = shown.filter((candidate) => candidate.split("/").at(-1) === short);
10252
+ if (near.length === 1) return `${label} is unavailable: ${id}; ids are exact as MNEMON VIEW ROUTES lists them: ${JSON.stringify(near[0])}`;
10253
+ const listed = [...near, ...shown.filter((candidate) => !near.includes(candidate))];
10254
+ return `${label} is unavailable: ${id}; MNEMON VIEW ROUTES offers ${listed.slice(0, 12).map((candidate) => JSON.stringify(candidate)).join(", ")}${listed.length > 12 ? ` and ${listed.length - 12} more` : ""}`;
10255
+ }
10256
+ function memoryWakeBindings(generation, view) {
10257
+ const instances = generation.sourceInstances();
10258
+ const routes = {}, actions = {};
10259
+ for (const typeId of NAMED_SOURCE_TYPES) {
10260
+ const selected = namedToolInstance(instances.filter((source) => source.sourceTypeId === typeId), typeId);
10261
+ if (selected === void 0) continue;
10262
+ for (const route of view.routes.filter((route) => route.sourceInstanceKey === selected.sourceInstanceKey)) {
10263
+ if (view.readGrants.find((grant) => grant.id === route.readGrantId)?.schema !== `dsh-mnemon.${typeId}/v1`) continue;
10264
+ const tool = ROUTE_TOOLS[typeId]?.[route.sourceRouteId];
10265
+ if (tool !== void 0) routes[route.id] = tool;
10280
10266
  }
10281
- if (space === void 0 || !inactive.has(space)) return void 0;
10282
- if (execution.name !== void 0 && SPACE_WRITE_TOOLS.has(execution.name)) return `Memory Space ${JSON.stringify(space)} is switched off, and a write would switch it on. Write to an active space, or finish without writing and say which space would fit.`;
10283
- if (isTool(execution.name, "mnemon_memory_body_update") && input.active === true) return `Memory Space ${JSON.stringify(space)} is switched off; only the user switches it on.`;
10267
+ for (const action of view.actionOffers.filter((action) => action.sourceInstanceKey === selected.sourceInstanceKey)) {
10268
+ const tool = ACTION_TOOLS[typeId]?.[action.sourceActionId];
10269
+ if (tool !== void 0) actions[action.id] = tool;
10270
+ }
10271
+ }
10272
+ return {
10273
+ routes,
10274
+ actions
10284
10275
  };
10285
10276
  }
10286
- /**
10287
- * Why a write cannot go to the Memory Space its caller named, before a writer runs (#387):
10288
- * the space is unknown, its Provider is off or takes no writes, or it is switched off and
10289
- * the caller did not ask to switch it on. A model names a space by its description, the
10290
- * judgement that misplaced memories before; switching a space back on is its own request.
10291
- */
10292
- function targetProblem(spaces, target, activate) {
10293
- const body = spaces.find((candidate) => candidate.id === target);
10294
- if (body === void 0) {
10295
- const known = spaces.slice(0, 20).map((candidate) => `${JSON.stringify(candidate.id)} (${candidate.name}${candidate.active ? "" : ", switched off"})`).join(", ");
10296
- return `Memory Space ${JSON.stringify(target)} does not exist. Name one by its id${known === "" ? "; there is none yet." : `: ${known}.`}`;
10297
- }
10298
- if (body.providerEnabled === false) return `Memory Space ${JSON.stringify(target)} uses ${body.provider.label}, which is turned off on the dsh-mnemon page under Plugins. Name another space.`;
10299
- if (body.provider.capabilities.remember !== true) return `Memory Space ${JSON.stringify(target)} (${body.provider.label}) does not take writes. Name another space.`;
10300
- if (!body.active && !activate) return `Memory Space ${JSON.stringify(target)} (${body.name}) is switched off, and a write would switch it back on. If the user wants it back, switch it on first with mnemon_memory_body_update (active: true); otherwise name an active space.`;
10277
+ /** Explicitly pick display fields; never copy grants, authority, or opaque Source state. */
10278
+ function inspectMemoryView(generation, view, state, turn, memoryText) {
10279
+ return structuredClone({
10280
+ id: view.id,
10281
+ digest: view.digest,
10282
+ generationId: view.runtimeGeneration,
10283
+ createdAt: view.createdAt,
10284
+ state,
10285
+ ...turn === void 0 ? {} : { turn },
10286
+ strategyTypeId: view.strategyTypeId,
10287
+ strategyInstanceKey: view.strategyInstanceKey,
10288
+ extensions: view.strategyExtensions ?? [],
10289
+ projection: view.projection.map(({ id, sourceInstanceKey, mode, text, revision }) => ({
10290
+ id,
10291
+ sourceInstanceKey,
10292
+ mode,
10293
+ text,
10294
+ revision
10295
+ })),
10296
+ sourcePresentations: view.sourcePresentations ?? [],
10297
+ routes: view.routes.map(({ id, sourceInstanceKey, sourceRouteId, description, maxCalls }) => ({
10298
+ id,
10299
+ sourceInstanceKey,
10300
+ operationId: sourceRouteId,
10301
+ description,
10302
+ maxCalls
10303
+ })),
10304
+ actions: view.actionOffers.map(({ id, sourceInstanceKey, sourceActionId, description }) => ({
10305
+ id,
10306
+ sourceInstanceKey,
10307
+ operationId: sourceActionId,
10308
+ description
10309
+ })),
10310
+ memoryText: memoryText ?? presentMemoryWake(generation, view).text,
10311
+ ...view.guidance === void 0 ? {} : { guidance: view.guidance },
10312
+ diagnostics: (view.diagnostics ?? []).map((diagnostic) => diagnostic.message)
10313
+ });
10301
10314
  }
10302
- /** Whether a write receipt put the memory in its space, or handed it to a Provider that confirms later. */
10315
+ //#endregion
10316
+ //#region src/host/agent-memory-turn.ts
10317
+ /** The durable log, not a parent session id, identifies the executing turn. */
10318
+ function openAgentTurn(agent) {
10319
+ let open;
10320
+ for (const event of agent.session.snapshotEvents()) {
10321
+ const turn = typeof event.data.turn === "number" ? event.data.turn : void 0;
10322
+ if (event.type === "turn/start" && turn !== void 0) open = turn;
10323
+ else if (event.type === "turn/end" && turn === open) open = void 0;
10324
+ }
10325
+ return open;
10326
+ }
10327
+ /**
10328
+ * One Agent owns each turn pin. A child additionally retains its delegation
10329
+ * across parent completion, collection, and runtime swaps until it is disposed.
10330
+ */
10331
+ var AgentMemoryTurn = class {
10332
+ agent;
10333
+ runtime;
10334
+ delegation;
10335
+ pinned;
10336
+ /** A turn that runs without memory because no composition was serving when it began. */
10337
+ unavailable;
10338
+ warned;
10339
+ pending;
10340
+ generation = 0;
10341
+ closed = false;
10342
+ lastInspection;
10343
+ lastWorkspace;
10344
+ releaseDelegation;
10345
+ constructor(agent, runtime, delegation) {
10346
+ this.agent = agent;
10347
+ this.runtime = runtime;
10348
+ this.delegation = delegation;
10349
+ if (delegation === void 0) return;
10350
+ this.releaseDelegation = runtime.executions.retain(agent, delegation);
10351
+ }
10352
+ get current() {
10353
+ return this.pinned;
10354
+ }
10355
+ /** Why the current turn has no View, when memory could not be composed for it. */
10356
+ get unavailableReason() {
10357
+ return this.unavailable?.reason;
10358
+ }
10359
+ inspect(workspaceRoot) {
10360
+ if (this.lastInspection === void 0) return void 0;
10361
+ if (workspaceRoot !== void 0 && resolve(workspaceRoot) !== this.lastWorkspace) return void 0;
10362
+ return structuredClone({
10363
+ ...this.lastInspection,
10364
+ state: this.pinned === void 0 ? "recent" : "active"
10365
+ });
10366
+ }
10367
+ clearInspection() {
10368
+ this.lastInspection = void 0;
10369
+ this.lastWorkspace = void 0;
10370
+ }
10371
+ /** Capture now; descendants must not resolve a later parent turn on demand. */
10372
+ delegate() {
10373
+ if (this.closed) throw new Error("memory Agent lifetime has ended");
10374
+ if (this.pinned !== void 0) return {
10375
+ graph: this.pinned.graph,
10376
+ scope: this.pinned.context.scope,
10377
+ viewId: this.pinned.context.view.id
10378
+ };
10379
+ if (this.delegation !== void 0) return this.delegation;
10380
+ const graph = this.runtime.forAgent(this.agent);
10381
+ return {
10382
+ graph,
10383
+ scope: agentScope(this.agent, graph.config)
10384
+ };
10385
+ }
10386
+ async begin(turn, signal) {
10387
+ if (this.closed) throw new Error("memory Agent lifetime has ended");
10388
+ signal?.throwIfAborted();
10389
+ if (this.pinned?.turn === turn || this.unavailable?.turn === turn) return;
10390
+ if (this.pending?.turn === turn) return this.pending.result;
10391
+ this.end();
10392
+ const generation = this.generation;
10393
+ const controller = new AbortController();
10394
+ const abort = () => controller.abort(signal.reason);
10395
+ signal?.addEventListener("abort", abort, { once: true });
10396
+ const result = this.pin(turn, generation, controller.signal).catch((error) => {
10397
+ if (!isMemoryCompositionUnavailable(error) || this.closed || this.generation !== generation) throw error;
10398
+ this.unavailable = {
10399
+ turn,
10400
+ reason: error.message
10401
+ };
10402
+ this.clearInspection();
10403
+ if (this.warned !== error.message) console.warn(`[dsh-mnemon] this turn runs without memory: ${error.message}`);
10404
+ this.warned = error.message;
10405
+ });
10406
+ this.pending = {
10407
+ turn,
10408
+ generation,
10409
+ controller,
10410
+ result
10411
+ };
10412
+ try {
10413
+ await result;
10414
+ } finally {
10415
+ signal?.removeEventListener("abort", abort);
10416
+ if (this.pending?.generation === generation) this.pending = void 0;
10417
+ }
10418
+ }
10419
+ end(turn) {
10420
+ if (turn !== void 0 && this.pinned?.turn !== turn && this.pending?.turn !== turn && this.unavailable?.turn !== turn) return;
10421
+ this.generation += 1;
10422
+ this.unavailable = void 0;
10423
+ this.pending?.controller.abort(/* @__PURE__ */ new Error("memory turn ended during View preparation"));
10424
+ this.pending = void 0;
10425
+ const pinned = this.pinned;
10426
+ this.pinned = void 0;
10427
+ pinned?.release();
10428
+ }
10429
+ dispose() {
10430
+ if (this.closed) return;
10431
+ this.closed = true;
10432
+ this.clearInspection();
10433
+ try {
10434
+ this.end();
10435
+ } finally {
10436
+ this.releaseDelegation?.();
10437
+ }
10438
+ }
10439
+ async pin(turn, generation, signal) {
10440
+ const execution = await this.runtime.executions.turn(this.agent, turn, signal, this.delegation);
10441
+ const { graph, context } = execution;
10442
+ try {
10443
+ if (this.closed || this.generation !== generation) throw new Error("memory turn ended during View preparation");
10444
+ signal?.throwIfAborted();
10445
+ const inspection = inspectMemoryView(graph.memoryComposition.generation(context.view.runtimeGeneration), context.view, "active", turn, modelMemoryWake(graph, context).text);
10446
+ this.pinned = {
10447
+ turn,
10448
+ graph,
10449
+ context,
10450
+ release: execution.release
10451
+ };
10452
+ this.warned = void 0;
10453
+ this.lastInspection = inspection;
10454
+ this.lastWorkspace = context.scope.workspaceId === void 0 ? void 0 : resolve(context.scope.workspaceId);
10455
+ } catch (error) {
10456
+ execution.release();
10457
+ throw error;
10458
+ }
10459
+ }
10460
+ };
10461
+ //#endregion
10462
+ //#region src/host/tool-execution.ts
10463
+ /** DSH 0.2.2 exposes executor registration and batch dispatch together. */
10464
+ function usesToolBatchExecution(tools) {
10465
+ return typeof tools.setExecutor === "function";
10466
+ }
10467
+ /** Preserve the Host's scoped executor, guards, policy and final result for one call. */
10468
+ async function executeHostTool(tools, input) {
10469
+ if (tools.execute === void 0) throw new Error("The DSH tool execution service is unavailable");
10470
+ if (!usesToolBatchExecution(tools)) return tools.execute(input);
10471
+ const results = (await tools.execute({
10472
+ calls: [{
10473
+ type: "tool-call",
10474
+ id: input.callId,
10475
+ name: input.name,
10476
+ arguments: JSON.stringify(input.arguments)
10477
+ }],
10478
+ agent: input.agent,
10479
+ signal: input.signal
10480
+ }))?.results;
10481
+ const result = results?.[0];
10482
+ if (!Array.isArray(results) || results.length !== 1 || result?.callId !== input.callId || typeof result.result?.isError !== "boolean") throw new Error("The DSH tool batch did not return the requested call outcome");
10483
+ return result.result;
10484
+ }
10485
+ //#endregion
10486
+ //#region src/host/review-admission.ts
10487
+ const RESULT_TOOL = "mnemon_subagent_result";
10488
+ const CHECK_TIMEOUT_MS = 5e3;
10489
+ /** An in-turn no-op challenge through the mandatory review completion channel.
10490
+ * Inventory alone cannot see execution-time guards. No errors are classified as
10491
+ * permission failures, and an unavailable checkpoint is never a permanent ban.
10492
+ */
10493
+ var AutomaticReviewAdmission = class {
10494
+ challenges = /* @__PURE__ */ new Map();
10495
+ controllers = /* @__PURE__ */ new Set();
10496
+ draining = /* @__PURE__ */ new WeakSet();
10497
+ disposed = false;
10498
+ /** Only an unexpired, host-issued challenge for this exact Agent can no-op. */
10499
+ acknowledge(args, execution) {
10500
+ if (typeof args !== "object" || args === null || !("requestId" in args) || typeof args.requestId !== "string") return false;
10501
+ const challenge = this.challenges.get(args.requestId);
10502
+ if (challenge === void 0 || challenge.agent !== execution.agent || challenge.execution !== void 0) return false;
10503
+ execution.signal.throwIfAborted();
10504
+ challenge.execution = execution;
10505
+ return true;
10506
+ }
10507
+ dispose() {
10508
+ this.disposed = true;
10509
+ this.challenges.clear();
10510
+ for (const controller of this.controllers) controller.abort();
10511
+ }
10512
+ async check(agent, signal) {
10513
+ signal.throwIfAborted();
10514
+ const tools = agent.ctx.tools;
10515
+ if (this.disposed || tools?.execute === void 0 || this.draining.has(agent) || openAgentTurn(agent) === void 0) return false;
10516
+ const controller = new AbortController();
10517
+ this.controllers.add(controller);
10518
+ const combined = AbortSignal.any([signal, controller.signal]);
10519
+ const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
10520
+ timeout.unref?.();
10521
+ const attempt = async (codeMode) => {
10522
+ combined.throwIfAborted();
10523
+ const requestId = randomUUID();
10524
+ const callId = randomUUID();
10525
+ const challenge = {
10526
+ agent,
10527
+ accepted: false
10528
+ };
10529
+ this.challenges.set(requestId, challenge);
10530
+ const revoke = () => this.challenges.delete(requestId);
10531
+ combined.addEventListener("abort", revoke, { once: true });
10532
+ const stops = [];
10533
+ let executing = false;
10534
+ const cleanup = () => {
10535
+ for (const stop of stops.reverse()) stop();
10536
+ this.draining.delete(agent);
10537
+ };
10538
+ try {
10539
+ stops.push(agent.ctx.on("tools/result", ((execution, result) => {
10540
+ if (execution === challenge.execution) challenge.accepted = result.isError === false;
10541
+ })));
10542
+ stops.push(agent.ctx.on("tools/pre-execute", (async (execution, next) => {
10543
+ const decision = await next();
10544
+ return execution.agent === agent && (execution.callId === callId || execution.rootCallId === callId) && decision.kind === "ask" ? { kind: "cancel" } : decision;
10545
+ }), { prepend: true }));
10546
+ const args = {
10547
+ requestId,
10548
+ result: {}
10549
+ };
10550
+ const pending = executeHostTool(tools, {
10551
+ callId,
10552
+ agent,
10553
+ signal: combined,
10554
+ name: codeMode ? "run_code" : RESULT_TOOL,
10555
+ arguments: codeMode ? {
10556
+ description: "Check automatic memory review availability",
10557
+ code: `await tools.mnemon_subagent_result(${JSON.stringify(args)})`
10558
+ } : args
10559
+ });
10560
+ executing = true;
10561
+ this.draining.add(agent);
10562
+ const outcome = await pending.finally(cleanup);
10563
+ combined.throwIfAborted();
10564
+ return challenge.execution !== void 0 && challenge.accepted && outcome.isError === false;
10565
+ } finally {
10566
+ this.challenges.delete(requestId);
10567
+ combined.removeEventListener("abort", revoke);
10568
+ if (!executing) cleanup();
10569
+ }
10570
+ };
10571
+ try {
10572
+ if (await attempt(false)) return true;
10573
+ return (usesToolBatchExecution(tools) || tools.get?.("run_code", agent) !== void 0) && await attempt(true);
10574
+ } catch {
10575
+ signal.throwIfAborted();
10576
+ return false;
10577
+ } finally {
10578
+ clearTimeout(timeout);
10579
+ this.controllers.delete(controller);
10580
+ }
10581
+ }
10582
+ };
10583
+ //#endregion
10584
+ //#region src/host/subagent.ts
10585
+ function evidenceInsights(evidence) {
10586
+ return evidence.items.map((item) => {
10587
+ return {
10588
+ ...optionalObject(item.provenance) ?? {},
10589
+ id: item.id,
10590
+ content: item.text,
10591
+ ...item.score === void 0 ? {} : { score: item.score },
10592
+ ...item.revision === void 0 ? {} : { revision: item.revision }
10593
+ };
10594
+ });
10595
+ }
10596
+ /**
10597
+ * Every task dsh-mnemon delegates to a child Agent starts with one line of its own, such as
10598
+ * `[dsh-mnemon:delegated review]` (#393). DSH records the task as the child's prompt from a
10599
+ * person, so a plugin that reads sessions, such as one that collects a workspace's work, can
10600
+ * rely on this line, documented as stable, to leave Mnemon's own children out.
10601
+ */
10602
+ const DELEGATED_TASK_PREFIX = "[dsh-mnemon:delegated ";
10603
+ function delegatedTaskLine(operation) {
10604
+ return `${DELEGATED_TASK_PREFIX}${operation}]`;
10605
+ }
10606
+ const WRITE_TOOLS = [
10607
+ ...[
10608
+ "mnemon_memory_bodies",
10609
+ "mnemon_recall",
10610
+ "mnemon_related"
10611
+ ],
10612
+ "mnemon_remember",
10613
+ "mnemon_link",
10614
+ "mnemon_forget",
10615
+ "mnemon_memory_body_create",
10616
+ "mnemon_memory_body_update",
10617
+ "mnemon_memory_body_merge"
10618
+ ];
10619
+ const AUTONOMOUS_WRITE_TOOLS = WRITE_TOOLS.filter((tool) => tool !== "mnemon_forget");
10620
+ const EXPLICIT_WRITE_TOOLS = WRITE_TOOLS;
10621
+ const REVIEW_TOOLS = [
10622
+ ...["mnemon_document_search"],
10623
+ "mnemon_runtime_memory",
10624
+ "mnemon_document_create"
10625
+ ];
10626
+ const RESULT_TOOL_NAME = "mnemon_subagent_result";
10627
+ /** How many finished delegated children stay refused memory tools if DSH resumes one. */
10628
+ const FINISHED_CHILDREN = 1e3;
10629
+ const FINISHED_CHILD_REFUSAL = "This Mnemon task has finished, so it no longer reads or changes memory. Reply without memory tools.";
10630
+ const RESULT_TOOL_INPUT_SCHEMA = {
10631
+ type: "object",
10632
+ properties: {
10633
+ requestId: {
10634
+ type: "string",
10635
+ minLength: 1
10636
+ },
10637
+ result: {
10638
+ type: "object",
10639
+ additionalProperties: true
10640
+ }
10641
+ },
10642
+ required: ["requestId", "result"],
10643
+ additionalProperties: false
10644
+ };
10645
+ const RESULT_TOOL_OUTPUT_SCHEMA = {
10646
+ type: "object",
10647
+ properties: { recorded: {
10648
+ type: "boolean",
10649
+ const: true
10650
+ } },
10651
+ required: ["recorded"],
10652
+ additionalProperties: false
10653
+ };
10654
+ const WRITE_OPERATION_RESULT_TOOL = {
10655
+ remember: "mnemon_remember",
10656
+ "supervised-writeback": "mnemon_remember",
10657
+ link: "mnemon_link",
10658
+ forget: "mnemon_forget",
10659
+ "create-memory-body": "mnemon_memory_body_create",
10660
+ "update-memory-body": "mnemon_memory_body_update",
10661
+ "merge-memory-bodies": "mnemon_memory_body_merge"
10662
+ };
10663
+ var IdleReviewError = class extends Error {
10664
+ review;
10665
+ constructor(message, review, options) {
10666
+ super(message, options);
10667
+ this.review = review;
10668
+ this.name = "IdleReviewError";
10669
+ }
10670
+ };
10671
+ /** What a memory saved without a vector means for recall, and how its vector comes. */
10672
+ const UNEMBEDDED_HINT = "Saved without a vector, because embedding did not succeed: semantic matching misses it until its vector is filled in, while keyword, link and recency recall still reach it. Once the embedding service answers, the user fills it in from Test status in the Mnemon Native embedding settings, which runs through the storage root and embedding settings DSH uses; a terminal mnemon command may reach neither.";
10673
+ /** The memories a write saved without a vector: Mnemon Native says so with `embedded: false` (#388). */
10674
+ function withoutVector(receipts) {
10675
+ return receipts.filter((receipt) => isTool(receipt.name, "mnemon_remember")).flatMap((receipt) => {
10676
+ const value = optionalObject(receipt.value);
10677
+ if (value?.embedded !== false) return [];
10678
+ return [{
10679
+ ...typeof value.memoryBodyId === "string" ? { memoryBodyId: value.memoryBodyId } : {},
10680
+ ...typeof value.id === "string" ? { id: value.id } : {}
10681
+ }];
10682
+ });
10683
+ }
10684
+ /** A Memory Space write tool: each switches on the space it goes to. */
10685
+ const SPACE_WRITE_TOOLS = /* @__PURE__ */ new Set(["mnemon_remember", "mnemon_link"]);
10686
+ /**
10687
+ * Holds a delegated write to the Memory Space its caller named, and keeps a writer
10688
+ * from switching a space back on: a write or a link switches on the space it goes to,
10689
+ * so a space that was inactive when the run started takes one only when the caller
10690
+ * named it (#387). A space the writer creates is not in `inactive` and takes its first
10691
+ * write. Saving a memory never merges spaces: a merge switches spaces on and off, and
10692
+ * is a request of its own.
10693
+ */
10694
+ function spaceWriteGuard(target, inactive) {
10695
+ return (execution) => {
10696
+ if (isTool(execution.name, "mnemon_memory_body_merge")) return "Saving a memory never merges Memory Spaces; a merge is a request of its own. Write the memory, or finish without writing.";
10697
+ const input = optionalObject(execution.arguments) ?? {};
10698
+ const space = typeof input.memoryBodyId === "string" ? input.memoryBodyId.trim() : void 0;
10699
+ if (target !== void 0) {
10700
+ if (execution.name !== void 0 && SPACE_WRITE_TOOLS.has(execution.name) && space !== target) return `This write was requested for Memory Space ${JSON.stringify(target)}. Call ${execution.name} with memoryBodyId ${JSON.stringify(target)}, or finish without writing and say why it does not fit.`;
10701
+ if (isTool(execution.name, "mnemon_memory_body_create", "mnemon_memory_body_update")) return `This write was requested for Memory Space ${JSON.stringify(target)}, so it creates or changes no space. Write there, or finish without writing and say why it does not fit.`;
10702
+ return;
10703
+ }
10704
+ if (space === void 0 || !inactive.has(space)) return void 0;
10705
+ if (execution.name !== void 0 && SPACE_WRITE_TOOLS.has(execution.name)) return `Memory Space ${JSON.stringify(space)} is switched off, and a write would switch it on. Write to an active space, or finish without writing and say which space would fit.`;
10706
+ if (isTool(execution.name, "mnemon_memory_body_update") && input.active === true) return `Memory Space ${JSON.stringify(space)} is switched off; only the user switches it on.`;
10707
+ };
10708
+ }
10709
+ /**
10710
+ * Why a write cannot go to the Memory Space its caller named, before a writer runs (#387):
10711
+ * the space is unknown, its Provider is off or takes no writes, or it is switched off and
10712
+ * the caller did not ask to switch it on. A model names a space by its description, the
10713
+ * judgement that misplaced memories before; switching a space back on is its own request.
10714
+ */
10715
+ function targetProblem(spaces, target, activate) {
10716
+ const body = spaces.find((candidate) => candidate.id === target);
10717
+ if (body === void 0) {
10718
+ const known = spaces.slice(0, 20).map((candidate) => `${JSON.stringify(candidate.id)} (${candidate.name}${candidate.active ? "" : ", switched off"})`).join(", ");
10719
+ return `Memory Space ${JSON.stringify(target)} does not exist. Name one by its id${known === "" ? "; there is none yet." : `: ${known}.`}`;
10720
+ }
10721
+ if (body.providerEnabled === false) return `Memory Space ${JSON.stringify(target)} uses ${body.provider.label}, which is turned off on the dsh-mnemon page under Plugins. Name another space.`;
10722
+ if (body.provider.capabilities.remember !== true) return `Memory Space ${JSON.stringify(target)} (${body.provider.label}) does not take writes. Name another space.`;
10723
+ if (!body.active && !activate) return `Memory Space ${JSON.stringify(target)} (${body.name}) is switched off, and a write would switch it back on. If the user wants it back, switch it on first with mnemon_memory_body_update (active: true); otherwise name an active space.`;
10724
+ }
10725
+ /** Whether a write receipt put the memory in its space, or handed it to a Provider that confirms later. */
10303
10726
  function writeLanded(value) {
10304
10727
  if (mutationResultCommitted(value)) return true;
10305
10728
  const state = [
@@ -10343,6 +10766,7 @@ var MnemonSubagentCoordinator = class {
10343
10766
  /** Delegated children whose run has ended, newest last, at most FINISHED_CHILDREN. */
10344
10767
  finishedChildren = /* @__PURE__ */ new Set();
10345
10768
  disposed = false;
10769
+ reviewAdmission = new AutomaticReviewAdmission();
10346
10770
  constructor(subagents, runtimeSource, resultRuntime, taskAgentModelResolver, runtimeMaintenanceMaxTokensResolver, runtimeMaintenanceTaskRunner) {
10347
10771
  this.subagents = subagents;
10348
10772
  this.runtimeSource = runtimeSource;
@@ -10363,6 +10787,7 @@ var MnemonSubagentCoordinator = class {
10363
10787
  }]
10364
10788
  },
10365
10789
  execute: async (args, execution) => {
10790
+ if (this.reviewAdmission.acknowledge(args, execution)) return { recorded: true };
10366
10791
  if (!isSubagent(execution.agent)) throw new Error("Mnemon subagent result tools are restricted to delegated children");
10367
10792
  execution.signal.throwIfAborted();
10368
10793
  assertDshOutputValue(RESULT_TOOL_INPUT_SCHEMA, args);
@@ -10377,6 +10802,7 @@ var MnemonSubagentCoordinator = class {
10377
10802
  }
10378
10803
  dispose() {
10379
10804
  this.disposed = true;
10805
+ this.reviewAdmission.dispose();
10380
10806
  this.resultRequests.clear();
10381
10807
  const resumeGuard = this.disposeResumeGuard;
10382
10808
  this.disposeResumeGuard = void 0;
@@ -10922,7 +11348,28 @@ ${naturalRequest(request, target === void 0 ? REQUEST_LABELS : TARGETED_WRITE_LA
10922
11348
  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
11349
  }
10924
11350
  }
10925
- async review(parent, signal) {
11351
+ /** Cheap scheduler admission: no model task when every review write target is unavailable. */
11352
+ canReview(parent) {
11353
+ return this.reviewTools(parent).some((tool) => tool !== "mnemon_document_search");
11354
+ }
11355
+ /** Automatic review admission; explicit user-requested reviews are unchanged. */
11356
+ checkReviewAccess(parent, signal) {
11357
+ return this.reviewAdmission.check(parent, signal);
11358
+ }
11359
+ availableTools(parent, tools) {
11360
+ const registry = parent.ctx?.tools;
11361
+ return typeof registry?.get === "function" ? tools.filter((name) => registry.get(name, parent) !== void 0) : [...tools];
11362
+ }
11363
+ reviewTools(parent) {
11364
+ const config = this.runtimeSource.config;
11365
+ if (!config.writeEnabled) return [];
11366
+ return this.availableTools(parent, REVIEW_TOOLS).filter((tool) => {
11367
+ if (tool === "mnemon_document_search") return true;
11368
+ const type = memoryTool(tool).sourceTypeId;
11369
+ return (tool !== "mnemon_runtime_memory" || config.idleReview.runtimeMemory) && allowsParticipation(config, type, "write", "automatic");
11370
+ });
11371
+ }
11372
+ async review(parent, signal, admission) {
10926
11373
  const config = this.runtimeSource.config.idleReview;
10927
11374
  if (idleReviewBlockReason(parent, config.agentTeams) !== void 0) return {
10928
11375
  delegated: false,
@@ -10930,6 +11377,13 @@ ${naturalRequest(request, target === void 0 ? REQUEST_LABELS : TARGETED_WRITE_LA
10930
11377
  summary: "Idle review is paused by the Agent Teams compatibility setting.",
10931
11378
  documentIds: []
10932
11379
  };
11380
+ const skipped = {
11381
+ delegated: false,
11382
+ action: "skipped",
11383
+ summary: "Idle review has no applicable writable Source target.",
11384
+ documentIds: []
11385
+ };
11386
+ if (!this.canReview(parent)) return skipped;
10933
11387
  let preferred = config.provider;
10934
11388
  if (preferred === "fork") try {
10935
11389
  this.provider("fork");
@@ -10942,24 +11396,40 @@ ${naturalRequest(request, target === void 0 ? REQUEST_LABELS : TARGETED_WRITE_LA
10942
11396
  };
10943
11397
  preferred = "spawn";
10944
11398
  }
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
- };
11399
+ const execution = await this.runtimeSource.executions.workflow(parent, "review", signal);
11400
+ try {
11401
+ if (admission !== void 0) {
11402
+ execution.signal.throwIfAborted();
11403
+ admission();
11404
+ }
11405
+ const view = execution.context.view;
11406
+ const instances = execution.graph.memoryComposition.generation(view.runtimeGeneration).sourceInstances();
11407
+ const tools = this.reviewTools(parent).filter((tool) => {
11408
+ if (tool === "mnemon_document_search") return true;
11409
+ const entry = memoryTool(tool);
11410
+ return view.actionOffers.some((offer) => offer.authority === void 0 && offer.sourceActionId === entry.operation && instances.some((instance) => instance.sourceInstanceKey === offer.sourceInstanceKey && instance.sourceTypeId === entry.sourceTypeId));
11411
+ });
11412
+ const writers = tools.filter((tool) => tool !== "mnemon_document_search");
11413
+ if (writers.length === 0) return skipped;
11414
+ const prompt = preferred === "fork" ? "Review the inherited completed checkpoint now." : reviewCheckpoint(parent.session, config.maxContextChars);
11415
+ const base = reviewPersona(tools.includes("mnemon_runtime_memory"));
11416
+ const persona = preferred === "fork" ? base : base.replaceAll("inherited", "supplied bounded");
11417
+ 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);
11418
+ const value = object$6(result.structured);
11419
+ return {
11420
+ delegated: true,
11421
+ runId,
11422
+ provider,
11423
+ summary: typeof value.summary === "string" ? value.summary : "",
11424
+ action: typeof value.action === "string" ? value.action : "failed",
11425
+ memoryBodyIds: strings(value.memoryBodyIds),
11426
+ documentIds: strings(value.documentIds)
11427
+ };
11428
+ } finally {
11429
+ execution.release();
11430
+ }
10961
11431
  }
10962
- async delegate(parent, operation, label, prompt, tools, outputSchema, signal, preferredProvider = "spawn", persona = WRITE_PERSONA, recovery, guard, route) {
11432
+ async delegate(parent, operation, label, prompt, tools, outputSchema, signal, preferredProvider = "spawn", persona = WRITE_PERSONA, recovery, guard, route, admission) {
10963
11433
  if (this.disposed) throw new Error("dsh-mnemon subagent coordinator is disposed");
10964
11434
  const provider = this.provider(preferredProvider);
10965
11435
  assertDshOutputSchema(outputSchema);
@@ -11076,19 +11546,25 @@ This is the only completion channel for this run. Do not finish with a plain-tex
11076
11546
  model: fixed.model
11077
11547
  };
11078
11548
  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));
11549
+ const start = () => startWithContinuationTurns(host, parent, () => {
11550
+ if (operation === "review") {
11551
+ signal.throwIfAborted();
11552
+ admission?.();
11553
+ }
11554
+ return startSubagent(this.subagents, host.agents, provider, {
11555
+ label,
11556
+ prompt: [{
11557
+ type: "text",
11558
+ text: `${delegatedTaskLine(operation)}\n${prompt}`
11559
+ }],
11560
+ parent,
11561
+ signal,
11562
+ ...resolvedAgentOptions === void 0 ? {} : { agentOptions: resolvedAgentOptions },
11563
+ maxDepth: 1,
11564
+ toolFilter: { allow: [...this.availableTools(parent, tools), resultToolName] },
11565
+ persona: completionPersona
11566
+ }, host);
11567
+ });
11092
11568
  run = operation === "review" ? await startGuardedReview(this.resultRuntime, parent, [...tools, resultToolName], start, (child) => {
11093
11569
  reviewChildId = child.id;
11094
11570
  }, reviewLayerPolicy()) : guard !== void 0 ? await startGuardedChild(this.resultRuntime, parent, start, guard, {
@@ -11276,7 +11752,7 @@ function bounded(value, maximum) {
11276
11752
  if (typeof value !== "string") return void 0;
11277
11753
  const normalized = value.replace(/\s+/gu, " ").trim();
11278
11754
  if (normalized === "") return void 0;
11279
- return normalized.length <= maximum ? normalized : normalized.slice(0, maximum - 1) + "…";
11755
+ return truncate(normalized, maximum);
11280
11756
  }
11281
11757
  function item(value, index) {
11282
11758
  const entry = object$5(value);
@@ -11430,337 +11906,223 @@ function emptyActivity(turn) {
11430
11906
  /**
11431
11907
  * Incremental durable-log projection. Repeated UI reads process only events
11432
11908
  * appended since the previous snapshot instead of rescanning the full session.
11433
- */
11434
- var TurnActivityProjection = class {
11435
- eventCount = 0;
11436
- lastEventSeq;
11437
- pending = /* @__PURE__ */ new Map();
11438
- byTurn = /* @__PURE__ */ new Map();
11439
- reset() {
11440
- this.eventCount = 0;
11441
- this.lastEventSeq = void 0;
11442
- this.pending.clear();
11443
- this.byTurn.clear();
11444
- }
11445
- snapshot(events) {
11446
- const currentLastSeq = events.at(-1)?.seq;
11447
- if (events.length < this.eventCount || events.length === this.eventCount && this.lastEventSeq !== currentLastSeq) this.reset();
11448
- for (let index = this.eventCount; index < events.length; index += 1) this.consume(events[index]);
11449
- this.eventCount = events.length;
11450
- this.lastEventSeq = currentLastSeq;
11451
- return {
11452
- cursor: typeof currentLastSeq === "number" ? currentLastSeq : events.length,
11453
- activities: [...this.byTurn.values()].sort((left, right) => left.turn - right.turn).map((activity) => ({
11454
- ...activity,
11455
- names: [...activity.names],
11456
- retrieved: activity.retrieved.map((read) => ({
11457
- ...read,
11458
- items: read.items.map((item) => ({ ...item }))
11459
- })),
11460
- writebacks: activity.writebacks.map((write) => ({
11461
- ...write,
11462
- item: { ...write.item }
11463
- }))
11464
- }))
11465
- };
11466
- }
11467
- consume(event) {
11468
- if (event.type === "tool/call") {
11469
- const turn = eventTurn$1(event);
11470
- const callId = event.data.callId;
11471
- const name = event.data.name;
11472
- if (turn !== void 0 && typeof callId === "string" && typeof name === "string" && name.startsWith("mnemon_")) this.pending.set(callId, {
11473
- turn,
11474
- name
11475
- });
11476
- return;
11477
- }
11478
- if (event.type !== "tool/result") return;
11479
- const callId = resultCallId(event);
11480
- if (callId === void 0) return;
11481
- const call = this.pending.get(callId);
11482
- if (call === void 0) return;
11483
- this.pending.delete(callId);
11484
- let activity = this.byTurn.get(call.turn);
11485
- if (activity === void 0) {
11486
- activity = emptyActivity(call.turn);
11487
- this.byTurn.set(call.turn, activity);
11488
- }
11489
- activity.count += 1;
11490
- activity.names.push(call.name);
11491
- if (event.data.error !== void 0) {
11492
- activity.failures += 1;
11493
- return;
11494
- }
11495
- const semantic = parseMnemonActivityMeta(event.data.meta, callId, call.name);
11496
- if (semantic.read !== void 0) activity.retrieved.push(semantic.read);
11497
- if (semantic.writeback !== void 0) activity.writebacks.push(semantic.writeback);
11498
- const counted = counter(call.name);
11499
- if (counted !== void 0) activity[counted] += 1;
11500
- }
11501
- };
11502
- //#endregion
11503
- //#region src/host/view-presentation.ts
11504
- /**
11505
- * The named tools bound to each Source type's Routes or Actions, from the tool table: these are
11506
- * existing DSH product-tool bindings, not Core operation names. Tools that share one operation are
11507
- * listed together, in table order, as the View's tool list names them.
11508
- */
11509
- function namedBindings(kind) {
11510
- const bindings = {};
11511
- for (const [name, tool] of memoryTools()) {
11512
- if (tool.sourceTypeId === void 0 || tool.operation === void 0 || tool.kind === "write" !== (kind === "action")) continue;
11513
- const operations = bindings[tool.sourceTypeId] ??= {};
11514
- const listed = operations[tool.operation];
11515
- operations[tool.operation] = listed === void 0 ? name : `${listed} / ${name}`;
11516
- }
11517
- return bindings;
11518
- }
11519
- const ROUTE_TOOLS = namedBindings("route");
11520
- const ACTION_TOOLS = namedBindings("action");
11521
- const NAMED_SOURCE_TYPES = [.../* @__PURE__ */ new Set([...Object.keys(ROUTE_TOOLS), ...Object.keys(ACTION_TOOLS)])];
11522
- /** Use a short availability list only where the Host has the matching named binding.
11523
- * Unbound external operations retain exact ids and schemas in the generic envelope. */
11524
- function modelMemoryWake(graph, turn) {
11525
- const generation = graph.memoryComposition.generation(turn.view.runtimeGeneration);
11526
- if (generation === void 0) throw new Error("Cannot present an unpinned Memory generation");
11527
- return graph.composableTurns.memoryWake(turn.view.id, memoryWakeBindings(generation, turn.view));
11528
- }
11529
- function presentMemoryWake(generation, view) {
11530
- return createMemoryWake(view, memoryWakeBindings(generation, view));
11531
- }
11532
- /**
11533
- * Why a generic View tool names no id this turn's View offers, told apart (#390); undefined when
11534
- * the id is offered. It names only what the MNEMON VIEW ROUTES envelope shows: an operation bound
11535
- * to a named tool is offered through that tool, and the envelope does not list its id.
11536
- */
11537
- function unofferedViewId(graph, turn, kind, id) {
11538
- const view = turn.view;
11539
- const offered = kind === "route" ? view.routes.map((route) => ({
11540
- id: route.id,
11541
- operation: route.sourceRouteId
11542
- })) : view.actionOffers.map((offer) => ({
11543
- id: offer.id,
11544
- operation: offer.sourceActionId
11545
- }));
11546
- const label = kind === "route" ? "memory View Route" : "memory ActionOffer";
11547
- if (typeof id !== "string" || id === "") return `${label} needs ${kind === "route" ? "routeId" : "offerId"}, the exact id of one it offers`;
11548
- if (offered.some((entry) => entry.id === id)) return void 0;
11549
- const generation = graph.memoryComposition.generation(view.runtimeGeneration);
11550
- const bindings = generation === void 0 ? {} : memoryWakeBindings(generation, view);
11551
- const named = (kind === "route" ? bindings.routes : bindings.actions) ?? {};
11552
- const short = id.split("/").at(-1);
11553
- const tools = [...new Set(offered.filter((entry) => named[entry.id] !== void 0 && (entry.operation === short || entry.id.split("/").at(-1) === short)).map((entry) => named[entry.id]))];
11554
- if (tools.length > 0) return `${label} is unavailable: ${id}; this View offers it as ${tools.join(" and ")}, so call that tool`;
11555
- const shown = offered.filter((entry) => named[entry.id] === void 0).map((entry) => entry.id);
11556
- if (shown.length === 0) return `${label} is unavailable: ${id}; MNEMON VIEW ROUTES offers none by id this turn, only the named tools in MNEMON VIEW TOOLS`;
11557
- const near = shown.filter((candidate) => candidate.split("/").at(-1) === short);
11558
- if (near.length === 1) return `${label} is unavailable: ${id}; ids are exact as MNEMON VIEW ROUTES lists them: ${JSON.stringify(near[0])}`;
11559
- const listed = [...near, ...shown.filter((candidate) => !near.includes(candidate))];
11560
- return `${label} is unavailable: ${id}; MNEMON VIEW ROUTES offers ${listed.slice(0, 12).map((candidate) => JSON.stringify(candidate)).join(", ")}${listed.length > 12 ? ` and ${listed.length - 12} more` : ""}`;
11561
- }
11562
- function memoryWakeBindings(generation, view) {
11563
- const instances = generation.sourceInstances();
11564
- const routes = {}, actions = {};
11565
- for (const typeId of NAMED_SOURCE_TYPES) {
11566
- const selected = namedToolInstance(instances.filter((source) => source.sourceTypeId === typeId), typeId);
11567
- if (selected === void 0) continue;
11568
- for (const route of view.routes.filter((route) => route.sourceInstanceKey === selected.sourceInstanceKey)) {
11569
- if (view.readGrants.find((grant) => grant.id === route.readGrantId)?.schema !== `dsh-mnemon.${typeId}/v1`) continue;
11570
- const tool = ROUTE_TOOLS[typeId]?.[route.sourceRouteId];
11571
- if (tool !== void 0) routes[route.id] = tool;
11572
- }
11573
- for (const action of view.actionOffers.filter((action) => action.sourceInstanceKey === selected.sourceInstanceKey)) {
11574
- const tool = ACTION_TOOLS[typeId]?.[action.sourceActionId];
11575
- if (tool !== void 0) actions[action.id] = tool;
11576
- }
11577
- }
11578
- return {
11579
- routes,
11580
- actions
11581
- };
11582
- }
11583
- /** Explicitly pick display fields; never copy grants, authority, or opaque Source state. */
11584
- function inspectMemoryView(generation, view, state, turn, memoryText) {
11585
- return structuredClone({
11586
- id: view.id,
11587
- digest: view.digest,
11588
- generationId: view.runtimeGeneration,
11589
- createdAt: view.createdAt,
11590
- state,
11591
- ...turn === void 0 ? {} : { turn },
11592
- strategyTypeId: view.strategyTypeId,
11593
- strategyInstanceKey: view.strategyInstanceKey,
11594
- extensions: view.strategyExtensions ?? [],
11595
- projection: view.projection.map(({ id, sourceInstanceKey, mode, text, revision }) => ({
11596
- id,
11597
- sourceInstanceKey,
11598
- mode,
11599
- text,
11600
- revision
11601
- })),
11602
- sourcePresentations: view.sourcePresentations ?? [],
11603
- routes: view.routes.map(({ id, sourceInstanceKey, sourceRouteId, description, maxCalls }) => ({
11604
- id,
11605
- sourceInstanceKey,
11606
- operationId: sourceRouteId,
11607
- description,
11608
- maxCalls
11609
- })),
11610
- actions: view.actionOffers.map(({ id, sourceInstanceKey, sourceActionId, description }) => ({
11611
- id,
11612
- sourceInstanceKey,
11613
- operationId: sourceActionId,
11614
- description
11615
- })),
11616
- memoryText: memoryText ?? presentMemoryWake(generation, view).text,
11617
- ...view.guidance === void 0 ? {} : { guidance: view.guidance },
11618
- diagnostics: (view.diagnostics ?? []).map((diagnostic) => diagnostic.message)
11619
- });
11620
- }
11621
- //#endregion
11622
- //#region src/host/agent-memory-turn.ts
11623
- /** The durable log, not a parent session id, identifies the executing turn. */
11624
- function openAgentTurn(agent) {
11625
- let open;
11626
- for (const event of agent.session.snapshotEvents()) {
11627
- const turn = typeof event.data.turn === "number" ? event.data.turn : void 0;
11628
- if (event.type === "turn/start" && turn !== void 0) open = turn;
11629
- else if (event.type === "turn/end" && turn === open) open = void 0;
11630
- }
11631
- return open;
11632
- }
11633
- /**
11634
- * One Agent owns each turn pin. A child additionally retains its delegation
11635
- * across parent completion, collection, and runtime swaps until it is disposed.
11636
- */
11637
- var AgentMemoryTurn = class {
11638
- agent;
11639
- runtime;
11640
- delegation;
11641
- pinned;
11642
- /** A turn that runs without memory because no composition was serving when it began. */
11643
- unavailable;
11644
- warned;
11645
- pending;
11646
- generation = 0;
11647
- closed = false;
11648
- lastInspection;
11649
- lastWorkspace;
11650
- releaseDelegation;
11651
- constructor(agent, runtime, delegation) {
11652
- this.agent = agent;
11653
- this.runtime = runtime;
11654
- this.delegation = delegation;
11655
- if (delegation === void 0) return;
11656
- this.releaseDelegation = runtime.executions.retain(agent, delegation);
11657
- }
11658
- get current() {
11659
- return this.pinned;
11660
- }
11661
- /** Why the current turn has no View, when memory could not be composed for it. */
11662
- get unavailableReason() {
11663
- return this.unavailable?.reason;
11664
- }
11665
- inspect(workspaceRoot) {
11666
- if (this.lastInspection === void 0) return void 0;
11667
- if (workspaceRoot !== void 0 && resolve(workspaceRoot) !== this.lastWorkspace) return void 0;
11668
- return structuredClone({
11669
- ...this.lastInspection,
11670
- state: this.pinned === void 0 ? "recent" : "active"
11671
- });
11672
- }
11673
- clearInspection() {
11674
- this.lastInspection = void 0;
11675
- this.lastWorkspace = void 0;
11909
+ */
11910
+ var TurnActivityProjection = class {
11911
+ eventCount = 0;
11912
+ lastEventSeq;
11913
+ pending = /* @__PURE__ */ new Map();
11914
+ byTurn = /* @__PURE__ */ new Map();
11915
+ reset() {
11916
+ this.eventCount = 0;
11917
+ this.lastEventSeq = void 0;
11918
+ this.pending.clear();
11919
+ this.byTurn.clear();
11676
11920
  }
11677
- /** Capture now; descendants must not resolve a later parent turn on demand. */
11678
- delegate() {
11679
- if (this.closed) throw new Error("memory Agent lifetime has ended");
11680
- if (this.pinned !== void 0) return {
11681
- graph: this.pinned.graph,
11682
- scope: this.pinned.context.scope,
11683
- viewId: this.pinned.context.view.id
11684
- };
11685
- if (this.delegation !== void 0) return this.delegation;
11686
- const graph = this.runtime.forAgent(this.agent);
11921
+ snapshot(events) {
11922
+ const currentLastSeq = events.at(-1)?.seq;
11923
+ if (events.length < this.eventCount || events.length === this.eventCount && this.lastEventSeq !== currentLastSeq) this.reset();
11924
+ for (let index = this.eventCount; index < events.length; index += 1) this.consume(events[index]);
11925
+ this.eventCount = events.length;
11926
+ this.lastEventSeq = currentLastSeq;
11687
11927
  return {
11688
- graph,
11689
- scope: agentScope(this.agent, graph.config)
11928
+ cursor: typeof currentLastSeq === "number" ? currentLastSeq : events.length,
11929
+ activities: [...this.byTurn.values()].sort((left, right) => left.turn - right.turn).map((activity) => ({
11930
+ ...activity,
11931
+ names: [...activity.names],
11932
+ retrieved: activity.retrieved.map((read) => ({
11933
+ ...read,
11934
+ items: read.items.map((item) => ({ ...item }))
11935
+ })),
11936
+ writebacks: activity.writebacks.map((write) => ({
11937
+ ...write,
11938
+ item: { ...write.item }
11939
+ }))
11940
+ }))
11690
11941
  };
11691
11942
  }
11692
- async begin(turn, signal) {
11693
- if (this.closed) throw new Error("memory Agent lifetime has ended");
11694
- signal?.throwIfAborted();
11695
- if (this.pinned?.turn === turn || this.unavailable?.turn === turn) return;
11696
- if (this.pending?.turn === turn) return this.pending.result;
11697
- this.end();
11698
- const generation = this.generation;
11699
- const controller = new AbortController();
11700
- const abort = () => controller.abort(signal.reason);
11701
- signal?.addEventListener("abort", abort, { once: true });
11702
- const result = this.pin(turn, generation, controller.signal).catch((error) => {
11703
- if (!isMemoryCompositionUnavailable(error) || this.closed || this.generation !== generation) throw error;
11704
- this.unavailable = {
11943
+ consume(event) {
11944
+ if (event.type === "tool/call") {
11945
+ const turn = eventTurn$1(event);
11946
+ const callId = event.data.callId;
11947
+ const name = event.data.name;
11948
+ if (turn !== void 0 && typeof callId === "string" && typeof name === "string" && name.startsWith("mnemon_")) this.pending.set(callId, {
11705
11949
  turn,
11706
- reason: error.message
11707
- };
11708
- this.clearInspection();
11709
- if (this.warned !== error.message) console.warn(`[dsh-mnemon] this turn runs without memory: ${error.message}`);
11710
- this.warned = error.message;
11711
- });
11712
- this.pending = {
11713
- turn,
11714
- generation,
11715
- controller,
11716
- result
11717
- };
11950
+ name
11951
+ });
11952
+ return;
11953
+ }
11954
+ if (event.type !== "tool/result") return;
11955
+ const callId = resultCallId(event);
11956
+ if (callId === void 0) return;
11957
+ const call = this.pending.get(callId);
11958
+ if (call === void 0) return;
11959
+ this.pending.delete(callId);
11960
+ let activity = this.byTurn.get(call.turn);
11961
+ if (activity === void 0) {
11962
+ activity = emptyActivity(call.turn);
11963
+ this.byTurn.set(call.turn, activity);
11964
+ }
11965
+ activity.count += 1;
11966
+ activity.names.push(call.name);
11967
+ if (event.data.error !== void 0) {
11968
+ activity.failures += 1;
11969
+ return;
11970
+ }
11971
+ const semantic = parseMnemonActivityMeta(event.data.meta, callId, call.name);
11972
+ if (semantic.read !== void 0) activity.retrieved.push(semantic.read);
11973
+ if (semantic.writeback !== void 0) activity.writebacks.push(semantic.writeback);
11974
+ const counted = counter(call.name);
11975
+ if (counted !== void 0) activity[counted] += 1;
11976
+ }
11977
+ };
11978
+ //#endregion
11979
+ //#region src/host/idle-review-watermark.ts
11980
+ const MAX_STATE_BYTES = 16384;
11981
+ const MAX_TIMESTAMP = 864e13;
11982
+ function validAttempts(value) {
11983
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
11984
+ }
11985
+ function validTimestamp(value) {
11986
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= MAX_TIMESTAMP;
11987
+ }
11988
+ /** Messages deliberately omit filesystem paths, session ids and raw input. */
11989
+ function failure$2(error) {
11990
+ const code = error?.code;
11991
+ const detail = code === "EBUSY" ? " (busy)" : typeof code === "string" && /^[A-Z_]+$/u.test(code) ? ` (${code})` : "";
11992
+ 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 });
11993
+ }
11994
+ /** Host-local admission state, deliberately outside Packs and Git sync. */
11995
+ var IdleReviewWatermarkStore = class {
11996
+ sessionId;
11997
+ directory;
11998
+ path;
11999
+ constructor(directory, sessionId) {
12000
+ this.sessionId = sessionId;
12001
+ if (!sessionId || Buffer.byteLength(sessionId) > 4096 || Buffer.byteLength(JSON.stringify(sessionId)) > 16128) throw failure$2(void 0);
12002
+ this.directory = join(resolve(directory), "state", "idle-review");
12003
+ this.path = join(this.directory, createHash("sha256").update(sessionId).digest("hex") + ".json");
12004
+ }
12005
+ load() {
12006
+ let descriptor;
11718
12007
  try {
11719
- await result;
12008
+ try {
12009
+ descriptor = fs.openSync(this.path, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
12010
+ } catch (error) {
12011
+ if (error.code === "ENOENT") return {
12012
+ version: 1,
12013
+ sessionId: this.sessionId,
12014
+ attempts: 0,
12015
+ lastReviewAttemptAt: null
12016
+ };
12017
+ throw error;
12018
+ }
12019
+ const info = fs.fstatSync(descriptor);
12020
+ if (!info.isFile() || info.size > MAX_STATE_BYTES) throw new Error("invalid state file");
12021
+ const bytes = Buffer.alloc(16385);
12022
+ let length = 0;
12023
+ while (length < bytes.length) {
12024
+ const count = fs.readSync(descriptor, bytes, length, bytes.length - length, null);
12025
+ if (count === 0) break;
12026
+ length += count;
12027
+ }
12028
+ if (length > MAX_STATE_BYTES) throw new Error("oversized state");
12029
+ const value = JSON.parse(bytes.subarray(0, length).toString("utf8"));
12030
+ if (value === null || typeof value !== "object") throw new Error("invalid state");
12031
+ const state = value;
12032
+ 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");
12033
+ return {
12034
+ version: 1,
12035
+ sessionId: this.sessionId,
12036
+ attempts: state.attempts,
12037
+ lastReviewAttemptAt: state.lastReviewAttemptAt
12038
+ };
12039
+ } catch (error) {
12040
+ throw failure$2(error);
11720
12041
  } finally {
11721
- signal?.removeEventListener("abort", abort);
11722
- if (this.pending?.generation === generation) this.pending = void 0;
12042
+ if (descriptor !== void 0) try {
12043
+ fs.closeSync(descriptor);
12044
+ } catch (error) {
12045
+ throw failure$2(error);
12046
+ }
11723
12047
  }
11724
12048
  }
11725
- end(turn) {
11726
- if (turn !== void 0 && this.pinned?.turn !== turn && this.pending?.turn !== turn && this.unavailable?.turn !== turn) return;
11727
- this.generation += 1;
11728
- this.unavailable = void 0;
11729
- this.pending?.controller.abort(/* @__PURE__ */ new Error("memory turn ended during View preparation"));
11730
- this.pending = void 0;
11731
- const pinned = this.pinned;
11732
- this.pinned = void 0;
11733
- pinned?.release();
12049
+ /** Preserve already-used in-process quota on opt-in without admitting work. */
12050
+ restore(floor) {
12051
+ if (floor === void 0 || floor.attempts === 0 && floor.lastReviewAttemptAt === null) return this.load();
12052
+ return this.withLock(() => {
12053
+ const saved = this.load();
12054
+ const merged = this.merge(saved, floor);
12055
+ if (merged.attempts !== saved.attempts || merged.lastReviewAttemptAt !== saved.lastReviewAttemptAt) this.write(merged);
12056
+ return merged;
12057
+ });
11734
12058
  }
11735
- dispose() {
11736
- if (this.closed) return;
11737
- this.closed = true;
11738
- this.clearInspection();
12059
+ reserve(policy) {
12060
+ if (!validTimestamp(policy.now) || !validAttempts(policy.maxPerSession) || !validAttempts(policy.minIntervalMs)) throw failure$2(void 0);
12061
+ return this.withLock(() => {
12062
+ const saved = this.merge(this.load(), policy.floor);
12063
+ if (saved.attempts >= policy.maxPerSession || policy.now - (saved.lastReviewAttemptAt ?? -Infinity) < policy.minIntervalMs) return void 0;
12064
+ const next = {
12065
+ version: 1,
12066
+ sessionId: this.sessionId,
12067
+ attempts: saved.attempts + 1,
12068
+ lastReviewAttemptAt: policy.now
12069
+ };
12070
+ this.write(next);
12071
+ return next;
12072
+ });
12073
+ }
12074
+ merge(saved, floor) {
12075
+ if (floor === void 0) return saved;
12076
+ if (!validAttempts(floor.attempts) || (floor.attempts === 0 ? floor.lastReviewAttemptAt !== null : !validTimestamp(floor.lastReviewAttemptAt))) throw failure$2(void 0);
12077
+ const previous = Math.max(saved.lastReviewAttemptAt ?? -Infinity, floor.lastReviewAttemptAt ?? -Infinity);
12078
+ return {
12079
+ version: 1,
12080
+ sessionId: this.sessionId,
12081
+ attempts: Math.max(saved.attempts, floor.attempts),
12082
+ lastReviewAttemptAt: previous === -Infinity ? null : previous
12083
+ };
12084
+ }
12085
+ write(state) {
12086
+ const temporary = this.path + "." + randomUUID() + ".tmp";
11739
12087
  try {
11740
- this.end();
12088
+ fs.writeFileSync(temporary, JSON.stringify(state) + "\n", {
12089
+ mode: 384,
12090
+ flag: "wx"
12091
+ });
12092
+ fs.renameSync(temporary, this.path);
11741
12093
  } finally {
11742
- this.releaseDelegation?.();
12094
+ fs.rmSync(temporary, { force: true });
11743
12095
  }
11744
12096
  }
11745
- async pin(turn, generation, signal) {
11746
- const execution = await this.runtime.executions.turn(this.agent, turn, signal, this.delegation);
11747
- const { graph, context } = execution;
12097
+ /**
12098
+ * One bounded synchronous read/check/write critical section. Exclusive mkdir
12099
+ * protects independent Hosts too; contention fails closed without stale-lock
12100
+ * takeover. Never hold this lock while starting or awaiting model work.
12101
+ */
12102
+ withLock(operation) {
12103
+ const lock = this.path + ".lock";
12104
+ let held = false;
11748
12105
  try {
11749
- if (this.closed || this.generation !== generation) throw new Error("memory turn ended during View preparation");
11750
- signal?.throwIfAborted();
11751
- const inspection = inspectMemoryView(graph.memoryComposition.generation(context.view.runtimeGeneration), context.view, "active", turn, modelMemoryWake(graph, context).text);
11752
- this.pinned = {
11753
- turn,
11754
- graph,
11755
- context,
11756
- release: execution.release
11757
- };
11758
- this.warned = void 0;
11759
- this.lastInspection = inspection;
11760
- this.lastWorkspace = context.scope.workspaceId === void 0 ? void 0 : resolve(context.scope.workspaceId);
12106
+ fs.mkdirSync(this.directory, {
12107
+ recursive: true,
12108
+ mode: 448
12109
+ });
12110
+ try {
12111
+ fs.mkdirSync(lock, { mode: 448 });
12112
+ } catch (error) {
12113
+ if (error.code === "EEXIST") throw Object.assign(/* @__PURE__ */ new Error("busy"), { code: "EBUSY" });
12114
+ throw error;
12115
+ }
12116
+ held = true;
12117
+ return operation();
11761
12118
  } catch (error) {
11762
- execution.release();
11763
- throw error;
12119
+ throw failure$2(error);
12120
+ } finally {
12121
+ if (held) try {
12122
+ fs.rmdirSync(lock);
12123
+ } catch (error) {
12124
+ throw failure$2(error);
12125
+ }
11764
12126
  }
11765
12127
  }
11766
12128
  };
@@ -11858,6 +12220,7 @@ var MnemonAgentLifecycle = class {
11858
12220
  config;
11859
12221
  counters;
11860
12222
  memoryTurn;
12223
+ runtimeDirectory;
11861
12224
  primePending = true;
11862
12225
  startSource;
11863
12226
  guidedTurns = /* @__PURE__ */ new Set();
@@ -11874,10 +12237,21 @@ var MnemonAgentLifecycle = class {
11874
12237
  idleReviewTimer;
11875
12238
  reviewController;
11876
12239
  reviewRunning = false;
12240
+ reviewAccessBlocked = false;
11877
12241
  lastReviewAt;
11878
12242
  lastReviewAction;
11879
12243
  lastReviewScore;
11880
12244
  lastReviewDocumentIds;
12245
+ disposed = false;
12246
+ reviewEpoch = 0;
12247
+ observedReviewPolicy;
12248
+ watermarkEnabled = false;
12249
+ /** Only this Agent's observed usage, attributed to the graph that composed each checkpoint. */
12250
+ reviewUsage = /* @__PURE__ */ new Map();
12251
+ watermarkKey;
12252
+ watermarkStore;
12253
+ watermarkError;
12254
+ restoredAttempts = 0;
11881
12255
  idleReviewAttempts = 0;
11882
12256
  lastReviewAttemptAt;
11883
12257
  lastReviewFailure;
@@ -11885,15 +12259,18 @@ var MnemonAgentLifecycle = class {
11885
12259
  lastAt;
11886
12260
  lastError;
11887
12261
  composedTurn;
11888
- constructor(agent, coordinator, config, counters, source, memoryTurn) {
12262
+ constructor(agent, coordinator, config, counters, source, memoryTurn, runtimeDirectory) {
11889
12263
  this.agent = agent;
11890
12264
  this.coordinator = coordinator;
11891
12265
  this.config = config;
11892
12266
  this.counters = counters;
11893
12267
  this.memoryTurn = memoryTurn;
12268
+ this.runtimeDirectory = runtimeDirectory;
11894
12269
  this.startSource = source;
11895
12270
  }
11896
12271
  start() {
12272
+ this.restoreWatermark();
12273
+ this.observedReviewPolicy = this.reviewPolicy();
11897
12274
  const disposers = [
11898
12275
  this.agent.ctx.on("session/event", ((session, event) => this.sessionEvent(session, event))),
11899
12276
  this.agent.ctx.on("system-prompt/assemble", ((assembly, context, next) => this.assemblePrompt(assembly, context, next))),
@@ -11901,12 +12278,14 @@ var MnemonAgentLifecycle = class {
11901
12278
  this.agent.ctx.on("agent/turn-stopping", ((payload) => this.finishTurn(payload)))
11902
12279
  ];
11903
12280
  return () => {
12281
+ this.disposed = true;
11904
12282
  this.releaseView();
11905
12283
  this.cancelIdleReview(true);
11906
12284
  for (const dispose of disposers.reverse()) dispose();
11907
12285
  };
11908
12286
  }
11909
12287
  snapshot() {
12288
+ this.restoreWatermark();
11910
12289
  return {
11911
12290
  sessionId: this.agent.id,
11912
12291
  status: this.agent.status,
@@ -11918,8 +12297,14 @@ var MnemonAgentLifecycle = class {
11918
12297
  reviewRunning: this.reviewRunning,
11919
12298
  reviewActivity: this.reviewActivity(),
11920
12299
  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() },
12300
+ ...this.config.idleReview.persistWatermark ? { idleReviewWatermark: {
12301
+ state: this.watermarkError === void 0 ? "ready" : "error",
12302
+ restoredAttempts: this.restoredAttempts,
12303
+ maxPerSession: this.config.idleReview.maxPerSession,
12304
+ ...this.watermarkError === void 0 ? {} : { error: this.watermarkError }
12305
+ } } : {},
12306
+ ...idleReviewBlockReason(this.agent, this.config.idleReview.agentTeams) !== void 0 ? { idleReviewBlocked: "agent-team" } : this.reviewAccessBlocked ? { idleReviewBlocked: "tool-access" } : {},
12307
+ ...this.lastReviewAttemptAt === void 0 ? {} : { nextReviewAt: new Date(Math.min(864e13, this.lastReviewAttemptAt + this.config.idleReview.minIntervalMs)).toISOString() },
11923
12308
  ...this.lastReviewFailure === void 0 ? {} : { lastReviewFailure: this.lastReviewFailure },
11924
12309
  lastPhase: this.lastPhase,
11925
12310
  ...this.lastReviewAt === void 0 ? {} : { lastReviewAt: this.lastReviewAt },
@@ -11987,6 +12372,7 @@ var MnemonAgentLifecycle = class {
11987
12372
  this.releaseView(payload.turn);
11988
12373
  return decision;
11989
12374
  }
12375
+ if (!this.disposed && openAgentTurn(this.agent) === payload.turn) await this.memoryTurn?.begin(payload.turn, payload.signal);
11990
12376
  if (!this.config.lifecycleEnabled) return decision;
11991
12377
  if (this.config.writeEnabled && this.config.writebackMode === "guided") this.recordTurnMessages(payload.turn, decision.messages);
11992
12378
  if (payload.step !== 1) return decision;
@@ -12030,7 +12416,35 @@ var MnemonAgentLifecycle = class {
12030
12416
  this.memoryTurn?.end(turn);
12031
12417
  }
12032
12418
  async finishTurn(payload) {
12033
- this.scheduleIdleReview(payload.turn);
12419
+ this.cancelIdleReview(true);
12420
+ this.recordComposedTurn(payload.turn);
12421
+ if (payload.signal.aborted || !this.restoreWatermark() || !this.idleReviewAllowed()) return;
12422
+ const activity = this.ensureTurnActivity(payload.turn);
12423
+ const tools = completedToolActivity(this.agent.session.snapshotEvents(), payload.turn);
12424
+ activity.toolCallCount = tools.count;
12425
+ activity.toolNames = tools.names;
12426
+ if (!this.reviewActivity().eligible || !this.reviewAdmitted(payload.turn)) return;
12427
+ const controller = new AbortController();
12428
+ const signal = AbortSignal.any([payload.signal, controller.signal]);
12429
+ const epoch = this.reviewEpoch;
12430
+ const policy = this.reviewPolicy();
12431
+ this.reviewController = controller;
12432
+ this.reviewRunning = true;
12433
+ let admitted = false;
12434
+ try {
12435
+ const available = await this.coordinator.checkReviewAccess(this.agent, signal);
12436
+ if (signal.aborted || epoch !== this.reviewEpoch || policy !== this.reviewPolicy() || !this.idleReviewAllowed()) return;
12437
+ this.reviewAccessBlocked = !available;
12438
+ admitted = available;
12439
+ } catch (error) {
12440
+ if (!signal.aborted) this.fail(error);
12441
+ } finally {
12442
+ if (this.reviewController === controller) {
12443
+ this.reviewRunning = false;
12444
+ this.reviewController = void 0;
12445
+ }
12446
+ }
12447
+ if (admitted) this.scheduleIdleReview(payload.turn);
12034
12448
  }
12035
12449
  sessionEvent(session, event) {
12036
12450
  if (session !== this.agent.session || event.type !== "turn/end") return;
@@ -12041,16 +12455,13 @@ var MnemonAgentLifecycle = class {
12041
12455
  }
12042
12456
  scheduleIdleReview(turn) {
12043
12457
  this.cancelIdleReview(true);
12044
- this.recordComposedTurn(turn);
12045
- if (!this.idleReviewAllowed()) return;
12046
- const activity = this.ensureTurnActivity(turn);
12047
- const tools = completedToolActivity(this.agent.session.snapshotEvents(), turn);
12048
- activity.toolCallCount = tools.count;
12049
- activity.toolNames = tools.names;
12458
+ if (!this.restoreWatermark() || !this.idleReviewAllowed()) return;
12050
12459
  if (!this.reviewActivity().eligible || !this.reviewAdmitted(turn)) return;
12460
+ const epoch = this.reviewEpoch;
12461
+ const policy = this.reviewPolicy();
12051
12462
  this.idleReviewTimer = setTimeout(() => {
12052
12463
  this.idleReviewTimer = void 0;
12053
- if (!this.idleReviewAllowed() || this.reviewRunning) return;
12464
+ if (epoch !== this.reviewEpoch || policy !== this.reviewPolicy() || !this.idleReviewAllowed() || this.reviewRunning) return;
12054
12465
  if (this.lastReviewAttemptAt !== void 0 && Date.now() < this.lastReviewAttemptAt + this.config.idleReview.minIntervalMs) {
12055
12466
  this.scheduleIdleReview(turn);
12056
12467
  return;
@@ -12058,7 +12469,73 @@ var MnemonAgentLifecycle = class {
12058
12469
  if (this.agent.status !== "idle") return;
12059
12470
  if (!this.agent.session.snapshotEvents().some((event) => event.type === "turn/end" && eventTurn(event) === turn) || !this.reviewActivity().eligible || !this.reviewAdmitted(turn)) return;
12060
12471
  this.runIdleReview();
12061
- }, Math.max(this.config.idleReviewMs, (this.lastReviewAttemptAt ?? -Infinity) + this.config.idleReview.minIntervalMs - Date.now()));
12472
+ }, Math.min(2147483647, Math.max(this.config.idleReviewMs, (this.lastReviewAttemptAt ?? -Infinity) + this.config.idleReview.minIntervalMs - Date.now())));
12473
+ }
12474
+ /** Resolve only the graph actually serving this parent, never a selected View or startup cwd. */
12475
+ restoreWatermark() {
12476
+ if (!this.config.idleReview.persistWatermark) {
12477
+ this.watermarkEnabled = false;
12478
+ return true;
12479
+ }
12480
+ try {
12481
+ const directory = this.runtimeDirectory?.();
12482
+ if (directory === void 0 || directory.trim() === "") throw new Error("runtime storage unavailable");
12483
+ const key = JSON.stringify([resolve(directory), this.agent.id]);
12484
+ if (this.watermarkKey === key && this.watermarkEnabled) return this.watermarkError === void 0;
12485
+ const sameRoot = this.watermarkKey === key;
12486
+ this.watermarkEnabled = true;
12487
+ this.watermarkKey = key;
12488
+ this.watermarkStore = new IdleReviewWatermarkStore(directory, this.agent.id);
12489
+ this.watermarkError = void 0;
12490
+ this.restoredAttempts = 0;
12491
+ const floor = this.reviewUsage.get(resolve(directory));
12492
+ if (!sameRoot) {
12493
+ this.idleReviewAttempts = floor?.attempts ?? 0;
12494
+ this.lastReviewAttemptAt = floor?.lastReviewAttemptAt ?? void 0;
12495
+ }
12496
+ const state = this.watermarkStore.restore(floor);
12497
+ this.restoredAttempts = state.attempts;
12498
+ this.idleReviewAttempts = state.attempts;
12499
+ this.lastReviewAttemptAt = state.lastReviewAttemptAt ?? void 0;
12500
+ this.reviewUsage.set(resolve(directory), state);
12501
+ return true;
12502
+ } catch (error) {
12503
+ this.pauseWatermark(error);
12504
+ return false;
12505
+ }
12506
+ }
12507
+ configurationChanged() {
12508
+ const policy = this.reviewPolicy();
12509
+ if (policy !== this.observedReviewPolicy) {
12510
+ this.cancelIdleReview(true);
12511
+ this.reviewAccessBlocked = false;
12512
+ }
12513
+ this.observedReviewPolicy = policy;
12514
+ this.restoreWatermark();
12515
+ }
12516
+ pauseWatermark(error) {
12517
+ 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.";
12518
+ this.watermarkError = message;
12519
+ }
12520
+ /** Policy/root changes invalidate an already queued checkpoint instead of catching up. */
12521
+ reviewPolicy() {
12522
+ try {
12523
+ return JSON.stringify([
12524
+ this.config.idleReview.persistWatermark ? this.runtimeDirectory?.() : void 0,
12525
+ this.config.lifecycleEnabled,
12526
+ this.config.writeEnabled,
12527
+ this.config.storageScope,
12528
+ this.config.dataDir,
12529
+ this.config.writebackMode,
12530
+ this.config.idleReviewMs,
12531
+ this.config.idleReview,
12532
+ this.config.taskAgentModel,
12533
+ mainStrategyTypeId(this.config)
12534
+ ]);
12535
+ } catch (error) {
12536
+ if (this.config.idleReview.persistWatermark) this.pauseWatermark(error);
12537
+ return;
12538
+ }
12062
12539
  }
12063
12540
  /** Which Strategy composed the turn, while its View is still pinned, and whether it declares idle review. */
12064
12541
  recordComposedTurn(turn) {
@@ -12069,7 +12546,8 @@ var MnemonAgentLifecycle = class {
12069
12546
  this.composedTurn = {
12070
12547
  turn,
12071
12548
  typeId: strategy.typeId,
12072
- idleReview: strategyDrives(strategy, "idle-review")
12549
+ idleReview: strategyDrives(strategy, "idle-review"),
12550
+ directory: pinned.graph.directory
12073
12551
  };
12074
12552
  } else if (this.composedTurn?.turn !== turn) this.composedTurn = void 0;
12075
12553
  }
@@ -12086,19 +12564,60 @@ var MnemonAgentLifecycle = class {
12086
12564
  if (this.memoryTurn === void 0) return strategyDrives({ typeId }, "idle-review");
12087
12565
  return this.composedTurn?.typeId === typeId && this.composedTurn.idleReview;
12088
12566
  }
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();
12567
+ idleReviewAllowed(reserved = false) {
12568
+ 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
12569
  }
12092
12570
  async runIdleReview() {
12093
12571
  const controller = new AbortController();
12094
12572
  const triggeredScore = this.reviewActivity().score;
12095
- this.idleReviewAttempts += 1;
12096
- this.lastReviewAttemptAt = Date.now();
12573
+ const epoch = this.reviewEpoch;
12574
+ const policy = this.reviewPolicy();
12097
12575
  this.reviewRunning = true;
12098
12576
  this.reviewController = controller;
12099
- this.mark("review");
12100
12577
  try {
12101
- const result = await this.coordinator.review(this.agent, controller.signal);
12578
+ if (epoch !== this.reviewEpoch || policy !== this.reviewPolicy() || !this.idleReviewAllowed()) controller.abort(/* @__PURE__ */ new Error("Automatic idle review admission changed"));
12579
+ controller.signal.throwIfAborted();
12580
+ if (this.config.idleReview.persistWatermark) try {
12581
+ if (!this.restoreWatermark() || this.watermarkStore === void 0 || !this.idleReviewAllowed()) return;
12582
+ const reserved = this.watermarkStore.reserve({
12583
+ ...this.config.idleReview,
12584
+ now: Date.now(),
12585
+ floor: {
12586
+ attempts: this.idleReviewAttempts,
12587
+ lastReviewAttemptAt: this.lastReviewAttemptAt ?? null
12588
+ }
12589
+ });
12590
+ const state = reserved ?? this.watermarkStore.load();
12591
+ this.idleReviewAttempts = Math.max(this.idleReviewAttempts, state.attempts);
12592
+ this.lastReviewAttemptAt = Math.max(this.lastReviewAttemptAt ?? -Infinity, state.lastReviewAttemptAt ?? -Infinity);
12593
+ if (this.lastReviewAttemptAt === -Infinity) this.lastReviewAttemptAt = void 0;
12594
+ const directory = this.runtimeDirectory?.();
12595
+ if (directory !== void 0) this.reviewUsage.set(resolve(directory), {
12596
+ attempts: this.idleReviewAttempts,
12597
+ lastReviewAttemptAt: this.lastReviewAttemptAt ?? null
12598
+ });
12599
+ if (reserved === void 0) return;
12600
+ } catch (error) {
12601
+ this.pauseWatermark(error);
12602
+ return;
12603
+ }
12604
+ else {
12605
+ this.idleReviewAttempts += 1;
12606
+ this.lastReviewAttemptAt = Date.now();
12607
+ const directory = this.composedTurn?.directory;
12608
+ if (directory !== void 0) {
12609
+ const key = resolve(directory);
12610
+ this.reviewUsage.set(key, {
12611
+ attempts: (this.reviewUsage.get(key)?.attempts ?? 0) + 1,
12612
+ lastReviewAttemptAt: this.lastReviewAttemptAt
12613
+ });
12614
+ }
12615
+ }
12616
+ this.mark("review");
12617
+ const result = await this.coordinator.review(this.agent, controller.signal, () => {
12618
+ if (epoch !== this.reviewEpoch || policy !== this.reviewPolicy() || !this.idleReviewAllowed(true)) controller.abort(/* @__PURE__ */ new Error("Automatic idle review admission changed"));
12619
+ controller.signal.throwIfAborted();
12620
+ });
12102
12621
  if (controller.signal.aborted) return;
12103
12622
  this.lastReviewAt = (/* @__PURE__ */ new Date()).toISOString();
12104
12623
  this.lastReviewAction = result.action;
@@ -12119,6 +12638,7 @@ var MnemonAgentLifecycle = class {
12119
12638
  }
12120
12639
  }
12121
12640
  cancelIdleReview(abortRunning) {
12641
+ this.reviewEpoch += 1;
12122
12642
  if (this.idleReviewTimer !== void 0) clearTimeout(this.idleReviewTimer);
12123
12643
  this.idleReviewTimer = void 0;
12124
12644
  if (abortRunning) this.reviewController?.abort();
@@ -12282,11 +12802,15 @@ var MnemonLifecycle = class {
12282
12802
  return this.skillEvolution.reviewNow(this.liveAgent(sessionId));
12283
12803
  }
12284
12804
  start() {
12805
+ const stopChanged = this.runtimeSource?.onChanged?.(() => {
12806
+ for (const { lifecycle } of this.owners.values()) lifecycle.configurationChanged();
12807
+ });
12285
12808
  const stopCreated = this.ctx.on("agent/created", (({ agent, source }) => {
12286
12809
  this.install(agent, source);
12287
12810
  }));
12288
12811
  for (const agent of this.ctx.agents.roots()) this.install(agent, "adopted");
12289
12812
  return () => {
12813
+ stopChanged?.();
12290
12814
  stopCreated();
12291
12815
  for (const dispose of [...this.children.values()].reverse()) dispose();
12292
12816
  for (const owner of [...this.owners.values()].reverse()) owner.dispose();
@@ -12791,7 +13315,7 @@ var MnemonLifecycle = class {
12791
13315
  }
12792
13316
  if (!this.ctx.agents.roots().includes(agent)) return;
12793
13317
  const memoryTurn = this.runtimeSource === void 0 ? void 0 : new AgentMemoryTurn(agent, this.runtimeSource);
12794
- const lifecycle = new MnemonAgentLifecycle(agent, this.coordinator, this.config, this.counters, source, memoryTurn);
13318
+ const lifecycle = new MnemonAgentLifecycle(agent, this.coordinator, this.config, this.counters, source, memoryTurn, () => this.runtimeSource?.forAgent(agent).directory);
12795
13319
  let dispose;
12796
13320
  dispose = agent.ctx.effect(() => {
12797
13321
  const stop = lifecycle.start();
@@ -12857,7 +13381,21 @@ var MnemonLifecycle = class {
12857
13381
  })));
12858
13382
  stops.push(agent.ctx.on("agent/pre-step", (async (payload, next) => {
12859
13383
  const decision = await next();
12860
- if (decision.kind === "reject" || payload.signal.aborted) memory.end(payload.turn);
13384
+ if (decision.kind === "reject" || payload.signal.aborted) {
13385
+ memory.end(payload.turn);
13386
+ return decision;
13387
+ }
13388
+ if (openAgentTurn(agent) !== payload.turn) return decision;
13389
+ const recovering = memory.current?.turn !== payload.turn;
13390
+ await memory.begin(payload.turn, payload.signal);
13391
+ const pinned = memory.current;
13392
+ if (recovering && payload.step === 1 && this.config.lifecycleEnabled && pinned !== void 0) {
13393
+ const wake = modelMemoryWake(pinned.graph, pinned.context);
13394
+ if (wake.text.trim() !== "") return {
13395
+ kind: "enter",
13396
+ messages: [...decision.messages, createPluginMessage(wake.text, "recall")]
13397
+ };
13398
+ }
12861
13399
  return decision;
12862
13400
  })));
12863
13401
  stops.push(agent.ctx.on("session/event", ((session, event) => {
@@ -13678,8 +14216,6 @@ var TrajectoryWindow = class {
13678
14216
  };
13679
14217
  //#endregion
13680
14218
  //#region src/host/skill-evolution.ts
13681
- /** A new skill goes to the user's own DSH skills unless the page chose another root. */
13682
- const DEFAULT_TARGET_ROOT = "user-dsh";
13683
14219
  /** A conversation's turn bars read at most this many of its reviews. */
13684
14220
  const ACTIVITY_REVIEWS = 50;
13685
14221
  const WAITING_STATUSES = /* @__PURE__ */ new Set([
@@ -13701,6 +14237,7 @@ const BACKTEST_CONCURRENCY = 2;
13701
14237
  */
13702
14238
  function retryingSource(source, attempts = 4) {
13703
14239
  return {
14240
+ ...source.scope === void 0 ? {} : { scope: source.scope },
13704
14241
  read: (operation, input, signal) => source.read(operation, input, signal),
13705
14242
  mutate: async (operation, input, signal) => {
13706
14243
  for (let attempt = 1;; attempt++) try {
@@ -13714,6 +14251,35 @@ function retryingSource(source, attempts = 4) {
13714
14251
  function message(error) {
13715
14252
  return redact(error instanceof Error ? error.message : String(error));
13716
14253
  }
14254
+ /** Resolve only roots supplied by this Source's scoped catalog, never the task's cwd. */
14255
+ function newSkillRoot(settings, source, catalog, explicit) {
14256
+ let root = explicit;
14257
+ if (root === void 0) {
14258
+ if (settings.targetScope === "storage" && source.scope === void 0) throw new Error("The skill destination needs a known storage scope; select User or Project in Skills settings.");
14259
+ root = settings.targetScope === "project" || settings.targetScope === "storage" && (source.scope?.storage === "workspace" || source.scope?.storage === "workspaces") ? "project-dsh" : "user-dsh";
14260
+ }
14261
+ const directory = catalog.roots.find((candidate) => candidate.kind === root)?.path;
14262
+ if (directory === void 0 || root.startsWith("project-") && catalog.projectRoot === void 0) throw new Error("Project skill roots need a selected workspace; select a workspace or choose User in Skills settings.");
14263
+ if (root.startsWith("project-")) try {
14264
+ if (!statSync(catalog.projectRoot).isDirectory()) throw new Error("not a directory");
14265
+ } catch {
14266
+ throw new Error("The selected workspace project is unavailable; select an existing workspace or choose User in Skills settings.");
14267
+ }
14268
+ let parent = directory;
14269
+ try {
14270
+ for (;;) try {
14271
+ if (!statSync(parent).isDirectory()) throw new Error("not a directory");
14272
+ accessSync(parent, constants.W_OK | constants.X_OK);
14273
+ break;
14274
+ } catch (error) {
14275
+ if (error.code !== "ENOENT" || dirname(parent) === parent) throw error;
14276
+ parent = dirname(parent);
14277
+ }
14278
+ } catch {
14279
+ throw new Error(`Cannot create a skill in ${root}: its directory is not writable. Choose another destination or fix its permissions.`);
14280
+ }
14281
+ return root;
14282
+ }
13717
14283
  /**
13718
14284
  * The Host half of skill evolution: it captures each conversation's
13719
14285
  * trajectory as turns end, decides when a window has earned a review, runs
@@ -13893,7 +14459,8 @@ var SkillEvolution = class {
13893
14459
  annotations: []
13894
14460
  };
13895
14461
  try {
13896
- const active = (await source.read("catalog", null, controller.signal)).skills.filter((skill) => skill.shadowedBy === void 0);
14462
+ const catalog = await source.read("catalog", null, controller.signal);
14463
+ const active = catalog.skills.filter((skill) => skill.shadowedBy === void 0);
13897
14464
  const openSuggestions = [...await source.read("suggestions", { status: [
13898
14465
  "pending",
13899
14466
  "drafting",
@@ -13940,6 +14507,7 @@ var SkillEvolution = class {
13940
14507
  expiryDays: current.expiryDays,
13941
14508
  maxPendingPerSession: current.maxPendingPerSession
13942
14509
  };
14510
+ const targetRoot = policy.allowCreate && parsed.suggestions.some((suggestion) => suggestion.action === "create" && !active.some((skill) => skill.name === suggestion.skillName)) ? newSkillRoot(current, source, catalog) : void 0;
13943
14511
  const result = await source.mutate("review-record", {
13944
14512
  review: {
13945
14513
  ...base,
@@ -13949,7 +14517,10 @@ var SkillEvolution = class {
13949
14517
  },
13950
14518
  trajectory,
13951
14519
  annotations: parsed.annotations,
13952
- suggestions: parsed.suggestions,
14520
+ suggestions: parsed.suggestions.map((suggestion) => suggestion.action === "create" && targetRoot !== void 0 ? {
14521
+ ...suggestion,
14522
+ targetRoot
14523
+ } : suggestion),
13953
14524
  cases: parsed.cases,
13954
14525
  policy
13955
14526
  });
@@ -13996,13 +14567,27 @@ var SkillEvolution = class {
13996
14567
  */
13997
14568
  async draft(port, workspace, input) {
13998
14569
  const source = retryingSource(port);
14570
+ const pack = await source.read("evidence-pack", {
14571
+ ...input.suggestionId === void 0 ? {} : { suggestionId: input.suggestionId },
14572
+ ...input.skill === void 0 ? {} : { skill: input.skill }
14573
+ });
14574
+ const previous = pack.suggestion?.draftId === void 0 ? (await source.read("drafts", {
14575
+ skill: pack.skillName,
14576
+ ...input.suggestionId === void 0 ? {} : { suggestionId: input.suggestionId }
14577
+ })).find((draft) => draft.suggestionId === input.suggestionId) : (await source.read("draft", { id: pack.suggestion.draftId })).draft;
14578
+ const targetRoot = pack.action === "update" ? pack.skill.root : newSkillRoot(this.host.settings(), source, await source.read("catalog"), input.targetRoot ?? previous?.targetRoot ?? pack.suggestion?.targetRoot);
14579
+ const guidance = [...previous?.guidance ?? [], ...input.guidance?.trim() ? [input.guidance.trim()] : []].slice(-8);
13999
14580
  if (input.suggestionId !== void 0) await source.mutate("suggestion-status", {
14000
14581
  id: input.suggestionId,
14001
14582
  status: "drafting"
14002
14583
  });
14003
14584
  this.background("skill draft", async (signal) => {
14004
14585
  try {
14005
- await this.writeDraft(source, workspace, input, signal);
14586
+ await this.writeDraft(source, workspace, input, {
14587
+ pack,
14588
+ targetRoot,
14589
+ guidance
14590
+ }, signal);
14006
14591
  } catch (error) {
14007
14592
  if (input.suggestionId !== void 0) await source.mutate("suggestion-status", {
14008
14593
  id: input.suggestionId,
@@ -14017,14 +14602,9 @@ var SkillEvolution = class {
14017
14602
  ...input.suggestionId === void 0 ? {} : { suggestionId: input.suggestionId }
14018
14603
  };
14019
14604
  }
14020
- async writeDraft(source, workspace, input, signal) {
14021
- const pack = await source.read("evidence-pack", {
14022
- ...input.suggestionId === void 0 ? {} : { suggestionId: input.suggestionId },
14023
- ...input.skill === void 0 ? {} : { skill: input.skill }
14024
- }, signal);
14605
+ async writeDraft(source, workspace, input, prepared, signal) {
14025
14606
  const settings = this.host.settings();
14026
- const guidance = [...pack.drafts.find((draft) => input.suggestionId === void 0 ? draft.suggestionId === void 0 : draft.suggestionId === input.suggestionId)?.guidance ?? [], ...input.guidance?.trim() ? [input.guidance.trim()] : []].slice(-8);
14027
- const targetRoot = input.targetRoot ?? DEFAULT_TARGET_ROOT;
14607
+ const { pack, targetRoot, guidance } = prepared;
14028
14608
  let prompt = skillDraftPrompt(pack, guidance, targetRoot);
14029
14609
  let draft;
14030
14610
  for (let attempt = 0; draft === void 0; attempt++) {
@@ -15333,16 +15913,59 @@ function documentActionReceipt(receipt) {
15333
15913
  }
15334
15914
  /**
15335
15915
  * Named product tools are views over the same Source Route/Action protocol. Each family registers its tools in the
15336
- * order the model has always been given them, which puts Memory Spaces' reads before the Documents tools and its
15337
- * writes after the runtime memory tool.
15916
+ * historical registry order, which puts Memory Spaces' reads before the Documents tools and its writes after
15917
+ * the runtime memory tool. DSH's prompt assembler independently orders wire schemas.
15338
15918
  */
15339
- function registerTools(ctx, runtimeSource, coordinator) {
15919
+ function registerTools(ctx, runtimeSource, coordinator, contributions, initialReady) {
15920
+ const definitions = [];
15921
+ const catalog = { tools: { register: (tool) => {
15922
+ definitions.push(tool);
15923
+ } } };
15340
15924
  const helpers = toolHelpers(runtimeSource);
15341
- registerViewTools(ctx, helpers, coordinator);
15342
- registerMemorySpaceReadTools(ctx, helpers, coordinator);
15343
- registerDocumentTools(ctx, helpers, coordinator);
15344
- registerRuntimeTools(ctx, helpers, coordinator);
15345
- registerMemorySpaceWriteTools(ctx, helpers, coordinator);
15925
+ registerViewTools(catalog, helpers, coordinator);
15926
+ registerMemorySpaceReadTools(catalog, helpers, coordinator);
15927
+ registerDocumentTools(catalog, helpers, coordinator);
15928
+ registerRuntimeTools(catalog, helpers, coordinator);
15929
+ registerMemorySpaceWriteTools(catalog, helpers, coordinator);
15930
+ const registered = /* @__PURE__ */ new Map();
15931
+ let active = true;
15932
+ let ready = initialReady === void 0;
15933
+ const reconcile = (snapshot) => {
15934
+ const types = new Set(snapshot.sources.map((source) => source.definition.manifest.typeId));
15935
+ for (const tool of definitions) {
15936
+ const type = memoryTool(tool.name)?.sourceTypeId;
15937
+ if (!(type === void 0 || ready && types.has(type))) {
15938
+ registered.get(tool.name)?.();
15939
+ registered.delete(tool.name);
15940
+ } else if (!registered.has(tool.name)) {
15941
+ const dispose = ctx.tools.register(tool);
15942
+ if (typeof dispose !== "function") throw new Error("dsh-mnemon tool registration did not return a disposer: " + tool.name);
15943
+ registered.set(tool.name, dispose);
15944
+ }
15945
+ }
15946
+ };
15947
+ const unsubscribe = contributions.onCommittedContributionsChanged(reconcile);
15948
+ const dispose = () => {
15949
+ if (!active) return;
15950
+ active = false;
15951
+ unsubscribe();
15952
+ for (const release of registered.values()) release();
15953
+ registered.clear();
15954
+ };
15955
+ try {
15956
+ reconcile(contributions.committedContributionSnapshot());
15957
+ } catch (error) {
15958
+ dispose();
15959
+ throw error;
15960
+ }
15961
+ initialReady?.then(() => {
15962
+ if (!active) return;
15963
+ ready = true;
15964
+ reconcile(contributions.committedContributionSnapshot());
15965
+ }).catch((error) => {
15966
+ if (active) console.warn("dsh-mnemon: could not initialize Source tool families", error);
15967
+ });
15968
+ return dispose;
15346
15969
  }
15347
15970
  /** What the handlers of every family share: the calling Agent's runtime graph and Sources, its checks, and a Source Action call. */
15348
15971
  function toolHelpers(runtimeSource) {
@@ -18274,7 +18897,10 @@ function apply(rawContext, rawConfig = {}) {
18274
18897
  runtime.dispose();
18275
18898
  };
18276
18899
  }, "dsh-mnemon.lifecycle-root()");
18277
- registerTools(ctx, runtime, coordinator);
18900
+ ctx.effect(() => {
18901
+ const loader = ctx.get("loader");
18902
+ return registerTools(ctx, runtime, coordinator, extensions, loader?.await?.());
18903
+ }, "dsh-mnemon: Source tool lifecycle");
18278
18904
  registerCommands(ctx.commands, runtime, coordinator);
18279
18905
  registerGuidance(ctx, resolved);
18280
18906
  ctx.inject(["connection", "webServer"], (webContext) => {