@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/README.md +1 -1
- package/lib/main-entry.js +127 -616
- package/lib/types/description-refresh.d.ts +14 -1
- package/lib/types/domain-bootstrap.d.ts +1 -1
- package/lib/types/domain-map.d.ts +5 -0
- package/lib/types/domain-policy.d.ts +1 -1
- package/lib/types/domain-projection.d.ts +1 -9
- package/lib/types/generation-composition.d.ts +1 -1
- package/lib/types/index.d.ts +0 -4
- package/lib/types/main-entry.d.ts +1 -4
- package/lib/types/projection-runtime.d.ts +6 -1
- package/lib/types/search.d.ts +6 -0
- package/lib/types/view-snapshot-store.d.ts +2 -0
- package/lib/types/view-state.d.ts +2 -4
- package/package.json +5 -7
- package/lib/types/assignment-queue.d.ts +0 -26
- package/lib/types/assignment-sidecar.d.ts +0 -33
- package/lib/types/assignment.d.ts +0 -131
- package/lib/types/worker.d.ts +0 -14
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|
|
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
|
|
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)
|
|
1668
|
-
|
|
1669
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2148
|
+
* @returns counts of marked and refreshed domains.
|
|
2599
2149
|
*/
|
|
2600
|
-
async function
|
|
2601
|
-
if (maxAgeHours === void 0 || maxAgeHours < 0) return
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
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
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
2880
|
-
if (marked > 0) ctx.logger.info("evolution-skill-discovery: marked " + marked + " aged domain descriptions stale");
|
|
2881
|
-
|
|
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
|
|
2445
|
+
text: "Usage: /skill-domain status | generate"
|
|
2935
2446
|
};
|
|
2936
2447
|
if (coordinator === void 0) return {
|
|
2937
2448
|
kind: "error",
|