@lmzhen/dsh-evolution-skill-discovery 0.32.3 → 0.32.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/main-entry.js CHANGED
@@ -1,10 +1,18 @@
1
1
  import { isAbsolute, join, resolve } from "node:path";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
- import { authorityFrom, evolutionIoAdapter, evolutionRoot, installEvolutionProjections, isRecord, makeSerialQueue, newSkillLibrary, parseFrontmatter } from "@lmzhen/dsh-evolution-core";
3
+ import { authorityFrom, evolutionIoAdapter, evolutionRoot, installEvolutionProjections, isRecord, makeSerialQueue, newSkillLibrary } from "@lmzhen/dsh-evolution-core";
4
4
  import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
5
5
  import { escapeText } from "@deepseek-ai/dsh-skill";
6
6
  import MiniSearch from "minisearch";
7
7
  import { defineTool } from "@deepseek-ai/dsh-tools";
8
+ //#region lib/types/domain-map.js
9
+ /** The next CAS version. ONE implementation: non-numeric versions get a suffix,
10
+ * never NaN, and every writer advances the same way. */
11
+ function nextMapVersion(version) {
12
+ const numeric = Number(version);
13
+ return Number.isSafeInteger(numeric) && numeric >= 0 ? String(numeric + 1) : version + ".1";
14
+ }
15
+ //#endregion
8
16
  //#region lib/types/domain-generation.js
9
17
  /** @returns stable id derived from title, not the title itself. */
10
18
  function mintDomainId(title) {
@@ -127,6 +135,7 @@ async function bootstrapDomainMapWithFacts(store, proposal, facts, expectedMapVe
127
135
  descriptionHash: "bootstrap",
128
136
  descriptionVersion: 1,
129
137
  descriptionStatus: "fresh",
138
+ descriptionUpdatedAt: now,
130
139
  provenance: {
131
140
  source: "llm",
132
141
  promptVersion: provenance.promptVersion,
@@ -156,7 +165,7 @@ async function bootstrapDomainMapWithFacts(store, proposal, facts, expectedMapVe
156
165
  });
157
166
  }
158
167
  return store.commit(expectedMapVersion, {
159
- mapVersion: String(Number(expectedMapVersion) + 1),
168
+ mapVersion: nextMapVersion(expectedMapVersion),
160
169
  domains,
161
170
  edges
162
171
  });
@@ -236,11 +245,7 @@ const ALLOWED = new Set([
236
245
  "disableDomains",
237
246
  "revision"
238
247
  ]);
239
- const MODES = new Set([
240
- "off",
241
- "auto",
242
- "on"
243
- ]);
248
+ const MODES = new Set(["off", "on"]);
244
249
  function checkLayer(layer) {
245
250
  for (const key of Object.keys(layer)) if (!ALLOWED.has(key)) throw new DomainPolicyValidationError("unsupported policy field " + key);
246
251
  if (layer.mode !== void 0 && !MODES.has(layer.mode)) throw new DomainPolicyValidationError("unsupported policy mode");
@@ -303,7 +308,7 @@ function canonical(value) {
303
308
  if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => [key, canonical(entry)]));
304
309
  return value;
305
310
  }
306
- function digest$1(value) {
311
+ function digest(value) {
307
312
  return sha256(JSON.stringify(canonical(value)));
308
313
  }
309
314
  /** @returns stable key for the winning provider/source/name tuple. */
@@ -316,11 +321,11 @@ function skillKeyOf(summary) {
316
321
  }
317
322
  /** @returns stable hash of the canonical cwd and optional scope key. */
318
323
  function viewKeyOf(input) {
319
- return digest$1([resolve(input.cwd), input.scopeKey ?? null]);
324
+ return digest([resolve(input.cwd), input.scopeKey ?? null]);
320
325
  }
321
326
  /** @returns hash of the three fields used by discovery/retrieval. */
322
327
  function threeFieldHashOf(input) {
323
- return digest$1([
328
+ return digest([
324
329
  input.name,
325
330
  input.description,
326
331
  input.whenToUse ?? null
@@ -344,7 +349,7 @@ function skillIndexRowOf(summary, managed, protectedSkill = false, pinned = fals
344
349
  }
345
350
  /** @returns digest of the sorted rows plus completeness. */
346
351
  function catalogRevisionOf(rows, complete) {
347
- return digest$1([complete, rows.map((row) => [
352
+ return digest([complete, rows.map((row) => [
348
353
  row.skillKey,
349
354
  row.name,
350
355
  row.description,
@@ -360,11 +365,11 @@ function catalogRevisionOf(rows, complete) {
360
365
  }
361
366
  /** @returns digest of a concrete per-view snapshot. */
362
367
  function viewCatalogDigestOf(viewKey, rows, complete) {
363
- return digest$1([viewKey, catalogRevisionOf(rows, complete)]);
368
+ return digest([viewKey, catalogRevisionOf(rows, complete)]);
364
369
  }
365
370
  /** @returns digest of effective policy plus its revisions. */
366
371
  function policyDigestOf(input) {
367
- return digest$1([
372
+ return digest([
368
373
  input.globalPolicyRevision,
369
374
  input.workspaceOverlayRevision ?? null,
370
375
  input.scopeOverlayRevision ?? null,
@@ -373,7 +378,7 @@ function policyDigestOf(input) {
373
378
  }
374
379
  /** @returns view-cache identity for one projection. */
375
380
  function projectionDigestOf(input) {
376
- return digest$1([
381
+ return digest([
377
382
  input.viewKey,
378
383
  input.mapVersion,
379
384
  input.policyDigest,
@@ -385,7 +390,7 @@ function projectionDigestOf(input) {
385
390
  }
386
391
  /** @returns view-cache identity for one search index. */
387
392
  function searchIndexKeyOf(input) {
388
- return digest$1([
393
+ return digest([
389
394
  input.viewKey,
390
395
  input.viewCatalogDigest,
391
396
  input.policyDigest,
@@ -875,7 +880,7 @@ function buildDomainProjection(input) {
875
880
  const linkedKeys = new Set(input.snapshot.edges.filter((edge) => edge.status === "active" || edge.status === "pending").map((edge) => edge.skillKey));
876
881
  const unclassifiedCount = input.rows.filter((row) => row.modelInvocable && !linkedKeys.has(row.skillKey)).length;
877
882
  const domains = input.snapshot.domains.filter((node) => node.status === "active" || node.status === "provisional");
878
- const cardDomains = domains.filter((node) => node.parentIds.length <= (input.maxVisibleDepth ?? 3) - 1 && node.parentIds.length === 0).filter((node) => !input.policy.disabled.has(node.id)).filter((node) => input.policy.pinned.has(node.id) || qualityPasses(node, domains)).sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0).slice(0, Math.max(1, input.maxCards ?? 12));
883
+ const cardDomains = domains.filter((node) => node.parentIds.length === 0).filter((node) => !input.policy.disabled.has(node.id)).filter((node) => input.policy.pinned.has(node.id) || qualityPasses(node, domains)).sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0).slice(0, Math.max(1, input.maxCards ?? 12));
879
884
  const domainById = new Map(domains.map((node) => [node.id, node]));
880
885
  const cards = cardDomains.map((node) => {
881
886
  let managedModelInvocableCount = 0;
@@ -897,7 +902,7 @@ function buildDomainProjection(input) {
897
902
  id: node.id,
898
903
  title: node.name,
899
904
  purpose: node.purpose.slice(0, 80),
900
- descriptionStatus: node.descriptionStatus === "fresh" && input.descriptionStaleAfterHours !== void 0 && input.now !== void 0 && input.now - node.updatedAt > input.descriptionStaleAfterHours * 36e5 ? "stale" : node.descriptionStatus,
905
+ descriptionStatus: node.descriptionStatus === "fresh" && input.descriptionStaleAfterHours !== void 0 && input.now !== void 0 && input.now - (node.descriptionUpdatedAt ?? node.updatedAt) > input.descriptionStaleAfterHours * 36e5 ? "stale" : node.descriptionStatus,
901
906
  triggers: node.triggers.slice(0, 3),
902
907
  managedModelInvocableCount,
903
908
  externalCatalogCount,
@@ -926,7 +931,7 @@ function buildDomainProjection(input) {
926
931
  wouldHide.add(edge.skillKey);
927
932
  }
928
933
  const activeManaged = mode === "on" && !mapStale && input.searchAvailable !== false && input.policy.degraded.length === 0 ? wouldHide : /* @__PURE__ */ new Set();
929
- const projected = projectCatalog({
934
+ return projectCatalog({
930
935
  entries: input.entries,
931
936
  rows: input.rows,
932
937
  activeManagedSkillKeys: activeManaged,
@@ -941,7 +946,6 @@ function buildDomainProjection(input) {
941
946
  discoveryMode: input.discoveryMode,
942
947
  complete: true
943
948
  });
944
- return mode === "on" ? projected : projected;
945
949
  }
946
950
  //#endregion
947
951
  //#region lib/types/skill-index.js
@@ -1182,7 +1186,7 @@ function applyProjectedDecision(decision, capture, result, seen) {
1182
1186
  /**
1183
1187
  * Derive one view's state without consulting another view.
1184
1188
  * @param input - per-view health and identity.
1185
- * @returns state with reasons; hidden safety is evaluated by canHide.
1189
+ * @returns state with reasons; card rendering is gated by canProject.
1186
1190
  */
1187
1191
  function deriveViewState(input) {
1188
1192
  const reasons = [...input.reasons ?? []];
@@ -1274,7 +1278,7 @@ function mapStateOf(snapshot, staleAfterHours, now) {
1274
1278
  return "ready";
1275
1279
  }
1276
1280
  function effectiveMode(mode) {
1277
- return mode === "off" ? "off" : mode === "auto" ? "shadow" : "on";
1281
+ return mode;
1278
1282
  }
1279
1283
  function catalogText(message) {
1280
1284
  return message.content.map((block) => block.type === "text" ? block.text ?? "" : "").join("");
@@ -1352,6 +1356,7 @@ var ProjectionRuntime = class {
1352
1356
  at: 0
1353
1357
  };
1354
1358
  lastByAgent = /* @__PURE__ */ new Map();
1359
+ warnedMissingHistory = false;
1355
1360
  constructor(ports) {
1356
1361
  this.ports = ports;
1357
1362
  }
@@ -1359,6 +1364,14 @@ var ProjectionRuntime = class {
1359
1364
  report(agent) {
1360
1365
  return agent === void 0 ? this.last : this.lastByAgent.get(String(agent.id)) ?? this.last;
1361
1366
  }
1367
+ hasStructuralLog(agent) {
1368
+ return typeof agent.session.readEvents === "function";
1369
+ }
1370
+ warnMissingHistory() {
1371
+ if (this.warnedMissingHistory) return;
1372
+ this.warnedMissingHistory = true;
1373
+ this.ports.warn?.("evolution-skill-discovery: no sessionProjections history and no structural readEvents for this session — catalog restore/dedupe is unavailable; failing open (mount the projection registry to keep replacement dedupe)");
1374
+ }
1362
1375
  mark(agent, action, digest, content = "") {
1363
1376
  const report = {
1364
1377
  action,
@@ -1382,6 +1395,10 @@ var ProjectionRuntime = class {
1382
1395
  const fullCurrent = currentCapture === void 0 ? void 0 : fullCaptureOf(currentCapture);
1383
1396
  const durable = projectedCurrent ? record?.base ?? fullSession ?? fullCurrent : fullCurrent ?? fullSession ?? record?.base;
1384
1397
  if (durable === void 0) {
1398
+ if (history === void 0 && !this.hasStructuralLog(agent)) {
1399
+ this.warnMissingHistory();
1400
+ return decision;
1401
+ }
1385
1402
  this.mark(agent, "blocked", "");
1386
1403
  return decision;
1387
1404
  }
@@ -1460,6 +1477,7 @@ var ProjectionRuntime = class {
1460
1477
  const { view, snapshot, policy, policyDigest, state } = await this.readState(agent, signal);
1461
1478
  const currentCapture = captureDecisionCatalog(decision);
1462
1479
  const history = this.ports.history?.(agent);
1480
+ if (history === void 0 && !this.hasStructuralLog(agent)) this.warnMissingHistory();
1463
1481
  const seen = new Set(sessionProjectionDigests(agent, history));
1464
1482
  if (decision.kind === "enter") for (const message of decision.messages) {
1465
1483
  if (message.source.kind !== "evolution-skill-projection") continue;
@@ -1473,6 +1491,7 @@ var ProjectionRuntime = class {
1473
1491
  if (capture === void 0) capture = sessionCatalogCapture(agent, history);
1474
1492
  if (!canProject(state) || snapshot === void 0) return this.restore(agent, view.viewKey, decision, currentCapture);
1475
1493
  if (capture === void 0) {
1494
+ if (history === void 0 && !this.hasStructuralLog(agent)) return decision;
1476
1495
  this.mark(agent, "blocked", "");
1477
1496
  return decision;
1478
1497
  }
@@ -1647,6 +1666,14 @@ var SkillSearchIndex = class SkillSearchIndex {
1647
1666
  /** Per-view cache keyed by searchIndexKey; never shares one engine across views. */
1648
1667
  var SkillSearchCache = class {
1649
1668
  entries = /* @__PURE__ */ new Map();
1669
+ maxEntries;
1670
+ constructor(options = {}) {
1671
+ this.maxEntries = Math.max(1, Math.floor(options.maxEntries ?? 32));
1672
+ }
1673
+ /** @returns the current number of cached view indices (bounded by maxEntries). */
1674
+ get size() {
1675
+ return this.entries.size;
1676
+ }
1650
1677
  /**
1651
1678
  * Get or build one view index. A failed rebuild retains the previous engine
1652
1679
  * and marks it stale; no previous engine means undefined.
@@ -1664,10 +1691,14 @@ var SkillSearchCache = class {
1664
1691
  discoveryMode: key.discoveryMode
1665
1692
  });
1666
1693
  const existing = this.entries.get(cacheKey);
1667
- if (existing !== void 0) return {
1668
- index: existing.index,
1669
- stale: false
1670
- };
1694
+ if (existing !== void 0) {
1695
+ this.entries.delete(cacheKey);
1696
+ this.entries.set(cacheKey, existing);
1697
+ return {
1698
+ index: existing.index,
1699
+ stale: false
1700
+ };
1701
+ }
1671
1702
  try {
1672
1703
  const index = SkillSearchIndex.fromRows(key.viewKey, rows);
1673
1704
  this.entries.set(cacheKey, {
@@ -1675,6 +1706,11 @@ var SkillSearchCache = class {
1675
1706
  viewKey: key.viewKey,
1676
1707
  index
1677
1708
  });
1709
+ while (this.entries.size > this.maxEntries) {
1710
+ const oldest = this.entries.keys().next().value;
1711
+ if (oldest === void 0) break;
1712
+ this.entries.delete(oldest);
1713
+ }
1678
1714
  return {
1679
1715
  index,
1680
1716
  stale: false
@@ -1978,12 +2014,49 @@ function buildGenerationRequest(input) {
1978
2014
  var ViewSnapshotStore = class {
1979
2015
  io;
1980
2016
  path;
2017
+ serial = Promise.resolve();
1981
2018
  constructor(io, path) {
1982
2019
  this.io = io;
1983
2020
  this.path = path;
1984
2021
  }
1985
2022
  async read() {
1986
- const content = await this.io.readText(this.path);
2023
+ return this.decode(await this.io.readText(this.path));
2024
+ }
2025
+ /** Persist one session snapshot; first writer wins unless overwrite is explicit. */
2026
+ async put(snapshot, options = {}) {
2027
+ const apply = (raw) => {
2028
+ const current = this.decode(raw);
2029
+ if (current.sessions[snapshot.sessionId] !== void 0 && options.overwrite !== true) return raw;
2030
+ const sessions = {
2031
+ ...current.sessions,
2032
+ [snapshot.sessionId]: {
2033
+ viewKey: snapshot.viewKey,
2034
+ cwd: snapshot.cwd,
2035
+ viewCatalogDigest: snapshot.viewCatalogDigest,
2036
+ mapVersion: snapshot.mapVersion,
2037
+ configDigest: snapshot.configDigest,
2038
+ featureEpoch: snapshot.featureEpoch,
2039
+ createdAt: snapshot.createdAt
2040
+ }
2041
+ };
2042
+ return JSON.stringify({
2043
+ schemaVersion: 1,
2044
+ sessions
2045
+ });
2046
+ };
2047
+ if (this.io.transact !== void 0) {
2048
+ await this.io.transact(this.path, (current) => apply(current));
2049
+ return;
2050
+ }
2051
+ const task = async () => {
2052
+ const current = await this.io.readText(this.path);
2053
+ const next = apply(current);
2054
+ if (next !== null && next !== current) await this.io.writeText(this.path, next);
2055
+ };
2056
+ this.serial = this.serial.then(task, task);
2057
+ await this.serial;
2058
+ }
2059
+ decode(content) {
1987
2060
  if (content === null) return {
1988
2061
  schemaVersion: 1,
1989
2062
  sessions: {}
@@ -2004,27 +2077,6 @@ var ViewSnapshotStore = class {
2004
2077
  };
2005
2078
  }
2006
2079
  }
2007
- /** Persist one session snapshot; first writer wins unless overwrite is explicit. */
2008
- async put(snapshot, options = {}) {
2009
- const current = await this.read();
2010
- if (current.sessions[snapshot.sessionId] !== void 0 && options.overwrite !== true) return;
2011
- const sessions = {
2012
- ...current.sessions,
2013
- [snapshot.sessionId]: {
2014
- viewKey: snapshot.viewKey,
2015
- cwd: snapshot.cwd,
2016
- viewCatalogDigest: snapshot.viewCatalogDigest,
2017
- mapVersion: snapshot.mapVersion,
2018
- configDigest: snapshot.configDigest,
2019
- featureEpoch: snapshot.featureEpoch,
2020
- createdAt: snapshot.createdAt
2021
- }
2022
- };
2023
- await this.io.writeText(this.path, JSON.stringify({
2024
- schemaVersion: 1,
2025
- sessions
2026
- }));
2027
- }
2028
2080
  /** @returns frozen snapshot for one session, if recorded. */
2029
2081
  async get(sessionId) {
2030
2082
  const entry = (await this.read()).sessions[sessionId];
@@ -2042,506 +2094,6 @@ var ViewSnapshotStore = class {
2042
2094
  }
2043
2095
  };
2044
2096
  //#endregion
2045
- //#region lib/types/assignment.js
2046
- function digest(value) {
2047
- return createHash("sha256").update(JSON.stringify(value)).digest("hex");
2048
- }
2049
- /** @returns idempotency key for one proposal observation. */
2050
- function assignmentKey(input) {
2051
- return digest([
2052
- input.skillKey,
2053
- input.threeFieldHash,
2054
- input.proposalDigest,
2055
- input.mapVersion
2056
- ]);
2057
- }
2058
- /** @returns stable proposal digest; undefined proposal is digest of null. */
2059
- function proposalDigest(proposal) {
2060
- return digest(proposal ?? null);
2061
- }
2062
- /** @returns a closed-set pending reason or undefined when the proposal can enqueue. */
2063
- function validateProposal(proposal) {
2064
- if (proposal.mode !== "existing" && proposal.mode !== "new") return "schema-invalid";
2065
- if (typeof proposal.confidence !== "number" || proposal.confidence < 0 || proposal.confidence > 1 || typeof proposal.reason !== "string" || typeof proposal.mapVersion !== "string") return "schema-invalid";
2066
- if (proposal.mode === "existing" && (typeof proposal.id !== "string" || proposal.id.length === 0)) return "invalid-id";
2067
- if (proposal.mode === "new") {
2068
- const proposed = proposal.proposed;
2069
- if (proposed === void 0 || proposed.title.trim().length === 0 || proposed.purpose.trim().length === 0 || proposed.inScope.length === 0 || proposed.outOfScope.length === 0 || proposed.triggers.length === 0) return "new-domain-conflict";
2070
- }
2071
- }
2072
- /** @returns true only when whenToUse is covered by in/out/triggers; names alone do not count. */
2073
- function coveredByDomain(whenToUse, domain) {
2074
- const text = whenToUse.toLowerCase();
2075
- if (text.length === 0) return false;
2076
- if (domain.outOfScope.some((value) => value.length > 0 && text.includes(value.toLowerCase()))) return false;
2077
- return domain.triggers.some((value) => value.length > 0 && text.includes(value.toLowerCase())) || domain.inScope.some((value) => value.length > 0 && text.includes(value.toLowerCase()));
2078
- }
2079
- /** One assignment sidecar service; the only map write path for create. */
2080
- var DomainAssignmentService = class {
2081
- queue;
2082
- store;
2083
- options;
2084
- constructor(queue, store, options) {
2085
- this.queue = queue;
2086
- this.store = store;
2087
- this.options = options;
2088
- }
2089
- /** Stage a create proposal without queueing it. */
2090
- stage(submission) {
2091
- return {
2092
- id: digest([
2093
- "staged",
2094
- submission.skillKey,
2095
- submission.threeFieldHash
2096
- ]),
2097
- submission
2098
- };
2099
- }
2100
- /** Approve a staged create; only this call enqueues. */
2101
- async approve(staged) {
2102
- return this.submit(staged.submission);
2103
- }
2104
- /**
2105
- * Submit a create proposal after SkillLibrary.create succeeded.
2106
- * @param submission - skill identity, three-field hash, permission, proposal.
2107
- * @returns queue/pending/unclassified; never rolls back create.
2108
- */
2109
- async submit(submission) {
2110
- if (!this.options.createClassificationEnabled) return {
2111
- kind: "unclassified",
2112
- reason: "feature-off"
2113
- };
2114
- if (!submission.modelInvocable) return {
2115
- kind: "unclassified",
2116
- reason: "not-model-invocable"
2117
- };
2118
- if (submission.proposal === void 0) {
2119
- if (this.options.createCardsAvailable === true) {
2120
- const reason = "schema-invalid";
2121
- return {
2122
- kind: "pending",
2123
- entry: {
2124
- key: digest([
2125
- "missing-proposal",
2126
- submission.skillKey,
2127
- submission.threeFieldHash
2128
- ]),
2129
- submission,
2130
- proposalDigest: proposalDigest(void 0),
2131
- createdAt: this.now(),
2132
- status: "pending",
2133
- pendingReason: reason
2134
- },
2135
- reason
2136
- };
2137
- }
2138
- return {
2139
- kind: "unclassified",
2140
- reason: "no-proposal"
2141
- };
2142
- }
2143
- const invalid = validateProposal(submission.proposal);
2144
- const pdigest = proposalDigest(submission.proposal);
2145
- const key = assignmentKey({
2146
- skillKey: submission.skillKey,
2147
- threeFieldHash: submission.threeFieldHash,
2148
- proposalDigest: pdigest,
2149
- mapVersion: submission.proposal.mapVersion
2150
- });
2151
- let queued;
2152
- try {
2153
- queued = await this.queue.enqueue({
2154
- key,
2155
- submission,
2156
- proposalDigest: pdigest,
2157
- createdAt: submission.now ?? this.now()
2158
- });
2159
- } catch {
2160
- return {
2161
- kind: "pending",
2162
- entry: {
2163
- key,
2164
- submission,
2165
- proposalDigest: pdigest,
2166
- createdAt: submission.now ?? this.now(),
2167
- status: "pending",
2168
- pendingReason: "queue-unavailable"
2169
- },
2170
- reason: "queue-unavailable"
2171
- };
2172
- }
2173
- if (invalid !== void 0) {
2174
- await this.queue.update(key, "pending", invalid);
2175
- return {
2176
- kind: "pending",
2177
- entry: {
2178
- ...queued.entry,
2179
- status: "pending",
2180
- pendingReason: invalid
2181
- },
2182
- reason: invalid
2183
- };
2184
- }
2185
- return {
2186
- kind: "queued",
2187
- entry: queued.entry,
2188
- duplicate: queued.duplicate
2189
- };
2190
- }
2191
- /** Process one queued entry with the current map; CAS failure becomes pending. */
2192
- async process(entry) {
2193
- const proposal = entry.submission.proposal;
2194
- if (proposal === void 0) return {
2195
- kind: "pending",
2196
- entry,
2197
- reason: "schema-invalid"
2198
- };
2199
- const current = await this.store.read();
2200
- if (proposal.mapVersion !== current.mapVersion) {
2201
- await this.queue.update(entry.key, "pending", "map-version-stale");
2202
- return {
2203
- kind: "pending",
2204
- entry: {
2205
- ...entry,
2206
- status: "pending",
2207
- pendingReason: "map-version-stale"
2208
- },
2209
- reason: "map-version-stale"
2210
- };
2211
- }
2212
- try {
2213
- const next = buildNext(current, entry, proposal, this.options.provisionalThreshold ?? 3);
2214
- const committed = await this.store.commit(current.mapVersion, {
2215
- mapVersion: next.mapVersion,
2216
- domains: next.domains,
2217
- edges: next.edges
2218
- });
2219
- const status = next.provisional ? "provisional" : "applied";
2220
- await this.queue.update(entry.key, status);
2221
- return {
2222
- kind: status,
2223
- entry: {
2224
- ...entry,
2225
- status
2226
- },
2227
- mapVersion: committed.mapVersion
2228
- };
2229
- } catch (error) {
2230
- if (error instanceof DomainMapVersionConflict) {
2231
- await this.queue.update(entry.key, "pending", "map-version-stale");
2232
- return {
2233
- kind: "pending",
2234
- entry: {
2235
- ...entry,
2236
- status: "pending",
2237
- pendingReason: "map-version-stale"
2238
- },
2239
- reason: "map-version-stale"
2240
- };
2241
- }
2242
- const reason = error instanceof Error && (error.message === "unknown-domain" || error.message === "new-domain-conflict" || error.message === "schema-invalid") ? error.message : "queue-unavailable";
2243
- await this.queue.update(entry.key, "pending", reason);
2244
- return {
2245
- kind: "pending",
2246
- entry: {
2247
- ...entry,
2248
- status: "pending",
2249
- pendingReason: reason
2250
- },
2251
- reason
2252
- };
2253
- }
2254
- }
2255
- /** Manually confirm one provisional domain; no semantic reclassification. */
2256
- async confirm(domainId) {
2257
- const current = await this.store.read();
2258
- const domain = current.domains.find((node) => node.id === domainId);
2259
- if (domain === void 0 || domain.status !== "provisional") return false;
2260
- const domains = current.domains.map((node) => node.id === domainId ? {
2261
- ...node,
2262
- status: "active",
2263
- updatedAt: this.now()
2264
- } : node);
2265
- await this.store.commit(current.mapVersion, {
2266
- mapVersion: nextMapVersion$1(current.mapVersion),
2267
- domains,
2268
- edges: current.edges
2269
- });
2270
- return true;
2271
- }
2272
- now() {
2273
- return this.options.now?.() ?? Date.now();
2274
- }
2275
- };
2276
- function nextMapVersion$1(version) {
2277
- const numeric = Number(version);
2278
- return Number.isSafeInteger(numeric) && numeric >= 0 ? String(numeric + 1) : version + "+1";
2279
- }
2280
- function buildNext(current, entry, proposal, threshold) {
2281
- const edge = {
2282
- skillKey: entry.submission.skillKey,
2283
- domainId: "",
2284
- role: "primary",
2285
- confidence: proposal.confidence,
2286
- source: "create",
2287
- skillHash: entry.submission.threeFieldHash,
2288
- mapVersion: nextMapVersion$1(current.mapVersion),
2289
- status: "active",
2290
- assignedAt: entry.createdAt
2291
- };
2292
- const secondaries = (proposal.secondaryDomainIds ?? []).map((id) => {
2293
- const secondary = current.domains.find((node) => node.id === id);
2294
- if (secondary === void 0) throw new Error("unknown-domain");
2295
- return {
2296
- ...edge,
2297
- domainId: secondary.id,
2298
- role: "secondary"
2299
- };
2300
- });
2301
- if (proposal.mode === "existing") {
2302
- const domain = current.domains.find((node) => node.id === proposal.id);
2303
- if (domain === void 0) throw new Error("unknown-domain");
2304
- if (!coveredByDomain(entry.submission.whenToUse ?? "", domain)) throw new Error("new-domain-conflict");
2305
- const members = new Set(current.edges.filter((item) => item.domainId === domain.id).map((item) => item.skillKey));
2306
- members.add(entry.submission.skillKey);
2307
- const promote = domain.status === "provisional" && members.size >= threshold;
2308
- const domains = promote ? current.domains.map((node) => node.id === domain.id ? {
2309
- ...node,
2310
- status: "active",
2311
- updatedAt: entry.createdAt
2312
- } : node) : current.domains;
2313
- return {
2314
- mapVersion: nextMapVersion$1(current.mapVersion),
2315
- domains,
2316
- edges: [
2317
- ...current.edges,
2318
- {
2319
- ...edge,
2320
- domainId: domain.id
2321
- },
2322
- ...secondaries.filter((secondary) => secondary.domainId !== domain.id)
2323
- ],
2324
- provisional: domain.status === "provisional" && !promote
2325
- };
2326
- }
2327
- const proposed = proposal.proposed;
2328
- if (proposed === void 0) throw new Error("schema-invalid");
2329
- const normalizedTitle = proposed.title.trim().toLowerCase();
2330
- const existing = current.domains.find((node) => node.name.trim().toLowerCase() === normalizedTitle);
2331
- if (existing !== void 0 && existing.status !== "provisional") throw new Error("new-domain-conflict");
2332
- const id = existing?.id ?? mintDomainId(proposed.title);
2333
- if (current.domains.some((node) => node.id !== id && node.triggers.some((trigger) => proposed.triggers.includes(trigger)))) throw new Error("new-domain-conflict");
2334
- const domain = existing ?? {
2335
- id,
2336
- name: proposed.title,
2337
- purpose: proposed.purpose,
2338
- description: proposed.purpose,
2339
- inScope: proposed.inScope,
2340
- outOfScope: proposed.outOfScope,
2341
- triggers: proposed.triggers,
2342
- parentIds: [],
2343
- status: "provisional",
2344
- version: 1,
2345
- createdAt: entry.createdAt,
2346
- updatedAt: entry.createdAt,
2347
- descriptionHash: "pending",
2348
- descriptionVersion: 1,
2349
- descriptionStatus: "fresh",
2350
- provenance: { source: "create" },
2351
- quality: {}
2352
- };
2353
- const members = new Set(current.edges.filter((item) => item.domainId === id).map((item) => item.skillKey));
2354
- members.add(entry.submission.skillKey);
2355
- const promote = members.size >= threshold;
2356
- const finalDomain = promote ? {
2357
- ...domain,
2358
- status: "active",
2359
- updatedAt: entry.createdAt
2360
- } : domain;
2361
- return {
2362
- mapVersion: nextMapVersion$1(current.mapVersion),
2363
- domains: existing === void 0 ? [...current.domains, finalDomain] : current.domains.map((node) => node.id === id ? finalDomain : node),
2364
- edges: [
2365
- ...current.edges,
2366
- {
2367
- ...edge,
2368
- domainId: id
2369
- },
2370
- ...secondaries.filter((secondary) => secondary.domainId !== id)
2371
- ],
2372
- provisional: !promote
2373
- };
2374
- }
2375
- //#endregion
2376
- //#region lib/types/assignment-queue.js
2377
- const COLLECTION = "assignment-queue";
2378
- /** Durable assignment queue; process-local cache over one JSON file. */
2379
- var JsonAssignmentQueue = class {
2380
- entries = /* @__PURE__ */ new Map();
2381
- loadPromise;
2382
- io;
2383
- path;
2384
- store;
2385
- constructor(io, path, store) {
2386
- this.io = io;
2387
- this.path = path;
2388
- this.store = store;
2389
- }
2390
- load() {
2391
- this.loadPromise ??= (async () => {
2392
- if (this.store !== void 0) {
2393
- for (const entry of await this.store.list(COLLECTION)) this.entries.set(entry.key, entry);
2394
- if (this.entries.size > 0) return;
2395
- }
2396
- const content = await this.io.readText(this.path);
2397
- if (content === null) return;
2398
- try {
2399
- const parsed = JSON.parse(content);
2400
- if (parsed.schemaVersion !== 1 || !Array.isArray(parsed.entries)) return;
2401
- for (const entry of parsed.entries) {
2402
- this.entries.set(entry.key, entry);
2403
- if (this.store !== void 0) await this.store.transact(COLLECTION, entry.key, async () => entry);
2404
- }
2405
- } catch {}
2406
- })();
2407
- return this.loadPromise;
2408
- }
2409
- async persist() {
2410
- if (this.store === void 0) await this.io.writeText(this.path, JSON.stringify({
2411
- schemaVersion: 1,
2412
- entries: [...this.entries.values()]
2413
- }));
2414
- }
2415
- async enqueue(input) {
2416
- await this.load();
2417
- const existing = this.entries.get(input.key);
2418
- if (existing !== void 0) return {
2419
- entry: existing,
2420
- duplicate: true
2421
- };
2422
- const entry = {
2423
- ...input,
2424
- status: "queued"
2425
- };
2426
- if (this.store !== void 0) {
2427
- let duplicate = false;
2428
- let stored = entry;
2429
- await this.store.transact(COLLECTION, input.key, async (current) => {
2430
- if (current !== void 0) {
2431
- duplicate = true;
2432
- stored = current;
2433
- return current;
2434
- }
2435
- return entry;
2436
- });
2437
- this.entries.set(input.key, stored);
2438
- return {
2439
- entry: stored,
2440
- duplicate
2441
- };
2442
- }
2443
- this.entries.set(input.key, entry);
2444
- await this.persist();
2445
- return {
2446
- entry,
2447
- duplicate: false
2448
- };
2449
- }
2450
- async update(key, status, pendingReason) {
2451
- await this.load();
2452
- const entry = this.entries.get(key);
2453
- if (entry === void 0) return;
2454
- const nextEntry = {
2455
- ...entry,
2456
- status,
2457
- pendingReason
2458
- };
2459
- if (this.store !== void 0) await this.store.transact(COLLECTION, key, async (current) => current === void 0 ? nextEntry : {
2460
- ...current,
2461
- status,
2462
- pendingReason
2463
- });
2464
- this.entries.set(key, nextEntry);
2465
- await this.persist();
2466
- }
2467
- list() {
2468
- return [...this.entries.values()];
2469
- }
2470
- };
2471
- //#endregion
2472
- //#region lib/types/worker.js
2473
- /** Drivable worker; the composition may call drain on a schedule. */
2474
- var AssignmentWorker = class {
2475
- service;
2476
- queue;
2477
- constructor(service, queue) {
2478
- this.service = service;
2479
- this.queue = queue;
2480
- }
2481
- /** Process queued entries once; returns processed count. */
2482
- async drain(limit = 10) {
2483
- const entries = this.queue.list().filter((entry) => entry.status === "queued").slice(0, Math.max(0, limit));
2484
- for (const entry of entries) await this.service.process(entry);
2485
- return entries.length;
2486
- }
2487
- };
2488
- //#endregion
2489
- //#region lib/types/assignment-sidecar.js
2490
- function submitCreate(service, args, options) {
2491
- if (typeof args.name !== "string" || args.name.length === 0) return void 0;
2492
- const parsed = parseFrontmatter(typeof args.content === "string" ? args.content : "");
2493
- const description = parsed?.frontmatter.description ?? "";
2494
- const rawWhenToUse = parsed?.frontmatter.whenToUse;
2495
- const whenToUse = typeof rawWhenToUse === "string" ? rawWhenToUse : void 0;
2496
- const domain = args.domain;
2497
- const proposal = typeof domain === "object" && domain !== null ? domain : void 0;
2498
- return service.submit({
2499
- skillKey: skillKeyOf({
2500
- provider: options.provider ?? "dsh-evolution",
2501
- source: options.source ?? "user-dsh",
2502
- name: args.name
2503
- }),
2504
- threeFieldHash: threeFieldHashOf({
2505
- name: args.name,
2506
- description,
2507
- ...whenToUse === void 0 ? {} : { whenToUse }
2508
- }),
2509
- modelInvocable: parsed?.frontmatter["disable-model-invocation"] !== true,
2510
- ...whenToUse === void 0 ? {} : { whenToUse },
2511
- ...proposal === void 0 ? {} : { proposal }
2512
- }).then(() => options.onSubmitted?.());
2513
- }
2514
- /**
2515
- * Observe successful create calls and submit their optional domain proposal.
2516
- * Direct creates submit immediately; staged creates submit only after approval.
2517
- * @param ctx - plugin context with tools and approval events.
2518
- * @param service - the one assignment service.
2519
- * @param options - provider/source identity for skillKey.
2520
- * @returns disposer for both listeners.
2521
- */
2522
- function installAssignmentSidecar(ctx, service, options = {}) {
2523
- const stopResult = ctx.on("tools/result", (exec, result) => {
2524
- if (result.isError || exec.name !== "skill_manage" || exec.agent === void 0) return;
2525
- if (result.value?.pending_id !== void 0) return;
2526
- const args = exec.arguments;
2527
- if (typeof args !== "object" || args === null) return;
2528
- const record = args;
2529
- if (record.action !== "create") return;
2530
- submitCreate(service, record, options)?.catch(() => {});
2531
- });
2532
- const stopApproval = ctx.on("evolution/approval-decided", ({ record, action, ok }) => {
2533
- if (!ok || action !== "approve" || record.kind !== "skill" || typeof record.args !== "object" || record.args === null) return;
2534
- const raw = record.args;
2535
- const operation = typeof raw.operation === "object" && raw.operation !== null ? raw.operation : raw;
2536
- if (operation.action !== "create") return;
2537
- submitCreate(service, operation, options)?.catch(() => {});
2538
- });
2539
- return () => {
2540
- stopResult();
2541
- stopApproval();
2542
- };
2543
- }
2544
- //#endregion
2545
2097
  //#region lib/types/awareness.js
2546
2098
  /** Static 1–2 sentence awareness; no domain list/tree. */
2547
2099
  const DISCOVERY_AWARENESS = "When skill coverage for a task is uncertain, the domain cards and skill_search can reduce it; search is optional, not a required step.";
@@ -2586,57 +2138,45 @@ function renderDomainDescription(domain) {
2586
2138
  }
2587
2139
  //#endregion
2588
2140
  //#region lib/types/description-refresh.js
2589
- function nextMapVersion(version) {
2590
- const numeric = Number(version);
2591
- return Number.isSafeInteger(numeric) && numeric >= 0 ? String(numeric + 1) : version + ".1";
2592
- }
2593
2141
  /**
2594
- * Mark aged fresh descriptions stale so the refresh pass has a producer.
2142
+ * Mark aged descriptions stale AND refresh every stale description in ONE
2143
+ * read/commit. Session start used to call the two producers back to back, so a
2144
+ * single start bumped mapVersion twice (P2-14).
2595
2145
  * @param store - domain map store.
2596
2146
  * @param maxAgeHours - age threshold; undefined disables the producer.
2597
2147
  * @param now - clock for deterministic tests.
2598
- * @returns number of domains marked stale.
2148
+ * @returns counts of marked and refreshed domains.
2599
2149
  */
2600
- async function markStaleDescriptions(store, maxAgeHours, now = Date.now()) {
2601
- if (maxAgeHours === void 0 || maxAgeHours < 0) return 0;
2602
- const snapshot = await store.read();
2603
- const aged = snapshot.domains.filter((node) => node.descriptionStatus === "fresh" && node.updatedAt > 0 && now - node.updatedAt > maxAgeHours * 36e5);
2604
- if (aged.length === 0) return 0;
2605
- const domains = snapshot.domains.map((node) => aged.includes(node) ? {
2606
- ...node,
2607
- descriptionStatus: "stale"
2608
- } : node);
2609
- await store.commit(snapshot.mapVersion, {
2610
- mapVersion: nextMapVersion(snapshot.mapVersion),
2611
- domains,
2612
- edges: snapshot.edges
2613
- });
2614
- return aged.length;
2615
- }
2616
- /**
2617
- * Re-render stale domain descriptions from their definition fields.
2618
- * @param store - domain map store.
2619
- * @returns number of refreshed domains.
2620
- */
2621
- async function refreshStaleDescriptions(store) {
2150
+ async function refreshAgedDescriptions(store, maxAgeHours, now = Date.now()) {
2151
+ if (maxAgeHours === void 0 || maxAgeHours < 0) return {
2152
+ marked: 0,
2153
+ refreshed: 0
2154
+ };
2622
2155
  const snapshot = await store.read();
2623
- const stale = snapshot.domains.filter((node) => node.descriptionStatus === "stale");
2624
- if (stale.length === 0) return 0;
2625
- const now = Date.now();
2626
- const domains = snapshot.domains.map((node) => node.descriptionStatus !== "stale" ? node : {
2156
+ const aged = new Set(snapshot.domains.filter((node) => node.descriptionStatus === "fresh" && (node.descriptionUpdatedAt ?? node.updatedAt) > 0 && now - (node.descriptionUpdatedAt ?? node.updatedAt) > maxAgeHours * 36e5).map((node) => node.id));
2157
+ const stale = snapshot.domains.filter((node) => node.descriptionStatus === "stale" || aged.has(node.id));
2158
+ if (stale.length === 0) return {
2159
+ marked: 0,
2160
+ refreshed: 0
2161
+ };
2162
+ const staleIds = new Set(stale.map((node) => node.id));
2163
+ const domains = snapshot.domains.map((node) => !staleIds.has(node.id) ? node : {
2627
2164
  ...node,
2628
2165
  description: renderDomainDescription(node),
2629
2166
  descriptionHash: descriptionHashOf(node, snapshot.domains.filter((child) => child.parentIds.includes(node.id))),
2630
2167
  descriptionVersion: node.descriptionVersion + 1,
2631
2168
  descriptionStatus: "fresh",
2632
- updatedAt: now
2169
+ descriptionUpdatedAt: now
2633
2170
  });
2634
2171
  await store.commit(snapshot.mapVersion, {
2635
2172
  mapVersion: nextMapVersion(snapshot.mapVersion),
2636
2173
  domains,
2637
2174
  edges: snapshot.edges
2638
2175
  });
2639
- return stale.length;
2176
+ return {
2177
+ marked: aged.size,
2178
+ refreshed: stale.length
2179
+ };
2640
2180
  }
2641
2181
  //#endregion
2642
2182
  //#region lib/types/main-entry.js
@@ -2788,7 +2328,7 @@ function apply(ctx, config) {
2788
2328
  const journal = new ProjectionJournal(io, config.projectionJournalPath ?? join(root, "evolution", "projection-journal.json"));
2789
2329
  const snapshots = new ViewSnapshotStore(io, config.viewSnapshotPath ?? join(root, "evolution", "view-snapshots.json"));
2790
2330
  const globalPolicy = {
2791
- mode: mode === "shadow" ? "auto" : mode,
2331
+ mode,
2792
2332
  pinDomains: config.pinDomains,
2793
2333
  disableDomains: config.disableDomains
2794
2334
  };
@@ -2825,6 +2365,7 @@ function apply(ctx, config) {
2825
2365
  journal,
2826
2366
  workingDirectory,
2827
2367
  history: (agent) => projections?.stateOf(agent.session, "evolutionProjectionHistory"),
2368
+ warn: (message) => ctx.logger.warn(message),
2828
2369
  featureEpoch: config.featureEpoch,
2829
2370
  codecVersion: "1",
2830
2371
  adapterVersion: "1",
@@ -2837,21 +2378,6 @@ function apply(ctx, config) {
2837
2378
  descriptionStaleAfterHours,
2838
2379
  isFrozen: async (agent) => snapshots.isFrozen(String(agent.session.id), config.featureEpoch)
2839
2380
  });
2840
- const queue = new JsonAssignmentQueue(io, config.assignmentQueuePath ?? join(root, "evolution", "assignment-queue.json"), ctx.get("evolutionStateStore"));
2841
- const assignment = new DomainAssignmentService(queue, map, {
2842
- createClassificationEnabled: config.createClassificationEnabled ?? false,
2843
- ...config.provisionalThreshold === void 0 ? {} : { provisionalThreshold: config.provisionalThreshold },
2844
- now: Date.now
2845
- });
2846
- const worker = new AssignmentWorker(assignment, queue);
2847
- queue.load().then(() => worker.drain()).catch(() => {
2848
- ctx.logger.warn("evolution-skill-discovery: assignment queue load failed; queued work stays for the next start");
2849
- });
2850
- installAssignmentSidecar(ctx, assignment, { onSubmitted: () => {
2851
- worker.drain().catch((error) => {
2852
- ctx.logger.warn("evolution-skill-discovery: assignment drain failed: " + (error instanceof Error ? error.message : String(error)));
2853
- });
2854
- } });
2855
2381
  ctx.on("agent/created", async ({ agent, source: start }) => {
2856
2382
  try {
2857
2383
  installDiscoveryAwareness(agent.ctx);
@@ -2876,10 +2402,9 @@ function apply(ctx, config) {
2876
2402
  featureEpoch: config.featureEpoch,
2877
2403
  createdAt: Date.now()
2878
2404
  });
2879
- const marked = await markStaleDescriptions(map, descriptionStaleAfterHours);
2880
- if (marked > 0) ctx.logger.info("evolution-skill-discovery: marked " + marked + " aged domain descriptions stale");
2881
- const refreshed = await refreshStaleDescriptions(map);
2882
- if (refreshed > 0) ctx.logger.info("evolution-skill-discovery: refreshed " + refreshed + " stale domain descriptions");
2405
+ const aged = await refreshAgedDescriptions(map, descriptionStaleAfterHours);
2406
+ if (aged.marked > 0) ctx.logger.info("evolution-skill-discovery: marked " + aged.marked + " aged domain descriptions stale");
2407
+ if (aged.refreshed > 0) ctx.logger.info("evolution-skill-discovery: refreshed " + aged.refreshed + " stale domain descriptions");
2883
2408
  } catch (error) {
2884
2409
  ctx.logger.warn("evolution-skill-discovery: initial view snapshot failed: " + (error instanceof Error ? error.message : String(error)));
2885
2410
  }
@@ -2915,23 +2440,9 @@ function apply(ctx, config) {
2915
2440
  text: "discovery mode=" + mode + "; view=" + view.viewKey + "; phase=" + state.phase + " map=" + state.map + " search=" + state.search + " policy=" + state.policy + " last=" + report.action + (state.reasons.length === 0 ? "" : " reasons=" + state.reasons.join(",")) + "\n" + rows.join("\n")
2916
2441
  };
2917
2442
  }
2918
- if (action === "confirm") {
2919
- const domainId = rawInput.trim().split(/\s+/)[1];
2920
- if (domainId === void 0) return {
2921
- kind: "error",
2922
- text: "Usage: /skill-domain confirm <id>"
2923
- };
2924
- return await assignment.confirm(domainId) ? {
2925
- kind: "success",
2926
- text: "Confirmed domain " + domainId + "."
2927
- } : {
2928
- kind: "error",
2929
- text: "Unknown or non-provisional domain: " + domainId
2930
- };
2931
- }
2932
2443
  if (action !== "generate") return {
2933
2444
  kind: "error",
2934
- text: "Usage: /skill-domain status | generate | confirm <id>"
2445
+ text: "Usage: /skill-domain status | generate"
2935
2446
  };
2936
2447
  if (coordinator === void 0) return {
2937
2448
  kind: "error",