dsh-mnemon 0.5.41 → 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,
@@ -419,7 +422,7 @@ const schema = z.object({
419
422
  const legacySchema = z.object({ sources: z.dict(z.object({ enabled: z.boolean() })).default({}) });
420
423
  const ENTRY_ID$1 = /^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,299}$/u;
421
424
  const clone$2 = (value) => JSON.parse(JSON.stringify(value));
422
- function record$10(value) {
425
+ function record$11(value) {
423
426
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("Plugin configuration must be an object");
424
427
  const json = JSON.stringify(value);
425
428
  if (json.length > 65536) throw new Error("Plugin configuration exceeds 64 KiB");
@@ -433,13 +436,13 @@ function record$10(value) {
433
436
  }
434
437
  function preferences(value) {
435
438
  if (value.strategyTypeId !== void 0 && !/^[a-z][a-z0-9-]{0,127}$/u.test(value.strategyTypeId)) throw new Error("Invalid Strategy type id");
436
- const entries = record$10(value.entries ?? {});
439
+ const entries = record$11(value.entries ?? {});
437
440
  if (Object.keys(entries).length > 64) throw new Error("At most 64 memory plugin Entries can be configured");
438
441
  for (const [entryId, item] of Object.entries(entries)) {
439
442
  if (!ENTRY_ID$1.test(entryId)) throw new Error("Invalid memory plugin Entry id");
440
- const candidate = record$10(item);
443
+ const candidate = record$11(item);
441
444
  if (candidate.enabled !== void 0 && typeof candidate.enabled !== "boolean") throw new Error("Plugin enabled must be boolean");
442
- record$10(candidate.config);
445
+ record$11(candidate.config);
443
446
  }
444
447
  return clone$2(value);
445
448
  }
@@ -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
@@ -1003,7 +1012,7 @@ function createStorageRoot(config, workspaceRoot) {
1003
1012
  const MNEMON_MACHINE_DIRECTORY = "state";
1004
1013
  const MNEMON_MACHINE_FILE = "machine.json";
1005
1014
  const MNEMON_MACHINE_VERSION = 1;
1006
- function record$9(value) {
1015
+ function record$10(value) {
1007
1016
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1008
1017
  }
1009
1018
  /** The name a fresh identity carries: the host name, or a neutral fallback. */
@@ -1085,7 +1094,7 @@ var MnemonMachineStore = class {
1085
1094
  } catch {
1086
1095
  return;
1087
1096
  }
1088
- const parsed = record$9(value);
1097
+ const parsed = record$10(value);
1089
1098
  if (parsed?.version !== MNEMON_MACHINE_VERSION) return void 0;
1090
1099
  if (typeof parsed.id !== "string" || parsed.id.trim() === "") return void 0;
1091
1100
  if (typeof parsed.label !== "string" || parsed.label.trim() === "") return void 0;
@@ -2059,7 +2068,7 @@ const COMPONENT_ORDER = MNEMON_PACK_COMPONENTS;
2059
2068
  */
2060
2069
  const CONTENT_HASH = /^[a-f0-9]{64}$/u;
2061
2070
  const SQLITE_HEADER = Buffer.from("SQLite format 3\0", "binary");
2062
- function record$8(value) {
2071
+ function record$9(value) {
2063
2072
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
2064
2073
  }
2065
2074
  function utf8(bytes, label) {
@@ -2122,7 +2131,7 @@ function componentsForScope(scope) {
2122
2131
  }
2123
2132
  /** Exported so the sync channel can read the manifest a commit holds without unpacking the archive. */
2124
2133
  function parseManifest(value) {
2125
- const manifest = record$8(value);
2134
+ const manifest = record$9(value);
2126
2135
  if (manifest?.format !== "mnemonpack" || manifest.version !== 1) throw new Error("unsupported Mnemon Pack format or version");
2127
2136
  if (manifest.scope !== "full" && !COMPONENT_ORDER.includes(manifest.scope)) throw new Error("Mnemon Pack scope is invalid");
2128
2137
  if (typeof manifest.exportedAt !== "string") throw new Error("Mnemon Pack exportedAt is invalid");
@@ -2131,11 +2140,11 @@ function parseManifest(value) {
2131
2140
  if (components.length === 0 || new Set(components).size !== components.length || components.some((component) => !COMPONENT_ORDER.includes(component))) throw new Error("Mnemon Pack components are invalid");
2132
2141
  const expected = componentsForScope(manifest.scope);
2133
2142
  if (components.length !== expected.length || expected.some((component) => !components.includes(component))) throw new Error("Mnemon Pack scope does not match its components");
2134
- const source = record$8(manifest.source);
2143
+ const source = record$9(manifest.source);
2135
2144
  if (source?.plugin !== "dsh-mnemon" || typeof source.pluginVersion !== "string") throw new Error("Mnemon Pack source is invalid");
2136
2145
  if (!Array.isArray(manifest.summary)) throw new Error("Mnemon Pack summary is invalid");
2137
2146
  const summary = manifest.summary.map((entry) => {
2138
- const item = record$8(entry);
2147
+ const item = record$9(entry);
2139
2148
  const component = item?.component;
2140
2149
  if (!COMPONENT_ORDER.includes(component) || !Number.isSafeInteger(item?.files) || !Number.isSafeInteger(item?.bytes) || !Number.isSafeInteger(item?.items) || Number(item?.files) < 0 || Number(item?.bytes) < 0 || Number(item?.items) < 0) throw new Error("Mnemon Pack summary entry is invalid");
2141
2150
  return {
@@ -2161,7 +2170,7 @@ function parseManifest(value) {
2161
2170
  /** The sync channel reads its machine identity with this parser; no Pack carries one. */
2162
2171
  function parseMachineIdentity(value) {
2163
2172
  if (value === void 0 || value === null) return void 0;
2164
- const machine = record$8(value);
2173
+ const machine = record$9(value);
2165
2174
  if (typeof machine?.id !== "string" || machine.id.trim() === "" || typeof machine.label !== "string" || typeof machine.createdAt !== "string") throw new Error("Mnemon Pack machine identity is invalid");
2166
2175
  return {
2167
2176
  id: machine.id,
@@ -2171,7 +2180,7 @@ function parseMachineIdentity(value) {
2171
2180
  }
2172
2181
  function parseEntryOrigin(value) {
2173
2182
  if (value === void 0 || value === null) return void 0;
2174
- const origin = record$8(value);
2183
+ const origin = record$9(value);
2175
2184
  if (typeof origin?.machine !== "string" || typeof origin.label !== "string" || typeof origin.at !== "string") return void 0;
2176
2185
  return {
2177
2186
  machine: origin.machine,
@@ -2216,8 +2225,8 @@ function parseArchive(base64, runtimeLimits = RUNTIME_MEMORY_LIMITS) {
2216
2225
  if (path !== "manifest.json" && path !== "checksums.json" && !allowedPayloadPath(path)) throw new Error(`unsupported Mnemon Pack entry: ${path}`);
2217
2226
  }
2218
2227
  const manifest = parseManifest(json$1(files["manifest.json"], "manifest.json"));
2219
- const checksumValue = record$8(json$1(files["checksums.json"], "checksums.json"));
2220
- const checksumFiles = record$8(checksumValue?.files);
2228
+ const checksumValue = record$9(json$1(files["checksums.json"], "checksums.json"));
2229
+ const checksumFiles = record$9(checksumValue?.files);
2221
2230
  if (checksumValue?.algorithm !== "sha256" || checksumFiles === void 0) throw new Error("Mnemon Pack checksums are invalid");
2222
2231
  const payloadNames = names.filter((path) => path.startsWith("payload/")).sort();
2223
2232
  if (Object.keys(checksumFiles).length !== payloadNames.length) throw new Error("Mnemon Pack checksum inventory does not match the payload");
@@ -2240,16 +2249,17 @@ function parseArchive(base64, runtimeLimits = RUNTIME_MEMORY_LIMITS) {
2240
2249
  };
2241
2250
  }
2242
2251
  function parseRuntime(value, limits = RUNTIME_MEMORY_LIMITS) {
2243
- const source = record$8(value);
2252
+ const source = record$9(value);
2244
2253
  if (source?.version !== RUNTIME_MEMORY_VERSION || !Array.isArray(source.entries)) throw new Error("runtime memories.json is invalid");
2245
2254
  const entries = source.entries.map((raw) => {
2246
- const entry = record$8(raw);
2255
+ const entry = record$9(raw);
2247
2256
  if (typeof entry?.content !== "string" || entry.target !== "memory" && entry.target !== "user" || ![
2248
2257
  "critical",
2249
2258
  "normal",
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`);
@@ -2280,12 +2292,12 @@ function emptyTombstones() {
2280
2292
  };
2281
2293
  }
2282
2294
  function parseTombstones(value) {
2283
- const source = record$8(value);
2295
+ const source = record$9(value);
2284
2296
  if (source?.version !== 1 || !Array.isArray(source.tombstones)) throw new Error("runtime tombstones.json is invalid");
2285
2297
  return {
2286
2298
  version: 1,
2287
2299
  tombstones: source.tombstones.map((raw) => {
2288
- const tombstone = record$8(raw);
2300
+ const tombstone = record$9(raw);
2289
2301
  if (tombstone?.target !== "memory" && tombstone?.target !== "user") throw new Error("runtime tombstones.json contains an invalid entry");
2290
2302
  if (typeof tombstone.contentHash !== "string" || !CONTENT_HASH.test(tombstone.contentHash) || typeof tombstone.deletedAt !== "string") throw new Error("runtime tombstones.json contains an invalid entry");
2291
2303
  return {
@@ -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);
@@ -2700,11 +2713,11 @@ function emptyRuntimeIndex() {
2700
2713
  }
2701
2714
  /** A damaged index only costs forgotten deletions, so it degrades instead of failing an export. */
2702
2715
  function parseRuntimeIndex(value) {
2703
- const source = record$8(value);
2716
+ const source = record$9(value);
2704
2717
  if (source?.version !== 1 || !Array.isArray(source.entries)) return emptyRuntimeIndex();
2705
2718
  const entries = [];
2706
2719
  for (const raw of source.entries) {
2707
- const entry = record$8(raw);
2720
+ const entry = record$9(raw);
2708
2721
  if (entry?.target !== "memory" && entry?.target !== "user") continue;
2709
2722
  if (typeof entry.contentHash !== "string" || !/^[a-f0-9]{64}$/u.test(entry.contentHash)) continue;
2710
2723
  entries.push({
@@ -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);
@@ -3356,7 +3376,7 @@ const STATUSES = [
3356
3376
  "accepted",
3357
3377
  "rejected"
3358
3378
  ];
3359
- function record$7(value) {
3379
+ function record$8(value) {
3360
3380
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
3361
3381
  }
3362
3382
  function text$2(value, limit = MAX_REVIEW_TEXT) {
@@ -3368,7 +3388,7 @@ function strings$1(value, limit = 64) {
3368
3388
  function parseOpinions(value) {
3369
3389
  if (!Array.isArray(value)) return [];
3370
3390
  return value.flatMap((raw) => {
3371
- const opinion = record$7(raw);
3391
+ const opinion = record$8(raw);
3372
3392
  if (opinion === void 0 || typeof opinion.id !== "string" || typeof opinion.text !== "string") return [];
3373
3393
  return [{
3374
3394
  id: opinion.id,
@@ -3380,11 +3400,11 @@ function parseOpinions(value) {
3380
3400
  }
3381
3401
  /** A proposal that no longer parses is dropped rather than failing the whole ledger. */
3382
3402
  function parseEntry(value) {
3383
- const entry = record$7(value);
3403
+ const entry = record$8(value);
3384
3404
  if (entry === void 0 || typeof entry.id !== "string" || entry.id.trim() === "") return void 0;
3385
3405
  if (typeof entry.status !== "string" || !STATUSES.includes(entry.status)) return void 0;
3386
3406
  if (!Array.isArray(entry.operations)) return void 0;
3387
- const machine = record$7(entry.machine) ?? {};
3407
+ const machine = record$8(entry.machine) ?? {};
3388
3408
  const createdAt = typeof entry.createdAt === "string" ? entry.createdAt : (/* @__PURE__ */ new Date()).toISOString();
3389
3409
  return {
3390
3410
  id: entry.id,
@@ -3534,7 +3554,7 @@ var MnemonReviewLedger = class {
3534
3554
  version: 1,
3535
3555
  entries: []
3536
3556
  };
3537
- const file = record$7(JSON.parse(readFileSync(this.path(), "utf8")));
3557
+ const file = record$8(JSON.parse(readFileSync(this.path(), "utf8")));
3538
3558
  if (file?.version !== 1 || !Array.isArray(file.entries)) throw new Error("review ledger is invalid: " + this.path());
3539
3559
  return {
3540
3560
  version: 1,
@@ -3985,7 +4005,7 @@ const MNEMON_GITHUB_MAX_INTERVAL_MS = 6e4;
3985
4005
  const MNEMON_GITHUB_FLOW_LIMIT_MS = 36e5;
3986
4006
  const MNEMON_GITHUB_USER_AGENT = "dsh-mnemon-sync";
3987
4007
  const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
3988
- function record$6(value) {
4008
+ function record$7(value) {
3989
4009
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
3990
4010
  }
3991
4011
  function text$1(value) {
@@ -3993,7 +4013,7 @@ function text$1(value) {
3993
4013
  }
3994
4014
  /** GitHub answers a refused request with a `error` code or an API `message`. */
3995
4015
  function codeOf(body) {
3996
- const fields = record$6(body);
4016
+ const fields = record$7(body);
3997
4017
  return text$1(fields?.error) ?? text$1(fields?.message);
3998
4018
  }
3999
4019
  /** A positive count GitHub sends as a JSON number, or as a string in a form-encoded answer. */
@@ -4008,7 +4028,7 @@ const FIELD_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/u;
4008
4028
  * device code or a token. Enough to tell a proxy's HTML page from a reply that lost a field.
4009
4029
  */
4010
4030
  function shapeOf(answer) {
4011
- const keys = Object.keys(record$6(answer.body) ?? {});
4031
+ const keys = Object.keys(record$7(answer.body) ?? {});
4012
4032
  const names = keys.filter((key) => FIELD_NAME.test(key)).slice(0, 8);
4013
4033
  const listed = keys.length > names.length ? [...names, String(keys.length - names.length) + " other"] : names;
4014
4034
  return "HTTP " + String(answer.status) + ", content-type " + (answer.type ?? "none") + ", fields: " + (listed.length === 0 ? "none" : listed.join(", "));
@@ -4020,9 +4040,9 @@ function failure$3(action, status, body) {
4020
4040
  }
4021
4041
  /** Read one grant out of a stored record, refusing anything else. */
4022
4042
  function githubGrantFromRecord(record_) {
4023
- const fields = record$6(record_);
4043
+ const fields = record$7(record_);
4024
4044
  if (fields?.kind !== "grant") return void 0;
4025
- const payload = record$6(fields.payload);
4045
+ const payload = record$7(fields.payload);
4026
4046
  if (payload?.version !== 1) return void 0;
4027
4047
  const accessToken = text$1(payload.accessToken);
4028
4048
  if (accessToken === void 0) return void 0;
@@ -4036,13 +4056,13 @@ function githubGrantFromRecord(record_) {
4036
4056
  };
4037
4057
  }
4038
4058
  function repositoryOf(value) {
4039
- const fields = record$6(value);
4059
+ const fields = record$7(value);
4040
4060
  const fullName = text$1(fields?.full_name);
4041
4061
  const url = text$1(fields?.clone_url);
4042
4062
  const name = text$1(fields?.name);
4043
4063
  if (fullName === void 0 || url === void 0 || name === void 0) return void 0;
4044
- const owner = record$6(fields?.owner);
4045
- const permissions = record$6(fields?.permissions);
4064
+ const owner = record$7(fields?.owner);
4065
+ const permissions = record$7(fields?.permissions);
4046
4066
  return {
4047
4067
  name,
4048
4068
  fullName,
@@ -4155,7 +4175,7 @@ var MnemonGitHubAuth = class {
4155
4175
  throw new Error("the GitHub device code request could not reach GitHub: " + describeTransportError(error));
4156
4176
  }
4157
4177
  if (answer.status !== 200) throw failure$3("the GitHub device code request", answer.status, answer.body);
4158
- const fields = record$6(answer.body);
4178
+ const fields = record$7(answer.body);
4159
4179
  const deviceCode = text$1(fields?.device_code);
4160
4180
  const userCode = text$1(fields?.user_code);
4161
4181
  const verificationUri = text$1(fields?.verification_uri);
@@ -4204,7 +4224,7 @@ var MnemonGitHubAuth = class {
4204
4224
  message: describeTransportError(error)
4205
4225
  };
4206
4226
  }
4207
- const fields = record$6(answer.body);
4227
+ const fields = record$7(answer.body);
4208
4228
  const code = text$1(fields?.error);
4209
4229
  if (code === void 0) {
4210
4230
  const accessToken = text$1(fields?.access_token);
@@ -4275,7 +4295,7 @@ var MnemonGitHubAuth = class {
4275
4295
  ...signal === void 0 ? {} : { signal }
4276
4296
  });
4277
4297
  if (status !== 200) return void 0;
4278
- return text$1(record$6(body)?.login);
4298
+ return text$1(record$7(body)?.login);
4279
4299
  } catch {
4280
4300
  return;
4281
4301
  }
@@ -4401,7 +4421,7 @@ const SETTING_KEYS = [
4401
4421
  "autoBackupMinutes"
4402
4422
  ];
4403
4423
  const REPOSITORY_HINT = "sync repository must be an https:// or ssh:// URL, a git@host:path address, or an absolute local path";
4404
- function record$5(value) {
4424
+ function record$6(value) {
4405
4425
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
4406
4426
  }
4407
4427
  function known(key) {
@@ -4505,7 +4525,7 @@ var MnemonSyncSettingsStore = class {
4505
4525
  } catch {
4506
4526
  throw new Error("sync configuration is not valid JSON");
4507
4527
  }
4508
- const stored = record$5(parsed);
4528
+ const stored = record$6(parsed);
4509
4529
  if (stored === void 0) throw new Error("sync configuration is not a JSON object");
4510
4530
  for (const key of Object.keys(stored)) if (!known(key)) throw new Error("sync configuration has an unknown field: " + key);
4511
4531
  const settings = this.defaults();
@@ -4552,7 +4572,7 @@ var MnemonSyncSettingsStore = class {
4552
4572
  * turns the automatic backup off.
4553
4573
  */
4554
4574
  patch(patch) {
4555
- const fields = record$5(patch);
4575
+ const fields = record$6(patch);
4556
4576
  if (fields === void 0) throw new Error("sync configuration must be an object");
4557
4577
  for (const key of Object.keys(fields)) if (!known(key)) throw new Error("unknown sync setting: " + key);
4558
4578
  const next = { ...this.read() };
@@ -4788,7 +4808,7 @@ function remoteFailure(message) {
4788
4808
  function rejectedPush(output) {
4789
4809
  return /\[rejected\]|fetch first|non-fast-forward/iu.test(output);
4790
4810
  }
4791
- function record$4(value) {
4811
+ function record$5(value) {
4792
4812
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
4793
4813
  }
4794
4814
  function tail(output) {
@@ -4853,7 +4873,7 @@ const TIMESTAMP_FIELDS = /* @__PURE__ */ new Set(["exportedAt", "pushedAt"]);
4853
4873
  /** The same manifest without its timestamps describes the same payload. */
4854
4874
  function withoutTimestamps(value) {
4855
4875
  if (Array.isArray(value)) return value.map(withoutTimestamps);
4856
- const parsed = record$4(value);
4876
+ const parsed = record$5(value);
4857
4877
  if (parsed === void 0) return value;
4858
4878
  return Object.fromEntries(Object.entries(parsed).filter(([key]) => !TIMESTAMP_FIELDS.has(key)).map(([key, item]) => [key, withoutTimestamps(item)]));
4859
4879
  }
@@ -4866,7 +4886,7 @@ function manifestIdentity(bytes) {
4866
4886
  } catch {
4867
4887
  return;
4868
4888
  }
4869
- return record$4(value) === void 0 ? void 0 : JSON.stringify(withoutTimestamps(value));
4889
+ return record$5(value) === void 0 ? void 0 : JSON.stringify(withoutTimestamps(value));
4870
4890
  }
4871
4891
  /**
4872
4892
  * Whether the mirror already holds exactly this payload. Every push re-exports
@@ -5071,7 +5091,20 @@ function readFiles(root, limit = MAX_PAYLOAD_BYTES) {
5071
5091
  function unpack(archive) {
5072
5092
  let entries;
5073
5093
  try {
5074
- entries = unzipSync(archive);
5094
+ if (archive.byteLength > 268435456) throw new Error("compressed archive exceeds safety limit");
5095
+ let expandedBytes = 0;
5096
+ let fileCount = 0;
5097
+ entries = unzipSync(archive, { filter(info) {
5098
+ const path = info.name;
5099
+ const normalized = path.endsWith("/") ? path.slice(0, -1) : path;
5100
+ if (path.includes("\\") || path.startsWith("/") || /^[a-z]:/i.test(path) || path.includes("\0") || normalized.split("/").some((part) => !part || part === "." || part === "..")) throw new Error("unsafe archive path");
5101
+ if (path.endsWith("/")) return false;
5102
+ if (info.compression === 0 && info.size !== info.originalSize) throw new Error("invalid stored archive entry size");
5103
+ expandedBytes += info.originalSize;
5104
+ fileCount += 1;
5105
+ if (!Number.isSafeInteger(info.originalSize) || info.originalSize < 0 || info.originalSize > 134217728 || expandedBytes > 268435456 || fileCount > 4096) throw new Error("expanded archive exceeds safety limit");
5106
+ return true;
5107
+ } });
5075
5108
  } catch {
5076
5109
  throw new Error("the Mnemon payload is not a readable archive");
5077
5110
  }
@@ -5229,7 +5262,7 @@ var MnemonGitSync = class {
5229
5262
  }
5230
5263
  async configure(patch) {
5231
5264
  const previous = this.store.read();
5232
- const fields = record$4(patch);
5265
+ const fields = record$5(patch);
5233
5266
  const { machineLabel: name, ...settings } = fields ?? {};
5234
5267
  const naming = fields !== void 0 && "machineLabel" in fields;
5235
5268
  if (naming && name !== null && name !== "") machineLabel(name);
@@ -5437,7 +5470,7 @@ var MnemonGitSync = class {
5437
5470
  /** When memory last travelled through this channel, or nothing when it never did. */
5438
5471
  lastRun() {
5439
5472
  try {
5440
- const value = record$4(JSON.parse(readFileSync(this.lastRunPath(), "utf8")));
5473
+ const value = record$5(JSON.parse(readFileSync(this.lastRunPath(), "utf8")));
5441
5474
  if (value === void 0 || typeof value.at !== "string" || typeof value.commit !== "string") return void 0;
5442
5475
  if (value.kind !== "sync" && value.kind !== "merge") return void 0;
5443
5476
  return {
@@ -5475,13 +5508,13 @@ var MnemonGitSync = class {
5475
5508
  async agreedDocuments(settings, tip, signal) {
5476
5509
  let value;
5477
5510
  try {
5478
- value = record$4(JSON.parse(readFileSync(this.basePath(), "utf8")));
5511
+ value = record$5(JSON.parse(readFileSync(this.basePath(), "utf8")));
5479
5512
  } catch {
5480
5513
  value = void 0;
5481
5514
  }
5482
5515
  if (value === void 0) return this.lastRunDocuments(settings, tip, signal);
5483
5516
  if (value.version !== 1 || value.scope !== this.agreementScope(settings)) return void 0;
5484
- const documents = record$4(value.documents);
5517
+ const documents = record$5(value.documents);
5485
5518
  if (typeof value.commit !== "string" || !AGREED_COMMIT.test(value.commit) || documents === void 0) return void 0;
5486
5519
  if ((await this.git([
5487
5520
  "merge-base",
@@ -5928,9 +5961,9 @@ var MnemonGitSync = class {
5928
5961
  let machine;
5929
5962
  let pushedAt;
5930
5963
  try {
5931
- const raw = record$4(parseJson(bytes, "the backup manifest"));
5964
+ const raw = record$5(parseJson(bytes, "the backup manifest"));
5932
5965
  if (raw === void 0) return void 0;
5933
- const extension = record$4(raw[SYNC_EXTENSION]);
5966
+ const extension = record$5(raw[SYNC_EXTENSION]);
5934
5967
  pushedAt = typeof extension?.pushedAt === "string" ? extension.pushedAt : void 0;
5935
5968
  machine = extensionMachine(extension);
5936
5969
  manifest = parseManifest(raw);
@@ -6237,11 +6270,11 @@ var MnemonGitSync = class {
6237
6270
  if (manifestBytes === void 0 || checksumBytes === void 0) return void 0;
6238
6271
  if (manifestBytes.byteLength > MAX_MANIFEST_BYTES) throw new Error("the remote Mnemon manifest exceeds the safety limit");
6239
6272
  for (const path of Object.keys(files)) if (path !== MANIFEST && path !== CHECKSUMS && !path.startsWith("payload/")) throw new Error("the remote Mnemon payload holds an unexpected file: " + path);
6240
- const parsed = record$4(parseJson(manifestBytes, "the remote Mnemon manifest"));
6273
+ const parsed = record$5(parseJson(manifestBytes, "the remote Mnemon manifest"));
6241
6274
  if (parsed === void 0) throw new Error("the remote Mnemon manifest is not a JSON object");
6242
6275
  if (parsed.format !== "mnemonpack" || parsed.version !== 1) throw new Error("the remote Mnemon manifest is not a supported Mnemon Pack");
6243
- const checksums = record$4(parseJson(checksumBytes, "the remote Mnemon checksum inventory"));
6244
- const inventory = record$4(checksums?.files);
6276
+ const checksums = record$5(parseJson(checksumBytes, "the remote Mnemon checksum inventory"));
6277
+ const inventory = record$5(checksums?.files);
6245
6278
  if (checksums?.algorithm !== "sha256" || inventory === void 0) throw new Error("the remote Mnemon checksum inventory is invalid");
6246
6279
  for (const [path, bytes] of Object.entries(files)) {
6247
6280
  if (path === MANIFEST || path === CHECKSUMS) continue;
@@ -6250,7 +6283,7 @@ var MnemonGitSync = class {
6250
6283
  if (expected !== sha256Hex(bytes)) throw new Error("the remote Mnemon payload failed its checksum: " + path);
6251
6284
  }
6252
6285
  for (const path of Object.keys(inventory)) if (files[path] === void 0) throw new Error("the remote Mnemon payload is missing a checksummed file: " + path);
6253
- const extension = record$4(parsed[SYNC_EXTENSION]);
6286
+ const extension = record$5(parsed[SYNC_EXTENSION]);
6254
6287
  const pushedAt = typeof extension?.pushedAt === "string" ? extension.pushedAt : void 0;
6255
6288
  const machine = extensionMachine(extension);
6256
6289
  const archive = zipSync(files, {
@@ -6471,7 +6504,7 @@ function abandoned(path, now) {
6471
6504
  }
6472
6505
  if (age > STALE_LOCK_MS) return true;
6473
6506
  try {
6474
- const pid = record$4(JSON.parse(readFileSync(join(path, LOCK_OWNER), "utf8")))?.pid;
6507
+ const pid = record$5(JSON.parse(readFileSync(join(path, LOCK_OWNER), "utf8")))?.pid;
6475
6508
  return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && pid !== process.pid && !processAlive(pid);
6476
6509
  } catch {
6477
6510
  return age > OWNERLESS_LOCK_MS;
@@ -7992,6 +8025,7 @@ function registerCommands(commands, service, coordinator) {
7992
8025
  const GUIDANCE_SECTION_NAME = "mnemon:routing";
7993
8026
  const RUNTIME_MEMORY_CONTEXT_NAME = "mnemon:runtime-memory";
7994
8027
  const STRATEGY_SECTION_NAME = "mnemon:strategy";
8028
+ const SNAPSHOT_GUIDANCE = "Mnemon memory snapshots are complete projections in chronological order. The latest snapshot supersedes earlier snapshots as the current projected state; do not treat older versions as additional current facts or authority. Absence may reflect filtering or projection budgets and does not imply deletion from storage.";
7995
8029
  const ROUTING_GUIDANCE = "Use memory only when needed. Follow the current Mnemon View: use mnemon_view_route or mnemon_view_action only for offered ids. Installed tools do not imply an available Source. Never infer missing historical facts. An action offer is not authorization; a write exists only after its receipt.";
7996
8030
  /** A View is an own-plugin message, never part of the shared context snapshot. */
7997
8031
  function withoutMemoryViewContext(assembly) {
@@ -8002,12 +8036,20 @@ function withoutMemoryViewContext(assembly) {
8002
8036
  }
8003
8037
  /** Inject trusted Strategy instructions after interpolation; Source text stays quoted in the Wake. */
8004
8038
  function applyMemoryViewGuidance(assembly, view, routingEnabled = true) {
8005
- if (view === void 0) return withoutMemoryViewContext(assembly);
8039
+ if (view === void 0) return withoutMemoryViewContext({
8040
+ ...assembly,
8041
+ sections: assembly.sections.filter((section) => section.name !== "mnemon:snapshot-history")
8042
+ });
8006
8043
  const sections = assembly.sections.filter((section) => ![
8007
8044
  GUIDANCE_SECTION_NAME,
8008
8045
  STRATEGY_SECTION_NAME,
8009
- "mnemon:runtime-memory-protocol"
8046
+ "mnemon:runtime-memory-protocol",
8047
+ "mnemon:snapshot-history"
8010
8048
  ].includes(section.name));
8049
+ sections.push({
8050
+ name: "mnemon:snapshot-history",
8051
+ text: SNAPSHOT_GUIDANCE
8052
+ });
8011
8053
  if (view.guidance?.system) sections.push({
8012
8054
  name: STRATEGY_SECTION_NAME,
8013
8055
  text: view.guidance.system
@@ -8143,7 +8185,7 @@ function pendingOpinions(entries) {
8143
8185
  createdAt: opinion.createdAt
8144
8186
  }))).sort((left, right) => left.createdAt.localeCompare(right.createdAt));
8145
8187
  }
8146
- function record$3(value) {
8188
+ function record$4(value) {
8147
8189
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
8148
8190
  }
8149
8191
  function stringField(value) {
@@ -8292,7 +8334,7 @@ function body(value, index, field) {
8292
8334
  return text.slice(0, MAX_FIELD);
8293
8335
  }
8294
8336
  function parseOperation(value, index, evidence) {
8295
- const operation = record$3(value);
8337
+ const operation = record$4(value);
8296
8338
  if (operation === void 0) throw new Error(`reconcile operation ${index} must be an object`);
8297
8339
  const kind = stringField(operation.kind);
8298
8340
  if (kind === void 0 || !KINDS.includes(kind)) throw new Error(`reconcile operation ${index} has an unsupported kind: ${JSON.stringify(operation.kind)}`);
@@ -8365,7 +8407,7 @@ function emptyDocumentSnapshot(directory, at) {
8365
8407
  }
8366
8408
  /** The host, not the model, decides what a proposal may contain. */
8367
8409
  function parseReconcileResult(value, evidence) {
8368
- const result = record$3(value);
8410
+ const result = record$4(value);
8369
8411
  if (result === void 0) throw new Error("memory reconciliation returned an invalid structured result");
8370
8412
  const action = stringField(result.action);
8371
8413
  if (action === void 0 || ![
@@ -8694,13 +8736,13 @@ async function startGuardedChild(host, parent, start, guard, options) {
8694
8736
  }
8695
8737
  //#endregion
8696
8738
  //#region src/host/review-checkpoint.ts
8697
- function record$2(value) {
8739
+ function record$3(value) {
8698
8740
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
8699
8741
  }
8700
8742
  function textContent(content, toolResult = false) {
8701
8743
  if (!Array.isArray(content)) return "";
8702
8744
  return content.map((block) => {
8703
- const value = record$2(block);
8745
+ const value = record$3(block);
8704
8746
  if (value.type === "text" && typeof value.text === "string") return value.text;
8705
8747
  if (toolResult && value.type === "tool-result" && value.isError !== true) return textContent(value.content);
8706
8748
  return "";
@@ -8724,8 +8766,8 @@ function reviewCheckpoint(session, maxChars) {
8724
8766
  "assistant/message",
8725
8767
  "tool/result"
8726
8768
  ].includes(event.type)) continue;
8727
- const message = record$2(event.type === "user/message" ? event.data : event.data.message);
8728
- const source = record$2(message.source);
8769
+ const message = record$3(event.type === "user/message" ? event.data : event.data.message);
8770
+ const source = record$3(message.source);
8729
8771
  if (event.type === "user/message" && source.kind !== "user") continue;
8730
8772
  const text = textContent(message.content, event.type === "tool/result");
8731
8773
  if (!text) continue;
@@ -8739,8 +8781,14 @@ function reviewCheckpoint(session, maxChars) {
8739
8781
  //#endregion
8740
8782
  //#region src/host/plugin-message.ts
8741
8783
  const MNEMON_PLUGIN_SOURCE = "dsh-mnemon";
8784
+ /** Includes the published V3 wrapper and its official V4 migration. */
8785
+ function isMnemonMessageSource(source) {
8786
+ if (typeof source !== "object" || source === null) return false;
8787
+ const { kind, plugin } = source;
8788
+ return kind === "dsh-mnemon" || kind === `plugin:dsh-mnemon` || kind === "plugin" && plugin === "dsh-mnemon";
8789
+ }
8742
8790
  /** A user-role message this plugin adds to a step, marked with its own source. */
8743
- function createPluginMessage(text, form) {
8791
+ function createPluginMessage(text, form, snapshot) {
8744
8792
  return structuredClone({
8745
8793
  id: randomUUID(),
8746
8794
  role: "user",
@@ -8750,7 +8798,8 @@ function createPluginMessage(text, form) {
8750
8798
  }],
8751
8799
  source: {
8752
8800
  kind: MNEMON_PLUGIN_SOURCE,
8753
- form
8801
+ form,
8802
+ ...snapshot === void 0 ? {} : { snapshot }
8754
8803
  }
8755
8804
  });
8756
8805
  }
@@ -9643,7 +9692,11 @@ function runtimeRouteChunks(entries) {
9643
9692
  function compactedBudget(plan) {
9644
9693
  const pendingBytes = plan.pending === void 0 ? 0 : Buffer.byteLength(plan.pending.content, "utf8");
9645
9694
  const separatorBytes = plan.pending === void 0 || plan.entries.length === 0 ? 0 : Buffer.byteLength(RUNTIME_ENTRY_DELIMITER, "utf8");
9646
- 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));
9647
9700
  }
9648
9701
  function runtimeMigrationSources(revision, entries) {
9649
9702
  return entries.map((entry, offset) => ({
@@ -9692,10 +9745,11 @@ async function archiveRuntimeLocally(counters, context, request, plan, reason, s
9692
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`);
9693
9746
  context.assertWritable?.();
9694
9747
  const runId = `host-${randomUUID()}`;
9695
- const compacted = plan.entries.map(({ content, importance, branches }) => ({
9748
+ const compacted = plan.entries.map(({ content, importance, branches, pinned }) => ({
9696
9749
  content,
9697
9750
  importance,
9698
- ...branches === void 0 ? {} : { branches }
9751
+ ...branches === void 0 ? {} : { branches },
9752
+ ...pinned === void 0 ? {} : { pinned }
9699
9753
  }));
9700
9754
  const mutation = await context.runtime.mutateResult("compact-and-mutate", {
9701
9755
  revision: plan.revision,
@@ -9738,7 +9792,9 @@ async function runtimeLocked(counters, context, request, signal) {
9738
9792
  if (context.expectedRevision !== void 0 && context.expectedRevision !== plan.revision) throw new Error("Runtime source revision conflict before capacity maintenance");
9739
9793
  if (!plan.requiresMaintenance) return context.commit();
9740
9794
  if (plan.entries.length === 0) throw new Error("runtime memory capacity was exceeded without entries available for maintenance");
9795
+ compactedBudget(plan);
9741
9796
  if (request.target === "user") return compactUserAndCommit(context, request, plan, signal);
9797
+ const archiveEntries = plan.entries.filter((entry) => entry.pinned !== true);
9742
9798
  const archive = await context.memorySpaces();
9743
9799
  if (archive === void 0) return archiveRuntimeLocally(counters, context, request, plan, "no Memory Space takes automatic writes here", signal);
9744
9800
  const memoryService = archive.source;
@@ -9773,11 +9829,11 @@ async function runtimeLocked(counters, context, request, signal) {
9773
9829
  let provider = "host";
9774
9830
  let runId = `host-${randomUUID()}`;
9775
9831
  let summary = "Routed every entry to the only eligible Memory Space without model work.";
9776
- 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);
9777
9833
  else {
9778
9834
  const summaries = [];
9779
9835
  const fallbackBody = eligibleBodies.find((body) => body.mnemonDefault) ?? eligibleBodies[0];
9780
- const chunks = runtimeRouteChunks(plan.entries);
9836
+ const chunks = runtimeRouteChunks(archiveEntries);
9781
9837
  for (const [chunkIndex, chunk] of chunks.entries()) {
9782
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.
9783
9839
 
@@ -9822,11 +9878,12 @@ ${chunk.context}
9822
9878
  }
9823
9879
  summary = summaries.join(" ");
9824
9880
  }
9825
- if (routed.size !== plan.entries.length) throw new Error("runtime memory migration omitted committed archive sources");
9826
- 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 }) => ({
9827
9883
  content,
9828
9884
  importance,
9829
- ...branches === void 0 ? {} : { branches }
9885
+ ...branches === void 0 ? {} : { branches },
9886
+ ...pinned === void 0 ? {} : { pinned }
9830
9887
  }));
9831
9888
  if ((await runtimeMemory.read("maintenance-plan", {
9832
9889
  ...request,
@@ -9836,9 +9893,9 @@ ${chunk.context}
9836
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`);
9837
9894
  signal.throwIfAborted();
9838
9895
  context.assertWritable?.();
9839
- const sources = runtimeMigrationSources(plan.revision, plan.entries);
9896
+ const sources = runtimeMigrationSources(plan.revision, archiveEntries);
9840
9897
  const requests = sources.map((source) => {
9841
- const entry = plan.entries[source.index - 1];
9898
+ const entry = archiveEntries[source.index - 1];
9842
9899
  return {
9843
9900
  content: entry.content,
9844
9901
  category: "context",
@@ -9881,7 +9938,7 @@ ${chunk.context}
9881
9938
  archiveResults.set(source.index, result);
9882
9939
  }
9883
9940
  if (written.length !== batch.length) throw new Error("runtime archive batch did not return one receipt per source entry");
9884
- 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));
9885
9942
  }
9886
9943
  const lineage = [];
9887
9944
  const memoryBodyIds = /* @__PURE__ */ new Set();
@@ -9948,11 +10005,13 @@ ${chunk.context}
9948
10005
  async function compactUserAndCommit(context, request, plan, signal) {
9949
10006
  const runtimeMemory = context.runtime;
9950
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);
9951
10010
  const prompt = `Run local USER.md compaction now.
9952
10011
  Pending mutation (uncommitted; do not include in compaction):
9953
10012
  ${pendingMutationContext(plan)}
9954
10013
 
9955
- ${runtimeSnapshotContext("user", plan.entries)}`;
10014
+ ${runtimeSnapshotContext("user", mutable)}`;
9956
10015
  const { provider, runId, result } = await context.model("compaction", "Consolidate local user profile", prompt, USER_COMPACTION_SCHEMA, USER_COMPACTION_PERSONA);
9957
10016
  const value = object$6(result.structured);
9958
10017
  if (value.action !== "compacted") throw new Error(typeof value.summary === "string" && value.summary !== "" ? value.summary : "USER.md compaction failed");
@@ -9981,17 +10040,21 @@ ${runtimeSnapshotContext("user", plan.entries)}`;
9981
10040
  if (entry.sourceIndexes.length === 0) throw new Error("USER.md compaction returned an entry without a source");
9982
10041
  let requiredRank = 0;
9983
10042
  for (const index of entry.sourceIndexes) {
9984
- 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");
9985
10044
  seen.add(index);
9986
- requiredRank = Math.max(requiredRank, importanceRank[plan.entries[index - 1].importance]);
10045
+ requiredRank = Math.max(requiredRank, importanceRank[mutable[index - 1].importance]);
9987
10046
  }
9988
10047
  if (importanceRank[entry.importance] < requiredRank) throw new Error("USER.md compaction lowered source importance");
9989
10048
  }
9990
- if (seen.size !== plan.entries.length) throw new Error("USER.md compaction omitted committed entries");
9991
- 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 }) => ({
9992
10055
  content,
9993
10056
  importance
9994
- }));
10057
+ }))];
9995
10058
  const candidateBytes = Buffer.byteLength(candidates.map((entry) => entry.content.trim().replace(/\s+/gu, " ")).join(RUNTIME_ENTRY_DELIMITER), "utf8");
9996
10059
  if (candidateBytes > budget) throw new Error(`USER.md compaction did not fit the host budget (${candidateBytes} > ${budget} bytes)`);
9997
10060
  signal.throwIfAborted();
@@ -10328,9 +10391,16 @@ const ACTION_TOOLS = namedBindings("action");
10328
10391
  const NAMED_SOURCE_TYPES = [.../* @__PURE__ */ new Set([...Object.keys(ROUTE_TOOLS), ...Object.keys(ACTION_TOOLS)])];
10329
10392
  /** Use a short availability list only where the Host has the matching named binding.
10330
10393
  * Unbound external operations retain exact ids and schemas in the generic envelope. */
10331
- function modelMemoryWake(graph, turn) {
10394
+ function modelMemoryWake(graph, turn, includeRuntime = true) {
10332
10395
  const generation = graph.memoryComposition.generation(turn.view.runtimeGeneration);
10333
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
+ }
10334
10404
  return graph.composableTurns.memoryWake(turn.view.id, memoryWakeBindings(generation, turn.view));
10335
10405
  }
10336
10406
  function presentMemoryWake(generation, view) {
@@ -10456,17 +10526,23 @@ var AgentMemoryTurn = class {
10456
10526
  lastInspection;
10457
10527
  lastWorkspace;
10458
10528
  releaseDelegation;
10529
+ includeRuntime;
10459
10530
  constructor(agent, runtime, delegation, independent = false) {
10460
10531
  this.agent = agent;
10461
10532
  this.runtime = runtime;
10462
10533
  this.delegation = delegation;
10463
10534
  this.independent = independent;
10535
+ this.includeRuntime = delegation === void 0 || runtime.config.runtimeMemory.inheritToSubagents;
10464
10536
  if (delegation === void 0) return;
10465
10537
  this.releaseDelegation = runtime.executions.retain(agent, delegation);
10466
10538
  }
10467
10539
  get current() {
10468
10540
  return this.pinned;
10469
10541
  }
10542
+ modelWake() {
10543
+ const pinned = this.pinned;
10544
+ return pinned === void 0 ? void 0 : modelMemoryWake(pinned.graph, pinned.context, this.includeRuntime);
10545
+ }
10470
10546
  /** Why the current turn has no View, when memory could not be composed for it. */
10471
10547
  get unavailableReason() {
10472
10548
  return this.unavailable?.reason;
@@ -10557,7 +10633,7 @@ var AgentMemoryTurn = class {
10557
10633
  try {
10558
10634
  if (this.closed || this.generation !== generation) throw new Error("memory turn ended during View preparation");
10559
10635
  signal?.throwIfAborted();
10560
- 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);
10561
10637
  this.pinned = {
10562
10638
  turn,
10563
10639
  graph,
@@ -12313,6 +12389,192 @@ var TurnActivityProjection = class {
12313
12389
  }
12314
12390
  };
12315
12391
  //#endregion
12392
+ //#region src/host/memory-snapshot-state.ts
12393
+ const digest = (value) => createHash("sha256").update(canonicalMemoryJson(value)).digest("hex");
12394
+ const hash$1 = (value) => typeof value === "string" && /^[a-f0-9]{64}$/u.test(value);
12395
+ const record$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
12396
+ function readMemorySnapshotState(value) {
12397
+ if (!record$2(value) || value.version !== 1 || !hash$1(value.boundary) || !Array.isArray(value.entries) || !value.entries.every(hash$1)) return void 0;
12398
+ return {
12399
+ version: 1,
12400
+ boundary: value.boundary,
12401
+ entries: [...value.entries]
12402
+ };
12403
+ }
12404
+ /**
12405
+ * Only Runtime's Source-owned, complete-projection provenance permits additive
12406
+ * history. Unknown Sources and opaque grants are compared in full. Do not use
12407
+ * View ids or source revisions here: ordinary Runtime writes change both.
12408
+ */
12409
+ function memorySnapshotState(view) {
12410
+ if (view === void 0) return void 0;
12411
+ const entries = [];
12412
+ const projection = view.projection.map((fragment) => {
12413
+ const provenance = fragment.provenance;
12414
+ const snapshot = record$2(provenance) ? provenance.snapshot : void 0;
12415
+ if (!record$2(provenance) || provenance.sourceTypeId !== "runtime" || !record$2(snapshot) || snapshot.version !== 1 || !hash$1(snapshot.scope) || !Array.isArray(snapshot.entries) || !snapshot.entries.every(hash$1)) return fragment;
12416
+ entries.push(...snapshot.entries.map((entry) => digest([
12417
+ fragment.sourceInstanceKey,
12418
+ fragment.id,
12419
+ entry
12420
+ ])));
12421
+ const { text: _text, revision: _revision, ...identity } = fragment;
12422
+ return {
12423
+ ...identity,
12424
+ provenance: {
12425
+ ...provenance,
12426
+ snapshot: {
12427
+ version: 1,
12428
+ scope: snapshot.scope
12429
+ }
12430
+ }
12431
+ };
12432
+ });
12433
+ return {
12434
+ version: 1,
12435
+ boundary: digest({
12436
+ generation: view.runtimeGeneration,
12437
+ scope: view.scope,
12438
+ strategy: [
12439
+ view.strategyInstanceKey,
12440
+ view.strategyTypeId,
12441
+ view.strategyExtensions ?? []
12442
+ ],
12443
+ projection,
12444
+ routes: view.routes,
12445
+ actions: view.actionOffers,
12446
+ grants: view.readGrants,
12447
+ guidance: view.guidance ?? null,
12448
+ diagnostics: view.diagnostics ?? []
12449
+ }),
12450
+ entries: entries.sort()
12451
+ };
12452
+ }
12453
+ /** A missing old occurrence is a deletion/correction, even with duplicate facts. */
12454
+ function retainsSnapshotEntries(previous, current) {
12455
+ const counts = /* @__PURE__ */ new Map();
12456
+ for (const entry of current) counts.set(entry, (counts.get(entry) ?? 0) + 1);
12457
+ for (const entry of previous) {
12458
+ const count = counts.get(entry) ?? 0;
12459
+ if (count === 0) return false;
12460
+ counts.set(entry, count - 1);
12461
+ }
12462
+ return true;
12463
+ }
12464
+ //#endregion
12465
+ //#region src/host/memory-snapshot.ts
12466
+ const MAX_SNAPSHOTS = 4;
12467
+ const MAX_SNAPSHOT_BYTES = 65536;
12468
+ const LEGACY_RUNTIME_SNAPSHOT = /^MNEMON RUNTIME MEMORY SNAPSHOT\n[^\n]+(?:\n[^\n]+)*\n\n(?:USER\.md \(user profile\)|Contents of USER\.md \(user profile; entries: \d+; UTF-8 bytes: \d+\/\d+\))\n<runtime-memory-file name="USER.md">\n(?:(?!\n\nMNEMON (?:RUNTIME MEMORY SNAPSHOT|VIEW ))[\s\S])*?\n<\/runtime-memory-file>\n\n(?:MEMORY\.md \(working reference\)|Contents of MEMORY\.md \(working reference; entries: \d+; UTF-8 bytes: \d+\/\d+\))\n<runtime-memory-file name="MEMORY.md">\n(?:(?!\n\nMNEMON (?:RUNTIME MEMORY SNAPSHOT|VIEW ))[\s\S])*?\n<\/runtime-memory-file>(?:$|\n\n)/u;
12469
+ const TRUNCATED_RUNTIME_START = /^MNEMON RUNTIME MEMORY SNAPSHOT\n(?:Revision: [a-f0-9]{64}(?:\nGit branch: [^\r\n]+)?(?:\nMetadata lines are annotations; created\/updated are ages at projection in whole days \(future\/unknown for future\/invalid timestamps\)\. Current instructions win\. For old_text\/oldText, use entry content only\.)?\n\nContents of USER\.md \(user profile; entries: \d+; UTF-8 bytes: \d+\/\d+\)|Entry annotations default to importance=normal and ages=0d \(whole days at projection; future\/unknown for future\/invalid timestamps\)\. Current instructions win\. For old_text\/oldText, use entry content only\.(?:\nGit branch: [^\r\n]+)?\n\nUSER\.md \(user profile\))\n<runtime-memory-file name="USER.md">\n/u;
12470
+ const RUNTIME_WAKE_SEPARATOR = /\n\n(?:MNEMON RUNTIME MEMORY SNAPSHOT\n|MNEMON VIEW TOOLS \(available in this View\): |MNEMON VIEW ROUTES \(quoted routing data; use mnemon_view_route or mnemon_view_action by exact id\): |MNEMON VIEW AVAILABILITY \(quoted diagnostics; unavailable Sources are not evidence\): )/u;
12471
+ function isTruncatedRuntimeSnapshot(text) {
12472
+ const start = TRUNCATED_RUNTIME_START.exec(text);
12473
+ if (start === null) return false;
12474
+ const body = text.slice(start[0].length);
12475
+ const end = body.search(RUNTIME_WAKE_SEPARATOR);
12476
+ const first = end < 0 ? body : body.slice(0, end);
12477
+ if (!first.endsWith("…")) return false;
12478
+ const prefix = first.slice(0, -1);
12479
+ const userEnd = prefix.indexOf("\n</runtime-memory-file>");
12480
+ if (userEnd < 0) return true;
12481
+ const heading = start[0].includes("\nContents of USER.md ") ? [
12482
+ "\n\nContents of MEMORY.md (working reference; entries: ",
12483
+ /^\d+/u,
12484
+ "; UTF-8 bytes: ",
12485
+ /^\d+/u,
12486
+ "/",
12487
+ /^\d+/u,
12488
+ ")"
12489
+ ] : ["\n\nMEMORY.md (working reference)"];
12490
+ heading.push("\n<runtime-memory-file name=\"MEMORY.md\">\n");
12491
+ let rest = prefix.slice(userEnd + 23);
12492
+ for (const part of heading) {
12493
+ if (rest === "") return true;
12494
+ if (typeof part === "string") {
12495
+ if (part.startsWith(rest)) return true;
12496
+ if (!rest.startsWith(part)) return false;
12497
+ rest = rest.slice(part.length);
12498
+ } else {
12499
+ const match = part.exec(rest);
12500
+ if (match === null) return false;
12501
+ rest = rest.slice(match[0].length);
12502
+ }
12503
+ }
12504
+ return true;
12505
+ }
12506
+ /** Read only this producer's snapshot formats; ordinary recalls retain history. */
12507
+ function snapshotText(event) {
12508
+ if (event?.type !== "user/message" || event.data.role !== "user") return void 0;
12509
+ const source = event.data.source;
12510
+ if (!isMnemonMessageSource(source)) return void 0;
12511
+ const content = event.data.content;
12512
+ if (!Array.isArray(content) || content.length !== 1) return void 0;
12513
+ const block = content[0];
12514
+ if (block?.type !== "text" || typeof block.text !== "string") return void 0;
12515
+ if (source?.form === "memory-snapshot") return block.text;
12516
+ if (source?.form !== "recall" || !(LEGACY_RUNTIME_SNAPSHOT.test(block.text) || isTruncatedRuntimeSnapshot(block.text))) return void 0;
12517
+ return block.text;
12518
+ }
12519
+ /**
12520
+ * Prepare a snapshot and its retirements without writing before step/start.
12521
+ * Commit after step/start publication, before the Host builds its request.
12522
+ * Empty developer nodes are a public Session representation that derives no model
12523
+ * message. They contain no instructions, Source text, or tool changes. Each
12524
+ * replacement shadows one owned node and cites it; the append-only audit keeps
12525
+ * the original full message, without touching interleaved conversation.
12526
+ */
12527
+ function prepareMemorySnapshot(session, text, state) {
12528
+ const current = text?.trim() === "" ? void 0 : text;
12529
+ const snapshots = session.surface.nodes.flatMap((seq) => {
12530
+ const event = session.eventAt(seq);
12531
+ const text = snapshotText(event);
12532
+ const source = event?.data.source;
12533
+ return text === void 0 ? [] : [{
12534
+ seq,
12535
+ text,
12536
+ state: source?.form === "memory-snapshot" ? readMemorySnapshotState(source.snapshot) : void 0
12537
+ }];
12538
+ });
12539
+ const latest = snapshots.at(-1);
12540
+ const compatible = state !== void 0 && snapshots.every((snapshot) => snapshot.state?.boundary === state.boundary && retainsSnapshotEntries(snapshot.state.entries, state.entries));
12541
+ const unchanged = current !== void 0 && latest?.text === current;
12542
+ const count = snapshots.length + (unchanged ? 0 : 1);
12543
+ const bytes = snapshots.reduce((total, snapshot) => total + Buffer.byteLength(snapshot.text, "utf8"), 0) + (unchanged || current === void 0 ? 0 : Buffer.byteLength(current, "utf8"));
12544
+ const retained = current === void 0 ? [] : compatible && count <= MAX_SNAPSHOTS && (count <= 1 || bytes <= MAX_SNAPSHOT_BYTES) ? snapshots : state === void 0 && unchanged ? [latest] : [];
12545
+ const stale = snapshots.filter((snapshot) => !retained.includes(snapshot));
12546
+ return {
12547
+ message: current === void 0 || unchanged && retained.includes(latest) ? void 0 : createPluginMessage(current, "memory-snapshot", state),
12548
+ retire(turn, step) {
12549
+ const visible = new Set(session.surface.nodes);
12550
+ for (const { seq } of stale) {
12551
+ if (!visible.has(seq)) continue;
12552
+ if (session.append === void 0) throw new Error("DSH Session append is required to retire superseded memory snapshots");
12553
+ session.append("developer/message", {
12554
+ turn,
12555
+ step,
12556
+ message: {
12557
+ id: randomUUID(),
12558
+ role: "developer",
12559
+ content: [],
12560
+ source: {
12561
+ kind: MNEMON_PLUGIN_SOURCE,
12562
+ form: "memory-snapshot-retired"
12563
+ }
12564
+ }
12565
+ }, {
12566
+ surfaceOp: {
12567
+ op: "replace",
12568
+ startSeq: seq,
12569
+ endSeq: seq
12570
+ },
12571
+ sourceEventSeqs: [seq]
12572
+ });
12573
+ }
12574
+ }
12575
+ };
12576
+ }
12577
+ //#endregion
12316
12578
  //#region src/host/idle-review-watermark.ts
12317
12579
  const MAX_STATE_BYTES = 16384;
12318
12580
  const MAX_TIMESTAMP = 864e13;
@@ -12477,11 +12739,6 @@ function llmService(value) {
12477
12739
  if (typeof value !== "object" || value === null || !("listProviders" in value) || typeof value.listProviders !== "function" || !("listModels" in value) || typeof value.listModels !== "function") return void 0;
12478
12740
  return value;
12479
12741
  }
12480
- function isMnemonMessageSource(source) {
12481
- if (typeof source !== "object" || source === null) return false;
12482
- const { kind, plugin } = source;
12483
- return kind === "dsh-mnemon" || kind === `plugin:dsh-mnemon` || kind === "plugin" && plugin === "dsh-mnemon";
12484
- }
12485
12742
  /**
12486
12743
  * Whether an event is a durable user message this plugin produced.
12487
12744
  */
@@ -12561,16 +12818,9 @@ var MnemonAgentLifecycle = class {
12561
12818
  primePending = true;
12562
12819
  startSource;
12563
12820
  guidedTurns = /* @__PURE__ */ new Set();
12821
+ pendingSnapshot;
12564
12822
  memoryActivity = new TurnActivityProjection();
12565
12823
  turnActivity = /* @__PURE__ */ new Map();
12566
- /**
12567
- * Text of the runtime memory snapshot most recently injected as this
12568
- * plugin's own message. `.context()` used to get supersede-on-change for free
12569
- * from the host's runtime-context projection; carrying the snapshot as an own
12570
- * message means re-emitting on change is this plugin's responsibility, and
12571
- * the rendered text (which carries the revision digest) is the key.
12572
- */
12573
- injectedMemoryText;
12574
12824
  idleReviewTimer;
12575
12825
  reviewController;
12576
12826
  reviewRunning = false;
@@ -12616,6 +12866,10 @@ var MnemonAgentLifecycle = class {
12616
12866
  ];
12617
12867
  return () => {
12618
12868
  this.disposed = true;
12869
+ const pending = this.pendingSnapshot;
12870
+ this.pendingSnapshot = void 0;
12871
+ const started = pending === void 0 ? void 0 : this.agent.session.snapshotEvents().findLast((event) => event.type === "step/start");
12872
+ if (pending !== void 0 && started !== void 0 && eventTurn(started) === pending.turn && started.data.step === pending.step) this.queueSnapshotRetirement(pending);
12619
12873
  this.releaseView();
12620
12874
  this.cancelIdleReview(true);
12621
12875
  for (const dispose of disposers.reverse()) dispose();
@@ -12685,24 +12939,8 @@ var MnemonAgentLifecycle = class {
12685
12939
  for (const seq of this.agent.session.surface.nodes) if (isOwnUserMessageEvent(this.agent.session.eventAt(seq))) return true;
12686
12940
  return false;
12687
12941
  }
12688
- /**
12689
- * The runtime memory snapshot, as this plugin's own message, when it changed.
12690
- *
12691
- * `.context()` used to carry this inside the host's shared runtime-context
12692
- * projection. Carrying it here instead attributes it to dsh-mnemon and keeps
12693
- * a memory write from re-emitting other contributors' sections; supersede
12694
- * stays intact because the block is still a complete state replacing its
12695
- * predecessor, keyed on the rendered text's revision digest.
12696
- */
12697
- memorySnapshotMessage() {
12698
- const wake = this.memoryWake();
12699
- if (wake === void 0) return void 0;
12700
- const text = wake.text;
12701
- if (text.trim() === "" || text === this.injectedMemoryText) return void 0;
12702
- this.injectedMemoryText = text;
12703
- return createPluginMessage(text, "recall");
12704
- }
12705
12942
  async preStep(payload, next) {
12943
+ this.pendingSnapshot = void 0;
12706
12944
  if (payload.step === 1) this.cancelIdleReview(true);
12707
12945
  const decision = await next();
12708
12946
  if (decision.kind === "reject" || payload.signal.aborted) {
@@ -12710,12 +12948,19 @@ var MnemonAgentLifecycle = class {
12710
12948
  return decision;
12711
12949
  }
12712
12950
  if (!this.disposed && openAgentTurn(this.agent) === payload.turn) await this.memoryTurn?.begin(payload.turn, payload.signal);
12951
+ const update = prepareMemorySnapshot(this.agent.session, this.config.lifecycleEnabled ? this.memoryWake()?.text : void 0, this.config.lifecycleEnabled ? memorySnapshotState(this.memoryTurn?.current?.context.view) : void 0);
12952
+ this.pendingSnapshot = {
12953
+ turn: payload.turn,
12954
+ step: payload.step,
12955
+ signal: payload.signal,
12956
+ update
12957
+ };
12713
12958
  if (!this.config.lifecycleEnabled) return decision;
12714
12959
  if (this.config.writeEnabled && this.config.writebackMode === "guided") this.recordTurnMessages(payload.turn, decision.messages);
12715
12960
  if (payload.step !== 1) return decision;
12716
12961
  if (decision.messages.some((message) => isMnemonMessageSource(message.source))) return decision;
12717
12962
  if (decision.messages.length === 0) return decision;
12718
- const snapshot = this.memorySnapshotMessage();
12963
+ const snapshot = update.message;
12719
12964
  const withSnapshot = (messages) => snapshot === void 0 ? messages : [...messages, snapshot];
12720
12965
  if (this.primePending) {
12721
12966
  this.primePending = false;
@@ -12784,12 +13029,40 @@ var MnemonAgentLifecycle = class {
12784
13029
  if (admitted) this.scheduleIdleReview(payload.turn);
12785
13030
  }
12786
13031
  sessionEvent(session, event) {
12787
- if (session !== this.agent.session || event.type !== "turn/end") return;
13032
+ if (session !== this.agent.session) return;
12788
13033
  const turn = eventTurn(event);
13034
+ if (event.type === "step/start") {
13035
+ const pending = this.pendingSnapshot;
13036
+ if (pending === void 0 || pending.turn !== turn || pending.step !== event.data.step) return;
13037
+ this.pendingSnapshot = void 0;
13038
+ this.queueSnapshotRetirement(pending);
13039
+ return;
13040
+ }
13041
+ if (event.type !== "turn/end") return;
13042
+ if (this.pendingSnapshot?.turn === turn) this.pendingSnapshot = void 0;
12789
13043
  const current = this.memoryTurn?.current;
12790
13044
  if ((turn === void 0 || current?.turn !== turn ? void 0 : current) === void 0) return;
12791
13045
  this.releaseView(turn);
12792
13046
  }
13047
+ queueSnapshotRetirement(pending) {
13048
+ queueMicrotask(() => {
13049
+ if (pending.signal.aborted || openAgentTurn(this.agent) !== pending.turn) return;
13050
+ try {
13051
+ pending.update.retire(pending.turn, pending.step);
13052
+ } catch {
13053
+ const reason = "Memory snapshot checkpoint failed; model request cancelled.";
13054
+ this.counters.failures += 1;
13055
+ this.lastPhase = "error";
13056
+ this.lastAt = (/* @__PURE__ */ new Date()).toISOString();
13057
+ this.lastError = reason;
13058
+ this.agent.cancel({
13059
+ kind: "hook",
13060
+ reason
13061
+ }, { keepInbox: true });
13062
+ console.warn("[dsh-mnemon] Memory snapshot checkpoint failed; model request cancelled.");
13063
+ }
13064
+ });
13065
+ }
12793
13066
  scheduleIdleReview(turn) {
12794
13067
  this.cancelIdleReview(true);
12795
13068
  if (!this.restoreWatermark() || !this.idleReviewAllowed()) return;
@@ -13645,6 +13918,16 @@ var MnemonLifecycle = class {
13645
13918
  ...typeof effort === "string" && effort !== "" ? { reasoningEffort: effort } : {}
13646
13919
  };
13647
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
+ }
13648
13931
  taskAgentModelOptions(fallbackSessionId, workspaceRoot) {
13649
13932
  return this.taskAgentModelRoute(fallbackSessionId, workspaceRoot)?.options;
13650
13933
  }
@@ -13732,12 +14015,11 @@ var MnemonLifecycle = class {
13732
14015
  return decision;
13733
14016
  }
13734
14017
  if (openAgentTurn(agent) !== payload.turn) return decision;
13735
- const recovering = memory.current?.turn !== payload.turn;
13736
14018
  await memory.begin(payload.turn, payload.signal);
13737
14019
  const pinned = memory.current;
13738
- if (recovering && payload.step === 1 && this.config.lifecycleEnabled && pinned !== void 0) {
13739
- const wake = modelMemoryWake(pinned.graph, pinned.context);
13740
- 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 {
13741
14023
  kind: "enter",
13742
14024
  messages: [...decision.messages, createPluginMessage(wake.text, "recall")]
13743
14025
  };
@@ -13757,6 +14039,219 @@ var MnemonLifecycle = class {
13757
14039
  }
13758
14040
  };
13759
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
13760
14255
  //#region src/host/skill-review.ts
13761
14256
  const SIGNALS = [
13762
14257
  "correction",
@@ -14589,7 +15084,7 @@ function retryingSource(source, attempts = 4) {
14589
15084
  for (let attempt = 1;; attempt++) try {
14590
15085
  return await source.mutate(operation, input, signal);
14591
15086
  } catch (error) {
14592
- 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;
14593
15088
  }
14594
15089
  }
14595
15090
  };
@@ -14597,6 +15092,27 @@ function retryingSource(source, attempts = 4) {
14597
15092
  function message(error) {
14598
15093
  return redact(error instanceof Error ? error.message : String(error));
14599
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
+ }
14600
15116
  /** Resolve only roots supplied by this Source's scoped catalog, never the task's cwd. */
14601
15117
  function newSkillRoot(settings, source, catalog, explicit) {
14602
15118
  let root = explicit;
@@ -14637,8 +15153,22 @@ var SkillEvolution = class {
14637
15153
  sessions = /* @__PURE__ */ new Map();
14638
15154
  jobs = /* @__PURE__ */ new Set();
14639
15155
  disposed = false;
15156
+ activeBacktests = 0;
15157
+ backtestJobs = 0;
15158
+ backtestWaiters = /* @__PURE__ */ new Set();
15159
+ automatic;
14640
15160
  constructor(host) {
14641
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();
14642
15172
  }
14643
15173
  source(agent) {
14644
15174
  const source = this.host.source(agent);
@@ -14872,6 +15402,7 @@ var SkillEvolution = class {
14872
15402
  });
14873
15403
  state.failures = 0;
14874
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);
14875
15406
  return result;
14876
15407
  } catch (error) {
14877
15408
  state.failures += 1;
@@ -14902,10 +15433,17 @@ var SkillEvolution = class {
14902
15433
  delete state.reviewing;
14903
15434
  }
14904
15435
  }
14905
- background(name, work) {
15436
+ background(name, work, deadlineMs) {
14906
15437
  const controller = new AbortController();
14907
15438
  this.jobs.add(controller);
14908
- 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
+ });
14909
15447
  }
14910
15448
  /**
14911
15449
  * Write a draft for a suggestion (or for a skill directly) in the
@@ -14915,12 +15453,15 @@ var SkillEvolution = class {
14915
15453
  const source = retryingSource(port);
14916
15454
  const pack = await source.read("evidence-pack", {
14917
15455
  ...input.suggestionId === void 0 ? {} : { suggestionId: input.suggestionId },
15456
+ ...input.draftId === void 0 ? {} : { draftId: input.draftId },
14918
15457
  ...input.skill === void 0 ? {} : { skill: input.skill }
14919
15458
  });
14920
- const previous = pack.suggestion?.draftId === void 0 ? (await source.read("drafts", {
15459
+ const previousId = input.draftId ?? pack.suggestion?.draftId;
15460
+ const previous = previousId === void 0 ? (await source.read("drafts", {
14921
15461
  skill: pack.skillName,
15462
+ ...pack.skill === void 0 ? {} : { path: pack.skill.path },
14922
15463
  ...input.suggestionId === void 0 ? {} : { suggestionId: input.suggestionId }
14923
- })).find((draft) => draft.suggestionId === input.suggestionId) : (await source.read("draft", { id: pack.suggestion.draftId })).draft;
15464
+ })).find((draft) => draft.suggestionId === input.suggestionId) : (await source.read("draft", { id: previousId })).draft;
14924
15465
  const targetRoot = pack.action === "update" ? pack.skill.root : newSkillRoot(this.host.settings(), source, await source.read("catalog"), input.targetRoot ?? previous?.targetRoot ?? pack.suggestion?.targetRoot);
14925
15466
  const guidance = [...previous?.guidance ?? [], ...input.guidance?.trim() ? [input.guidance.trim()] : []].slice(-8);
14926
15467
  if (input.suggestionId !== void 0) await source.mutate("suggestion-status", {
@@ -14928,26 +15469,83 @@ var SkillEvolution = class {
14928
15469
  status: "drafting"
14929
15470
  });
14930
15471
  this.background("skill draft", async (signal) => {
15472
+ let recorded = false;
14931
15473
  try {
14932
- await this.writeDraft(source, workspace, input, {
15474
+ await abortable(signal, () => this.writeDraft(source, workspace, input, {
14933
15475
  pack,
14934
15476
  targetRoot,
14935
- guidance
14936
- }, signal);
15477
+ guidance,
15478
+ onRecorded: () => {
15479
+ recorded = true;
15480
+ }
15481
+ }, signal));
14937
15482
  } catch (error) {
14938
- if (input.suggestionId !== void 0) await source.mutate("suggestion-status", {
14939
- id: input.suggestionId,
14940
- status: "pending",
14941
- error: message(error)
14942
- }).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
+ }
14943
15490
  throw error;
14944
15491
  }
14945
- });
15492
+ }, DRAFT_DEADLINE_MS);
14946
15493
  return {
14947
15494
  started: true,
14948
15495
  ...input.suggestionId === void 0 ? {} : { suggestionId: input.suggestionId }
14949
15496
  };
14950
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
+ }
14951
15549
  async writeDraft(source, workspace, input, prepared, signal) {
14952
15550
  const settings = this.host.settings();
14953
15551
  const { pack, targetRoot, guidance } = prepared;
@@ -14978,20 +15576,25 @@ var SkillEvolution = class {
14978
15576
  signal.throwIfAborted();
14979
15577
  draft = await source.mutate("draft-record", {
14980
15578
  ...input.suggestionId === void 0 ? {} : { suggestionId: input.suggestionId },
15579
+ ...input.draftId === void 0 ? {} : { draftId: input.draftId },
14981
15580
  skillName: pack.skillName,
14982
15581
  content: parsed.content,
14983
15582
  notes: parsed.notes,
14984
15583
  changes: parsed.changes,
14985
15584
  guidance,
15585
+ expectedBaseHash: pack.skill?.hash ?? null,
15586
+ ...prepared.expectedDraft === void 0 ? {} : { expectedDraft: prepared.expectedDraft },
14986
15587
  ...pack.action === "create" ? { targetRoot } : {}
14987
15588
  }, signal);
15589
+ prepared.onRecorded?.();
14988
15590
  } catch (error) {
14989
15591
  signal.throwIfAborted();
14990
- if (attempt > 0) throw error;
15592
+ if (attempt > 0 || /changed while this draft/.test(message(error))) throw error;
14991
15593
  prompt += `\n\nYour previous SKILL.md was rejected: ${message(error)}. Return a corrected complete file.`;
14992
15594
  }
14993
15595
  }
14994
- if (settings.autoBacktest && pack.cases.length > 0) {
15596
+ signal.throwIfAborted();
15597
+ if (!prepared.deferBacktests && (settings.autoBacktest || input.backtest) && pack.cases.length > 0) {
14995
15598
  await this.backtest(source, workspace, {
14996
15599
  skillName: pack.skillName,
14997
15600
  target: {
@@ -15007,7 +15610,10 @@ var SkillEvolution = class {
15007
15610
  else {
15008
15611
  const baseline = await this.backtest(source, workspace, {
15009
15612
  skillName: pack.skillName,
15010
- target: { kind: pack.action === "update" ? "current" : "none" },
15613
+ target: {
15614
+ kind: pack.action === "update" ? "current" : "none",
15615
+ ...pack.action === "update" ? { draftId: draft.id } : {}
15616
+ },
15011
15617
  ...input.route === void 0 ? {} : { route: input.route }
15012
15618
  });
15013
15619
  await source.mutate("run-link", {
@@ -15019,67 +15625,138 @@ var SkillEvolution = class {
15019
15625
  return draft;
15020
15626
  }
15021
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
+ }
15022
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++;
15023
15659
  const source = retryingSource(port);
15024
- const settings = this.host.settings();
15025
- 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);
15026
- if (cases.length === 0) throw new Error(`${input.skillName} has no enabled eval case to run`);
15027
- if (cases.some((entry) => entry.requiresVision !== void 0 && typeof entry.requiresVision !== "boolean")) throw new Error("requiresVision must be a boolean");
15028
- const resolved = await source.read("target-content", {
15029
- skillName: input.skillName,
15030
- target: input.target
15031
- });
15032
- const run = await source.mutate("run-start", {
15033
- skillName: input.skillName,
15034
- target: resolved.target,
15035
- caseIds: cases.map((entry) => entry.id)
15036
- });
15037
- this.background("skill backtest", async (signal) => {
15038
- try {
15039
- for (const entry of cases.filter((entry) => entry.requiresVision === true)) {
15040
- signal.throwIfAborted();
15041
- await source.mutate("run-progress", {
15042
- id: run.id,
15043
- result: {
15044
- caseId: entry.id,
15045
- title: entry.title,
15046
- verdict: "error",
15047
- score: 0,
15048
- unmeasuredReason: "requires-vision",
15049
- checks: [],
15050
- steps: [],
15051
- answer: "",
15052
- error: "Not measured: this case requires image input, which text-only backtests cannot receive."
15053
- }
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;
15054
15692
  });
15055
- }
15056
- const queue = cases.filter((entry) => entry.requiresVision !== true);
15057
- if (queue.length > 0) await this.host.runTask(workspace, signal, async (task) => {
15058
- const worker = async () => {
15059
- 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));
15060
15731
  signal.throwIfAborted();
15061
- await source.mutate("run-progress", {
15732
+ await source.mutate("run-finish", {
15062
15733
  id: run.id,
15063
- result: await this.runCase(task, entry, input.skillName, resolved.content, signal)
15064
- });
15734
+ status: "completed"
15735
+ }, signal);
15736
+ } finally {
15737
+ release();
15065
15738
  }
15066
- };
15067
- await Promise.all(Array.from({ length: Math.min(BACKTEST_CONCURRENCY, queue.length) }, worker));
15068
- }, input.route);
15069
- await source.mutate("run-finish", {
15070
- id: run.id,
15071
- status: "completed"
15072
- });
15073
- } catch (error) {
15074
- await source.mutate("run-finish", {
15075
- id: run.id,
15076
- status: "failed",
15077
- error: message(error)
15078
- }).catch(() => void 0);
15079
- throw error;
15080
- }
15081
- });
15082
- 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
+ }
15083
15760
  }
15084
15761
  /**
15085
15762
  * Execute and grade one case. A run that fails (an output limit, a missing
@@ -15093,9 +15770,15 @@ var SkillEvolution = class {
15093
15770
  name: skillName,
15094
15771
  ...content === void 0 ? {} : { content }
15095
15772
  }), SKILL_EXECUTOR_SCHEMA, SKILL_EXECUTOR_PERSONA, signal)).structured);
15096
- 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);
15097
15779
  return {
15098
15780
  caseId: entry.id,
15781
+ fullyGraded,
15099
15782
  title: entry.title,
15100
15783
  verdict: grade.verdict,
15101
15784
  score: grade.score,
@@ -15205,6 +15888,7 @@ var SkillEvolution = class {
15205
15888
  };
15206
15889
  }
15207
15890
  dispose() {
15891
+ this.automatic.dispose();
15208
15892
  this.disposed = true;
15209
15893
  for (const state of this.sessions.values()) if (state.timer !== void 0) clearTimeout(state.timer);
15210
15894
  this.sessions.clear();
@@ -15543,14 +16227,17 @@ async function skillAssistance(runtime, lifecycle, operation, input, signal) {
15543
16227
  "user-agents"
15544
16228
  ].includes(root)) throw new Error("unknown skill root: " + root);
15545
16229
  const suggestionId = optionalString(input.suggestionId, "suggestionId");
16230
+ const draftId = optionalString(input.draftId, "draftId");
15546
16231
  const skill = optionalString(input.skill, "skill");
15547
- if (suggestionId === void 0 && skill === void 0) throw new Error("a draft needs a suggestion or a skill");
16232
+ if (suggestionId === void 0 && draftId === void 0 && skill === void 0) throw new Error("a draft needs a suggestion, a draft, or a skill");
15548
16233
  const guidance = optionalString(input.guidance, "guidance");
15549
16234
  if (guidance !== void 0 && guidance.length > 4e3) throw new Error("guidance is too long (max 4000 characters)");
15550
16235
  const draftRoute = modelRoute(input);
15551
16236
  return skills.draft(source, workspaceRoot, {
16237
+ ...input.backtest === true ? { backtest: true } : {},
15552
16238
  ...suggestionId === void 0 ? {} : { suggestionId },
15553
16239
  ...skill === void 0 ? {} : { skill },
16240
+ ...draftId === void 0 ? {} : { draftId },
15554
16241
  ...guidance === void 0 ? {} : { guidance },
15555
16242
  ...root === void 0 ? {} : { targetRoot: root },
15556
16243
  ...draftRoute === void 0 ? {} : { route: draftRoute }
@@ -19251,9 +19938,11 @@ function apply(rawContext, rawConfig = {}) {
19251
19938
  const skills = new SkillEvolution({
19252
19939
  settings: () => skillSettings.get(),
19253
19940
  source: (agent) => skillsSourceFor(runtime, agent),
19941
+ taskRoute: (workspace) => lifecycle.skillTaskRoute(workspace),
19254
19942
  runTask: (workspaceRoot, signal, operation, route) => lifecycle.runSkillTask(workspaceRoot, signal, operation, route),
19255
19943
  skillTask: (parent, label, prompt, schema, persona, signal, _route, maxTokens) => coordinator.skillTask(parent, label, prompt, schema, persona, signal, taskRoute(parent), maxTokens)
19256
19944
  });
19945
+ ctx.effect(() => hostSettings.onUpdated(() => skills.settingsChanged()), "dsh-mnemon: automatic skill adoption settings");
19257
19946
  lifecycle.useSkillEvolution(skills);
19258
19947
  ctx.effect(() => {
19259
19948
  const stop = autoBackup.start();