dsh-mnemon 0.5.42 → 0.5.43

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
@@ -7,7 +7,7 @@ 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
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";
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,7 @@ const DEFAULT_SKILLS_CONFIG = Object.freeze({
389
391
  allowUpdate: true,
390
392
  targetScope: "user",
391
393
  autoBacktest: true,
394
+ autoAdopt: false,
392
395
  draftMaxTokens: 32768,
393
396
  draftRetryMaxTokens: 65536,
394
397
  hints: true,
@@ -483,6 +486,7 @@ const SkillsConfig = z.object({
483
486
  "project"
484
487
  ]).default(DEFAULT_SKILLS_CONFIG.targetScope),
485
488
  autoBacktest: z.boolean().default(DEFAULT_SKILLS_CONFIG.autoBacktest),
489
+ autoAdopt: z.boolean().default(DEFAULT_SKILLS_CONFIG.autoAdopt),
486
490
  draftMaxTokens: z.number().step(1).min(128).max(131072).default(DEFAULT_SKILLS_CONFIG.draftMaxTokens),
487
491
  draftRetryMaxTokens: z.number().step(1).min(128).max(131072).default(DEFAULT_SKILLS_CONFIG.draftRetryMaxTokens),
488
492
  hints: z.boolean().default(DEFAULT_SKILLS_CONFIG.hints),
@@ -539,6 +543,7 @@ const RecallQualitySchema = z.object({
539
543
  maxUnknownResults: z.number().step(1).min(0).max(50).default(DEFAULT_RECALL_MAX_UNKNOWN_RESULTS)
540
544
  });
541
545
  const RuntimeMemorySchema = z.object({
546
+ inheritToSubagents: z.boolean().default(false),
542
547
  memoryLimitBytes: z.number().step(1).min(1).max(MAX_RUNTIME_MEMORY_LIMIT_BYTES).default(DEFAULT_RUNTIME_MEMORY_LIMIT_BYTES),
543
548
  userLimitBytes: z.number().step(1).min(1).max(MAX_RUNTIME_MEMORY_LIMIT_BYTES).default(DEFAULT_RUNTIME_USER_LIMIT_BYTES),
544
549
  maintenanceMaxTokens: z.number().step(1).min(1).max(MAX_RUNTIME_MAINTENANCE_MAX_TOKENS).default(DEFAULT_RUNTIME_MAINTENANCE_MAX_TOKENS)
@@ -595,6 +600,7 @@ const Config = z.object({
595
600
  timeoutMs: z.number().step(1).min(100).max(12e4).default(DEFAULT_TIMEOUT_MS),
596
601
  defaultRecallLimit: z.number().step(1).min(1).max(50).default(10),
597
602
  runtimeMemory: RuntimeMemorySchema.default({
603
+ inheritToSubagents: false,
598
604
  memoryLimitBytes: DEFAULT_RUNTIME_MEMORY_LIMIT_BYTES,
599
605
  userLimitBytes: DEFAULT_RUNTIME_USER_LIMIT_BYTES,
600
606
  maintenanceMaxTokens: DEFAULT_RUNTIME_MAINTENANCE_MAX_TOKENS
@@ -719,6 +725,8 @@ function resolveTaskAgentModel(value) {
719
725
  };
720
726
  }
721
727
  function resolveRuntimeMemory(value) {
728
+ const inheritToSubagents = value?.inheritToSubagents ?? false;
729
+ if (typeof inheritToSubagents !== "boolean") throw new Error("dsh-mnemon: Runtime inheritToSubagents must be a boolean");
722
730
  const memoryLimitBytes = value?.memoryLimitBytes ?? DEFAULT_RUNTIME_MEMORY_LIMIT_BYTES;
723
731
  const userLimitBytes = value?.userLimitBytes ?? DEFAULT_RUNTIME_USER_LIMIT_BYTES;
724
732
  const maintenanceMaxTokens = value?.maintenanceMaxTokens ?? 8192;
@@ -726,6 +734,7 @@ function resolveRuntimeMemory(value) {
726
734
  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
735
  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
736
  return {
737
+ inheritToSubagents,
729
738
  memoryLimitBytes,
730
739
  userLimitBytes,
731
740
  maintenanceMaxTokens
@@ -2250,6 +2259,7 @@ function parseRuntime(value, limits = RUNTIME_MEMORY_LIMITS) {
2250
2259
  "low"
2251
2260
  ].includes(String(entry.importance))) throw new Error("runtime memories.json contains an invalid entry");
2252
2261
  if (typeof entry.created_at !== "string" || typeof entry.updated_at !== "string") throw new Error("runtime memories.json contains invalid timestamps");
2262
+ if (entry.pinned !== void 0 && typeof entry.pinned !== "boolean") throw new Error("runtime memories.json contains invalid pin state");
2253
2263
  const content = entry.content.trim().replace(/\s+/gu, " ");
2254
2264
  if (content === "" || content.includes("§") || Buffer.byteLength(content, "utf8") > RUNTIME_ENTRY_MAX_BYTES) throw new Error("runtime memories.json contains invalid content");
2255
2265
  const branches = entry.target === "memory" ? parseRuntimeBranches(entry.branches) : void 0;
@@ -2260,10 +2270,12 @@ function parseRuntime(value, limits = RUNTIME_MEMORY_LIMITS) {
2260
2270
  importance: entry.importance,
2261
2271
  created_at: entry.created_at,
2262
2272
  updated_at: entry.updated_at,
2273
+ ...entry.pinned === void 0 ? {} : { pinned: entry.pinned },
2263
2274
  ...branches === void 0 ? {} : { branches },
2264
2275
  ...origin === void 0 ? {} : { origin }
2265
2276
  };
2266
2277
  });
2278
+ assertRuntimePinnedBudget(entries);
2267
2279
  for (const target of ["user", "memory"]) {
2268
2280
  const used = runtimeBytes(entries, target);
2269
2281
  if (used > limits[target]) throw new Error(`runtime ${target} memory holds ${used} bytes and exceeds its ${limits[target]} byte limit`);
@@ -2476,6 +2488,7 @@ function archiveRuntimeOverflow(stagedRuntime, entries) {
2476
2488
  }
2477
2489
  /** Retain the same hot set on both machines, without asking a model to rewrite evidence. */
2478
2490
  function fitMergedRuntime(entries, limits) {
2491
+ 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
2492
  if (runtimeBytes(entries, "memory") <= limits.memory) return {
2480
2493
  entries,
2481
2494
  archived: []
@@ -2486,7 +2499,7 @@ function fitMergedRuntime(entries, limits) {
2486
2499
  low: 0
2487
2500
  };
2488
2501
  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)));
2502
+ 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
2503
  const kept = [];
2491
2504
  const archived = [];
2492
2505
  for (const entry of ranked) if (runtimeBytes([...kept, entry], "memory") <= limits.memory) kept.push(entry);
@@ -2835,7 +2848,14 @@ function mergeRuntime(root, pack, runtimeLimits, options = {}) {
2835
2848
  }
2836
2849
  for (const tombstone of hidden) if (!revived.includes(tombstone)) revived.push(tombstone);
2837
2850
  }
2838
- if (keys.has(key)) continue;
2851
+ if (keys.has(key)) {
2852
+ const existing = entries.find((candidate) => entryKey(candidate) === key);
2853
+ if (entry.pinned !== void 0 && (existing.pinned === void 0 || entry.updated_at > existing.updated_at)) {
2854
+ existing.pinned = entry.pinned;
2855
+ if (entry.updated_at > existing.updated_at) existing.updated_at = entry.updated_at;
2856
+ }
2857
+ continue;
2858
+ }
2839
2859
  keys.add(key);
2840
2860
  entries.push(entry);
2841
2861
  incomingKeys.add(key);
@@ -9672,7 +9692,11 @@ function runtimeRouteChunks(entries) {
9672
9692
  function compactedBudget(plan) {
9673
9693
  const pendingBytes = plan.pending === void 0 ? 0 : Buffer.byteLength(plan.pending.content, "utf8");
9674
9694
  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);
9695
+ const pinned = plan.entries.filter((entry) => entry.pinned === true);
9696
+ const pinnedBytes = Buffer.byteLength(pinned.map((entry) => entry.content).join(RUNTIME_ENTRY_DELIMITER), "utf8");
9697
+ const available = plan.limit - pendingBytes - separatorBytes;
9698
+ 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.");
9699
+ return Math.max(pinnedBytes, Math.max(0, Math.floor(plan.limit * .7) - pendingBytes - separatorBytes));
9676
9700
  }
9677
9701
  function runtimeMigrationSources(revision, entries) {
9678
9702
  return entries.map((entry, offset) => ({
@@ -9721,10 +9745,11 @@ async function archiveRuntimeLocally(counters, context, request, plan, reason, s
9721
9745
  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
9746
  context.assertWritable?.();
9723
9747
  const runId = `host-${randomUUID()}`;
9724
- const compacted = plan.entries.map(({ content, importance, branches }) => ({
9748
+ const compacted = plan.entries.map(({ content, importance, branches, pinned }) => ({
9725
9749
  content,
9726
9750
  importance,
9727
- ...branches === void 0 ? {} : { branches }
9751
+ ...branches === void 0 ? {} : { branches },
9752
+ ...pinned === void 0 ? {} : { pinned }
9728
9753
  }));
9729
9754
  const mutation = await context.runtime.mutateResult("compact-and-mutate", {
9730
9755
  revision: plan.revision,
@@ -9767,7 +9792,9 @@ async function runtimeLocked(counters, context, request, signal) {
9767
9792
  if (context.expectedRevision !== void 0 && context.expectedRevision !== plan.revision) throw new Error("Runtime source revision conflict before capacity maintenance");
9768
9793
  if (!plan.requiresMaintenance) return context.commit();
9769
9794
  if (plan.entries.length === 0) throw new Error("runtime memory capacity was exceeded without entries available for maintenance");
9795
+ compactedBudget(plan);
9770
9796
  if (request.target === "user") return compactUserAndCommit(context, request, plan, signal);
9797
+ const archiveEntries = plan.entries.filter((entry) => entry.pinned !== true);
9771
9798
  const archive = await context.memorySpaces();
9772
9799
  if (archive === void 0) return archiveRuntimeLocally(counters, context, request, plan, "no Memory Space takes automatic writes here", signal);
9773
9800
  const memoryService = archive.source;
@@ -9802,11 +9829,11 @@ async function runtimeLocked(counters, context, request, signal) {
9802
9829
  let provider = "host";
9803
9830
  let runId = `host-${randomUUID()}`;
9804
9831
  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);
9832
+ if (eligibleBodies.length === 1) for (const index of archiveEntries.keys()) routed.set(index + 1, eligibleBodies[0].id);
9806
9833
  else {
9807
9834
  const summaries = [];
9808
9835
  const fallbackBody = eligibleBodies.find((body) => body.mnemonDefault) ?? eligibleBodies[0];
9809
- const chunks = runtimeRouteChunks(plan.entries);
9836
+ const chunks = runtimeRouteChunks(archiveEntries);
9810
9837
  for (const [chunkIndex, chunk] of chunks.entries()) {
9811
9838
  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
9839
 
@@ -9851,11 +9878,12 @@ ${chunk.context}
9851
9878
  }
9852
9879
  summary = summaries.join(" ");
9853
9880
  }
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 }) => ({
9881
+ if (routed.size !== archiveEntries.length) throw new Error("runtime memory migration omitted committed archive sources");
9882
+ const compactedEntries = plan.entries.map(({ content, importance, branches, pinned }) => ({
9856
9883
  content,
9857
9884
  importance,
9858
- ...branches === void 0 ? {} : { branches }
9885
+ ...branches === void 0 ? {} : { branches },
9886
+ ...pinned === void 0 ? {} : { pinned }
9859
9887
  }));
9860
9888
  if ((await runtimeMemory.read("maintenance-plan", {
9861
9889
  ...request,
@@ -9865,9 +9893,9 @@ ${chunk.context}
9865
9893
  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
9894
  signal.throwIfAborted();
9867
9895
  context.assertWritable?.();
9868
- const sources = runtimeMigrationSources(plan.revision, plan.entries);
9896
+ const sources = runtimeMigrationSources(plan.revision, archiveEntries);
9869
9897
  const requests = sources.map((source) => {
9870
- const entry = plan.entries[source.index - 1];
9898
+ const entry = archiveEntries[source.index - 1];
9871
9899
  return {
9872
9900
  content: entry.content,
9873
9901
  category: "context",
@@ -9910,7 +9938,7 @@ ${chunk.context}
9910
9938
  archiveResults.set(source.index, result);
9911
9939
  }
9912
9940
  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));
9941
+ for (const source of batch) verified.set(source.index, await archiveRuntimeEntry(memoryService, memoryBodyId, archiveEntries[source.index - 1], archiveResults.get(source.index), signal));
9914
9942
  }
9915
9943
  const lineage = [];
9916
9944
  const memoryBodyIds = /* @__PURE__ */ new Set();
@@ -9977,11 +10005,13 @@ ${chunk.context}
9977
10005
  async function compactUserAndCommit(context, request, plan, signal) {
9978
10006
  const runtimeMemory = context.runtime;
9979
10007
  const budget = compactedBudget(plan);
10008
+ const pinned = plan.entries.filter((entry) => entry.pinned === true);
10009
+ const mutable = plan.entries.filter((entry) => entry.pinned !== true);
9980
10010
  const prompt = `Run local USER.md compaction now.
9981
10011
  Pending mutation (uncommitted; do not include in compaction):
9982
10012
  ${pendingMutationContext(plan)}
9983
10013
 
9984
- ${runtimeSnapshotContext("user", plan.entries)}`;
10014
+ ${runtimeSnapshotContext("user", mutable)}`;
9985
10015
  const { provider, runId, result } = await context.model("compaction", "Consolidate local user profile", prompt, USER_COMPACTION_SCHEMA, USER_COMPACTION_PERSONA);
9986
10016
  const value = object$6(result.structured);
9987
10017
  if (value.action !== "compacted") throw new Error(typeof value.summary === "string" && value.summary !== "" ? value.summary : "USER.md compaction failed");
@@ -10010,17 +10040,21 @@ ${runtimeSnapshotContext("user", plan.entries)}`;
10010
10040
  if (entry.sourceIndexes.length === 0) throw new Error("USER.md compaction returned an entry without a source");
10011
10041
  let requiredRank = 0;
10012
10042
  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");
10043
+ if (index < 1 || index > mutable.length || seen.has(index)) throw new Error("USER.md compaction source coverage is invalid");
10014
10044
  seen.add(index);
10015
- requiredRank = Math.max(requiredRank, importanceRank[plan.entries[index - 1].importance]);
10045
+ requiredRank = Math.max(requiredRank, importanceRank[mutable[index - 1].importance]);
10016
10046
  }
10017
10047
  if (importanceRank[entry.importance] < requiredRank) throw new Error("USER.md compaction lowered source importance");
10018
10048
  }
10019
- if (seen.size !== plan.entries.length) throw new Error("USER.md compaction omitted committed entries");
10020
- const candidates = compactedEntries.map(({ content, importance }) => ({
10049
+ if (seen.size !== mutable.length) throw new Error("USER.md compaction omitted committed entries");
10050
+ const candidates = [...pinned.map((entry) => ({
10051
+ content: entry.content,
10052
+ importance: entry.importance,
10053
+ pinned: true
10054
+ })), ...compactedEntries.map(({ content, importance }) => ({
10021
10055
  content,
10022
10056
  importance
10023
- }));
10057
+ }))];
10024
10058
  const candidateBytes = Buffer.byteLength(candidates.map((entry) => entry.content.trim().replace(/\s+/gu, " ")).join(RUNTIME_ENTRY_DELIMITER), "utf8");
10025
10059
  if (candidateBytes > budget) throw new Error(`USER.md compaction did not fit the host budget (${candidateBytes} > ${budget} bytes)`);
10026
10060
  signal.throwIfAborted();
@@ -10357,9 +10391,16 @@ const ACTION_TOOLS = namedBindings("action");
10357
10391
  const NAMED_SOURCE_TYPES = [.../* @__PURE__ */ new Set([...Object.keys(ROUTE_TOOLS), ...Object.keys(ACTION_TOOLS)])];
10358
10392
  /** Use a short availability list only where the Host has the matching named binding.
10359
10393
  * Unbound external operations retain exact ids and schemas in the generic envelope. */
10360
- function modelMemoryWake(graph, turn) {
10394
+ function modelMemoryWake(graph, turn, includeRuntime = true) {
10361
10395
  const generation = graph.memoryComposition.generation(turn.view.runtimeGeneration);
10362
10396
  if (generation === void 0) throw new Error("Cannot present an unpinned Memory generation");
10397
+ if (!includeRuntime) {
10398
+ const runtimeSources = new Set(generation.sourceInstances().filter((source) => source.sourceTypeId === "runtime").map((source) => source.sourceInstanceKey));
10399
+ return createMemoryWake({
10400
+ ...turn.view,
10401
+ projection: turn.view.projection.filter((fragment) => !runtimeSources.has(fragment.sourceInstanceKey))
10402
+ }, memoryWakeBindings(generation, turn.view));
10403
+ }
10363
10404
  return graph.composableTurns.memoryWake(turn.view.id, memoryWakeBindings(generation, turn.view));
10364
10405
  }
10365
10406
  function presentMemoryWake(generation, view) {
@@ -10485,17 +10526,23 @@ var AgentMemoryTurn = class {
10485
10526
  lastInspection;
10486
10527
  lastWorkspace;
10487
10528
  releaseDelegation;
10529
+ includeRuntime;
10488
10530
  constructor(agent, runtime, delegation, independent = false) {
10489
10531
  this.agent = agent;
10490
10532
  this.runtime = runtime;
10491
10533
  this.delegation = delegation;
10492
10534
  this.independent = independent;
10535
+ this.includeRuntime = delegation === void 0 || runtime.config.runtimeMemory.inheritToSubagents;
10493
10536
  if (delegation === void 0) return;
10494
10537
  this.releaseDelegation = runtime.executions.retain(agent, delegation);
10495
10538
  }
10496
10539
  get current() {
10497
10540
  return this.pinned;
10498
10541
  }
10542
+ modelWake() {
10543
+ const pinned = this.pinned;
10544
+ return pinned === void 0 ? void 0 : modelMemoryWake(pinned.graph, pinned.context, this.includeRuntime);
10545
+ }
10499
10546
  /** Why the current turn has no View, when memory could not be composed for it. */
10500
10547
  get unavailableReason() {
10501
10548
  return this.unavailable?.reason;
@@ -10586,7 +10633,7 @@ var AgentMemoryTurn = class {
10586
10633
  try {
10587
10634
  if (this.closed || this.generation !== generation) throw new Error("memory turn ended during View preparation");
10588
10635
  signal?.throwIfAborted();
10589
- const inspection = inspectMemoryView(graph.memoryComposition.generation(context.view.runtimeGeneration), context.view, "active", turn, modelMemoryWake(graph, context).text);
10636
+ const inspection = inspectMemoryView(graph.memoryComposition.generation(context.view.runtimeGeneration), context.view, "active", turn, modelMemoryWake(graph, context, this.includeRuntime).text);
10590
10637
  this.pinned = {
10591
10638
  turn,
10592
10639
  graph,
@@ -13871,6 +13918,16 @@ var MnemonLifecycle = class {
13871
13918
  ...typeof effort === "string" && effort !== "" ? { reasoningEffort: effort } : {}
13872
13919
  };
13873
13920
  }
13921
+ /** Freeze the effective task route for a paired skill evaluation. */
13922
+ skillTaskRoute(workspaceRoot) {
13923
+ const options = this.taskAgentModelOptions("", workspaceRoot);
13924
+ if (!options?.provider || !options.model) return void 0;
13925
+ return {
13926
+ provider: options.provider,
13927
+ model: options.model,
13928
+ ...options.reasoningEffort === void 0 ? {} : { reasoningEffort: options.reasoningEffort }
13929
+ };
13930
+ }
13874
13931
  taskAgentModelOptions(fallbackSessionId, workspaceRoot) {
13875
13932
  return this.taskAgentModelRoute(fallbackSessionId, workspaceRoot)?.options;
13876
13933
  }
@@ -13958,12 +14015,11 @@ var MnemonLifecycle = class {
13958
14015
  return decision;
13959
14016
  }
13960
14017
  if (openAgentTurn(agent) !== payload.turn) return decision;
13961
- const recovering = memory.current?.turn !== payload.turn;
13962
14018
  await memory.begin(payload.turn, payload.signal);
13963
14019
  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 {
14020
+ if (payload.step === 1 && this.config.lifecycleEnabled && pinned !== void 0) {
14021
+ const wake = memory.modelWake();
14022
+ if (wake !== void 0 && wake.text.trim() !== "") return {
13967
14023
  kind: "enter",
13968
14024
  messages: [...decision.messages, createPluginMessage(wake.text, "recall")]
13969
14025
  };
@@ -13983,6 +14039,219 @@ var MnemonLifecycle = class {
13983
14039
  }
13984
14040
  };
13985
14041
  //#endregion
14042
+ //#region src/host/skill-auto-adoption.ts
14043
+ var AutomaticSkillStop = class extends Error {
14044
+ reason;
14045
+ constructor(reason, detail) {
14046
+ super(detail ?? reason);
14047
+ this.reason = reason;
14048
+ }
14049
+ };
14050
+ function hashSkillCases(cases) {
14051
+ return createHash("sha256").update(SkillsContracts.skillCaseSignature(cases)).digest("hex");
14052
+ }
14053
+ function hashSkillDraft(draft) {
14054
+ return createHash("sha256").update(draft.content).digest("hex");
14055
+ }
14056
+ /** Only fresh idle-review suggestions enter this bounded, in-process queue. */
14057
+ var SkillAutoAdoption = class {
14058
+ host;
14059
+ jobs = /* @__PURE__ */ new Map();
14060
+ tail = Promise.resolve();
14061
+ disposed = false;
14062
+ constructor(host) {
14063
+ this.host = host;
14064
+ }
14065
+ enqueue(source, workspace, ids, available) {
14066
+ if (this.disposed || !this.host.settings().autoAdopt) return;
14067
+ for (const id of ids) {
14068
+ if (this.jobs.has(id)) continue;
14069
+ if (this.jobs.size >= 16) {
14070
+ this.record({
14071
+ source,
14072
+ id
14073
+ }, {
14074
+ stage: "review",
14075
+ attempt: 0,
14076
+ reason: "queue-full"
14077
+ }).catch((error) => this.host.warn("Could not record automatic adoption queue limit", error));
14078
+ continue;
14079
+ }
14080
+ const job = {
14081
+ source,
14082
+ workspace,
14083
+ id,
14084
+ available,
14085
+ controller: new AbortController()
14086
+ };
14087
+ this.jobs.set(id, job);
14088
+ this.tail = this.tail.then(() => this.run(job)).catch((error) => this.host.warn("Automatic skill adoption failed", error)).finally(() => {
14089
+ this.jobs.delete(id);
14090
+ });
14091
+ }
14092
+ }
14093
+ allowed(job) {
14094
+ const settings = this.host.settings();
14095
+ return !this.disposed && settings.evolution && settings.autoAdopt && settings.autoBacktest && job.available() && (job.action === void 0 || (job.action === "create" ? settings.allowCreate : settings.allowUpdate));
14096
+ }
14097
+ settingsChanged() {
14098
+ for (const job of this.jobs.values()) if (!this.allowed(job)) job.controller.abort(new AutomaticSkillStop("disabled"));
14099
+ }
14100
+ dispose() {
14101
+ this.disposed = true;
14102
+ for (const job of this.jobs.values()) job.controller.abort(new AutomaticSkillStop("interrupted"));
14103
+ }
14104
+ checkpoint(job) {
14105
+ if (!this.allowed(job)) job.controller.abort(new AutomaticSkillStop(this.disposed ? "interrupted" : "disabled"));
14106
+ job.controller.signal.throwIfAborted();
14107
+ }
14108
+ record(job, event) {
14109
+ return job.source.mutate("auto-adoption-record", {
14110
+ id: job.id,
14111
+ ...event
14112
+ });
14113
+ }
14114
+ async unchanged(job, draft, casesHash) {
14115
+ this.checkpoint(job);
14116
+ const current = await job.source.read("draft", { id: draft.id }, job.controller.signal);
14117
+ const suggestion = (await job.source.read("suggestions", null, job.controller.signal)).find((entry) => entry.id === job.id);
14118
+ const cases = (await job.source.read("cases", { skill: draft.skillName }, job.controller.signal)).filter((entry) => entry.enabled);
14119
+ 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");
14120
+ }
14121
+ async runs(job, ids) {
14122
+ const deadline = AbortSignal.timeout(276e4);
14123
+ const signal = AbortSignal.any([job.controller.signal, deadline]);
14124
+ for (;;) {
14125
+ this.checkpoint(job);
14126
+ signal.throwIfAborted();
14127
+ const runs = await Promise.all(ids.map((id) => job.source.read("run", { id }, signal)));
14128
+ if (runs.some((run) => run.status !== "running" && run.status !== "completed") || runs.every((run) => run.status !== "running")) return runs;
14129
+ await setTimeout$1(250, void 0, { signal });
14130
+ }
14131
+ }
14132
+ async run(job) {
14133
+ const runIds = /* @__PURE__ */ new Set();
14134
+ let attempt = 0;
14135
+ let scores = {};
14136
+ try {
14137
+ this.checkpoint(job);
14138
+ const suggestion = (await job.source.read("suggestions")).find((entry) => entry.id === job.id);
14139
+ if (suggestion?.status !== "pending" || suggestion.draftId !== void 0) throw new AutomaticSkillStop("changed");
14140
+ job.action = suggestion.action;
14141
+ this.checkpoint(job);
14142
+ await this.record(job, {
14143
+ stage: "queued",
14144
+ attempt
14145
+ });
14146
+ const cases = (await job.source.read("cases", { skill: suggestion.skillName })).filter((entry) => entry.enabled);
14147
+ if (cases.length === 0) throw new AutomaticSkillStop("no-cases");
14148
+ if (cases.some((entry) => entry.requiresVision === true || entry.checks.length === 0)) throw new AutomaticSkillStop("unmeasured");
14149
+ if (cases.length > this.host.settings().casesPerRun) throw new AutomaticSkillStop("case-limit");
14150
+ const casesHash = hashSkillCases(cases);
14151
+ const preferences = await job.source.read("draft-preferences");
14152
+ const defaultRoute = this.host.route(job.workspace);
14153
+ const runRoute = preferences.skillRunModels[suggestion.skillName] ?? preferences.runModel ?? defaultRoute;
14154
+ const draftRoute = preferences.suggestionModels[job.id] ?? preferences.model ?? defaultRoute;
14155
+ if (runRoute === void 0 || draftRoute === void 0) throw new AutomaticSkillStop("model-unavailable");
14156
+ let previous;
14157
+ let guidance = "";
14158
+ for (attempt = 1; attempt <= 3; attempt++) {
14159
+ this.checkpoint(job);
14160
+ await this.record(job, {
14161
+ stage: "drafting",
14162
+ attempt
14163
+ });
14164
+ const draft = await this.host.draft(job.source, job.workspace, {
14165
+ suggestionId: job.id,
14166
+ action: suggestion.action,
14167
+ ...previous === void 0 ? {} : { previous },
14168
+ guidance,
14169
+ route: draftRoute
14170
+ }, job.controller.signal);
14171
+ await this.unchanged(job, draft, casesHash);
14172
+ await this.record(job, {
14173
+ stage: "backtesting",
14174
+ attempt
14175
+ });
14176
+ const baseline = await this.host.backtest(job.source, job.workspace, {
14177
+ skillName: draft.skillName,
14178
+ target: {
14179
+ kind: draft.action === "update" ? "current" : "none",
14180
+ ...draft.action === "update" ? { draftId: draft.id } : {}
14181
+ },
14182
+ route: runRoute,
14183
+ expectedCasesHash: casesHash,
14184
+ signal: job.controller.signal
14185
+ });
14186
+ runIds.add(baseline.runId);
14187
+ this.checkpoint(job);
14188
+ const candidate = await this.host.backtest(job.source, job.workspace, {
14189
+ skillName: draft.skillName,
14190
+ target: {
14191
+ kind: "draft",
14192
+ draftId: draft.id
14193
+ },
14194
+ route: runRoute,
14195
+ expectedCasesHash: casesHash,
14196
+ signal: job.controller.signal
14197
+ });
14198
+ runIds.add(candidate.runId);
14199
+ await job.source.mutate("run-link", {
14200
+ draftId: draft.id,
14201
+ runId: baseline.runId
14202
+ }, job.controller.signal);
14203
+ const [before, after] = await this.runs(job, [baseline.runId, candidate.runId]);
14204
+ await this.unchanged(job, draft, casesHash);
14205
+ const comparison = SkillsContracts.compareAutomaticSkillRuns(cases, before, after, casesHash);
14206
+ scores = {
14207
+ ...comparison.baselineScore === void 0 ? {} : { baselineScore: comparison.baselineScore },
14208
+ ...comparison.draftScore === void 0 ? {} : { draftScore: comparison.draftScore }
14209
+ };
14210
+ if (comparison.outcome === "unmeasured") throw new AutomaticSkillStop("unmeasured");
14211
+ if (comparison.outcome === "not-worse") {
14212
+ this.checkpoint(job);
14213
+ await job.source.mutate("draft-adopt", {
14214
+ id: draft.id,
14215
+ automatic: {
14216
+ draftHash: hashSkillDraft(draft),
14217
+ baselineRunId: baseline.runId,
14218
+ draftRunId: candidate.runId,
14219
+ casesHash,
14220
+ attempt
14221
+ }
14222
+ }, job.controller.signal);
14223
+ return;
14224
+ }
14225
+ if (attempt === 3) throw new AutomaticSkillStop("regressed");
14226
+ await this.record(job, {
14227
+ stage: "rewriting",
14228
+ attempt,
14229
+ reason: "regressed",
14230
+ ...scores
14231
+ });
14232
+ 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) => ({
14233
+ title: entry.title,
14234
+ baseline: before.results.find((result) => result.caseId === entry.id),
14235
+ draft: after.results.find((result) => result.caseId === entry.id)
14236
+ }))).slice(0, 8e3);
14237
+ previous = draft;
14238
+ }
14239
+ } catch (error) {
14240
+ const failure = job.controller.signal.aborted ? job.controller.signal.reason : error;
14241
+ const reason = failure instanceof AutomaticSkillStop ? failure.reason : "failed";
14242
+ await this.record(job, {
14243
+ stage: reason === "disabled" || reason === "interrupted" ? "paused" : "review",
14244
+ attempt: Math.min(attempt, 3),
14245
+ reason,
14246
+ ...scores,
14247
+ ...failure instanceof AutomaticSkillStop ? {} : { detail: failure instanceof Error ? failure.message : String(failure) }
14248
+ }).catch((recordError) => this.host.warn("Could not record automatic adoption outcome", recordError));
14249
+ } finally {
14250
+ for (const id of runIds) await job.source.mutate("run-cancel", { id }).catch((error) => this.host.warn("Could not stop automatic skill backtest", error));
14251
+ }
14252
+ }
14253
+ };
14254
+ //#endregion
13986
14255
  //#region src/host/skill-review.ts
13987
14256
  const SIGNALS = [
13988
14257
  "correction",
@@ -14815,7 +15084,7 @@ function retryingSource(source, attempts = 4) {
14815
15084
  for (let attempt = 1;; attempt++) try {
14816
15085
  return await source.mutate(operation, input, signal);
14817
15086
  } 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;
15087
+ 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
15088
  }
14820
15089
  }
14821
15090
  };
@@ -14823,6 +15092,27 @@ function retryingSource(source, attempts = 4) {
14823
15092
  function message(error) {
14824
15093
  return redact(error instanceof Error ? error.message : String(error));
14825
15094
  }
15095
+ /**
15096
+ * A draft task that stops reporting is abandoned here. The subagent behind it
15097
+ * can die without settling, and the suggestion would otherwise stay `drafting`
15098
+ * - blocking dismiss, delete and a retry - until the Host restarts.
15099
+ */
15100
+ const DRAFT_DEADLINE_MS = 9e5;
15101
+ /** Distinguishes a deadline abort from the plain abort a Host shutdown sends. */
15102
+ var DraftDeadlineError = class extends Error {};
15103
+ /** The deadline error a task ran into, so the Source records why the draft ended. */
15104
+ function deadlineReason(signal) {
15105
+ return signal.reason instanceof DraftDeadlineError ? signal.reason : void 0;
15106
+ }
15107
+ /** Cancellation settles the owner even when an underlying task ignores its signal. */
15108
+ function abortable(signal, work) {
15109
+ signal.throwIfAborted();
15110
+ return new Promise((resolve, reject) => {
15111
+ const abort = () => reject(signal.reason);
15112
+ signal.addEventListener("abort", abort, { once: true });
15113
+ Promise.resolve().then(work).then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
15114
+ });
15115
+ }
14826
15116
  /** Resolve only roots supplied by this Source's scoped catalog, never the task's cwd. */
14827
15117
  function newSkillRoot(settings, source, catalog, explicit) {
14828
15118
  let root = explicit;
@@ -14863,8 +15153,22 @@ var SkillEvolution = class {
14863
15153
  sessions = /* @__PURE__ */ new Map();
14864
15154
  jobs = /* @__PURE__ */ new Set();
14865
15155
  disposed = false;
15156
+ activeBacktests = 0;
15157
+ backtestJobs = 0;
15158
+ backtestWaiters = /* @__PURE__ */ new Set();
15159
+ automatic;
14866
15160
  constructor(host) {
14867
15161
  this.host = host;
15162
+ this.automatic = new SkillAutoAdoption({
15163
+ settings: () => host.settings(),
15164
+ route: (workspace) => host.taskRoute?.(workspace),
15165
+ draft: (source, workspace, input, signal) => this.automaticDraft(source, workspace, input, signal),
15166
+ backtest: (source, workspace, input) => this.backtest(source, workspace, input),
15167
+ warn: (text, error) => this.warn(text, error)
15168
+ });
15169
+ }
15170
+ settingsChanged() {
15171
+ this.automatic.settingsChanged();
14868
15172
  }
14869
15173
  source(agent) {
14870
15174
  const source = this.host.source(agent);
@@ -15098,6 +15402,7 @@ var SkillEvolution = class {
15098
15402
  });
15099
15403
  state.failures = 0;
15100
15404
  state.lastReviewAt = this.now().getTime();
15405
+ if (trigger === "automatic" && result.createdSuggestionIds !== void 0) this.automatic.enqueue(source, workspace, result.createdSuggestionIds, () => this.host.source(agent) !== void 0);
15101
15406
  return result;
15102
15407
  } catch (error) {
15103
15408
  state.failures += 1;
@@ -15128,10 +15433,17 @@ var SkillEvolution = class {
15128
15433
  delete state.reviewing;
15129
15434
  }
15130
15435
  }
15131
- background(name, work) {
15436
+ background(name, work, deadlineMs) {
15132
15437
  const controller = new AbortController();
15133
15438
  this.jobs.add(controller);
15134
- work(controller.signal).catch((error) => this.warn(`${name} failed`, error)).finally(() => this.jobs.delete(controller));
15439
+ const timer = deadlineMs === void 0 ? void 0 : setTimeout(() => {
15440
+ controller.abort(new DraftDeadlineError(`${name} did not finish within ${Math.round(deadlineMs / 6e4)} minutes`));
15441
+ }, deadlineMs);
15442
+ timer?.unref?.();
15443
+ return work(controller.signal).catch((error) => this.warn(`${name} failed`, deadlineReason(controller.signal) ?? error)).finally(() => {
15444
+ if (timer !== void 0) clearTimeout(timer);
15445
+ this.jobs.delete(controller);
15446
+ });
15135
15447
  }
15136
15448
  /**
15137
15449
  * Write a draft for a suggestion (or for a skill directly) in the
@@ -15157,26 +15469,83 @@ var SkillEvolution = class {
15157
15469
  status: "drafting"
15158
15470
  });
15159
15471
  this.background("skill draft", async (signal) => {
15472
+ let recorded = false;
15160
15473
  try {
15161
- await this.writeDraft(source, workspace, input, {
15474
+ await abortable(signal, () => this.writeDraft(source, workspace, input, {
15162
15475
  pack,
15163
15476
  targetRoot,
15164
- guidance
15165
- }, signal);
15477
+ guidance,
15478
+ onRecorded: () => {
15479
+ recorded = true;
15480
+ }
15481
+ }, signal));
15166
15482
  } 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);
15483
+ if (!recorded && input.suggestionId !== void 0) {
15484
+ if ((await source.read("suggestions")).find((item) => item.id === input.suggestionId)?.status === "drafting") await source.mutate("suggestion-status", {
15485
+ id: input.suggestionId,
15486
+ status: "pending",
15487
+ error: message(deadlineReason(signal) ?? error)
15488
+ }).catch(() => void 0);
15489
+ }
15172
15490
  throw error;
15173
15491
  }
15174
- });
15492
+ }, DRAFT_DEADLINE_MS);
15175
15493
  return {
15176
15494
  started: true,
15177
15495
  ...input.suggestionId === void 0 ? {} : { suggestionId: input.suggestionId }
15178
15496
  };
15179
15497
  }
15498
+ async automaticDraft(source, workspace, input, parentSignal) {
15499
+ const controller = new AbortController();
15500
+ const signal = AbortSignal.any([parentSignal, controller.signal]);
15501
+ const timer = setTimeout(() => controller.abort(new DraftDeadlineError("Automatic skill draft did not finish within 15 minutes")), DRAFT_DEADLINE_MS);
15502
+ timer.unref?.();
15503
+ const expectedDraft = input.previous === void 0 ? null : {
15504
+ id: input.previous.id,
15505
+ hash: hashSkillDraft(input.previous)
15506
+ };
15507
+ let recorded = false;
15508
+ let claimed = false;
15509
+ try {
15510
+ return await abortable(signal, async () => {
15511
+ const pack = await source.read("evidence-pack", { suggestionId: input.suggestionId }, signal);
15512
+ signal.throwIfAborted();
15513
+ if (pack.action !== input.action || pack.action === "update" && pack.suggestion?.skillFileHash !== pack.skill?.hash) throw new AutomaticSkillStop("changed");
15514
+ 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);
15515
+ const guidance = [...input.previous?.guidance ?? [], ...input.guidance === "" ? [] : [input.guidance]].slice(-8);
15516
+ await source.mutate("suggestion-status", {
15517
+ id: input.suggestionId,
15518
+ status: "drafting",
15519
+ expectedDraft
15520
+ }, signal);
15521
+ claimed = true;
15522
+ return this.writeDraft(source, workspace, {
15523
+ suggestionId: input.suggestionId,
15524
+ ...input.previous === void 0 ? {} : { draftId: input.previous.id },
15525
+ route: input.route
15526
+ }, {
15527
+ pack,
15528
+ targetRoot,
15529
+ guidance,
15530
+ expectedDraft,
15531
+ deferBacktests: true,
15532
+ onRecorded: () => {
15533
+ recorded = true;
15534
+ }
15535
+ }, signal);
15536
+ });
15537
+ } catch (error) {
15538
+ if (claimed && !recorded) await source.mutate("suggestion-status", {
15539
+ id: input.suggestionId,
15540
+ status: "pending",
15541
+ expectedDraft,
15542
+ ...parentSignal.aborted ? {} : { error: message(signal.aborted ? signal.reason : error) }
15543
+ }).catch(() => void 0);
15544
+ throw signal.aborted ? signal.reason : error;
15545
+ } finally {
15546
+ clearTimeout(timer);
15547
+ }
15548
+ }
15180
15549
  async writeDraft(source, workspace, input, prepared, signal) {
15181
15550
  const settings = this.host.settings();
15182
15551
  const { pack, targetRoot, guidance } = prepared;
@@ -15213,15 +15582,19 @@ var SkillEvolution = class {
15213
15582
  notes: parsed.notes,
15214
15583
  changes: parsed.changes,
15215
15584
  guidance,
15585
+ expectedBaseHash: pack.skill?.hash ?? null,
15586
+ ...prepared.expectedDraft === void 0 ? {} : { expectedDraft: prepared.expectedDraft },
15216
15587
  ...pack.action === "create" ? { targetRoot } : {}
15217
15588
  }, signal);
15589
+ prepared.onRecorded?.();
15218
15590
  } catch (error) {
15219
15591
  signal.throwIfAborted();
15220
- if (attempt > 0) throw error;
15592
+ if (attempt > 0 || /changed while this draft/.test(message(error))) throw error;
15221
15593
  prompt += `\n\nYour previous SKILL.md was rejected: ${message(error)}. Return a corrected complete file.`;
15222
15594
  }
15223
15595
  }
15224
- if (settings.autoBacktest && pack.cases.length > 0) {
15596
+ signal.throwIfAborted();
15597
+ if (!prepared.deferBacktests && (settings.autoBacktest || input.backtest) && pack.cases.length > 0) {
15225
15598
  await this.backtest(source, workspace, {
15226
15599
  skillName: pack.skillName,
15227
15600
  target: {
@@ -15252,67 +15625,138 @@ var SkillEvolution = class {
15252
15625
  return draft;
15253
15626
  }
15254
15627
  /** Start a backtest of one version of a skill against its enabled cases; it runs in the background. */
15628
+ backtestSlot(signal) {
15629
+ signal.throwIfAborted();
15630
+ return new Promise((resolve, reject) => {
15631
+ const abort = () => {
15632
+ this.backtestWaiters.delete(acquire);
15633
+ reject(signal.reason);
15634
+ };
15635
+ const acquire = () => {
15636
+ if (signal.aborted) {
15637
+ abort();
15638
+ return;
15639
+ }
15640
+ if (this.activeBacktests >= 2) return;
15641
+ this.backtestWaiters.delete(acquire);
15642
+ signal.removeEventListener("abort", abort);
15643
+ this.activeBacktests++;
15644
+ resolve(() => {
15645
+ this.activeBacktests--;
15646
+ for (const waiter of [...this.backtestWaiters]) waiter();
15647
+ });
15648
+ };
15649
+ this.backtestWaiters.add(acquire);
15650
+ signal.addEventListener("abort", abort, { once: true });
15651
+ acquire();
15652
+ });
15653
+ }
15255
15654
  async backtest(port, workspace, input) {
15655
+ if (this.disposed) throw new Error("Skill evolution has stopped");
15656
+ input.signal?.throwIfAborted();
15657
+ if (this.backtestJobs >= 16) throw new Error("The skill backtest queue is full. Cancel a run or retry after it finishes.");
15658
+ this.backtestJobs++;
15256
15659
  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
- }
15660
+ try {
15661
+ const settings = this.host.settings();
15662
+ 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);
15663
+ if (cases.length === 0) throw new Error(`${input.skillName} has no enabled eval case to run`);
15664
+ if (input.expectedCasesHash !== void 0 && hashSkillCases(cases) !== input.expectedCasesHash) throw new Error("Eval cases changed before this backtest started");
15665
+ if (cases.some((entry) => entry.requiresVision !== void 0 && typeof entry.requiresVision !== "boolean")) throw new Error("requiresVision must be a boolean");
15666
+ const resolved = await source.read("target-content", {
15667
+ skillName: input.skillName,
15668
+ target: input.target
15669
+ }, input.signal);
15670
+ const run = await source.mutate("run-start", {
15671
+ skillName: input.skillName,
15672
+ target: resolved.target,
15673
+ caseIds: cases.map((entry) => entry.id),
15674
+ ...input.expectedCasesHash === void 0 ? {} : { expectedCasesHash: input.expectedCasesHash },
15675
+ ...input.route === void 0 ? {} : { model: input.route }
15676
+ }, input.signal);
15677
+ this.background("skill backtest", async (deadline) => {
15678
+ const controller = new AbortController();
15679
+ const abort = () => controller.abort(deadline.aborted ? deadline.reason : input.signal?.reason);
15680
+ deadline.addEventListener("abort", abort, { once: true });
15681
+ input.signal?.addEventListener("abort", abort, { once: true });
15682
+ if (deadline.aborted || input.signal?.aborted) abort();
15683
+ const signal = controller.signal;
15684
+ let checking = false;
15685
+ const watcher = setInterval(() => {
15686
+ if (checking || signal.aborted) return;
15687
+ checking = true;
15688
+ source.read("run", { id: run.id }).then((current) => {
15689
+ if (current.status !== "running") controller.abort(new Error(current.error ?? "Backtest interrupted"));
15690
+ }).catch((error) => controller.abort(error)).finally(() => {
15691
+ checking = false;
15287
15692
  });
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()) {
15693
+ }, 1e3);
15694
+ watcher.unref?.();
15695
+ try {
15696
+ await abortable(signal, async () => {
15697
+ const release = await this.backtestSlot(signal);
15698
+ try {
15699
+ for (const entry of cases.filter((entry) => entry.requiresVision === true)) {
15700
+ signal.throwIfAborted();
15701
+ await source.mutate("run-progress", {
15702
+ id: run.id,
15703
+ result: {
15704
+ caseId: entry.id,
15705
+ title: entry.title,
15706
+ verdict: "error",
15707
+ score: 0,
15708
+ unmeasuredReason: "requires-vision",
15709
+ checks: [],
15710
+ steps: [],
15711
+ answer: "",
15712
+ error: "Not measured: this case requires image input, which text-only backtests cannot receive."
15713
+ }
15714
+ }, signal);
15715
+ }
15716
+ const queue = cases.filter((entry) => entry.requiresVision !== true);
15717
+ if (queue.length > 0) await abortable(signal, () => this.host.runTask(workspace, signal, async (task) => {
15718
+ const worker = async () => {
15719
+ for (let entry = queue.shift(); entry !== void 0; entry = queue.shift()) {
15720
+ signal.throwIfAborted();
15721
+ const result = await this.runCase(task, entry, input.skillName, resolved.content, signal);
15722
+ signal.throwIfAborted();
15723
+ await source.mutate("run-progress", {
15724
+ id: run.id,
15725
+ result
15726
+ }, signal);
15727
+ }
15728
+ };
15729
+ await Promise.all(Array.from({ length: Math.min(BACKTEST_CONCURRENCY, queue.length) }, worker));
15730
+ }, input.route));
15293
15731
  signal.throwIfAborted();
15294
- await source.mutate("run-progress", {
15732
+ await source.mutate("run-finish", {
15295
15733
  id: run.id,
15296
- result: await this.runCase(task, entry, input.skillName, resolved.content, signal)
15297
- });
15734
+ status: "completed"
15735
+ }, signal);
15736
+ } finally {
15737
+ release();
15298
15738
  }
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 };
15739
+ });
15740
+ } catch (error) {
15741
+ await source.mutate("run-finish", {
15742
+ id: run.id,
15743
+ status: signal.aborted ? "interrupted" : "failed",
15744
+ error: message(signal.aborted ? signal.reason : error)
15745
+ }).catch(() => void 0);
15746
+ throw error;
15747
+ } finally {
15748
+ clearInterval(watcher);
15749
+ deadline.removeEventListener("abort", abort);
15750
+ input.signal?.removeEventListener("abort", abort);
15751
+ }
15752
+ }, 27e5).finally(() => {
15753
+ this.backtestJobs--;
15754
+ });
15755
+ return { runId: run.id };
15756
+ } catch (error) {
15757
+ this.backtestJobs--;
15758
+ throw error;
15759
+ }
15316
15760
  }
15317
15761
  /**
15318
15762
  * Execute and grade one case. A run that fails (an output limit, a missing
@@ -15326,9 +15770,15 @@ var SkillEvolution = class {
15326
15770
  name: skillName,
15327
15771
  ...content === void 0 ? {} : { content }
15328
15772
  }), 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);
15773
+ signal.throwIfAborted();
15774
+ 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);
15775
+ const grade = gradeCase(entry, judged.structured);
15776
+ const raw = judged.structured;
15777
+ const checks = Array.isArray(raw?.checks) ? raw.checks : [];
15778
+ 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
15779
  return {
15331
15780
  caseId: entry.id,
15781
+ fullyGraded,
15332
15782
  title: entry.title,
15333
15783
  verdict: grade.verdict,
15334
15784
  score: grade.score,
@@ -15438,6 +15888,7 @@ var SkillEvolution = class {
15438
15888
  };
15439
15889
  }
15440
15890
  dispose() {
15891
+ this.automatic.dispose();
15441
15892
  this.disposed = true;
15442
15893
  for (const state of this.sessions.values()) if (state.timer !== void 0) clearTimeout(state.timer);
15443
15894
  this.sessions.clear();
@@ -15783,6 +16234,7 @@ async function skillAssistance(runtime, lifecycle, operation, input, signal) {
15783
16234
  if (guidance !== void 0 && guidance.length > 4e3) throw new Error("guidance is too long (max 4000 characters)");
15784
16235
  const draftRoute = modelRoute(input);
15785
16236
  return skills.draft(source, workspaceRoot, {
16237
+ ...input.backtest === true ? { backtest: true } : {},
15786
16238
  ...suggestionId === void 0 ? {} : { suggestionId },
15787
16239
  ...skill === void 0 ? {} : { skill },
15788
16240
  ...draftId === void 0 ? {} : { draftId },
@@ -19486,9 +19938,11 @@ function apply(rawContext, rawConfig = {}) {
19486
19938
  const skills = new SkillEvolution({
19487
19939
  settings: () => skillSettings.get(),
19488
19940
  source: (agent) => skillsSourceFor(runtime, agent),
19941
+ taskRoute: (workspace) => lifecycle.skillTaskRoute(workspace),
19489
19942
  runTask: (workspaceRoot, signal, operation, route) => lifecycle.runSkillTask(workspaceRoot, signal, operation, route),
19490
19943
  skillTask: (parent, label, prompt, schema, persona, signal, _route, maxTokens) => coordinator.skillTask(parent, label, prompt, schema, persona, signal, taskRoute(parent), maxTokens)
19491
19944
  });
19945
+ ctx.effect(() => hostSettings.onUpdated(() => skills.settingsChanged()), "dsh-mnemon: automatic skill adoption settings");
19492
19946
  lifecycle.useSkillEvolution(skills);
19493
19947
  ctx.effect(() => {
19494
19948
  const stop = autoBackup.start();