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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/lib/bundle.js +1 -1
- package/lib/client.js +166 -82
- package/lib/extension-sdk.js +39 -1
- package/lib/index.js +886 -197
- package/lib/{starter-resolution-xVjONc6g.js → starter-resolution-CTY14Exj.js} +8 -8
- package/lib/starter.js +1 -1
- package/lib/types/src/client/locales.d.ts +17 -0
- package/lib/types/src/host/protocol.d.ts +5 -0
- package/lib/types/src/sdk/index.d.ts +1 -0
- package/lib/types/src/sdk/projection-reservation.d.ts +3 -0
- package/package.json +10 -9
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
2220
|
-
const checksumFiles = record$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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))
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
4043
|
+
const fields = record$7(record_);
|
|
4024
4044
|
if (fields?.kind !== "grant") return void 0;
|
|
4025
|
-
const payload = record$
|
|
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$
|
|
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$
|
|
4045
|
-
const permissions = record$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
5964
|
+
const raw = record$5(parseJson(bytes, "the backup manifest"));
|
|
5932
5965
|
if (raw === void 0) return void 0;
|
|
5933
|
-
const extension = record$
|
|
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$
|
|
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$
|
|
6244
|
-
const inventory = record$
|
|
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$
|
|
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$
|
|
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(
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
8728
|
-
const source = record$
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 !==
|
|
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,
|
|
9896
|
+
const sources = runtimeMigrationSources(plan.revision, archiveEntries);
|
|
9840
9897
|
const requests = sources.map((source) => {
|
|
9841
|
-
const entry =
|
|
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,
|
|
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",
|
|
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 >
|
|
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[
|
|
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 !==
|
|
9991
|
-
const candidates =
|
|
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 =
|
|
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
|
|
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 (
|
|
13739
|
-
const wake =
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
15477
|
+
guidance,
|
|
15478
|
+
onRecorded: () => {
|
|
15479
|
+
recorded = true;
|
|
15480
|
+
}
|
|
15481
|
+
}, signal));
|
|
14937
15482
|
} catch (error) {
|
|
14938
|
-
if (input.suggestionId !== void 0)
|
|
14939
|
-
id
|
|
14940
|
-
|
|
14941
|
-
|
|
14942
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
15025
|
-
|
|
15026
|
-
|
|
15027
|
-
|
|
15028
|
-
|
|
15029
|
-
|
|
15030
|
-
|
|
15031
|
-
|
|
15032
|
-
|
|
15033
|
-
|
|
15034
|
-
|
|
15035
|
-
|
|
15036
|
-
|
|
15037
|
-
|
|
15038
|
-
|
|
15039
|
-
|
|
15040
|
-
|
|
15041
|
-
|
|
15042
|
-
|
|
15043
|
-
|
|
15044
|
-
|
|
15045
|
-
|
|
15046
|
-
|
|
15047
|
-
|
|
15048
|
-
|
|
15049
|
-
|
|
15050
|
-
|
|
15051
|
-
|
|
15052
|
-
|
|
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
|
-
|
|
15057
|
-
|
|
15058
|
-
|
|
15059
|
-
|
|
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-
|
|
15732
|
+
await source.mutate("run-finish", {
|
|
15062
15733
|
id: run.id,
|
|
15063
|
-
|
|
15064
|
-
});
|
|
15734
|
+
status: "completed"
|
|
15735
|
+
}, signal);
|
|
15736
|
+
} finally {
|
|
15737
|
+
release();
|
|
15065
15738
|
}
|
|
15066
|
-
};
|
|
15067
|
-
|
|
15068
|
-
|
|
15069
|
-
|
|
15070
|
-
|
|
15071
|
-
|
|
15072
|
-
|
|
15073
|
-
|
|
15074
|
-
|
|
15075
|
-
|
|
15076
|
-
|
|
15077
|
-
|
|
15078
|
-
}
|
|
15079
|
-
|
|
15080
|
-
|
|
15081
|
-
|
|
15082
|
-
|
|
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
|
-
|
|
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();
|