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

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,6 +1,6 @@
1
1
  import { isAbsolute, join, resolve } from "node:path";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
- import { authorityFrom, evolutionIoAdapter, evolutionRoot, isRecord, makeSerialQueue, newSkillLibrary, parseFrontmatter } from "@lmzhen/dsh-evolution-core";
3
+ import { authorityFrom, evolutionIoAdapter, evolutionRoot, installEvolutionProjections, isRecord, makeSerialQueue, newSkillLibrary, parseFrontmatter } 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";
@@ -661,6 +661,18 @@ var ProjectionJournal = class {
661
661
  await this.io.writeText(this.path, JSON.stringify(this.cache));
662
662
  });
663
663
  }
664
+ /** Delete every record for one session/view after a successful restore. */
665
+ async removeView(sessionId, viewKey, _codecVersion) {
666
+ await this.serial(async () => {
667
+ const current = await this.read();
668
+ const records = Object.fromEntries(Object.entries(current.records).filter(([, record]) => record.sessionId !== sessionId || record.viewKey !== viewKey));
669
+ this.cache = {
670
+ schemaVersion: 1,
671
+ records
672
+ };
673
+ await this.io.writeText(this.path, JSON.stringify(this.cache));
674
+ });
675
+ }
664
676
  };
665
677
  //#endregion
666
678
  //#region lib/types/domain-quality.js
@@ -1100,8 +1112,9 @@ function restoreCapturedProjection(decision, capture) {
1100
1112
  * @param result - projection result.
1101
1113
  * @returns decision with replacement catalog plus optional context.
1102
1114
  */
1103
- function applyReplacementDecision(decision, sourceEntries, result) {
1115
+ function applyReplacementDecision(decision, sourceEntries, result, seen) {
1104
1116
  if (decision.kind !== "enter") return decision;
1117
+ if (seen?.has(result.digest) === true) return decision;
1105
1118
  const update = renderCatalogUpdate(sourceEntries, result.catalogEntries);
1106
1119
  const context = renderProjectionContext(result);
1107
1120
  const messages = context === void 0 ? [...decision.messages, update] : [
@@ -1155,11 +1168,11 @@ function renderProjectionContext(result) {
1155
1168
  * @param result - complete projection.
1156
1169
  * @returns new decision; unchanged when no catalog exists.
1157
1170
  */
1158
- function applyProjectedDecision(decision, capture, result) {
1171
+ function applyProjectedDecision(decision, capture, result, seen) {
1159
1172
  const withCatalog = applyCapturedProjection(decision, capture, result.catalogEntries);
1160
1173
  const context = renderProjectionContext(result);
1161
1174
  if (context === void 0 || withCatalog.kind !== "enter") return withCatalog;
1162
- return withCatalog.messages.some((message) => message.source.kind === "evolution-skill-projection" && message.source.digest === result.digest) ? withCatalog : {
1175
+ return withCatalog.messages.some((message) => message.source.kind === "evolution-skill-projection" && message.source.digest === result.digest) || seen?.has(result.digest) === true ? withCatalog : {
1163
1176
  ...withCatalog,
1164
1177
  messages: [...withCatalog.messages, context]
1165
1178
  };
@@ -1269,9 +1282,36 @@ function catalogText(message) {
1269
1282
  function fullCatalogText(capture) {
1270
1283
  return catalogText(renderCatalog(capture, capture.entries));
1271
1284
  }
1272
- function sessionCatalogCapture(agent) {
1285
+ function captureOfFrame(frame) {
1286
+ return {
1287
+ text: frame.text,
1288
+ source: {
1289
+ kind: "skill-catalog",
1290
+ form: "catalog",
1291
+ ...frame.update ? { update: true } : {},
1292
+ entries: frame.entries
1293
+ },
1294
+ entries: frame.entries,
1295
+ update: frame.update
1296
+ };
1297
+ }
1298
+ function sessionCatalogCapture(agent, history) {
1299
+ if (history !== void 0 && history.catalogs.length > 0) {
1300
+ let full;
1301
+ let projected;
1302
+ for (const frame of history.catalogs) {
1303
+ const capture = captureOfFrame(frame);
1304
+ projected = capture;
1305
+ if (catalogText({ content: [{
1306
+ type: "text",
1307
+ text: frame.text
1308
+ }] }) === fullCatalogText(capture)) full = capture;
1309
+ }
1310
+ return full ?? projected;
1311
+ }
1273
1312
  const readEvents = agent.session.readEvents;
1274
1313
  if (readEvents === void 0) return void 0;
1314
+ let full;
1275
1315
  let projected;
1276
1316
  for (const event of readEvents()) {
1277
1317
  if (event.type !== "user/message" || typeof event.data !== "object" || event.data === null) continue;
@@ -1280,9 +1320,27 @@ function sessionCatalogCapture(agent) {
1280
1320
  const capture = captureCatalog(message);
1281
1321
  if (capture === void 0) continue;
1282
1322
  projected = capture;
1283
- if (catalogText(message) === fullCatalogText(capture)) return capture;
1323
+ if (catalogText(message) === fullCatalogText(capture)) full = capture;
1284
1324
  }
1285
- return projected;
1325
+ return full ?? projected;
1326
+ }
1327
+ /** The full platform catalog carried by a (possibly projected) capture. */
1328
+ function fullCaptureOf(capture) {
1329
+ return captureCatalog(renderCatalog(capture, capture.entries));
1330
+ }
1331
+ /** Digest of every projection context already present in session history. */
1332
+ function sessionProjectionDigests(agent, history) {
1333
+ const digests = new Set(history?.digests ?? []);
1334
+ const readEvents = agent.session.readEvents;
1335
+ if (readEvents === void 0) return digests;
1336
+ for (const event of readEvents()) {
1337
+ if (event.type !== "user/message" || typeof event.data !== "object" || event.data === null) continue;
1338
+ const message = event.data;
1339
+ if (message.source.kind !== "evolution-skill-projection") continue;
1340
+ const digest = message.source.digest;
1341
+ if (typeof digest === "string") digests.add(digest);
1342
+ }
1343
+ return digests;
1286
1344
  }
1287
1345
  /** Per-view projection runtime; no global readiness and no history edits. */
1288
1346
  var ProjectionRuntime = class {
@@ -1317,9 +1375,12 @@ var ProjectionRuntime = class {
1317
1375
  }
1318
1376
  async restore(agent, viewKey, decision, currentCapture) {
1319
1377
  const record = await this.ports.journal.latest(String(agent.id), viewKey, this.ports.codecVersion);
1320
- const sessionBase = sessionCatalogCapture(agent);
1321
- const fullSession = sessionBase !== void 0 && sessionBase.text === fullCatalogText(sessionBase) ? sessionBase : void 0;
1322
- const durable = currentCapture !== void 0 && currentCapture.text !== fullCatalogText(currentCapture) ? fullSession ?? record?.base : currentCapture ?? fullSession ?? record?.base;
1378
+ const history = this.ports.history?.(agent);
1379
+ const sessionBase = sessionCatalogCapture(agent, history);
1380
+ const fullSession = sessionBase === void 0 ? void 0 : sessionBase.text === fullCatalogText(sessionBase) ? sessionBase : fullCaptureOf(sessionBase);
1381
+ const projectedCurrent = currentCapture !== void 0 && currentCapture.text !== fullCatalogText(currentCapture);
1382
+ const fullCurrent = currentCapture === void 0 ? void 0 : fullCaptureOf(currentCapture);
1383
+ const durable = projectedCurrent ? record?.base ?? fullSession ?? fullCurrent : fullCurrent ?? fullSession ?? record?.base;
1323
1384
  if (durable === void 0) {
1324
1385
  this.mark(agent, "blocked", "");
1325
1386
  return decision;
@@ -1328,7 +1389,7 @@ var ProjectionRuntime = class {
1328
1389
  const previous = this.report(agent);
1329
1390
  if (previous.action === "restored" && previous.content === fullText) return decision;
1330
1391
  const restored = currentCapture === void 0 ? applyRestoreUpdate(decision, durable) : restoreCapturedProjection(decision, durable);
1331
- if (record !== void 0) await this.ports.journal.remove(record.sessionId, record.viewKey, record.projectionDigest, record.codecVersion);
1392
+ await this.ports.journal.removeView(String(agent.id), viewKey, this.ports.codecVersion);
1332
1393
  this.mark(agent, "restored", record?.projectionDigest ?? "", fullText);
1333
1394
  return restored;
1334
1395
  }
@@ -1398,9 +1459,18 @@ var ProjectionRuntime = class {
1398
1459
  if (this.ports.isFrozen !== void 0 && await this.ports.isFrozen(agent)) return decision;
1399
1460
  const { view, snapshot, policy, policyDigest, state } = await this.readState(agent, signal);
1400
1461
  const currentCapture = captureDecisionCatalog(decision);
1462
+ const history = this.ports.history?.(agent);
1463
+ const seen = new Set(sessionProjectionDigests(agent, history));
1464
+ if (decision.kind === "enter") for (const message of decision.messages) {
1465
+ if (message.source.kind !== "evolution-skill-projection") continue;
1466
+ const digest = message.source.digest;
1467
+ if (typeof digest === "string") seen.add(digest);
1468
+ }
1469
+ const previous = this.report(agent);
1470
+ if (previous.action === "applied" && previous.digest !== "") seen.add(previous.digest);
1401
1471
  let capture = currentCapture;
1402
1472
  if (capture === void 0) capture = (await this.ports.journal.latest(String(agent.id), view.viewKey, this.ports.codecVersion))?.base;
1403
- if (capture === void 0) capture = sessionCatalogCapture(agent);
1473
+ if (capture === void 0) capture = sessionCatalogCapture(agent, history);
1404
1474
  if (!canProject(state) || snapshot === void 0) return this.restore(agent, view.viewKey, decision, currentCapture);
1405
1475
  if (capture === void 0) {
1406
1476
  this.mark(agent, "blocked", "");
@@ -1431,8 +1501,8 @@ var ProjectionRuntime = class {
1431
1501
  });
1432
1502
  if (!projection.complete) return this.restore(agent, view.viewKey, decision, currentCapture);
1433
1503
  const expectedText = renderCatalog(capture, projection.catalogEntries).content.map((block) => block.type === "text" ? block.text : "").join("");
1434
- if (currentCapture !== void 0 && currentCapture.text === expectedText) return decision;
1435
- const previous = this.report(agent);
1504
+ if (currentCapture !== void 0 && currentCapture.text === expectedText) return seen.has(projection.digest) ? decision : applyProjectedDecision(decision, currentCapture, projection, seen);
1505
+ if (currentCapture === void 0 && seen.has(projection.digest)) return decision;
1436
1506
  if (currentCapture === void 0 && previous.action === "applied" && previous.digest === projection.digest && previous.content === expectedText) return decision;
1437
1507
  await this.ports.journal.put({
1438
1508
  sessionId: String(agent.id),
@@ -1445,7 +1515,7 @@ var ProjectionRuntime = class {
1445
1515
  createdAt: Date.now()
1446
1516
  });
1447
1517
  this.mark(agent, "applied", projection.digest, expectedText);
1448
- return currentCapture === void 0 ? applyReplacementDecision(decision, capture.source.entries, projection) : applyProjectedDecision(decision, capture, projection);
1518
+ return currentCapture === void 0 ? applyReplacementDecision(decision, capture.source.entries, projection, seen) : applyProjectedDecision(decision, capture, projection, seen);
1449
1519
  }
1450
1520
  };
1451
1521
  //#endregion
@@ -2521,6 +2591,29 @@ function nextMapVersion(version) {
2521
2591
  return Number.isSafeInteger(numeric) && numeric >= 0 ? String(numeric + 1) : version + ".1";
2522
2592
  }
2523
2593
  /**
2594
+ * Mark aged fresh descriptions stale so the refresh pass has a producer.
2595
+ * @param store - domain map store.
2596
+ * @param maxAgeHours - age threshold; undefined disables the producer.
2597
+ * @param now - clock for deterministic tests.
2598
+ * @returns number of domains marked stale.
2599
+ */
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
+ /**
2524
2617
  * Re-render stale domain descriptions from their definition fields.
2525
2618
  * @param store - domain map store.
2526
2619
  * @returns number of refreshed domains.
@@ -2604,6 +2697,7 @@ function searchUnavailable() {
2604
2697
  }
2605
2698
  /** Apply the full production discovery composition. */
2606
2699
  function apply(ctx, config) {
2700
+ installEvolutionProjections(ctx);
2607
2701
  if (typeof config.featureEpoch !== "string" || config.featureEpoch.length === 0) throw new Error("evolution-skill-discovery: featureEpoch is required");
2608
2702
  const evolutionIo = ctx.evolutionIo;
2609
2703
  const io = evolutionIoAdapter(() => evolutionIo.provider());
@@ -2629,7 +2723,7 @@ function apply(ctx, config) {
2629
2723
  ...config.generationAttempts === void 0 ? {} : { attempts: config.generationAttempts }
2630
2724
  });
2631
2725
  const searchCache = new SkillSearchCache();
2632
- let searchHealthy = true;
2726
+ let searchHealthy = false;
2633
2727
  const search = {
2634
2728
  async allowedSkillKeys(agent, signal) {
2635
2729
  if (mode === "off" || agent === void 0) return void 0;
@@ -2690,6 +2784,7 @@ function apply(ctx, config) {
2690
2784
  });
2691
2785
  if (mode === "off") return;
2692
2786
  const map = new DomainMapFileStore(io, config.domainMapPath ?? join(root, "evolution", "domain-map.json"));
2787
+ const descriptionStaleAfterHours = config.descriptionStaleAfterHours ?? 168;
2693
2788
  const journal = new ProjectionJournal(io, config.projectionJournalPath ?? join(root, "evolution", "projection-journal.json"));
2694
2789
  const snapshots = new ViewSnapshotStore(io, config.viewSnapshotPath ?? join(root, "evolution", "view-snapshots.json"));
2695
2790
  const globalPolicy = {
@@ -2721,6 +2816,7 @@ function apply(ctx, config) {
2721
2816
  ...scope === void 0 ? {} : { scope }
2722
2817
  });
2723
2818
  };
2819
+ const projections = ctx.get("sessionProjections");
2724
2820
  const runtime = new ProjectionRuntime({
2725
2821
  skillIndexSource: source,
2726
2822
  map,
@@ -2728,6 +2824,7 @@ function apply(ctx, config) {
2728
2824
  policyFor,
2729
2825
  journal,
2730
2826
  workingDirectory,
2827
+ history: (agent) => projections?.stateOf(agent.session, "evolutionProjectionHistory"),
2731
2828
  featureEpoch: config.featureEpoch,
2732
2829
  codecVersion: "1",
2733
2830
  adapterVersion: "1",
@@ -2737,7 +2834,7 @@ function apply(ctx, config) {
2737
2834
  ...config.tokenBudget === void 0 ? {} : { tokenBudget: config.tokenBudget },
2738
2835
  searchAvailable: () => searchHealthy && config.searchEnabled !== false,
2739
2836
  ...config.staleAfterHours === void 0 ? {} : { staleAfterHours: config.staleAfterHours },
2740
- ...config.descriptionStaleAfterHours === void 0 ? {} : { descriptionStaleAfterHours: config.descriptionStaleAfterHours },
2837
+ descriptionStaleAfterHours,
2741
2838
  isFrozen: async (agent) => snapshots.isFrozen(String(agent.session.id), config.featureEpoch)
2742
2839
  });
2743
2840
  const queue = new JsonAssignmentQueue(io, config.assignmentQueuePath ?? join(root, "evolution", "assignment-queue.json"), ctx.get("evolutionStateStore"));
@@ -2779,6 +2876,8 @@ function apply(ctx, config) {
2779
2876
  featureEpoch: config.featureEpoch,
2780
2877
  createdAt: Date.now()
2781
2878
  });
2879
+ const marked = await markStaleDescriptions(map, descriptionStaleAfterHours);
2880
+ if (marked > 0) ctx.logger.info("evolution-skill-discovery: marked " + marked + " aged domain descriptions stale");
2782
2881
  const refreshed = await refreshStaleDescriptions(map);
2783
2882
  if (refreshed > 0) ctx.logger.info("evolution-skill-discovery: refreshed " + refreshed + " stale domain descriptions");
2784
2883
  } catch (error) {
@@ -1,4 +1,12 @@
1
1
  import type { DomainMapStore } from './domain-map.ts';
2
+ /**
3
+ * Mark aged fresh descriptions stale so the refresh pass has a producer.
4
+ * @param store - domain map store.
5
+ * @param maxAgeHours - age threshold; undefined disables the producer.
6
+ * @param now - clock for deterministic tests.
7
+ * @returns number of domains marked stale.
8
+ */
9
+ export declare function markStaleDescriptions(store: DomainMapStore, maxAgeHours: number | undefined, now?: number): Promise<number>;
2
10
  /**
3
11
  * Re-render stale domain descriptions from their definition fields.
4
12
  * @param store - domain map store.
@@ -18,6 +18,7 @@ export * from './domain-map-store.ts';
18
18
  export * from './domain-policy.ts';
19
19
  export * from './domain-generation.ts';
20
20
  export * from './domain-description.ts';
21
+ export * from './description-refresh.ts';
21
22
  export * from './domain-quality.ts';
22
23
  export * from './domain-projection.ts';
23
24
  export * from './tool.ts';
@@ -37,7 +37,7 @@ export declare function restoreCapturedProjection(decision: PreStepDecision, cap
37
37
  * @param result - projection result.
38
38
  * @returns decision with replacement catalog plus optional context.
39
39
  */
40
- export declare function applyReplacementDecision(decision: PreStepDecision, sourceEntries: readonly CatalogEntry[], result: ProjectionResult): PreStepDecision;
40
+ export declare function applyReplacementDecision(decision: PreStepDecision, sourceEntries: readonly CatalogEntry[], result: ProjectionResult, seen?: ReadonlySet<string> | undefined): PreStepDecision;
41
41
  /**
42
42
  * Append a full replacement catalog without a current catalog message.
43
43
  * @param decision - current pre-step decision.
@@ -56,5 +56,5 @@ export declare function renderProjectionContext(result: ProjectionResult): UserM
56
56
  * @param result - complete projection.
57
57
  * @returns new decision; unchanged when no catalog exists.
58
58
  */
59
- export declare function applyProjectedDecision(decision: PreStepDecision, capture: CatalogCapture, result: ProjectionResult): PreStepDecision;
59
+ export declare function applyProjectedDecision(decision: PreStepDecision, capture: CatalogCapture, result: ProjectionResult, seen?: ReadonlySet<string> | undefined): PreStepDecision;
60
60
  //# sourceMappingURL=projection-adapter.d.ts.map
@@ -30,5 +30,7 @@ export declare class ProjectionJournal {
30
30
  latest(sessionId: string, viewKey: string, codecVersion: string): Promise<ProjectionJournalRecord | undefined>;
31
31
  /** Delete one record after a successful restore. */
32
32
  remove(sessionId: string, viewKey: string, projectionDigest: string, codecVersion: string): Promise<void>;
33
+ /** Delete every record for one session/view after a successful restore. */
34
+ removeView(sessionId: string, viewKey: string, _codecVersion: string): Promise<void>;
33
35
  }
34
36
  //# sourceMappingURL=projection-journal.d.ts.map
@@ -8,6 +8,18 @@ import { type ViewState } from './view-state.ts';
8
8
  export interface WorkingDirectoryPort {
9
9
  ensure(agent: Agent, signal?: AbortSignal | undefined): Promise<string>;
10
10
  }
11
+ /** Bounded catalog/projection history from the evolution-core projection unit. */
12
+ export interface ProjectionHistoryView {
13
+ readonly catalogs: readonly {
14
+ readonly text: string;
15
+ readonly entries: readonly {
16
+ readonly name: string;
17
+ readonly description: string;
18
+ }[];
19
+ readonly update: boolean;
20
+ }[];
21
+ readonly digests: readonly string[];
22
+ }
11
23
  /** Inputs for the per-view projection runtime. */
12
24
  export interface ProjectionRuntimePorts {
13
25
  readonly skillIndexSource: SkillIndexSource;
@@ -16,6 +28,7 @@ export interface ProjectionRuntimePorts {
16
28
  readonly policyFor?: ((cwd: string) => Promise<DomainPolicyStore>) | undefined;
17
29
  readonly journal: ProjectionJournal;
18
30
  readonly workingDirectory: WorkingDirectoryPort;
31
+ readonly history?: ((agent: Agent) => ProjectionHistoryView | undefined) | undefined;
19
32
  readonly featureEpoch: string;
20
33
  readonly codecVersion: string;
21
34
  readonly adapterVersion: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-skill-discovery",
3
3
  "description": "Read model and view identity for evolution skill discovery (community build)",
4
- "version": "0.32.2",
4
+ "version": "0.32.3",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -32,9 +32,9 @@
32
32
  "@deepseek-ai/dsh-llm": "^0.2.1-rc.2",
33
33
  "@deepseek-ai/dsh-tools": "^0.2.1-rc.2",
34
34
  "@deepseek-ai/dsh-system-prompt": "^0.2.1-rc.2",
35
- "@lmzhen/dsh-evolution-contract": "^0.32.2",
36
- "@lmzhen/dsh-evolution-core": "^0.32.2",
37
- "@lmzhen/dsh-evolution-io": "^0.32.2"
35
+ "@lmzhen/dsh-evolution-contract": "^0.32.3",
36
+ "@lmzhen/dsh-evolution-core": "^0.32.3",
37
+ "@lmzhen/dsh-evolution-io": "^0.32.3"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@deepseek-ai/dsh-skill": "^0.2.1-rc.2",
@@ -44,9 +44,9 @@
44
44
  "@deepseek-ai/dsh-agent-loop-testkit": "^0.2.1-rc.2",
45
45
  "@deepseek-ai/dsh-system-prompt": "^0.2.1-rc.2",
46
46
  "@deepseek-ai/dsh-tools": "^0.2.1-rc.2",
47
- "@lmzhen/dsh-evolution-contract": "^0.32.2",
48
- "@lmzhen/dsh-evolution-core": "^0.32.2",
49
- "@lmzhen/dsh-evolution-io": "^0.32.2"
47
+ "@lmzhen/dsh-evolution-contract": "^0.32.3",
48
+ "@lmzhen/dsh-evolution-core": "^0.32.3",
49
+ "@lmzhen/dsh-evolution-io": "^0.32.3"
50
50
  },
51
51
  "dependencies": {
52
52
  "@deepseek-ai/dsh-llm": "^0.2.1-rc.2",