dsh-mnemon 0.5.42 → 0.5.44

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
@@ -6,8 +6,8 @@ import { Context } from "@deepseek-ai/cordis";
6
6
  import z from "@deepseek-ai/schemastery";
7
7
  import { basename, delimiter, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
8
8
  import { resolveEmbedding, resolvePersistenceStrategy, resolveRecallQuality } from "dsh-mnemon-source-memory-spaces";
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
- 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";
9
+ import { CATEGORIES, DEFAULT_EMBEDDING_ENDPOINT, DEFAULT_EMBEDDING_MODEL, DEFAULT_EMBEDDING_PROTOCOL, EDGE_TYPES, EMBEDDING_CREDENTIAL_PREFIX, 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
+ 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, assertRuntimePinnedBudget, parseRuntimeBranches, runtimeBytes, runtimeProjection, summarizeRuntimeMemories } from "dsh-mnemon-source-runtime/contracts";
11
11
  import { createHash, randomUUID } from "node:crypto";
12
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";
@@ -23,6 +23,8 @@ import { Agent, request as request$1 } from "node:https";
23
23
  import { connect } from "node:tls";
24
24
  import { readFile, stat } from "node:fs/promises";
25
25
  import { AsyncLocalStorage } from "node:async_hooks";
26
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
27
+ import * as SkillsContracts from "dsh-mnemon-source-skills/contracts";
26
28
  import { z as z$1 } from "zod";
27
29
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
28
30
  import { isVolatile } from "@deepseek-ai/cosmokit";
@@ -389,6 +391,9 @@ const DEFAULT_SKILLS_CONFIG = Object.freeze({
389
391
  allowUpdate: true,
390
392
  targetScope: "user",
391
393
  autoBacktest: true,
394
+ autoAdopt: false,
395
+ autoAdoptDailyLimit: 2,
396
+ autoAdoptTimeZone: "local",
392
397
  draftMaxTokens: 32768,
393
398
  draftRetryMaxTokens: 65536,
394
399
  hints: true,
@@ -483,6 +488,18 @@ const SkillsConfig = z.object({
483
488
  "project"
484
489
  ]).default(DEFAULT_SKILLS_CONFIG.targetScope),
485
490
  autoBacktest: z.boolean().default(DEFAULT_SKILLS_CONFIG.autoBacktest),
491
+ autoAdopt: z.boolean().default(DEFAULT_SKILLS_CONFIG.autoAdopt),
492
+ autoAdoptDailyLimit: z.number().step(1).min(0).max(100).default(DEFAULT_SKILLS_CONFIG.autoAdoptDailyLimit),
493
+ autoAdoptTimeZone: z.transform(z.string(), (value) => {
494
+ if (value === "local") return value;
495
+ if (!value || value.length > 100) throw new Error("skills.autoAdoptTimeZone must be an IANA time zone or local");
496
+ try {
497
+ new Intl.DateTimeFormat("en", { timeZone: value }).format(0);
498
+ } catch {
499
+ throw new Error("skills.autoAdoptTimeZone must be an IANA time zone or local");
500
+ }
501
+ return value;
502
+ }).default(DEFAULT_SKILLS_CONFIG.autoAdoptTimeZone),
486
503
  draftMaxTokens: z.number().step(1).min(128).max(131072).default(DEFAULT_SKILLS_CONFIG.draftMaxTokens),
487
504
  draftRetryMaxTokens: z.number().step(1).min(128).max(131072).default(DEFAULT_SKILLS_CONFIG.draftRetryMaxTokens),
488
505
  hints: z.boolean().default(DEFAULT_SKILLS_CONFIG.hints),
@@ -526,7 +543,9 @@ const MnemonEmbeddingSchema = z.object({
526
543
  enabled: z.boolean().default(false),
527
544
  endpoint: z.string().default(DEFAULT_EMBEDDING_ENDPOINT),
528
545
  model: z.string().default(DEFAULT_EMBEDDING_MODEL),
529
- apiKey: z.string().default(""),
546
+ apiKey: z.string().role("secret").default(""),
547
+ apiKeyEnv: z.string(),
548
+ apiKeyRef: z.string(),
530
549
  protocol: z.union(MNEMON_EMBEDDING_PROTOCOLS).default(DEFAULT_EMBEDDING_PROTOCOL)
531
550
  });
532
551
  const RecallQualitySchema = z.object({
@@ -539,6 +558,7 @@ const RecallQualitySchema = z.object({
539
558
  maxUnknownResults: z.number().step(1).min(0).max(50).default(DEFAULT_RECALL_MAX_UNKNOWN_RESULTS)
540
559
  });
541
560
  const RuntimeMemorySchema = z.object({
561
+ inheritToSubagents: z.boolean().default(false),
542
562
  memoryLimitBytes: z.number().step(1).min(1).max(MAX_RUNTIME_MEMORY_LIMIT_BYTES).default(DEFAULT_RUNTIME_MEMORY_LIMIT_BYTES),
543
563
  userLimitBytes: z.number().step(1).min(1).max(MAX_RUNTIME_MEMORY_LIMIT_BYTES).default(DEFAULT_RUNTIME_USER_LIMIT_BYTES),
544
564
  maintenanceMaxTokens: z.number().step(1).min(1).max(MAX_RUNTIME_MAINTENANCE_MAX_TOKENS).default(DEFAULT_RUNTIME_MAINTENANCE_MAX_TOKENS)
@@ -595,6 +615,7 @@ const Config = z.object({
595
615
  timeoutMs: z.number().step(1).min(100).max(12e4).default(DEFAULT_TIMEOUT_MS),
596
616
  defaultRecallLimit: z.number().step(1).min(1).max(50).default(10),
597
617
  runtimeMemory: RuntimeMemorySchema.default({
618
+ inheritToSubagents: false,
598
619
  memoryLimitBytes: DEFAULT_RUNTIME_MEMORY_LIMIT_BYTES,
599
620
  userLimitBytes: DEFAULT_RUNTIME_USER_LIMIT_BYTES,
600
621
  maintenanceMaxTokens: DEFAULT_RUNTIME_MAINTENANCE_MAX_TOKENS
@@ -719,6 +740,8 @@ function resolveTaskAgentModel(value) {
719
740
  };
720
741
  }
721
742
  function resolveRuntimeMemory(value) {
743
+ const inheritToSubagents = value?.inheritToSubagents ?? false;
744
+ if (typeof inheritToSubagents !== "boolean") throw new Error("dsh-mnemon: Runtime inheritToSubagents must be a boolean");
722
745
  const memoryLimitBytes = value?.memoryLimitBytes ?? DEFAULT_RUNTIME_MEMORY_LIMIT_BYTES;
723
746
  const userLimitBytes = value?.userLimitBytes ?? DEFAULT_RUNTIME_USER_LIMIT_BYTES;
724
747
  const maintenanceMaxTokens = value?.maintenanceMaxTokens ?? 8192;
@@ -726,6 +749,7 @@ function resolveRuntimeMemory(value) {
726
749
  if (!Number.isInteger(userLimitBytes) || userLimitBytes < 1 || userLimitBytes > MAX_RUNTIME_MEMORY_LIMIT_BYTES) throw new Error(`dsh-mnemon: Runtime USER.md limit must be an integer within 1..${MAX_RUNTIME_MEMORY_LIMIT_BYTES} bytes`);
727
750
  if (!Number.isInteger(maintenanceMaxTokens) || maintenanceMaxTokens < 1 || maintenanceMaxTokens > 1e6) throw new Error(`dsh-mnemon: Runtime maintenance maxTokens must be an integer within 1..${MAX_RUNTIME_MAINTENANCE_MAX_TOKENS}`);
728
751
  return {
752
+ inheritToSubagents,
729
753
  memoryLimitBytes,
730
754
  userLimitBytes,
731
755
  maintenanceMaxTokens
@@ -2250,6 +2274,7 @@ function parseRuntime(value, limits = RUNTIME_MEMORY_LIMITS) {
2250
2274
  "low"
2251
2275
  ].includes(String(entry.importance))) throw new Error("runtime memories.json contains an invalid entry");
2252
2276
  if (typeof entry.created_at !== "string" || typeof entry.updated_at !== "string") throw new Error("runtime memories.json contains invalid timestamps");
2277
+ if (entry.pinned !== void 0 && typeof entry.pinned !== "boolean") throw new Error("runtime memories.json contains invalid pin state");
2253
2278
  const content = entry.content.trim().replace(/\s+/gu, " ");
2254
2279
  if (content === "" || content.includes("§") || Buffer.byteLength(content, "utf8") > RUNTIME_ENTRY_MAX_BYTES) throw new Error("runtime memories.json contains invalid content");
2255
2280
  const branches = entry.target === "memory" ? parseRuntimeBranches(entry.branches) : void 0;
@@ -2260,10 +2285,12 @@ function parseRuntime(value, limits = RUNTIME_MEMORY_LIMITS) {
2260
2285
  importance: entry.importance,
2261
2286
  created_at: entry.created_at,
2262
2287
  updated_at: entry.updated_at,
2288
+ ...entry.pinned === void 0 ? {} : { pinned: entry.pinned },
2263
2289
  ...branches === void 0 ? {} : { branches },
2264
2290
  ...origin === void 0 ? {} : { origin }
2265
2291
  };
2266
2292
  });
2293
+ assertRuntimePinnedBudget(entries);
2267
2294
  for (const target of ["user", "memory"]) {
2268
2295
  const used = runtimeBytes(entries, target);
2269
2296
  if (used > limits[target]) throw new Error(`runtime ${target} memory holds ${used} bytes and exceeds its ${limits[target]} byte limit`);
@@ -2476,6 +2503,7 @@ function archiveRuntimeOverflow(stagedRuntime, entries) {
2476
2503
  }
2477
2504
  /** Retain the same hot set on both machines, without asking a model to rewrite evidence. */
2478
2505
  function fitMergedRuntime(entries, limits) {
2506
+ if (runtimeBytes(entries.filter((entry) => entry.pinned === true), "memory") > limits.memory) throw new Error("Pinned runtime entries exceed the merge capacity. Unpin entries or increase the capacity; no data was changed.");
2479
2507
  if (runtimeBytes(entries, "memory") <= limits.memory) return {
2480
2508
  entries,
2481
2509
  archived: []
@@ -2486,7 +2514,7 @@ function fitMergedRuntime(entries, limits) {
2486
2514
  low: 0
2487
2515
  };
2488
2516
  const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
2489
- const ranked = entries.filter((entry) => entry.target === "memory").sort((left, right) => importance[right.importance] - importance[left.importance] || compare(right.updated_at, left.updated_at) || compare(entryKey(left), entryKey(right)));
2517
+ const ranked = entries.filter((entry) => entry.target === "memory").sort((left, right) => Number(right.pinned === true) - Number(left.pinned === true) || importance[right.importance] - importance[left.importance] || compare(right.updated_at, left.updated_at) || compare(entryKey(left), entryKey(right)));
2490
2518
  const kept = [];
2491
2519
  const archived = [];
2492
2520
  for (const entry of ranked) if (runtimeBytes([...kept, entry], "memory") <= limits.memory) kept.push(entry);
@@ -2835,7 +2863,14 @@ function mergeRuntime(root, pack, runtimeLimits, options = {}) {
2835
2863
  }
2836
2864
  for (const tombstone of hidden) if (!revived.includes(tombstone)) revived.push(tombstone);
2837
2865
  }
2838
- if (keys.has(key)) continue;
2866
+ if (keys.has(key)) {
2867
+ const existing = entries.find((candidate) => entryKey(candidate) === key);
2868
+ if (entry.pinned !== void 0 && (existing.pinned === void 0 || entry.updated_at > existing.updated_at)) {
2869
+ existing.pinned = entry.pinned;
2870
+ if (entry.updated_at > existing.updated_at) existing.updated_at = entry.updated_at;
2871
+ }
2872
+ continue;
2873
+ }
2839
2874
  keys.add(key);
2840
2875
  entries.push(entry);
2841
2876
  incomingKeys.add(key);
@@ -3560,6 +3595,44 @@ var MnemonReviewLedger = class {
3560
3595
  }
3561
3596
  };
3562
3597
  //#endregion
3598
+ //#region src/host/disposable-directory.ts
3599
+ function makeWritable(path) {
3600
+ let entry;
3601
+ try {
3602
+ entry = lstatSync(path);
3603
+ } catch (error) {
3604
+ if (error.code === "ENOENT") return;
3605
+ throw error;
3606
+ }
3607
+ if (entry.isSymbolicLink()) return;
3608
+ if ((entry.mode & 128) === 0) chmodSync(path, entry.mode | 128);
3609
+ if (entry.isDirectory()) for (const name of readdirSync(path)) makeWritable(join(path, name));
3610
+ }
3611
+ /** Remove only a caller-owned disposable tree, including read-only Git packs on Windows. */
3612
+ function removeDisposableDirectory(path) {
3613
+ try {
3614
+ rmSync(path, {
3615
+ recursive: true,
3616
+ force: true
3617
+ });
3618
+ } catch (error) {
3619
+ const code = error.code;
3620
+ if (process.platform !== "win32" || ![
3621
+ "EPERM",
3622
+ "EACCES",
3623
+ "ENOTEMPTY",
3624
+ "EBUSY"
3625
+ ].includes(code ?? "")) throw error;
3626
+ makeWritable(path);
3627
+ rmSync(path, {
3628
+ recursive: true,
3629
+ force: true,
3630
+ maxRetries: 3,
3631
+ retryDelay: 100
3632
+ });
3633
+ }
3634
+ }
3635
+ //#endregion
3563
3636
  //#region src/host/github-transport.ts
3564
3637
  /**
3565
3638
  * How GitHub sign-in reaches GitHub.
@@ -4598,10 +4671,7 @@ var MnemonSyncSettingsStore = class {
4598
4671
  };
4599
4672
  }
4600
4673
  clearMirror() {
4601
- rmSync(this.mirror(), {
4602
- recursive: true,
4603
- force: true
4604
- });
4674
+ removeDisposableDirectory(this.mirror());
4605
4675
  }
4606
4676
  };
4607
4677
  //#endregion
@@ -5983,16 +6053,10 @@ var MnemonGitSync = class {
5983
6053
  cwd: mirror,
5984
6054
  signal
5985
6055
  });
5986
- if (origin.exitCode !== 0 || origin.stdout.trim() !== settings.repoUrl) rmSync(mirror, {
5987
- recursive: true,
5988
- force: true
5989
- });
6056
+ if (origin.exitCode !== 0 || origin.stdout.trim() !== settings.repoUrl) removeDisposableDirectory(mirror);
5990
6057
  }
5991
6058
  if (!existsSync(join(mirror, ".git"))) {
5992
- rmSync(mirror, {
5993
- recursive: true,
5994
- force: true
5995
- });
6059
+ removeDisposableDirectory(mirror);
5996
6060
  const initialized = await this.git([
5997
6061
  "init",
5998
6062
  "--quiet",
@@ -6119,10 +6183,7 @@ var MnemonGitSync = class {
6119
6183
  const root = this.payloadRoot(subdir);
6120
6184
  const mirror = resolve(this.store.mirror());
6121
6185
  if (root === mirror || !root.startsWith(mirror + (process.platform === "win32" ? "\\" : "/"))) throw new Error("sync directory escapes the mirror");
6122
- rmSync(root, {
6123
- recursive: true,
6124
- force: true
6125
- });
6186
+ removeDisposableDirectory(root);
6126
6187
  for (const [path, bytes] of Object.entries(entries)) {
6127
6188
  const file = resolve(root, path);
6128
6189
  mkdirSync(join(file, ".."), {
@@ -9456,7 +9517,7 @@ const WRITE_TOOL_FALLBACK_ACTION = {
9456
9517
  };
9457
9518
  function reviewReceipt(receipt) {
9458
9519
  if (!mutationResultCommitted(receipt.value)) return void 0;
9459
- const value = object$6(receipt.value);
9520
+ const value = object$7(receipt.value);
9460
9521
  const args = optionalObject(receipt.arguments);
9461
9522
  const document = optionalObject(value.document);
9462
9523
  return {
@@ -9467,7 +9528,7 @@ function reviewReceipt(receipt) {
9467
9528
  ...typeof value.revision === "string" ? { revision: value.revision } : {}
9468
9529
  };
9469
9530
  }
9470
- function object$6(value) {
9531
+ function object$7(value) {
9471
9532
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("memory subagent returned an invalid structured result");
9472
9533
  return value;
9473
9534
  }
@@ -9672,7 +9733,11 @@ function runtimeRouteChunks(entries) {
9672
9733
  function compactedBudget(plan) {
9673
9734
  const pendingBytes = plan.pending === void 0 ? 0 : Buffer.byteLength(plan.pending.content, "utf8");
9674
9735
  const separatorBytes = plan.pending === void 0 || plan.entries.length === 0 ? 0 : Buffer.byteLength(RUNTIME_ENTRY_DELIMITER, "utf8");
9675
- return Math.max(0, Math.floor(plan.limit * .7) - pendingBytes - separatorBytes);
9736
+ const pinned = plan.entries.filter((entry) => entry.pinned === true);
9737
+ const pinnedBytes = Buffer.byteLength(pinned.map((entry) => entry.content).join(RUNTIME_ENTRY_DELIMITER), "utf8");
9738
+ const available = plan.limit - pendingBytes - separatorBytes;
9739
+ if ((pinned.length > 0 || plan.pending?.pinned === true) && pinnedBytes > available) throw new Error("Pinned runtime entries leave no capacity for this write. Unpin entries or increase the capacity; no archive writes were attempted.");
9740
+ return Math.max(pinnedBytes, Math.max(0, Math.floor(plan.limit * .7) - pendingBytes - separatorBytes));
9676
9741
  }
9677
9742
  function runtimeMigrationSources(revision, entries) {
9678
9743
  return entries.map((entry, offset) => ({
@@ -9703,7 +9768,7 @@ async function runtimeArchiveSource(runtimeSource, graph, scope, view, generatio
9703
9768
  return {
9704
9769
  source,
9705
9770
  cleanup,
9706
- memoryBodyIds: new Set(strings(object$6(grant.value).memoryBodyIds)),
9771
+ memoryBodyIds: new Set(strings(object$7(grant.value).memoryBodyIds)),
9707
9772
  writeScope: {
9708
9773
  viewId: view.id,
9709
9774
  grant
@@ -9721,10 +9786,11 @@ async function archiveRuntimeLocally(counters, context, request, plan, reason, s
9721
9786
  if (plan.localArchive !== true) throw new Error(`runtime memory archival found no Memory Space that can take the archive (${reason}), and this Runtime Source cannot archive locally; pending write was not committed and existing runtime entries are unchanged; update dsh-mnemon-source-runtime, activate a supported Memory Space, or increase runtimeMemory.memoryLimitBytes`);
9722
9787
  context.assertWritable?.();
9723
9788
  const runId = `host-${randomUUID()}`;
9724
- const compacted = plan.entries.map(({ content, importance, branches }) => ({
9789
+ const compacted = plan.entries.map(({ content, importance, branches, pinned }) => ({
9725
9790
  content,
9726
9791
  importance,
9727
- ...branches === void 0 ? {} : { branches }
9792
+ ...branches === void 0 ? {} : { branches },
9793
+ ...pinned === void 0 ? {} : { pinned }
9728
9794
  }));
9729
9795
  const mutation = await context.runtime.mutateResult("compact-and-mutate", {
9730
9796
  revision: plan.revision,
@@ -9767,7 +9833,9 @@ async function runtimeLocked(counters, context, request, signal) {
9767
9833
  if (context.expectedRevision !== void 0 && context.expectedRevision !== plan.revision) throw new Error("Runtime source revision conflict before capacity maintenance");
9768
9834
  if (!plan.requiresMaintenance) return context.commit();
9769
9835
  if (plan.entries.length === 0) throw new Error("runtime memory capacity was exceeded without entries available for maintenance");
9836
+ compactedBudget(plan);
9770
9837
  if (request.target === "user") return compactUserAndCommit(context, request, plan, signal);
9838
+ const archiveEntries = plan.entries.filter((entry) => entry.pinned !== true);
9771
9839
  const archive = await context.memorySpaces();
9772
9840
  if (archive === void 0) return archiveRuntimeLocally(counters, context, request, plan, "no Memory Space takes automatic writes here", signal);
9773
9841
  const memoryService = archive.source;
@@ -9802,11 +9870,11 @@ async function runtimeLocked(counters, context, request, signal) {
9802
9870
  let provider = "host";
9803
9871
  let runId = `host-${randomUUID()}`;
9804
9872
  let summary = "Routed every entry to the only eligible Memory Space without model work.";
9805
- if (eligibleBodies.length === 1) for (const index of plan.entries.keys()) routed.set(index + 1, eligibleBodies[0].id);
9873
+ if (eligibleBodies.length === 1) for (const index of archiveEntries.keys()) routed.set(index + 1, eligibleBodies[0].id);
9806
9874
  else {
9807
9875
  const summaries = [];
9808
9876
  const fallbackBody = eligibleBodies.find((body) => body.mnemonDefault) ?? eligibleBodies[0];
9809
- const chunks = runtimeRouteChunks(plan.entries);
9877
+ const chunks = runtimeRouteChunks(archiveEntries);
9810
9878
  for (const [chunkIndex, chunk] of chunks.entries()) {
9811
9879
  const prompt = `Route this bounded MEMORY.md archive batch now. The host retains and writes the exact source content; these excerpts exist only for destination selection.
9812
9880
 
@@ -9825,13 +9893,13 @@ ${chunk.context}
9825
9893
  provider = delegated.provider;
9826
9894
  runId = delegated.runId;
9827
9895
  }
9828
- const value = object$6(delegated.result.structured);
9896
+ const value = object$7(delegated.result.structured);
9829
9897
  if (value.action !== "planned") throw new Error(typeof value.summary === "string" && value.summary !== "" ? value.summary : "runtime memory archival routing failed");
9830
9898
  if (!Array.isArray(value.routes) || value.routes.length === 0) throw new Error("runtime memory migration returned no routes");
9831
9899
  const allowedIndexes = new Set(chunk.indexes);
9832
9900
  const proposed = /* @__PURE__ */ new Map();
9833
9901
  for (const candidate of value.routes) {
9834
- const route = object$6(candidate);
9902
+ const route = object$7(candidate);
9835
9903
  const memoryBodyId = typeof route.memoryBodyId === "string" ? route.memoryBodyId.trim() : "";
9836
9904
  if (memoryBodyId === "" || !eligibleById.has(memoryBodyId)) throw new Error(`runtime memory migration selected an invalid Memory Space: ${memoryBodyId || "(empty)"}`);
9837
9905
  if (!Array.isArray(route.sourceIndexes) || route.sourceIndexes.length === 0) throw new Error("runtime memory migration route must contain source indexes");
@@ -9851,11 +9919,12 @@ ${chunk.context}
9851
9919
  }
9852
9920
  summary = summaries.join(" ");
9853
9921
  }
9854
- if (routed.size !== plan.entries.length) throw new Error("runtime memory migration omitted committed archive sources");
9855
- const compactedEntries = plan.entries.map(({ content, importance, branches }) => ({
9922
+ if (routed.size !== archiveEntries.length) throw new Error("runtime memory migration omitted committed archive sources");
9923
+ const compactedEntries = plan.entries.map(({ content, importance, branches, pinned }) => ({
9856
9924
  content,
9857
9925
  importance,
9858
- ...branches === void 0 ? {} : { branches }
9926
+ ...branches === void 0 ? {} : { branches },
9927
+ ...pinned === void 0 ? {} : { pinned }
9859
9928
  }));
9860
9929
  if ((await runtimeMemory.read("maintenance-plan", {
9861
9930
  ...request,
@@ -9865,9 +9934,9 @@ ${chunk.context}
9865
9934
  for (const memoryBodyId of new Set(routed.values())) if (!destinations.items.some((body) => body.id === memoryBodyId && eligible(body))) throw new Error(`runtime archive Memory Space ${memoryBodyId} is no longer eligible; no archive writes were attempted`);
9866
9935
  signal.throwIfAborted();
9867
9936
  context.assertWritable?.();
9868
- const sources = runtimeMigrationSources(plan.revision, plan.entries);
9937
+ const sources = runtimeMigrationSources(plan.revision, archiveEntries);
9869
9938
  const requests = sources.map((source) => {
9870
- const entry = plan.entries[source.index - 1];
9939
+ const entry = archiveEntries[source.index - 1];
9871
9940
  return {
9872
9941
  content: entry.content,
9873
9942
  category: "context",
@@ -9910,7 +9979,7 @@ ${chunk.context}
9910
9979
  archiveResults.set(source.index, result);
9911
9980
  }
9912
9981
  if (written.length !== batch.length) throw new Error("runtime archive batch did not return one receipt per source entry");
9913
- for (const source of batch) verified.set(source.index, await archiveRuntimeEntry(memoryService, memoryBodyId, plan.entries[source.index - 1], archiveResults.get(source.index), signal));
9982
+ for (const source of batch) verified.set(source.index, await archiveRuntimeEntry(memoryService, memoryBodyId, archiveEntries[source.index - 1], archiveResults.get(source.index), signal));
9914
9983
  }
9915
9984
  const lineage = [];
9916
9985
  const memoryBodyIds = /* @__PURE__ */ new Set();
@@ -9977,16 +10046,18 @@ ${chunk.context}
9977
10046
  async function compactUserAndCommit(context, request, plan, signal) {
9978
10047
  const runtimeMemory = context.runtime;
9979
10048
  const budget = compactedBudget(plan);
10049
+ const pinned = plan.entries.filter((entry) => entry.pinned === true);
10050
+ const mutable = plan.entries.filter((entry) => entry.pinned !== true);
9980
10051
  const prompt = `Run local USER.md compaction now.
9981
10052
  Pending mutation (uncommitted; do not include in compaction):
9982
10053
  ${pendingMutationContext(plan)}
9983
10054
 
9984
- ${runtimeSnapshotContext("user", plan.entries)}`;
10055
+ ${runtimeSnapshotContext("user", mutable)}`;
9985
10056
  const { provider, runId, result } = await context.model("compaction", "Consolidate local user profile", prompt, USER_COMPACTION_SCHEMA, USER_COMPACTION_PERSONA);
9986
- const value = object$6(result.structured);
10057
+ const value = object$7(result.structured);
9987
10058
  if (value.action !== "compacted") throw new Error(typeof value.summary === "string" && value.summary !== "" ? value.summary : "USER.md compaction failed");
9988
10059
  const compactedEntries = Array.isArray(value.compactedEntries) ? value.compactedEntries.map((entry) => {
9989
- const item = object$6(entry);
10060
+ const item = object$7(entry);
9990
10061
  if (typeof item.content !== "string" || ![
9991
10062
  "critical",
9992
10063
  "normal",
@@ -10010,17 +10081,21 @@ ${runtimeSnapshotContext("user", plan.entries)}`;
10010
10081
  if (entry.sourceIndexes.length === 0) throw new Error("USER.md compaction returned an entry without a source");
10011
10082
  let requiredRank = 0;
10012
10083
  for (const index of entry.sourceIndexes) {
10013
- if (index < 1 || index > plan.entries.length || seen.has(index)) throw new Error("USER.md compaction source coverage is invalid");
10084
+ if (index < 1 || index > mutable.length || seen.has(index)) throw new Error("USER.md compaction source coverage is invalid");
10014
10085
  seen.add(index);
10015
- requiredRank = Math.max(requiredRank, importanceRank[plan.entries[index - 1].importance]);
10086
+ requiredRank = Math.max(requiredRank, importanceRank[mutable[index - 1].importance]);
10016
10087
  }
10017
10088
  if (importanceRank[entry.importance] < requiredRank) throw new Error("USER.md compaction lowered source importance");
10018
10089
  }
10019
- if (seen.size !== plan.entries.length) throw new Error("USER.md compaction omitted committed entries");
10020
- const candidates = compactedEntries.map(({ content, importance }) => ({
10090
+ if (seen.size !== mutable.length) throw new Error("USER.md compaction omitted committed entries");
10091
+ const candidates = [...pinned.map((entry) => ({
10092
+ content: entry.content,
10093
+ importance: entry.importance,
10094
+ pinned: true
10095
+ })), ...compactedEntries.map(({ content, importance }) => ({
10021
10096
  content,
10022
10097
  importance
10023
- }));
10098
+ }))];
10024
10099
  const candidateBytes = Buffer.byteLength(candidates.map((entry) => entry.content.trim().replace(/\s+/gu, " ")).join(RUNTIME_ENTRY_DELIMITER), "utf8");
10025
10100
  if (candidateBytes > budget) throw new Error(`USER.md compaction did not fit the host budget (${candidateBytes} > ${budget} bytes)`);
10026
10101
  signal.throwIfAborted();
@@ -10160,7 +10235,7 @@ async function archiveDocumentLocked(host, parent, id, signal) {
10160
10235
  const cleanup = graph.source("memory-spaces", agentScope(parent, graph.config)).forInstance(identity.sourceInstanceKey).forGeneration(lease.generation);
10161
10236
  const grant = authority?.context.view.readGrants.find((grant) => grant.sourceInstanceKey === identity.sourceInstanceKey && grant.schema === "dsh-mnemon.memory-spaces/v1");
10162
10237
  if (authority !== void 0 && grant === void 0) throw new Error("document archive requires a Memory Space namespace grant");
10163
- const allowed = grant === void 0 ? void 0 : new Set(strings(object$6(grant.value).memoryBodyIds));
10238
+ const allowed = grant === void 0 ? void 0 : new Set(strings(object$7(grant.value).memoryBodyIds));
10164
10239
  const directory = await memoryService.read("body-directory", null, signal);
10165
10240
  const indexable = (body) => archivableSpace(directory, body);
10166
10241
  const eligible = (body) => indexable(body) && (allowed === void 0 || allowed.has(body.id));
@@ -10170,7 +10245,7 @@ async function archiveDocumentLocked(host, parent, id, signal) {
10170
10245
  throw new Error("document archive requires an existing active Memory Space with exact writes and safe forget");
10171
10246
  }
10172
10247
  const { provider, runId, result } = await host.delegate(parent, "document-archive", "Plan managed document archive", documentArchivePrompt(document, source, bodies), [], DOCUMENT_ARCHIVE_SCHEMA, signal, "spawn", DOCUMENT_ARCHIVE_PERSONA);
10173
- const value = object$6(result.structured);
10248
+ const value = object$7(result.structured);
10174
10249
  const summary = typeof value.summary === "string" ? value.summary.trim() : "";
10175
10250
  if (value.action !== "planned") throw new Error(summary || "document archive planning failed");
10176
10251
  if (summary === "" || summary.length > 1e3) throw new Error("document archive summary must contain 1–1000 characters");
@@ -10357,9 +10432,16 @@ const ACTION_TOOLS = namedBindings("action");
10357
10432
  const NAMED_SOURCE_TYPES = [.../* @__PURE__ */ new Set([...Object.keys(ROUTE_TOOLS), ...Object.keys(ACTION_TOOLS)])];
10358
10433
  /** Use a short availability list only where the Host has the matching named binding.
10359
10434
  * Unbound external operations retain exact ids and schemas in the generic envelope. */
10360
- function modelMemoryWake(graph, turn) {
10435
+ function modelMemoryWake(graph, turn, includeRuntime = true) {
10361
10436
  const generation = graph.memoryComposition.generation(turn.view.runtimeGeneration);
10362
10437
  if (generation === void 0) throw new Error("Cannot present an unpinned Memory generation");
10438
+ if (!includeRuntime) {
10439
+ const runtimeSources = new Set(generation.sourceInstances().filter((source) => source.sourceTypeId === "runtime").map((source) => source.sourceInstanceKey));
10440
+ return createMemoryWake({
10441
+ ...turn.view,
10442
+ projection: turn.view.projection.filter((fragment) => !runtimeSources.has(fragment.sourceInstanceKey))
10443
+ }, memoryWakeBindings(generation, turn.view));
10444
+ }
10363
10445
  return graph.composableTurns.memoryWake(turn.view.id, memoryWakeBindings(generation, turn.view));
10364
10446
  }
10365
10447
  function presentMemoryWake(generation, view) {
@@ -10466,6 +10548,23 @@ function openAgentTurn(agent) {
10466
10548
  }
10467
10549
  return open;
10468
10550
  }
10551
+ /** PTC dispatch records belong to a real open step, not just an open turn. */
10552
+ function openAgentStep(agent) {
10553
+ let turn;
10554
+ let open;
10555
+ for (const event of agent.session.snapshotEvents()) if (event.type === "turn/start") {
10556
+ turn = typeof event.data.turn === "number" ? event.data.turn : void 0;
10557
+ open = void 0;
10558
+ } else if (event.type === "turn/end" && event.data.turn === turn) {
10559
+ turn = void 0;
10560
+ open = void 0;
10561
+ } else if (event.type === "step/start" && turn !== void 0 && event.data.turn === turn && typeof event.data.step === "number") open = {
10562
+ turn,
10563
+ step: event.data.step
10564
+ };
10565
+ else if (event.type === "step/end" && open?.turn === event.data.turn && open?.step === event.data.step) open = void 0;
10566
+ return open;
10567
+ }
10469
10568
  /**
10470
10569
  * One Agent owns each turn pin. A child additionally retains its delegation
10471
10570
  * across parent completion, collection, and runtime swaps until it is disposed.
@@ -10485,17 +10584,23 @@ var AgentMemoryTurn = class {
10485
10584
  lastInspection;
10486
10585
  lastWorkspace;
10487
10586
  releaseDelegation;
10587
+ includeRuntime;
10488
10588
  constructor(agent, runtime, delegation, independent = false) {
10489
10589
  this.agent = agent;
10490
10590
  this.runtime = runtime;
10491
10591
  this.delegation = delegation;
10492
10592
  this.independent = independent;
10593
+ this.includeRuntime = delegation === void 0 || runtime.config.runtimeMemory.inheritToSubagents;
10493
10594
  if (delegation === void 0) return;
10494
10595
  this.releaseDelegation = runtime.executions.retain(agent, delegation);
10495
10596
  }
10496
10597
  get current() {
10497
10598
  return this.pinned;
10498
10599
  }
10600
+ modelWake() {
10601
+ const pinned = this.pinned;
10602
+ return pinned === void 0 ? void 0 : modelMemoryWake(pinned.graph, pinned.context, this.includeRuntime);
10603
+ }
10499
10604
  /** Why the current turn has no View, when memory could not be composed for it. */
10500
10605
  get unavailableReason() {
10501
10606
  return this.unavailable?.reason;
@@ -10586,7 +10691,7 @@ var AgentMemoryTurn = class {
10586
10691
  try {
10587
10692
  if (this.closed || this.generation !== generation) throw new Error("memory turn ended during View preparation");
10588
10693
  signal?.throwIfAborted();
10589
- const inspection = inspectMemoryView(graph.memoryComposition.generation(context.view.runtimeGeneration), context.view, "active", turn, modelMemoryWake(graph, context).text);
10694
+ const inspection = inspectMemoryView(graph.memoryComposition.generation(context.view.runtimeGeneration), context.view, "active", turn, modelMemoryWake(graph, context, this.includeRuntime).text);
10590
10695
  this.pinned = {
10591
10696
  turn,
10592
10697
  graph,
@@ -10630,7 +10735,7 @@ async function executeHostTool(tools, input) {
10630
10735
  //#region src/host/review-admission.ts
10631
10736
  const RESULT_TOOL = "mnemon_subagent_result";
10632
10737
  const CHECK_TIMEOUT_MS = 5e3;
10633
- /** An in-turn no-op challenge through the mandatory review completion channel.
10738
+ /** An in-step no-op challenge through the mandatory review completion channel.
10634
10739
  * Inventory alone cannot see execution-time guards. No errors are classified as
10635
10740
  * permission failures, and an unavailable checkpoint is never a permanent ban.
10636
10741
  */
@@ -10656,7 +10761,7 @@ var AutomaticReviewAdmission = class {
10656
10761
  async check(agent, signal) {
10657
10762
  signal.throwIfAborted();
10658
10763
  const tools = agent.ctx.tools;
10659
- if (this.disposed || tools?.execute === void 0 || this.draining.has(agent) || openAgentTurn(agent) === void 0) return false;
10764
+ if (this.disposed || tools?.execute === void 0 || this.draining.has(agent) || openAgentStep(agent) === void 0) return false;
10660
10765
  const controller = new AbortController();
10661
10766
  this.controllers.add(controller);
10662
10767
  const combined = AbortSignal.any([signal, controller.signal]);
@@ -11069,7 +11174,7 @@ var MnemonSubagentCoordinator = class {
11069
11174
  if (!isSubagent(execution.agent)) throw new Error("Mnemon subagent result tools are restricted to delegated children");
11070
11175
  execution.signal.throwIfAborted();
11071
11176
  assertDshOutputValue(RESULT_TOOL_INPUT_SCHEMA, args);
11072
- const request = object$6(args);
11177
+ const request = object$7(args);
11073
11178
  const submit = this.resultRequests.get(String(request.requestId));
11074
11179
  if (this.disposed || submit === void 0) throw new Error("Mnemon subagent result request is unknown or no longer active");
11075
11180
  return submit(request.result, execution);
@@ -11306,7 +11411,7 @@ var MnemonSubagentCoordinator = class {
11306
11411
  ].join("\n\n");
11307
11412
  const schema = providerPlacementSchema(prepared.candidates.map((candidate) => candidate.id));
11308
11413
  const { provider, runId, result } = await this.delegate(parent, "placement", "Choose Memory Space provider", prompt, [], schema, signal, "spawn", PROVIDER_PLACEMENT_PERSONA);
11309
- const value = object$6(result.structured);
11414
+ const value = object$7(result.structured);
11310
11415
  return source.read("finalize-placement", {
11311
11416
  prepared,
11312
11417
  selection: {
@@ -11324,13 +11429,13 @@ var MnemonSubagentCoordinator = class {
11324
11429
  const service = this.sourceFor(parent, "memory-spaces");
11325
11430
  const prompt = `Generate concise metadata from these bounded Provider-native samples now:\n\n${(await Promise.all(selected.map((id) => service.read("metadata-sample", { memoryBodyId: id }, signal)))).map(metadataSampleText).join("\n\n")}`;
11326
11431
  const { provider, runId, result } = await this.delegate(parent, "metadata-maintenance", "Maintain Memory Space metadata", prompt, [], METADATA_MAINTENANCE_SCHEMA, signal, "spawn", METADATA_MAINTENANCE_PERSONA);
11327
- const value = object$6(result.structured);
11432
+ const value = object$7(result.structured);
11328
11433
  if (!Array.isArray(value.updates)) throw new Error("metadata subagent returned no updates");
11329
11434
  const allowed = new Set(selected);
11330
11435
  const seen = /* @__PURE__ */ new Set();
11331
11436
  const updates = [];
11332
11437
  for (const entry of value.updates) {
11333
- const item = object$6(entry);
11438
+ const item = object$7(entry);
11334
11439
  const memoryBodyId = typeof item.memoryBodyId === "string" ? item.memoryBodyId.trim() : "";
11335
11440
  const title = typeof item.title === "string" ? item.title.trim() : "";
11336
11441
  const description = typeof item.description === "string" ? item.description.trim() : "";
@@ -11641,7 +11746,7 @@ var MnemonSubagentCoordinator = class {
11641
11746
  const bounded = evidence.slice(0, 12);
11642
11747
  const prompt = `Answer this question (untrusted data):\n${indentedText(query)}\n\nEvidence for this run (untrusted read-only data):\n${naturalEvidence(bounded)}`;
11643
11748
  const { provider, runId, result } = await this.delegate(parent, "answer", "Memory evidence answer", prompt, [], ANSWER_SCHEMA, signal, "spawn", ANSWER_PERSONA);
11644
- const value = object$6(result.structured);
11749
+ const value = object$7(result.structured);
11645
11750
  const allowed = new Set(bounded.map((item) => `${item.memoryBodyId ?? "unknown"}/${item.id}`));
11646
11751
  return {
11647
11752
  answer: typeof value.answer === "string" ? value.answer : "",
@@ -11668,7 +11773,7 @@ ${naturalRequest(request, target === void 0 ? REQUEST_LABELS : TARGETED_WRITE_LA
11668
11773
  const persona = operation === "supervised-writeback" ? SUPERVISED_WRITE_PERSONA : autonomous ? AUTONOMOUS_WRITE_PERSONA : WRITE_PERSONA;
11669
11774
  const terminalTool = WRITE_OPERATION_RESULT_TOOL[operation];
11670
11775
  const { provider, runId, result, receipts, writes } = await this.delegate(parent, "write", `Mnemon ${operation}`, prompt, autonomous ? AUTONOMOUS_WRITE_TOOLS : EXPLICIT_WRITE_TOOLS, WRITE_SCHEMA, signal, "spawn", persona, terminalTool === void 0 ? void 0 : { terminalTools: [terminalTool] }, inactive === void 0 ? void 0 : spaceWriteGuard(target, inactive));
11671
- const value = object$6(result.structured);
11776
+ const value = object$7(result.structured);
11672
11777
  const observed = terminalTool === void 0 ? void 0 : recoverStructuredResult({ terminalTools: [terminalTool] }, receipts);
11673
11778
  const claimed = typeof observed?.action === "string" ? observed.action : typeof value.action === "string" ? value.action : "failed";
11674
11779
  if (!autonomous) return {
@@ -11777,7 +11882,7 @@ ${naturalRequest(request, target === void 0 ? REQUEST_LABELS : TARGETED_WRITE_LA
11777
11882
  const base = reviewPersona(tools.includes("mnemon_runtime_memory"));
11778
11883
  const persona = preferred === "fork" ? base : base.replaceAll("inherited", "supplied bounded");
11779
11884
  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);
11780
- const value = object$6(result.structured);
11885
+ const value = object$7(result.structured);
11781
11886
  return {
11782
11887
  delegated: true,
11783
11888
  runId,
@@ -11948,7 +12053,7 @@ This is the only completion channel for this run. Do not finish with a plain-tex
11948
12053
  "failed",
11949
12054
  "partial",
11950
12055
  "unknown"
11951
- ].includes(String(object$6(structured).action))) throw new Error(`memory review reported ${object$6(structured).action}`);
12056
+ ].includes(String(object$7(structured).action))) throw new Error(`memory review reported ${object$7(structured).action}`);
11952
12057
  completed = {
11953
12058
  provider,
11954
12059
  runId: activeRun.id,
@@ -12111,7 +12216,7 @@ const MNEMON_ACTIVITY_SCHEMA = "dsh-mnemon.activity/v1";
12111
12216
  const MAX_ITEMS = 8;
12112
12217
  const MAX_TITLE = 160;
12113
12218
  const MAX_EXCERPT = 500;
12114
- function object$5(value) {
12219
+ function object$6(value) {
12115
12220
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
12116
12221
  }
12117
12222
  function bounded(value, maximum) {
@@ -12121,7 +12226,7 @@ function bounded(value, maximum) {
12121
12226
  return truncate(normalized, maximum);
12122
12227
  }
12123
12228
  function item(value, index) {
12124
- const entry = object$5(value);
12229
+ const entry = object$6(value);
12125
12230
  if (entry === void 0) return void 0;
12126
12231
  const content = bounded(entry.content ?? entry.text ?? entry.excerpt, MAX_EXCERPT);
12127
12232
  const explicitTitle = bounded(entry.title ?? entry.label ?? entry.name ?? entry.memoryBodyName, MAX_TITLE);
@@ -12135,7 +12240,7 @@ function item(value, index) {
12135
12240
  };
12136
12241
  }
12137
12242
  function resultItems(value) {
12138
- const root = object$5(value);
12243
+ const root = object$6(value);
12139
12244
  if (root === void 0) return [];
12140
12245
  const candidates = [
12141
12246
  root.results,
@@ -12154,8 +12259,8 @@ function resultItems(value) {
12154
12259
  return items;
12155
12260
  }
12156
12261
  function writeItem(argsValue, operationId) {
12157
- const outer = object$5(argsValue) ?? {};
12158
- const input = object$5(outer.input) ?? outer;
12262
+ const outer = object$6(argsValue) ?? {};
12263
+ const input = object$6(outer.input) ?? outer;
12159
12264
  const content = bounded(input.content ?? input.description ?? input.reason ?? input.summary ?? input.old_text ?? input.oldText, MAX_EXCERPT);
12160
12265
  const explicitTitle = bounded(input.title ?? input.name, MAX_TITLE);
12161
12266
  const identity = bounded(input.id ?? input.memoryBodyId ?? input.targetMemoryBodyId ?? input.sourceId, 300);
@@ -12167,7 +12272,7 @@ function writeItem(argsValue, operationId) {
12167
12272
  };
12168
12273
  }
12169
12274
  function reference(argsValue, key) {
12170
- return bounded(object$5(argsValue)?.[key], 600);
12275
+ return bounded(object$6(argsValue)?.[key], 600);
12171
12276
  }
12172
12277
  function memoryReadPresentation(sourceTypeId, operationId, referenceKey) {
12173
12278
  return (args, value) => ({
@@ -12186,7 +12291,7 @@ function memoryWritePresentation(sourceTypeId, operationId, referenceKey) {
12186
12291
  operationId,
12187
12292
  ...sourceTypeId === void 0 ? {} : { sourceTypeId },
12188
12293
  ...referenceKey === void 0 || reference(args, referenceKey) === void 0 ? {} : { reference: reference(args, referenceKey) },
12189
- committed: mutationResultCommitted(value) || operationId === "mutate" && object$5(value)?.success === true,
12294
+ committed: mutationResultCommitted(value) || operationId === "mutate" && object$6(value)?.success === true,
12190
12295
  item: writeItem(args, operationId)
12191
12296
  });
12192
12297
  }
@@ -12196,7 +12301,7 @@ function namedToolPresentation(name) {
12196
12301
  return tool.kind === "write" ? memoryWritePresentation(tool.sourceTypeId, tool.operation) : memoryReadPresentation(tool.sourceTypeId, tool.operation);
12197
12302
  }
12198
12303
  function validItem(value) {
12199
- const entry = object$5(value);
12304
+ const entry = object$6(value);
12200
12305
  return entry !== void 0 && bounded(entry.id, 300) === entry.id && bounded(entry.title, MAX_TITLE) === entry.title && (entry.excerpt === void 0 || bounded(entry.excerpt, MAX_EXCERPT) === entry.excerpt);
12201
12306
  }
12202
12307
  function cleanItem(value) {
@@ -12209,7 +12314,7 @@ function cleanItem(value) {
12209
12314
  }
12210
12315
  /** Fail closed on arbitrary third-party/session metadata. */
12211
12316
  function parseMnemonActivityMeta(value, callId, toolName) {
12212
- const meta = object$5(value);
12317
+ const meta = object$6(value);
12213
12318
  if (meta?.schema !== "dsh-mnemon.activity/v1" || meta.kind !== "read" && meta.kind !== "write") return {};
12214
12319
  const operationId = bounded(meta.operationId, 300);
12215
12320
  const sourceTypeId = bounded(meta.sourceTypeId, 128);
@@ -12778,6 +12883,7 @@ var MnemonAgentLifecycle = class {
12778
12883
  reviewController;
12779
12884
  reviewRunning = false;
12780
12885
  reviewAccessBlocked = false;
12886
+ reviewAdmission;
12781
12887
  lastReviewAt;
12782
12888
  lastReviewAction;
12783
12889
  lastReviewScore;
@@ -12815,6 +12921,10 @@ var MnemonAgentLifecycle = class {
12815
12921
  this.agent.ctx.on("session/event", ((session, event) => this.sessionEvent(session, event))),
12816
12922
  this.agent.ctx.on("system-prompt/assemble", ((assembly, context, next) => this.assemblePrompt(assembly, context, next))),
12817
12923
  this.agent.ctx.on("agent/pre-step", ((payload, next) => this.preStep(payload, next)), { prepend: true }),
12924
+ this.agent.ctx.on("agent/request", (async (payload, next) => {
12925
+ await this.prepareReviewAccess(payload);
12926
+ return next();
12927
+ }), { prepend: true }),
12818
12928
  this.agent.ctx.on("agent/turn-stopping", ((payload) => this.finishTurn(payload)))
12819
12929
  ];
12820
12930
  return () => {
@@ -12959,18 +13069,28 @@ var MnemonAgentLifecycle = class {
12959
13069
  activity.toolCallCount = tools.count;
12960
13070
  activity.toolNames = tools.names;
12961
13071
  if (!this.reviewActivity().eligible || !this.reviewAdmitted(payload.turn)) return;
13072
+ const admission = this.reviewAdmission;
13073
+ if (admission?.turn === payload.turn && admission.policy === this.reviewPolicy() && admission.available) this.scheduleIdleReview(payload.turn);
13074
+ }
13075
+ async prepareReviewAccess(payload) {
13076
+ this.recordComposedTurn(payload.turn);
13077
+ if (payload.signal.aborted || !this.restoreWatermark() || !this.idleReviewAllowed() || this.turnActivity.get(payload.turn)?.noMaintenance) return;
13078
+ const policy = this.reviewPolicy();
13079
+ if (this.reviewAdmission?.turn === payload.turn && this.reviewAdmission.policy === policy) return;
12962
13080
  const controller = new AbortController();
12963
13081
  const signal = AbortSignal.any([payload.signal, controller.signal]);
12964
13082
  const epoch = this.reviewEpoch;
12965
- const policy = this.reviewPolicy();
12966
13083
  this.reviewController = controller;
12967
13084
  this.reviewRunning = true;
12968
- let admitted = false;
12969
13085
  try {
12970
13086
  const available = await this.coordinator.checkReviewAccess(this.agent, signal);
12971
13087
  if (signal.aborted || epoch !== this.reviewEpoch || policy !== this.reviewPolicy() || !this.idleReviewAllowed()) return;
12972
13088
  this.reviewAccessBlocked = !available;
12973
- admitted = available;
13089
+ this.reviewAdmission = {
13090
+ turn: payload.turn,
13091
+ policy,
13092
+ available
13093
+ };
12974
13094
  } catch (error) {
12975
13095
  if (!signal.aborted) this.fail(error);
12976
13096
  } finally {
@@ -12979,7 +13099,6 @@ var MnemonAgentLifecycle = class {
12979
13099
  this.reviewController = void 0;
12980
13100
  }
12981
13101
  }
12982
- if (admitted) this.scheduleIdleReview(payload.turn);
12983
13102
  }
12984
13103
  sessionEvent(session, event) {
12985
13104
  if (session !== this.agent.session) return;
@@ -13072,6 +13191,7 @@ var MnemonAgentLifecycle = class {
13072
13191
  if (policy !== this.observedReviewPolicy) {
13073
13192
  this.cancelIdleReview(true);
13074
13193
  this.reviewAccessBlocked = false;
13194
+ this.reviewAdmission = void 0;
13075
13195
  }
13076
13196
  this.observedReviewPolicy = policy;
13077
13197
  this.restoreWatermark();
@@ -13871,6 +13991,16 @@ var MnemonLifecycle = class {
13871
13991
  ...typeof effort === "string" && effort !== "" ? { reasoningEffort: effort } : {}
13872
13992
  };
13873
13993
  }
13994
+ /** Freeze the effective task route for a paired skill evaluation. */
13995
+ skillTaskRoute(workspaceRoot) {
13996
+ const options = this.taskAgentModelOptions("", workspaceRoot);
13997
+ if (!options?.provider || !options.model) return void 0;
13998
+ return {
13999
+ provider: options.provider,
14000
+ model: options.model,
14001
+ ...options.reasoningEffort === void 0 ? {} : { reasoningEffort: options.reasoningEffort }
14002
+ };
14003
+ }
13874
14004
  taskAgentModelOptions(fallbackSessionId, workspaceRoot) {
13875
14005
  return this.taskAgentModelRoute(fallbackSessionId, workspaceRoot)?.options;
13876
14006
  }
@@ -13958,12 +14088,11 @@ var MnemonLifecycle = class {
13958
14088
  return decision;
13959
14089
  }
13960
14090
  if (openAgentTurn(agent) !== payload.turn) return decision;
13961
- const recovering = memory.current?.turn !== payload.turn;
13962
14091
  await memory.begin(payload.turn, payload.signal);
13963
14092
  const pinned = memory.current;
13964
- if (recovering && payload.step === 1 && this.config.lifecycleEnabled && pinned !== void 0) {
13965
- const wake = modelMemoryWake(pinned.graph, pinned.context);
13966
- if (wake.text.trim() !== "") return {
14093
+ if (payload.step === 1 && this.config.lifecycleEnabled && pinned !== void 0) {
14094
+ const wake = memory.modelWake();
14095
+ if (wake !== void 0 && wake.text.trim() !== "") return {
13967
14096
  kind: "enter",
13968
14097
  messages: [...decision.messages, createPluginMessage(wake.text, "recall")]
13969
14098
  };
@@ -13983,6 +14112,228 @@ var MnemonLifecycle = class {
13983
14112
  }
13984
14113
  };
13985
14114
  //#endregion
14115
+ //#region src/host/skill-auto-adoption.ts
14116
+ var AutomaticSkillStop = class extends Error {
14117
+ reason;
14118
+ constructor(reason, detail) {
14119
+ super(detail ?? reason);
14120
+ this.reason = reason;
14121
+ }
14122
+ };
14123
+ function hashSkillCases(cases) {
14124
+ return createHash("sha256").update(SkillsContracts.skillCaseSignature(cases)).digest("hex");
14125
+ }
14126
+ function hashSkillDraft(draft) {
14127
+ return createHash("sha256").update(draft.content).digest("hex");
14128
+ }
14129
+ /** Only fresh idle-review suggestions enter this bounded, in-process queue. */
14130
+ var SkillAutoAdoption = class {
14131
+ host;
14132
+ jobs = /* @__PURE__ */ new Map();
14133
+ tail = Promise.resolve();
14134
+ disposed = false;
14135
+ constructor(host) {
14136
+ this.host = host;
14137
+ }
14138
+ enqueue(source, workspace, ids, available) {
14139
+ if (this.disposed || !this.host.settings().autoAdopt) return;
14140
+ for (const id of ids) {
14141
+ if (this.jobs.has(id)) continue;
14142
+ if (this.jobs.size >= 16) {
14143
+ this.record({
14144
+ source,
14145
+ id
14146
+ }, {
14147
+ stage: "review",
14148
+ attempt: 0,
14149
+ reason: "queue-full"
14150
+ }).catch((error) => this.host.warn("Could not record automatic adoption queue limit", error));
14151
+ continue;
14152
+ }
14153
+ const job = {
14154
+ source,
14155
+ workspace,
14156
+ id,
14157
+ available,
14158
+ controller: new AbortController()
14159
+ };
14160
+ this.jobs.set(id, job);
14161
+ this.tail = this.tail.then(() => this.run(job)).catch((error) => this.host.warn("Automatic skill adoption failed", error)).finally(() => {
14162
+ this.jobs.delete(id);
14163
+ });
14164
+ }
14165
+ }
14166
+ allowed(job) {
14167
+ const settings = this.host.settings();
14168
+ return !this.disposed && settings.evolution && settings.autoAdopt && settings.autoBacktest && job.available() && (job.action === void 0 || (job.action === "create" ? settings.allowCreate : settings.allowUpdate));
14169
+ }
14170
+ settingsChanged() {
14171
+ for (const job of this.jobs.values()) if (!this.allowed(job)) job.controller.abort(new AutomaticSkillStop("disabled"));
14172
+ }
14173
+ dispose() {
14174
+ this.disposed = true;
14175
+ for (const job of this.jobs.values()) job.controller.abort(new AutomaticSkillStop("interrupted"));
14176
+ }
14177
+ checkpoint(job) {
14178
+ if (!this.allowed(job)) job.controller.abort(new AutomaticSkillStop(this.disposed ? "interrupted" : "disabled"));
14179
+ job.controller.signal.throwIfAborted();
14180
+ }
14181
+ record(job, event) {
14182
+ return job.source.mutate("auto-adoption-record", {
14183
+ id: job.id,
14184
+ ...event
14185
+ });
14186
+ }
14187
+ async unchanged(job, draft, casesHash) {
14188
+ this.checkpoint(job);
14189
+ const current = await job.source.read("draft", { id: draft.id }, job.controller.signal);
14190
+ const suggestion = (await job.source.read("suggestions", null, job.controller.signal)).find((entry) => entry.id === job.id);
14191
+ const cases = (await job.source.read("cases", { skill: draft.skillName }, job.controller.signal)).filter((entry) => entry.enabled);
14192
+ if (current.conflict !== void 0 || current.draft.status !== "ready" || suggestion?.status !== "review" || suggestion.draftId !== draft.id || hashSkillDraft(current.draft) !== hashSkillDraft(draft) || hashSkillCases(cases) !== casesHash) throw new AutomaticSkillStop("changed");
14193
+ }
14194
+ async runs(job, ids) {
14195
+ const deadline = AbortSignal.timeout(276e4);
14196
+ const signal = AbortSignal.any([job.controller.signal, deadline]);
14197
+ for (;;) {
14198
+ this.checkpoint(job);
14199
+ signal.throwIfAborted();
14200
+ const runs = await Promise.all(ids.map((id) => job.source.read("run", { id }, signal)));
14201
+ if (runs.some((run) => run.status !== "running" && run.status !== "completed") || runs.every((run) => run.status !== "running")) return runs;
14202
+ await setTimeout$1(250, void 0, { signal });
14203
+ }
14204
+ }
14205
+ async run(job) {
14206
+ const runIds = /* @__PURE__ */ new Set();
14207
+ let attempt = 0;
14208
+ let scores = {};
14209
+ try {
14210
+ this.checkpoint(job);
14211
+ const suggestion = (await job.source.read("suggestions")).find((entry) => entry.id === job.id);
14212
+ if (suggestion?.status !== "pending" || suggestion.draftId !== void 0) throw new AutomaticSkillStop("changed");
14213
+ job.action = suggestion.action;
14214
+ this.checkpoint(job);
14215
+ const settings = this.host.settings();
14216
+ const reserved = await job.source.mutate("auto-adoption-reserve", {
14217
+ id: job.id,
14218
+ limit: settings.autoAdoptDailyLimit,
14219
+ timeZone: settings.autoAdoptTimeZone
14220
+ }, job.controller.signal);
14221
+ if (!reserved.allowed) throw new AutomaticSkillStop(reserved.reason, `${reserved.used}/${reserved.limit} · ${reserved.date} · ${reserved.timeZone}`);
14222
+ await this.record(job, {
14223
+ stage: "queued",
14224
+ attempt,
14225
+ detail: `${reserved.used}/${reserved.limit} · ${reserved.date} · ${reserved.timeZone}`
14226
+ });
14227
+ const cases = (await job.source.read("cases", { skill: suggestion.skillName })).filter((entry) => entry.enabled);
14228
+ if (cases.length === 0) throw new AutomaticSkillStop("no-cases");
14229
+ if (cases.some((entry) => entry.requiresVision === true || entry.checks.length === 0)) throw new AutomaticSkillStop("unmeasured");
14230
+ if (cases.length > this.host.settings().casesPerRun) throw new AutomaticSkillStop("case-limit");
14231
+ const casesHash = hashSkillCases(cases);
14232
+ const preferences = await job.source.read("draft-preferences");
14233
+ const defaultRoute = this.host.route(job.workspace);
14234
+ const runRoute = preferences.skillRunModels[suggestion.skillName] ?? preferences.runModel ?? defaultRoute;
14235
+ const draftRoute = preferences.suggestionModels[job.id] ?? preferences.model ?? defaultRoute;
14236
+ if (runRoute === void 0 || draftRoute === void 0) throw new AutomaticSkillStop("model-unavailable");
14237
+ let previous;
14238
+ let guidance = "";
14239
+ for (attempt = 1; attempt <= 3; attempt++) {
14240
+ this.checkpoint(job);
14241
+ await this.record(job, {
14242
+ stage: "drafting",
14243
+ attempt
14244
+ });
14245
+ const draft = await this.host.draft(job.source, job.workspace, {
14246
+ suggestionId: job.id,
14247
+ action: suggestion.action,
14248
+ ...previous === void 0 ? {} : { previous },
14249
+ guidance,
14250
+ route: draftRoute
14251
+ }, job.controller.signal);
14252
+ await this.unchanged(job, draft, casesHash);
14253
+ await this.record(job, {
14254
+ stage: "backtesting",
14255
+ attempt
14256
+ });
14257
+ const baseline = await this.host.backtest(job.source, job.workspace, {
14258
+ skillName: draft.skillName,
14259
+ target: {
14260
+ kind: draft.action === "update" ? "current" : "none",
14261
+ ...draft.action === "update" ? { draftId: draft.id } : {}
14262
+ },
14263
+ route: runRoute,
14264
+ expectedCasesHash: casesHash,
14265
+ signal: job.controller.signal
14266
+ });
14267
+ runIds.add(baseline.runId);
14268
+ this.checkpoint(job);
14269
+ const candidate = await this.host.backtest(job.source, job.workspace, {
14270
+ skillName: draft.skillName,
14271
+ target: {
14272
+ kind: "draft",
14273
+ draftId: draft.id
14274
+ },
14275
+ route: runRoute,
14276
+ expectedCasesHash: casesHash,
14277
+ signal: job.controller.signal
14278
+ });
14279
+ runIds.add(candidate.runId);
14280
+ await job.source.mutate("run-link", {
14281
+ draftId: draft.id,
14282
+ runId: baseline.runId
14283
+ }, job.controller.signal);
14284
+ const [before, after] = await this.runs(job, [baseline.runId, candidate.runId]);
14285
+ await this.unchanged(job, draft, casesHash);
14286
+ const comparison = SkillsContracts.compareAutomaticSkillRuns(cases, before, after, casesHash);
14287
+ scores = {
14288
+ ...comparison.baselineScore === void 0 ? {} : { baselineScore: comparison.baselineScore },
14289
+ ...comparison.draftScore === void 0 ? {} : { draftScore: comparison.draftScore }
14290
+ };
14291
+ if (comparison.outcome === "unmeasured") throw new AutomaticSkillStop("unmeasured");
14292
+ if (comparison.outcome === "not-worse") {
14293
+ this.checkpoint(job);
14294
+ await job.source.mutate("draft-adopt", {
14295
+ id: draft.id,
14296
+ automatic: {
14297
+ reservationId: reserved.reservationId,
14298
+ draftHash: hashSkillDraft(draft),
14299
+ baselineRunId: baseline.runId,
14300
+ draftRunId: candidate.runId,
14301
+ casesHash,
14302
+ attempt
14303
+ }
14304
+ }, job.controller.signal);
14305
+ return;
14306
+ }
14307
+ if (attempt === 3) throw new AutomaticSkillStop("regressed");
14308
+ await this.record(job, {
14309
+ stage: "rewriting",
14310
+ attempt,
14311
+ reason: "regressed",
14312
+ ...scores
14313
+ });
14314
+ guidance = `The previous draft regressed in paired backtests. Baseline score: ${comparison.baselineScore}; previous draft score: ${comparison.draftScore}. Improve each regressed case without losing any previously passing check.\n` + JSON.stringify(cases.map((entry) => ({
14315
+ title: entry.title,
14316
+ baseline: before.results.find((result) => result.caseId === entry.id),
14317
+ draft: after.results.find((result) => result.caseId === entry.id)
14318
+ }))).slice(0, 8e3);
14319
+ previous = draft;
14320
+ }
14321
+ } catch (error) {
14322
+ const failure = job.controller.signal.aborted ? job.controller.signal.reason : error;
14323
+ const reason = failure instanceof AutomaticSkillStop ? failure.reason : "failed";
14324
+ await this.record(job, {
14325
+ stage: reason === "disabled" || reason === "interrupted" ? "paused" : "review",
14326
+ attempt: Math.min(attempt, 3),
14327
+ reason,
14328
+ ...scores,
14329
+ ...failure instanceof AutomaticSkillStop && failure.message === failure.reason ? {} : { detail: failure instanceof Error ? failure.message : String(failure) }
14330
+ }).catch((recordError) => this.host.warn("Could not record automatic adoption outcome", recordError));
14331
+ } finally {
14332
+ for (const id of runIds) await job.source.mutate("run-cancel", { id }).catch((error) => this.host.warn("Could not stop automatic skill backtest", error));
14333
+ }
14334
+ }
14335
+ };
14336
+ //#endregion
13986
14337
  //#region src/host/skill-review.ts
13987
14338
  const SIGNALS = [
13988
14339
  "correction",
@@ -14815,7 +15166,7 @@ function retryingSource(source, attempts = 4) {
14815
15166
  for (let attempt = 1;; attempt++) try {
14816
15167
  return await source.mutate(operation, input, signal);
14817
15168
  } catch (error) {
14818
- if (attempt >= attempts || !/revision conflict|changed since it was read; reload/u.test(error instanceof Error ? error.message : String(error))) throw error;
15169
+ if (attempt >= attempts || !/revision conflict|Skills Source changed since it was read; reload/u.test(error instanceof Error ? error.message : String(error))) throw error;
14819
15170
  }
14820
15171
  }
14821
15172
  };
@@ -14823,6 +15174,27 @@ function retryingSource(source, attempts = 4) {
14823
15174
  function message(error) {
14824
15175
  return redact(error instanceof Error ? error.message : String(error));
14825
15176
  }
15177
+ /**
15178
+ * A draft task that stops reporting is abandoned here. The subagent behind it
15179
+ * can die without settling, and the suggestion would otherwise stay `drafting`
15180
+ * - blocking dismiss, delete and a retry - until the Host restarts.
15181
+ */
15182
+ const DRAFT_DEADLINE_MS = 9e5;
15183
+ /** Distinguishes a deadline abort from the plain abort a Host shutdown sends. */
15184
+ var DraftDeadlineError = class extends Error {};
15185
+ /** The deadline error a task ran into, so the Source records why the draft ended. */
15186
+ function deadlineReason(signal) {
15187
+ return signal.reason instanceof DraftDeadlineError ? signal.reason : void 0;
15188
+ }
15189
+ /** Cancellation settles the owner even when an underlying task ignores its signal. */
15190
+ function abortable(signal, work) {
15191
+ signal.throwIfAborted();
15192
+ return new Promise((resolve, reject) => {
15193
+ const abort = () => reject(signal.reason);
15194
+ signal.addEventListener("abort", abort, { once: true });
15195
+ Promise.resolve().then(work).then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
15196
+ });
15197
+ }
14826
15198
  /** Resolve only roots supplied by this Source's scoped catalog, never the task's cwd. */
14827
15199
  function newSkillRoot(settings, source, catalog, explicit) {
14828
15200
  let root = explicit;
@@ -14830,7 +15202,9 @@ function newSkillRoot(settings, source, catalog, explicit) {
14830
15202
  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.");
14831
15203
  root = settings.targetScope === "project" || settings.targetScope === "storage" && (source.scope?.storage === "workspace" || source.scope?.storage === "workspaces") ? "project-dsh" : "user-dsh";
14832
15204
  }
14833
- const directory = catalog.roots.find((candidate) => candidate.kind === root)?.path;
15205
+ const destination = catalog.roots.find((candidate) => candidate.kind === root);
15206
+ if (destination?.readOnly) throw new Error(`Cannot create a skill in ${root}: linked skill roots are read-only.`);
15207
+ const directory = destination?.path;
14834
15208
  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.");
14835
15209
  if (root.startsWith("project-")) try {
14836
15210
  if (!statSync(catalog.projectRoot).isDirectory()) throw new Error("not a directory");
@@ -14863,8 +15237,22 @@ var SkillEvolution = class {
14863
15237
  sessions = /* @__PURE__ */ new Map();
14864
15238
  jobs = /* @__PURE__ */ new Set();
14865
15239
  disposed = false;
15240
+ activeBacktests = 0;
15241
+ backtestJobs = 0;
15242
+ backtestWaiters = /* @__PURE__ */ new Set();
15243
+ automatic;
14866
15244
  constructor(host) {
14867
15245
  this.host = host;
15246
+ this.automatic = new SkillAutoAdoption({
15247
+ settings: () => host.settings(),
15248
+ route: (workspace) => host.taskRoute?.(workspace),
15249
+ draft: (source, workspace, input, signal) => this.automaticDraft(source, workspace, input, signal),
15250
+ backtest: (source, workspace, input) => this.backtest(source, workspace, input),
15251
+ warn: (text, error) => this.warn(text, error)
15252
+ });
15253
+ }
15254
+ settingsChanged() {
15255
+ this.automatic.settingsChanged();
14868
15256
  }
14869
15257
  source(agent) {
14870
15258
  const source = this.host.source(agent);
@@ -15098,6 +15486,7 @@ var SkillEvolution = class {
15098
15486
  });
15099
15487
  state.failures = 0;
15100
15488
  state.lastReviewAt = this.now().getTime();
15489
+ if (trigger === "automatic" && result.createdSuggestionIds !== void 0) this.automatic.enqueue(source, workspace, result.createdSuggestionIds, () => this.host.source(agent) !== void 0);
15101
15490
  return result;
15102
15491
  } catch (error) {
15103
15492
  state.failures += 1;
@@ -15128,10 +15517,17 @@ var SkillEvolution = class {
15128
15517
  delete state.reviewing;
15129
15518
  }
15130
15519
  }
15131
- background(name, work) {
15520
+ background(name, work, deadlineMs) {
15132
15521
  const controller = new AbortController();
15133
15522
  this.jobs.add(controller);
15134
- work(controller.signal).catch((error) => this.warn(`${name} failed`, error)).finally(() => this.jobs.delete(controller));
15523
+ const timer = deadlineMs === void 0 ? void 0 : setTimeout(() => {
15524
+ controller.abort(new DraftDeadlineError(`${name} did not finish within ${Math.round(deadlineMs / 6e4)} minutes`));
15525
+ }, deadlineMs);
15526
+ timer?.unref?.();
15527
+ return work(controller.signal).catch((error) => this.warn(`${name} failed`, deadlineReason(controller.signal) ?? error)).finally(() => {
15528
+ if (timer !== void 0) clearTimeout(timer);
15529
+ this.jobs.delete(controller);
15530
+ });
15135
15531
  }
15136
15532
  /**
15137
15533
  * Write a draft for a suggestion (or for a skill directly) in the
@@ -15144,6 +15540,7 @@ var SkillEvolution = class {
15144
15540
  ...input.draftId === void 0 ? {} : { draftId: input.draftId },
15145
15541
  ...input.skill === void 0 ? {} : { skill: input.skill }
15146
15542
  });
15543
+ if (pack.skill?.readOnly) throw new Error("Linked skills are read-only; use a local copy before generating an improvement draft.");
15147
15544
  const previousId = input.draftId ?? pack.suggestion?.draftId;
15148
15545
  const previous = previousId === void 0 ? (await source.read("drafts", {
15149
15546
  skill: pack.skillName,
@@ -15157,26 +15554,84 @@ var SkillEvolution = class {
15157
15554
  status: "drafting"
15158
15555
  });
15159
15556
  this.background("skill draft", async (signal) => {
15557
+ let recorded = false;
15160
15558
  try {
15161
- await this.writeDraft(source, workspace, input, {
15559
+ await abortable(signal, () => this.writeDraft(source, workspace, input, {
15162
15560
  pack,
15163
15561
  targetRoot,
15164
- guidance
15165
- }, signal);
15562
+ guidance,
15563
+ onRecorded: () => {
15564
+ recorded = true;
15565
+ }
15566
+ }, signal));
15166
15567
  } catch (error) {
15167
- if (input.suggestionId !== void 0) await source.mutate("suggestion-status", {
15168
- id: input.suggestionId,
15169
- status: "pending",
15170
- error: message(error)
15171
- }).catch(() => void 0);
15568
+ if (!recorded && input.suggestionId !== void 0) {
15569
+ if ((await source.read("suggestions")).find((item) => item.id === input.suggestionId)?.status === "drafting") await source.mutate("suggestion-status", {
15570
+ id: input.suggestionId,
15571
+ status: "pending",
15572
+ error: message(deadlineReason(signal) ?? error)
15573
+ }).catch(() => void 0);
15574
+ }
15172
15575
  throw error;
15173
15576
  }
15174
- });
15577
+ }, DRAFT_DEADLINE_MS);
15175
15578
  return {
15176
15579
  started: true,
15177
15580
  ...input.suggestionId === void 0 ? {} : { suggestionId: input.suggestionId }
15178
15581
  };
15179
15582
  }
15583
+ async automaticDraft(source, workspace, input, parentSignal) {
15584
+ const controller = new AbortController();
15585
+ const signal = AbortSignal.any([parentSignal, controller.signal]);
15586
+ const timer = setTimeout(() => controller.abort(new DraftDeadlineError("Automatic skill draft did not finish within 15 minutes")), DRAFT_DEADLINE_MS);
15587
+ timer.unref?.();
15588
+ const expectedDraft = input.previous === void 0 ? null : {
15589
+ id: input.previous.id,
15590
+ hash: hashSkillDraft(input.previous)
15591
+ };
15592
+ let recorded = false;
15593
+ let claimed = false;
15594
+ try {
15595
+ return await abortable(signal, async () => {
15596
+ const pack = await source.read("evidence-pack", { suggestionId: input.suggestionId }, signal);
15597
+ signal.throwIfAborted();
15598
+ if (pack.skill?.readOnly) throw new AutomaticSkillStop("changed");
15599
+ if (pack.action !== input.action || pack.action === "update" && pack.suggestion?.skillFileHash !== pack.skill?.hash) throw new AutomaticSkillStop("changed");
15600
+ const targetRoot = pack.action === "update" ? pack.skill.root : newSkillRoot(this.host.settings(), source, await source.read("catalog", null, signal), input.previous?.targetRoot ?? pack.suggestion?.targetRoot);
15601
+ const guidance = [...input.previous?.guidance ?? [], ...input.guidance === "" ? [] : [input.guidance]].slice(-8);
15602
+ await source.mutate("suggestion-status", {
15603
+ id: input.suggestionId,
15604
+ status: "drafting",
15605
+ expectedDraft
15606
+ }, signal);
15607
+ claimed = true;
15608
+ return this.writeDraft(source, workspace, {
15609
+ suggestionId: input.suggestionId,
15610
+ ...input.previous === void 0 ? {} : { draftId: input.previous.id },
15611
+ route: input.route
15612
+ }, {
15613
+ pack,
15614
+ targetRoot,
15615
+ guidance,
15616
+ expectedDraft,
15617
+ deferBacktests: true,
15618
+ onRecorded: () => {
15619
+ recorded = true;
15620
+ }
15621
+ }, signal);
15622
+ });
15623
+ } catch (error) {
15624
+ if (claimed && !recorded) await source.mutate("suggestion-status", {
15625
+ id: input.suggestionId,
15626
+ status: "pending",
15627
+ expectedDraft,
15628
+ ...parentSignal.aborted ? {} : { error: message(signal.aborted ? signal.reason : error) }
15629
+ }).catch(() => void 0);
15630
+ throw signal.aborted ? signal.reason : error;
15631
+ } finally {
15632
+ clearTimeout(timer);
15633
+ }
15634
+ }
15180
15635
  async writeDraft(source, workspace, input, prepared, signal) {
15181
15636
  const settings = this.host.settings();
15182
15637
  const { pack, targetRoot, guidance } = prepared;
@@ -15213,15 +15668,19 @@ var SkillEvolution = class {
15213
15668
  notes: parsed.notes,
15214
15669
  changes: parsed.changes,
15215
15670
  guidance,
15671
+ expectedBaseHash: pack.skill?.hash ?? null,
15672
+ ...prepared.expectedDraft === void 0 ? {} : { expectedDraft: prepared.expectedDraft },
15216
15673
  ...pack.action === "create" ? { targetRoot } : {}
15217
15674
  }, signal);
15675
+ prepared.onRecorded?.();
15218
15676
  } catch (error) {
15219
15677
  signal.throwIfAborted();
15220
- if (attempt > 0) throw error;
15678
+ if (attempt > 0 || /changed while this draft/.test(message(error))) throw error;
15221
15679
  prompt += `\n\nYour previous SKILL.md was rejected: ${message(error)}. Return a corrected complete file.`;
15222
15680
  }
15223
15681
  }
15224
- if (settings.autoBacktest && pack.cases.length > 0) {
15682
+ signal.throwIfAborted();
15683
+ if (!prepared.deferBacktests && (settings.autoBacktest || input.backtest) && pack.cases.length > 0) {
15225
15684
  await this.backtest(source, workspace, {
15226
15685
  skillName: pack.skillName,
15227
15686
  target: {
@@ -15252,67 +15711,138 @@ var SkillEvolution = class {
15252
15711
  return draft;
15253
15712
  }
15254
15713
  /** Start a backtest of one version of a skill against its enabled cases; it runs in the background. */
15714
+ backtestSlot(signal) {
15715
+ signal.throwIfAborted();
15716
+ return new Promise((resolve, reject) => {
15717
+ const abort = () => {
15718
+ this.backtestWaiters.delete(acquire);
15719
+ reject(signal.reason);
15720
+ };
15721
+ const acquire = () => {
15722
+ if (signal.aborted) {
15723
+ abort();
15724
+ return;
15725
+ }
15726
+ if (this.activeBacktests >= 2) return;
15727
+ this.backtestWaiters.delete(acquire);
15728
+ signal.removeEventListener("abort", abort);
15729
+ this.activeBacktests++;
15730
+ resolve(() => {
15731
+ this.activeBacktests--;
15732
+ for (const waiter of [...this.backtestWaiters]) waiter();
15733
+ });
15734
+ };
15735
+ this.backtestWaiters.add(acquire);
15736
+ signal.addEventListener("abort", abort, { once: true });
15737
+ acquire();
15738
+ });
15739
+ }
15255
15740
  async backtest(port, workspace, input) {
15741
+ if (this.disposed) throw new Error("Skill evolution has stopped");
15742
+ input.signal?.throwIfAborted();
15743
+ if (this.backtestJobs >= 16) throw new Error("The skill backtest queue is full. Cancel a run or retry after it finishes.");
15744
+ this.backtestJobs++;
15256
15745
  const source = retryingSource(port);
15257
- const settings = this.host.settings();
15258
- const cases = (await source.read("cases", { skill: input.skillName })).filter((entry) => entry.enabled && (input.caseIds === void 0 || input.caseIds.includes(entry.id))).slice(0, settings.casesPerRun);
15259
- if (cases.length === 0) throw new Error(`${input.skillName} has no enabled eval case to run`);
15260
- if (cases.some((entry) => entry.requiresVision !== void 0 && typeof entry.requiresVision !== "boolean")) throw new Error("requiresVision must be a boolean");
15261
- const resolved = await source.read("target-content", {
15262
- skillName: input.skillName,
15263
- target: input.target
15264
- });
15265
- const run = await source.mutate("run-start", {
15266
- skillName: input.skillName,
15267
- target: resolved.target,
15268
- caseIds: cases.map((entry) => entry.id)
15269
- });
15270
- this.background("skill backtest", async (signal) => {
15271
- try {
15272
- for (const entry of cases.filter((entry) => entry.requiresVision === true)) {
15273
- signal.throwIfAborted();
15274
- await source.mutate("run-progress", {
15275
- id: run.id,
15276
- result: {
15277
- caseId: entry.id,
15278
- title: entry.title,
15279
- verdict: "error",
15280
- score: 0,
15281
- unmeasuredReason: "requires-vision",
15282
- checks: [],
15283
- steps: [],
15284
- answer: "",
15285
- error: "Not measured: this case requires image input, which text-only backtests cannot receive."
15286
- }
15746
+ try {
15747
+ const settings = this.host.settings();
15748
+ const cases = (await source.read("cases", { skill: input.skillName }, input.signal)).filter((entry) => entry.enabled && (input.caseIds === void 0 || input.caseIds.includes(entry.id))).slice(0, settings.casesPerRun);
15749
+ if (cases.length === 0) throw new Error(`${input.skillName} has no enabled eval case to run`);
15750
+ if (input.expectedCasesHash !== void 0 && hashSkillCases(cases) !== input.expectedCasesHash) throw new Error("Eval cases changed before this backtest started");
15751
+ if (cases.some((entry) => entry.requiresVision !== void 0 && typeof entry.requiresVision !== "boolean")) throw new Error("requiresVision must be a boolean");
15752
+ const resolved = await source.read("target-content", {
15753
+ skillName: input.skillName,
15754
+ target: input.target
15755
+ }, input.signal);
15756
+ const run = await source.mutate("run-start", {
15757
+ skillName: input.skillName,
15758
+ target: resolved.target,
15759
+ caseIds: cases.map((entry) => entry.id),
15760
+ ...input.expectedCasesHash === void 0 ? {} : { expectedCasesHash: input.expectedCasesHash },
15761
+ ...input.route === void 0 ? {} : { model: input.route }
15762
+ }, input.signal);
15763
+ this.background("skill backtest", async (deadline) => {
15764
+ const controller = new AbortController();
15765
+ const abort = () => controller.abort(deadline.aborted ? deadline.reason : input.signal?.reason);
15766
+ deadline.addEventListener("abort", abort, { once: true });
15767
+ input.signal?.addEventListener("abort", abort, { once: true });
15768
+ if (deadline.aborted || input.signal?.aborted) abort();
15769
+ const signal = controller.signal;
15770
+ let checking = false;
15771
+ const watcher = setInterval(() => {
15772
+ if (checking || signal.aborted) return;
15773
+ checking = true;
15774
+ source.read("run", { id: run.id }).then((current) => {
15775
+ if (current.status !== "running") controller.abort(new Error(current.error ?? "Backtest interrupted"));
15776
+ }).catch((error) => controller.abort(error)).finally(() => {
15777
+ checking = false;
15287
15778
  });
15288
- }
15289
- const queue = cases.filter((entry) => entry.requiresVision !== true);
15290
- if (queue.length > 0) await this.host.runTask(workspace, signal, async (task) => {
15291
- const worker = async () => {
15292
- for (let entry = queue.shift(); entry !== void 0; entry = queue.shift()) {
15779
+ }, 1e3);
15780
+ watcher.unref?.();
15781
+ try {
15782
+ await abortable(signal, async () => {
15783
+ const release = await this.backtestSlot(signal);
15784
+ try {
15785
+ for (const entry of cases.filter((entry) => entry.requiresVision === true)) {
15786
+ signal.throwIfAborted();
15787
+ await source.mutate("run-progress", {
15788
+ id: run.id,
15789
+ result: {
15790
+ caseId: entry.id,
15791
+ title: entry.title,
15792
+ verdict: "error",
15793
+ score: 0,
15794
+ unmeasuredReason: "requires-vision",
15795
+ checks: [],
15796
+ steps: [],
15797
+ answer: "",
15798
+ error: "Not measured: this case requires image input, which text-only backtests cannot receive."
15799
+ }
15800
+ }, signal);
15801
+ }
15802
+ const queue = cases.filter((entry) => entry.requiresVision !== true);
15803
+ if (queue.length > 0) await abortable(signal, () => this.host.runTask(workspace, signal, async (task) => {
15804
+ const worker = async () => {
15805
+ for (let entry = queue.shift(); entry !== void 0; entry = queue.shift()) {
15806
+ signal.throwIfAborted();
15807
+ const result = await this.runCase(task, entry, input.skillName, resolved.content, signal);
15808
+ signal.throwIfAborted();
15809
+ await source.mutate("run-progress", {
15810
+ id: run.id,
15811
+ result
15812
+ }, signal);
15813
+ }
15814
+ };
15815
+ await Promise.all(Array.from({ length: Math.min(BACKTEST_CONCURRENCY, queue.length) }, worker));
15816
+ }, input.route));
15293
15817
  signal.throwIfAborted();
15294
- await source.mutate("run-progress", {
15818
+ await source.mutate("run-finish", {
15295
15819
  id: run.id,
15296
- result: await this.runCase(task, entry, input.skillName, resolved.content, signal)
15297
- });
15820
+ status: "completed"
15821
+ }, signal);
15822
+ } finally {
15823
+ release();
15298
15824
  }
15299
- };
15300
- await Promise.all(Array.from({ length: Math.min(BACKTEST_CONCURRENCY, queue.length) }, worker));
15301
- }, input.route);
15302
- await source.mutate("run-finish", {
15303
- id: run.id,
15304
- status: "completed"
15305
- });
15306
- } catch (error) {
15307
- await source.mutate("run-finish", {
15308
- id: run.id,
15309
- status: "failed",
15310
- error: message(error)
15311
- }).catch(() => void 0);
15312
- throw error;
15313
- }
15314
- });
15315
- return { runId: run.id };
15825
+ });
15826
+ } catch (error) {
15827
+ await source.mutate("run-finish", {
15828
+ id: run.id,
15829
+ status: signal.aborted ? "interrupted" : "failed",
15830
+ error: message(signal.aborted ? signal.reason : error)
15831
+ }).catch(() => void 0);
15832
+ throw error;
15833
+ } finally {
15834
+ clearInterval(watcher);
15835
+ deadline.removeEventListener("abort", abort);
15836
+ input.signal?.removeEventListener("abort", abort);
15837
+ }
15838
+ }, 27e5).finally(() => {
15839
+ this.backtestJobs--;
15840
+ });
15841
+ return { runId: run.id };
15842
+ } catch (error) {
15843
+ this.backtestJobs--;
15844
+ throw error;
15845
+ }
15316
15846
  }
15317
15847
  /**
15318
15848
  * Execute and grade one case. A run that fails (an output limit, a missing
@@ -15326,9 +15856,15 @@ var SkillEvolution = class {
15326
15856
  name: skillName,
15327
15857
  ...content === void 0 ? {} : { content }
15328
15858
  }), SKILL_EXECUTOR_SCHEMA, SKILL_EXECUTOR_PERSONA, signal)).structured);
15329
- const grade = gradeCase(entry, (await this.host.skillTask(task, `Mnemon skill judge: ${entry.title}`.slice(0, 120), skillJudgePrompt(entry, executed.steps, executed.answer), SKILL_JUDGE_SCHEMA, SKILL_JUDGE_PERSONA, signal)).structured);
15859
+ signal.throwIfAborted();
15860
+ const judged = await this.host.skillTask(task, `Mnemon skill judge: ${entry.title}`.slice(0, 120), skillJudgePrompt(entry, executed.steps, executed.answer), SKILL_JUDGE_SCHEMA, SKILL_JUDGE_PERSONA, signal);
15861
+ const grade = gradeCase(entry, judged.structured);
15862
+ const raw = judged.structured;
15863
+ const checks = Array.isArray(raw?.checks) ? raw.checks : [];
15864
+ const fullyGraded = checks.length === entry.checks.length && entry.checks.every((_check, index) => checks.filter((check) => check?.index === index + 1 && typeof check.pass === "boolean").length === 1);
15330
15865
  return {
15331
15866
  caseId: entry.id,
15867
+ fullyGraded,
15332
15868
  title: entry.title,
15333
15869
  verdict: grade.verdict,
15334
15870
  score: grade.score,
@@ -15438,6 +15974,7 @@ var SkillEvolution = class {
15438
15974
  };
15439
15975
  }
15440
15976
  dispose() {
15977
+ this.automatic.dispose();
15441
15978
  this.disposed = true;
15442
15979
  for (const state of this.sessions.values()) if (state.timer !== void 0) clearTimeout(state.timer);
15443
15980
  this.sessions.clear();
@@ -15472,7 +16009,7 @@ function skillsSourceFor(runtime, agent) {
15472
16009
  }
15473
16010
  //#endregion
15474
16011
  //#region src/host/rpc.ts
15475
- function object$4(value) {
16012
+ function object$5(value) {
15476
16013
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("payload must be an object");
15477
16014
  return value;
15478
16015
  }
@@ -15668,7 +16205,7 @@ async function assisted(runtime, lifecycle, typeId, operation, input, signal, ta
15668
16205
  ...signal === void 0 ? {} : { signal }
15669
16206
  });
15670
16207
  return {
15671
- ...object$4(result.value),
16208
+ ...object$5(result.value),
15672
16209
  revision: result.revision
15673
16210
  };
15674
16211
  } finally {
@@ -15783,6 +16320,7 @@ async function skillAssistance(runtime, lifecycle, operation, input, signal) {
15783
16320
  if (guidance !== void 0 && guidance.length > 4e3) throw new Error("guidance is too long (max 4000 characters)");
15784
16321
  const draftRoute = modelRoute(input);
15785
16322
  return skills.draft(source, workspaceRoot, {
16323
+ ...input.backtest === true ? { backtest: true } : {},
15786
16324
  ...suggestionId === void 0 ? {} : { suggestionId },
15787
16325
  ...skill === void 0 ? {} : { skill },
15788
16326
  ...draftId === void 0 ? {} : { draftId },
@@ -15794,7 +16332,7 @@ async function skillAssistance(runtime, lifecycle, operation, input, signal) {
15794
16332
  case "skill-backtest": {
15795
16333
  const skillName = optionalString(input.skillName, "skillName");
15796
16334
  if (skillName === void 0) throw new Error("skillName is required");
15797
- const target = object$4(input.target);
16335
+ const target = object$5(input.target);
15798
16336
  if (![
15799
16337
  "current",
15800
16338
  "draft",
@@ -15834,7 +16372,7 @@ async function skillAssistance(runtime, lifecycle, operation, input, signal) {
15834
16372
  function createReadHandler(input, lifecycle, versions) {
15835
16373
  return async (endpoint, rawPayload, signal) => {
15836
16374
  try {
15837
- const payload = object$4(rawPayload);
16375
+ const payload = object$5(rawPayload);
15838
16376
  if (endpoint === "versions") {
15839
16377
  if (versions === void 0) throw new Error("version checks are unavailable");
15840
16378
  return success$1(await versions.check());
@@ -15855,7 +16393,7 @@ function createReadHandler(input, lifecycle, versions) {
15855
16393
  if (lifecycle === void 0 || payload.operation !== "agent-search") throw new Error("Read-only Host assistance is unavailable");
15856
16394
  const source = (await catalog(runtime, lifecycle)).sources.find((item) => item.sourceInstanceKey === payload.sourceInstanceKey);
15857
16395
  if (source === void 0 || !source.assistance.includes("agent-search")) throw new Error("Host assistance is not available for this Source instance");
15858
- const value = await assisted(runtime, lifecycle, source.sourceTypeId, "agent-search", object$4(payload.input), signal);
16396
+ const value = await assisted(runtime, lifecycle, source.sourceTypeId, "agent-search", object$5(payload.input), signal);
15859
16397
  return success$1({
15860
16398
  revision: (await catalog(runtime)).sources.find((item) => item.sourceInstanceKey === source.sourceInstanceKey)?.revision ?? source.revision,
15861
16399
  value
@@ -15967,9 +16505,9 @@ function createActivationHandler(input) {
15967
16505
  return async (endpoint, rawPayload, signal) => {
15968
16506
  try {
15969
16507
  if (endpoint === "source-assistance") {
15970
- const payload = object$4(rawPayload);
16508
+ const payload = object$5(rawPayload);
15971
16509
  if (payload.operation !== "activation" || payload.confirmed !== true) throw new Error("Unsupported activation operation");
15972
- const fields = object$4(payload.input);
16510
+ const fields = object$5(payload.input);
15973
16511
  if (Object.keys(fields).some((key) => key !== "memoryBodyId" && key !== "active") || typeof fields.active !== "boolean" || typeof fields.memoryBodyId !== "string") throw new Error("Activation accepts only a Memory Space id and boolean state");
15974
16512
  const runtime = scoped(input, payload);
15975
16513
  requireWritable(runtime);
@@ -15993,7 +16531,7 @@ function createActivationHandler(input) {
15993
16531
  }
15994
16532
  }
15995
16533
  if (endpoint !== "body") return badRequest$1("unknown activation endpoint: " + endpoint);
15996
- const payload = object$4(rawPayload);
16534
+ const payload = object$5(rawPayload);
15997
16535
  const unexpected = Object.keys(payload).filter((field) => !ACTIVATION_FIELDS.has(field));
15998
16536
  if (unexpected.length > 0) return badRequest$1("unsupported activation fields: " + unexpected.join(", "));
15999
16537
  if (typeof payload.memoryBodyId !== "string" || payload.memoryBodyId.trim() === "") return badRequest$1("memoryBodyId must be a non-empty string");
@@ -16012,7 +16550,7 @@ function createActivationHandler(input) {
16012
16550
  function createWriteHandler(input, lifecycle, versions) {
16013
16551
  return async (endpoint, rawPayload, signal) => {
16014
16552
  try {
16015
- const payload = object$4(rawPayload);
16553
+ const payload = object$5(rawPayload);
16016
16554
  if (endpoint === "version-update") {
16017
16555
  if (versions === void 0) throw new Error("version updates are unavailable");
16018
16556
  if (!input.config.writeEnabled) throw new Error("dsh-mnemon is configured read-only (writeEnabled: false)");
@@ -16048,12 +16586,12 @@ function createWriteHandler(input, lifecycle, versions) {
16048
16586
  if (source === void 0 || !source.assistance.includes(operation)) throw new Error("Host assistance is not available for this Source instance");
16049
16587
  requireLayerOn(runtime, source.sourceTypeId);
16050
16588
  if (operation !== "agent-search" && (payload.confirmed !== true || payload.expectedRevision !== source.revision)) throw new Error("Host assistance requires confirmation of the current Source revision");
16051
- const value = await assisted(runtime, lifecycle, source.sourceTypeId, operation, object$4(payload.input), signal, {
16589
+ const value = await assisted(runtime, lifecycle, source.sourceTypeId, operation, object$5(payload.input), signal, {
16052
16590
  sourceInstanceKey: source.sourceInstanceKey,
16053
16591
  expectedRevision: source.revision
16054
16592
  });
16055
16593
  if (source.sourceTypeId === "runtime") return success$1({
16056
- revision: object$4(value).revision,
16594
+ revision: object$5(value).revision,
16057
16595
  value
16058
16596
  });
16059
16597
  return success$1({
@@ -16097,7 +16635,7 @@ function createWriteHandler(input, lifecycle, versions) {
16097
16635
  function createPackHandler(input, nativeDirectoryPicker = () => false) {
16098
16636
  return async (endpoint, rawPayload) => {
16099
16637
  try {
16100
- const payload = object$4(rawPayload);
16638
+ const payload = object$5(rawPayload);
16101
16639
  const runtime = scoped(input, payload);
16102
16640
  const manager = runtime.graph.packs;
16103
16641
  if (endpoint === "target") return success$1({
@@ -16134,7 +16672,7 @@ function githubAuth(input) {
16134
16672
  function createSyncHandler(input, onConfigured) {
16135
16673
  return async (endpoint, rawPayload, signal) => {
16136
16674
  try {
16137
- const payload = object$4(rawPayload);
16675
+ const payload = object$5(rawPayload);
16138
16676
  const runtime = scoped(input, payload);
16139
16677
  const sync = runtime.graph.sync;
16140
16678
  if (endpoint === "status") return success$1(await sync.status(signal));
@@ -16207,7 +16745,7 @@ function createSyncHandler(input, onConfigured) {
16207
16745
  function createReviewHandler(input, lifecycle) {
16208
16746
  return async (endpoint, rawPayload, signal) => {
16209
16747
  try {
16210
- const payload = object$4(rawPayload);
16748
+ const payload = object$5(rawPayload);
16211
16749
  const ledger = input.reviews;
16212
16750
  if (endpoint === "view") return success$1(ledger.view());
16213
16751
  if (endpoint === "opinion") {
@@ -16294,6 +16832,106 @@ function hasNativeDirectoryPicker(ctx) {
16294
16832
  }
16295
16833
  }
16296
16834
  //#endregion
16835
+ //#region src/host/embedding-settings.ts
16836
+ const object$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
16837
+ /** Preserve other Host redaction while never returning a legacy inline key. */
16838
+ function publicEmbeddingSettings(value, original, metadata = false) {
16839
+ const config = object$4(value);
16840
+ if (!metadata && config.embedding === void 0) return value;
16841
+ const embedding = object$4(config.embedding);
16842
+ const raw = object$4(object$4(original).embedding);
16843
+ const apiKeyStorage = raw.apiKeyEnv ? "environment" : raw.apiKeyRef ? "credentials" : raw.apiKey ? "legacy" : "none";
16844
+ return {
16845
+ ...config,
16846
+ embedding: {
16847
+ ...embedding,
16848
+ ..."apiKey" in embedding ? { apiKey: "" } : {},
16849
+ ...metadata ? {
16850
+ apiKeyStorage,
16851
+ apiKeyConfigured: apiKeyStorage !== "none"
16852
+ } : {}
16853
+ }
16854
+ };
16855
+ }
16856
+ /** Write/verify a new record before referencing it; failures keep the old configuration and key intact. */
16857
+ async function prepareEmbeddingSettings(settings, namespace, operations, revision, store) {
16858
+ const last = operations.findLastIndex((operation) => operation.path.length === 1 && operation.path[0] === "embedding");
16859
+ if (namespace !== "mnemon" || last === -1) return {
16860
+ operations,
16861
+ revision
16862
+ };
16863
+ const operation = operations[last];
16864
+ const edits = operations.filter((entry, index) => index === last || entry.path[0] !== "embedding");
16865
+ if (operation.op === "unset") return {
16866
+ operations: edits,
16867
+ revision
16868
+ };
16869
+ const view = settings.describe({ redactSecrets: false }).find((candidate) => candidate.ns === namespace);
16870
+ if (view === void 0) throw new Error("Mnemon settings are unavailable");
16871
+ if (revision !== void 0 && revision !== view.revision) throw new Error("Settings changed; reload before saving the embedding connection");
16872
+ const previous = resolveEmbedding(object$4(object$4(view.value).embedding));
16873
+ const input = object$4(operation.value);
16874
+ if (typeof operation.value !== "object" || operation.value === null || Array.isArray(operation.value)) throw new Error("Embedding settings must be an object");
16875
+ if (input.apiKey !== void 0 && typeof input.apiKey !== "string") throw new Error("Embedding API key must be text");
16876
+ if (input.clearApiKey !== void 0 && typeof input.clearApiKey !== "boolean") throw new Error("clearApiKey must be boolean");
16877
+ const { apiKeyStorage: _storage, apiKeyConfigured: _configured, clearApiKey: _clear, ...next } = input;
16878
+ let resolved = resolveEmbedding(next);
16879
+ let rollback;
16880
+ if (input.clearApiKey === true) {
16881
+ next.apiKey = "";
16882
+ next.apiKeyRef = "";
16883
+ next.apiKeyEnv = "";
16884
+ } else if (resolved.apiKeyEnv) {
16885
+ if (resolved.apiKey) throw new Error("Choose an embedding API key or an environment variable, not both");
16886
+ next.apiKey = "";
16887
+ next.apiKeyRef = "";
16888
+ } else {
16889
+ if (resolved.apiKeyRef && resolved.apiKeyRef !== previous.apiKeyRef) throw new Error("Save a key to create an embedding credential reference");
16890
+ const sameEndpoint = resolved.endpoint === previous.endpoint;
16891
+ if (!resolved.apiKey && !sameEndpoint && (previous.apiKeyRef || previous.apiKey)) throw new Error("Changing the embedding endpoint requires a new key, an environment variable, or explicitly removing the saved key");
16892
+ const key = resolved.apiKey || (sameEndpoint ? previous.apiKey : "");
16893
+ if (key) {
16894
+ const credentials = store?.();
16895
+ if (credentials === void 0) throw new Error("DSH credentials storage is unavailable; configure apiKeyEnv instead. The existing connection was not changed");
16896
+ const reference = EMBEDDING_CREDENTIAL_PREFIX + randomUUID();
16897
+ const payload = {
16898
+ version: 1,
16899
+ endpoint: resolved.endpoint,
16900
+ apiKey: key
16901
+ };
16902
+ rollback = async () => {
16903
+ await credentials.deleteRecord(reference);
16904
+ };
16905
+ try {
16906
+ await credentials.modifyRecord(reference, async () => ({
16907
+ kind: "grant",
16908
+ payload
16909
+ }));
16910
+ const record = await credentials.readRecord(reference);
16911
+ if (record?.kind !== "grant" || record.payload?.version !== 1 || record.payload.endpoint !== payload.endpoint || record.payload.apiKey !== key) throw new Error("Credential readback failed");
16912
+ } catch {
16913
+ await rollback().catch(() => {});
16914
+ throw new Error("Could not save and verify the embedding credential; the existing connection was not changed");
16915
+ }
16916
+ next.apiKey = "";
16917
+ next.apiKeyRef = reference;
16918
+ } else if (sameEndpoint && previous.apiKeyRef) {
16919
+ next.apiKey = "";
16920
+ next.apiKeyRef = previous.apiKeyRef;
16921
+ }
16922
+ }
16923
+ resolved = resolveEmbedding(next);
16924
+ if (resolved.apiKey) throw new Error("Embedding settings must not persist a plaintext key");
16925
+ return {
16926
+ operations: edits.map((entry) => entry === operation ? {
16927
+ ...entry,
16928
+ value: next
16929
+ } : entry),
16930
+ revision: revision ?? view.revision,
16931
+ ...rollback === void 0 ? {} : { rollback }
16932
+ };
16933
+ }
16934
+ //#endregion
16297
16935
  //#region src/host/settings.ts
16298
16936
  function success(value) {
16299
16937
  return {
@@ -16324,14 +16962,16 @@ function badRequest(message) {
16324
16962
  function descriptor(settings, namespace) {
16325
16963
  const view = settings.describe({ redactSecrets: true }).find((candidate) => candidate.ns === namespace);
16326
16964
  if (view === void 0) throw new Error(`${namespace} settings namespace is unavailable`);
16965
+ const raw = namespace === "mnemon" ? settings.describe({ redactSecrets: false }).find((candidate) => candidate.ns === namespace) : void 0;
16966
+ const visible = namespace === "mnemon" ? publicEmbeddingSettings(view.value, raw?.value, true) : view.value;
16327
16967
  return {
16328
16968
  status: "ready",
16329
16969
  value: namespace === "mnemon" && displayModeOf(view.value) === "buildin" ? {
16330
- ...object$3(view.value),
16970
+ ...object$3(visible),
16331
16971
  displayMode: "builtin"
16332
- } : view.value,
16333
- base: view.base,
16334
- user: view.user,
16972
+ } : visible,
16973
+ base: namespace === "mnemon" ? publicEmbeddingSettings(view.base, raw?.base) : view.base,
16974
+ user: namespace === "mnemon" ? publicEmbeddingSettings(view.user, raw?.user) : view.user,
16335
16975
  revision: view.revision,
16336
16976
  writable: settings.writable,
16337
16977
  mode: "host",
@@ -16408,7 +17048,7 @@ function mutablePath(namespace, path) {
16408
17048
  if (path.length === 4 && path[0] === "memoryTopology" && path[1] === "layers" && path[3] === "enabled") return MEMORY_LAYER_ID.test(path[2]);
16409
17049
  return INTERACTION_PATHS.some((allowed) => allowed.length === path.length && allowed.every((segment, index) => segment === path[index]));
16410
17050
  }
16411
- function createSettingsHandler(settings) {
17051
+ function createSettingsHandler(settings, credentials) {
16412
17052
  return async (endpoint, rawPayload) => {
16413
17053
  let namespace = MNEMON_SETTINGS_NAMESPACE;
16414
17054
  try {
@@ -16435,7 +17075,13 @@ function createSettingsHandler(settings) {
16435
17075
  };
16436
17076
  });
16437
17077
  const revision = payload.expectedRevision === void 0 ? void 0 : Number(payload.expectedRevision);
16438
- await settings.mutate(namespace, ops, revision);
17078
+ const prepared = await prepareEmbeddingSettings(settings, namespace, ops, revision, credentials);
17079
+ try {
17080
+ await settings.mutate(namespace, prepared.operations, prepared.revision);
17081
+ } catch (error) {
17082
+ await prepared.rollback?.().catch(() => {});
17083
+ throw error;
17084
+ }
16439
17085
  if (namespace === "mnemon") await migrateLegacyDisplayMode(settings);
16440
17086
  return success(descriptor(settings, namespace));
16441
17087
  } catch (error) {
@@ -16443,8 +17089,8 @@ function createSettingsHandler(settings) {
16443
17089
  }
16444
17090
  };
16445
17091
  }
16446
- function registerSettingsRpc(connection, settings) {
16447
- const handler = createSettingsHandler(settings);
17092
+ function registerSettingsRpc(connection, settings, credentials) {
17093
+ const handler = createSettingsHandler(settings, credentials);
16448
17094
  connection.rpc.handle(MNEMON_SETTINGS_CHANNEL, handler);
16449
17095
  return handler;
16450
17096
  }
@@ -19486,9 +20132,11 @@ function apply(rawContext, rawConfig = {}) {
19486
20132
  const skills = new SkillEvolution({
19487
20133
  settings: () => skillSettings.get(),
19488
20134
  source: (agent) => skillsSourceFor(runtime, agent),
20135
+ taskRoute: (workspace) => lifecycle.skillTaskRoute(workspace),
19489
20136
  runTask: (workspaceRoot, signal, operation, route) => lifecycle.runSkillTask(workspaceRoot, signal, operation, route),
19490
20137
  skillTask: (parent, label, prompt, schema, persona, signal, _route, maxTokens) => coordinator.skillTask(parent, label, prompt, schema, persona, signal, taskRoute(parent), maxTokens)
19491
20138
  });
20139
+ ctx.effect(() => hostSettings.onUpdated(() => skills.settingsChanged()), "dsh-mnemon: automatic skill adoption settings");
19492
20140
  lifecycle.useSkillEvolution(skills);
19493
20141
  ctx.effect(() => {
19494
20142
  const stop = autoBackup.start();
@@ -19527,7 +20175,7 @@ function apply(rawContext, rawConfig = {}) {
19527
20175
  pluginCommand: () => dshPluginCommand(ctx.get("profileContext"))
19528
20176
  });
19529
20177
  const rpc = registerRpc(connection, runtime, lifecycle, versions, () => autoBackup.refresh(), () => hasNativeDirectoryPicker(ctx));
19530
- const settings = registerSettingsRpc(connection, hostSettings);
20178
+ const settings = registerSettingsRpc(connection, hostSettings, () => ctx.get("credentials") ?? ctx.credentials);
19531
20179
  const view = registerViewRpc(connection, runtime, extensions, memoryPlugins, lifecycle, pluginInstallation);
19532
20180
  if (Context.is(webContext)) new MnemonRemoteService(webContext, {
19533
20181
  ...rpc,