@evomap/evolver-core 2.0.0-beta.19 → 2.0.0-beta.22

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.
Files changed (160) hide show
  1. package/assets/gep/genes.jsonl +5 -5
  2. package/dist/algo/bans.d.ts +10 -1
  3. package/dist/algo/bans.js +56 -6
  4. package/dist/algo/candidateAssembly.d.ts +25 -2
  5. package/dist/algo/candidateAssembly.js +143 -20
  6. package/dist/algo/capabilityCandidates.js +10 -0
  7. package/dist/algo/cycleEngine.d.ts +40 -5
  8. package/dist/algo/cycleEngine.js +277 -37
  9. package/dist/algo/evolutionEvent.d.ts +26 -0
  10. package/dist/algo/evolutionEvent.js +32 -0
  11. package/dist/algo/exploration.d.ts +7 -0
  12. package/dist/algo/exploration.js +16 -3
  13. package/dist/algo/geneHealth.d.ts +34 -1
  14. package/dist/algo/geneHealth.js +42 -0
  15. package/dist/algo/geneIntake.d.ts +46 -1
  16. package/dist/algo/geneIntake.js +121 -10
  17. package/dist/algo/geneSelection.d.ts +115 -6
  18. package/dist/algo/geneSelection.js +459 -32
  19. package/dist/algo/index.d.ts +4 -1
  20. package/dist/algo/index.js +4 -1
  21. package/dist/algo/kautoProjection.d.ts +41 -0
  22. package/dist/algo/kautoProjection.js +95 -0
  23. package/dist/algo/kautoValidator.d.ts +68 -0
  24. package/dist/algo/kautoValidator.js +256 -0
  25. package/dist/algo/orchestrator.d.ts +14 -1
  26. package/dist/algo/orchestrator.js +9 -2
  27. package/dist/algo/solidify.d.ts +11 -2
  28. package/dist/algo/solidify.js +37 -7
  29. package/dist/algo/ucb1.d.ts +53 -0
  30. package/dist/algo/ucb1.js +156 -0
  31. package/dist/assetrepair/hubRejection.d.ts +12 -0
  32. package/dist/assetrepair/hubRejection.js +109 -0
  33. package/dist/assetrepair/index.d.ts +2 -0
  34. package/dist/assetrepair/index.js +2 -0
  35. package/dist/assetrepair/repair.d.ts +33 -0
  36. package/dist/assetrepair/repair.js +155 -0
  37. package/dist/assetstore/assetSidecarRecords.js +5 -1
  38. package/dist/assetstore/assetStoreHealth.d.ts +2 -3
  39. package/dist/assetstore/assetStoreHealth.js +4 -10
  40. package/dist/assetstore/foreignJsonlSource.d.ts +48 -0
  41. package/dist/assetstore/foreignJsonlSource.js +150 -0
  42. package/dist/assetstore/index.d.ts +3 -0
  43. package/dist/assetstore/index.js +3 -0
  44. package/dist/assetstore/learningHistory.js +3 -3
  45. package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
  46. package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
  47. package/dist/assetstore/localJsonl.d.ts +8 -0
  48. package/dist/assetstore/localJsonl.js +128 -3
  49. package/dist/assetstore/provenance.d.ts +38 -3
  50. package/dist/assetstore/provenance.js +103 -45
  51. package/dist/assetstore/provider.d.ts +4 -0
  52. package/dist/assetstore/reviewFilter.d.ts +19 -1
  53. package/dist/assetstore/reviewFilter.js +36 -0
  54. package/dist/assetstore/unionReadStore.d.ts +25 -0
  55. package/dist/assetstore/unionReadStore.js +119 -0
  56. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  57. package/dist/benchmark/antiGeneBenchmark.js +4 -3
  58. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  59. package/dist/benchmark/antiGeneRollout.js +4 -3
  60. package/dist/benchmark/index.d.ts +2 -1
  61. package/dist/benchmark/index.js +2 -1
  62. package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
  63. package/dist/benchmark/selectionFlatAbstention.js +481 -0
  64. package/dist/bootstrap/envFingerprint.d.ts +9 -0
  65. package/dist/bootstrap/envFingerprint.js +5 -0
  66. package/dist/bootstrap/index.d.ts +2 -1
  67. package/dist/bootstrap/index.js +2 -1
  68. package/dist/bootstrap/lifecycleBootstrap.d.ts +111 -0
  69. package/dist/bootstrap/lifecycleBootstrap.js +433 -0
  70. package/dist/bootstrap/v1EnvCompat.d.ts +3 -0
  71. package/dist/bootstrap/v1EnvCompat.js +48 -1
  72. package/dist/events/eventArchive.d.ts +2 -0
  73. package/dist/events/eventArchive.js +13 -3
  74. package/dist/events/eventSchema.d.ts +7 -7
  75. package/dist/events/eventStore.d.ts +2 -0
  76. package/dist/events/eventStore.js +5 -1
  77. package/dist/events/ingest.d.ts +2 -1
  78. package/dist/events/ingest.js +6 -0
  79. package/dist/exec/autoExec.d.ts +30 -3
  80. package/dist/exec/autoExec.js +294 -29
  81. package/dist/exec/autonomousCycle.d.ts +7 -0
  82. package/dist/exec/autonomousCycle.js +2 -0
  83. package/dist/exec/claudeBridge.d.ts +28 -6
  84. package/dist/exec/claudeBridge.js +411 -258
  85. package/dist/exec/executionBinding.d.ts +414 -0
  86. package/dist/exec/executionBinding.js +588 -0
  87. package/dist/exec/index.d.ts +1 -0
  88. package/dist/exec/index.js +1 -0
  89. package/dist/exec/proofOfWork.d.ts +1 -1
  90. package/dist/exec/proofOfWork.js +2 -2
  91. package/dist/exec/runnerRegistry.d.ts +5 -0
  92. package/dist/exec/runnerRegistry.js +54 -14
  93. package/dist/hub/capability.d.ts +40 -1
  94. package/dist/hub/conversationDistiller.d.ts +19 -0
  95. package/dist/hub/conversationDistiller.js +115 -37
  96. package/dist/hub/fake.d.ts +1 -0
  97. package/dist/hub/fake.js +1 -0
  98. package/dist/hub/index.d.ts +1 -0
  99. package/dist/hub/index.js +1 -0
  100. package/dist/hub/recipeCompose.d.ts +27 -0
  101. package/dist/hub/recipeCompose.js +90 -0
  102. package/dist/hub/sanitize.js +119 -5
  103. package/dist/index.d.ts +2 -0
  104. package/dist/index.js +2 -0
  105. package/dist/mailbox/ipcServer.d.ts +2 -0
  106. package/dist/mailbox/ipcServer.js +20 -0
  107. package/dist/mailbox/store.d.ts +52 -2
  108. package/dist/mailbox/store.js +546 -81
  109. package/dist/modelCompatibility.d.ts +164 -0
  110. package/dist/modelCompatibility.js +309 -0
  111. package/dist/observers/valueDigestObserver.d.ts +9 -0
  112. package/dist/observers/valueDigestObserver.js +35 -2
  113. package/dist/ops/cleanup.js +1 -1
  114. package/dist/ops/evolutionGraphProjection.d.ts +20 -0
  115. package/dist/ops/evolutionGraphProjection.js +315 -0
  116. package/dist/ops/index.d.ts +2 -1
  117. package/dist/ops/index.js +2 -1
  118. package/dist/ops/selfUpdate.d.ts +8 -0
  119. package/dist/ops/selfUpdate.js +24 -8
  120. package/dist/ops/valueOutreach.d.ts +3 -1
  121. package/dist/ops/valueOutreach.js +5 -1
  122. package/dist/personality/schema.d.ts +18 -18
  123. package/dist/schema/evolutionGraph.d.ts +784 -0
  124. package/dist/schema/evolutionGraph.js +187 -0
  125. package/dist/schema/index.d.ts +1 -0
  126. package/dist/schema/index.js +1 -0
  127. package/dist/schema/proofOfWork.d.ts +125 -6
  128. package/dist/schema/proofOfWork.js +102 -4
  129. package/dist/shadow/shadowHub.js +1 -0
  130. package/dist/signals/expand.d.ts +15 -1
  131. package/dist/signals/expand.js +148 -0
  132. package/dist/signals/index.d.ts +3 -1
  133. package/dist/signals/index.js +3 -1
  134. package/dist/signals/scopeVocabulary.d.ts +75 -0
  135. package/dist/signals/scopeVocabulary.js +91 -0
  136. package/dist/signals/taskDomain.d.ts +22 -0
  137. package/dist/signals/taskDomain.js +43 -0
  138. package/dist/trace/learningTrace.d.ts +7 -0
  139. package/dist/trace/learningTrace.js +22 -0
  140. package/dist/trace/trajectory.d.ts +8 -0
  141. package/dist/trace/trajectory.js +14 -2
  142. package/dist/util/fileLock.d.ts +92 -5
  143. package/dist/util/fileLock.js +809 -70
  144. package/dist/util/index.d.ts +2 -1
  145. package/dist/util/index.js +1 -1
  146. package/dist/verify/index.d.ts +2 -1
  147. package/dist/verify/index.js +1 -1
  148. package/dist/verify/sandboxRunner.d.ts +2 -0
  149. package/dist/verify/sandboxRunner.js +154 -10
  150. package/dist/verify/sandboxedValidation.d.ts +7 -2
  151. package/dist/verify/sandboxedValidation.js +102 -24
  152. package/dist/verify/validation.d.ts +24 -3
  153. package/dist/verify/validation.js +141 -24
  154. package/dist/wire/geneHints.d.ts +88 -16
  155. package/dist/wire/geneHints.js +124 -15
  156. package/dist/wire/index.d.ts +8 -3
  157. package/dist/wire/index.js +2 -2
  158. package/dist/wire/schemaGate.d.ts +21 -0
  159. package/dist/wire/schemaGate.js +119 -10
  160. package/package.json +5 -4
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  // Signal expansion & tag classification (ported from v1 src/gep/learningSignals.js).
2
3
  // Raw signals (e.g. '429') are expanded into broader semantic tags (e.g. 'problem:reliability',
3
4
  // 'action:repair') so gene selection can match by MEANING, not just literal string intersection —
@@ -41,6 +42,18 @@ const EXPANSION_RULES = [
41
42
  { re: /(tool_bypass|tool_loop|工具绕过|工具循环|ツール迂回|ツールループ|도구우회|도구반복)/, tags: ['area:orchestration', 'risk:validation'] },
42
43
  { re: /(memory|narrative|reflection)/, tags: ['area:memory'] },
43
44
  { re: /(skill|dashboard)/, tags: ['area:skills'] },
45
+ // Harness context budget (v1 context-compression gene family port): an agent harness that injects large
46
+ // tool/MCP schemas, long skill manuals, agent-type descriptions, a memory index, or a pasted transcript can
47
+ // exhaust the model's context window before the task starts. That is a prompt-assembly (area:prompt) cost
48
+ // problem, and shrinking it is an optimize action — NOT a reliability failure. v2 has no seed-gene catalog,
49
+ // so these rules ARE the shared vocabulary that lets a context-bloat signal reach a compression strategy.
50
+ { re: /(context_bloat|context_explosion|context window|token_budget|prompt_budget|上下文爆|上下文预算|token 超限|token超限)/, tags: ['problem:context_budget', 'action:optimize', 'area:prompt'] },
51
+ // Tool/MCP schema weight is the always-on half of the budget; lazy-loading it is the corrective action.
52
+ { re: /(tool_schema|schema_bloat|mcp_tool_schema|lazy_load_schema|just-in-time schema|工具 schema|工具schema)/, tags: ['problem:context_budget', 'action:optimize', 'area:prompt', 'area:orchestration'] },
53
+ // Skill manuals / agent-type descriptions are the other always-on half (area:skills already exists above).
54
+ { re: /(skill_list_bloat|skill_manual_bloat|agent type description|mcp server instructions|skill 列表太长|技能列表太长)/, tags: ['problem:context_budget', 'action:optimize', 'area:skills', 'area:prompt'] },
55
+ // A pasted transcript / session handoff is the caller-supplied half — compressing it is a memory-shaped concern.
56
+ { re: /(transcript_bloat|transcript_context_bloat|conversation_handoff|session handoff|pasted transcript|会话上下文就爆|完整转录)/, tags: ['problem:context_budget', 'action:optimize', 'area:memory'] },
44
57
  { re: /(validation|canary|rollback|constraint|blast radius|destructive)/, tags: ['risk:validation'] },
45
58
  ];
46
59
  /**
@@ -142,4 +155,139 @@ export function tagOverlapScore(signals, gene) {
142
155
  if (geneTagSet.has(t))
143
156
  hits++;
144
157
  return hits / sigTags.length;
158
+ }
159
+ const SEMANTIC_IDF_DOCUMENT_LIMIT = 1_000;
160
+ const SEMANTIC_IDF_TEXT_CHARS_PER_DOCUMENT = 4_096;
161
+ const SEMANTIC_IDF_TAG_INPUTS_PER_DOCUMENT = 256;
162
+ const SEMANTIC_IDF_TAG_CHARS = 1_024;
163
+ const SEMANTIC_IDF_TAGS_PER_DOCUMENT = 64;
164
+ const SEMANTIC_IDF_TOKENS_PER_DOCUMENT = 128;
165
+ const SEMANTIC_IDF_TAG_VOCABULARY_LIMIT = 8_192;
166
+ const SEMANTIC_IDF_TOKEN_VOCABULARY_LIMIT = 32_768;
167
+ function normalizeSemanticTag(tag) {
168
+ return tag.normalize('NFKC').trim().toLowerCase();
169
+ }
170
+ function inverseDocumentFrequency(documentCount, documentFrequency) {
171
+ return Math.log(documentCount / documentFrequency);
172
+ }
173
+ function tokenFrequency(text) {
174
+ const frequencies = new Map();
175
+ for (const token of tokenize(text)) {
176
+ frequencies.set(token, (frequencies.get(token) ?? 0) + 1);
177
+ }
178
+ return frequencies;
179
+ }
180
+ function compareCodeUnits(left, right) {
181
+ return left < right ? -1 : left > right ? 1 : 0;
182
+ }
183
+ function boundedUnique(values, limit) {
184
+ return [...new Set(values.filter(Boolean))].sort(compareCodeUnits).slice(0, limit);
185
+ }
186
+ function boundedVocabulary(valuesByDocument, limit) {
187
+ const vocabulary = new Set();
188
+ for (const values of valuesByDocument) {
189
+ for (const value of values)
190
+ vocabulary.add(value);
191
+ }
192
+ // Selecting by code-unit order makes the cap independent of corpus/document order. The source set is itself
193
+ // bounded by the per-document and document-count limits above, so this sort cannot grow without bound.
194
+ return new Set([...vocabulary].sort(compareCodeUnits).slice(0, limit));
195
+ }
196
+ function semanticProfileVersion(documentCount, tagDocumentFrequency, tokenDocumentFrequency) {
197
+ // Hash only normalized document-frequency entries so decision telemetry never contains plaintext corpus data.
198
+ // A 128-bit SHA-256 prefix keeps the identifier compact while avoiding the collision rate of a 32-bit checksum.
199
+ const hash = createHash('sha256');
200
+ hash.update(`documents:${documentCount};`, 'utf8');
201
+ for (const [tag, frequency] of [...tagDocumentFrequency].sort(([left], [right]) => compareCodeUnits(left, right))) {
202
+ hash.update(`tag:${tag.length}:${tag}:${frequency};`, 'utf8');
203
+ }
204
+ for (const [token, frequency] of [...tokenDocumentFrequency].sort(([left], [right]) => compareCodeUnits(left, right))) {
205
+ hash.update(`token:${token.length}:${token}:${frequency};`, 'utf8');
206
+ }
207
+ return `idf-2:n=${documentCount};tags=${tagDocumentFrequency.size};tokens=${tokenDocumentFrequency.size};sha256=${hash.digest('hex').slice(0, 32)}`;
208
+ }
209
+ export function buildSemanticIdfProfile(documents) {
210
+ const boundedDocuments = documents.slice(0, SEMANTIC_IDF_DOCUMENT_LIMIT).map((document) => ({
211
+ tags: boundedUnique(document.tags
212
+ .slice(0, SEMANTIC_IDF_TAG_INPUTS_PER_DOCUMENT)
213
+ .map((tag) => normalizeSemanticTag(tag.slice(0, SEMANTIC_IDF_TAG_CHARS))), SEMANTIC_IDF_TAGS_PER_DOCUMENT),
214
+ tokens: boundedUnique(tokenize(document.text.slice(0, SEMANTIC_IDF_TEXT_CHARS_PER_DOCUMENT)), SEMANTIC_IDF_TOKENS_PER_DOCUMENT),
215
+ }));
216
+ const tagVocabulary = boundedVocabulary(boundedDocuments.map((document) => document.tags), SEMANTIC_IDF_TAG_VOCABULARY_LIMIT);
217
+ const tokenVocabulary = boundedVocabulary(boundedDocuments.map((document) => document.tokens), SEMANTIC_IDF_TOKEN_VOCABULARY_LIMIT);
218
+ const tagDocumentFrequency = new Map();
219
+ const tokenDocumentFrequency = new Map();
220
+ for (const document of boundedDocuments) {
221
+ for (const tag of document.tags) {
222
+ if (!tagVocabulary.has(tag))
223
+ continue;
224
+ tagDocumentFrequency.set(tag, (tagDocumentFrequency.get(tag) ?? 0) + 1);
225
+ }
226
+ for (const token of document.tokens) {
227
+ if (!tokenVocabulary.has(token))
228
+ continue;
229
+ tokenDocumentFrequency.set(token, (tokenDocumentFrequency.get(token) ?? 0) + 1);
230
+ }
231
+ }
232
+ const documentCount = boundedDocuments.length;
233
+ const tagIdf = new Map();
234
+ const tokenIdf = new Map();
235
+ for (const [tag, frequency] of tagDocumentFrequency) {
236
+ tagIdf.set(tag, inverseDocumentFrequency(documentCount, frequency));
237
+ }
238
+ for (const [token, frequency] of tokenDocumentFrequency) {
239
+ tokenIdf.set(token, inverseDocumentFrequency(documentCount, frequency));
240
+ }
241
+ return {
242
+ documentCount,
243
+ tagIdf,
244
+ tokenIdf,
245
+ version: semanticProfileVersion(documentCount, tagDocumentFrequency, tokenDocumentFrequency),
246
+ };
247
+ }
248
+ export function idfTagOverlapScore(signals, gene, profile) {
249
+ if (profile.documentCount <= 1)
250
+ return tagOverlapScore(signals, gene);
251
+ const signalTags = [...new Set(expandSignals(signals).map(normalizeSemanticTag))];
252
+ const geneTagSet = new Set(geneTags(gene).map(normalizeSemanticTag));
253
+ let matchedWeight = 0;
254
+ let totalWeight = 0;
255
+ for (const tag of signalTags) {
256
+ const weight = profile.tagIdf.get(tag);
257
+ if (weight === undefined)
258
+ continue;
259
+ totalWeight += weight;
260
+ if (geneTagSet.has(tag))
261
+ matchedWeight += weight;
262
+ }
263
+ return totalWeight > 0 ? matchedWeight / totalWeight : 0;
264
+ }
265
+ export function idfBagCosine(a, b, profile) {
266
+ if (profile.documentCount <= 1)
267
+ return bagCosine(a, b);
268
+ const bagA = tokenFrequency(a);
269
+ const bagB = tokenFrequency(b);
270
+ let dot = 0;
271
+ let normA = 0;
272
+ let normB = 0;
273
+ for (const [token, count] of bagA) {
274
+ const weight = profile.tokenIdf.get(token);
275
+ if (weight === undefined)
276
+ continue;
277
+ const weightedCount = count * weight;
278
+ normA += weightedCount * weightedCount;
279
+ const otherCount = bagB.get(token);
280
+ if (otherCount !== undefined)
281
+ dot += weightedCount * otherCount * weight;
282
+ }
283
+ for (const [token, count] of bagB) {
284
+ const weight = profile.tokenIdf.get(token);
285
+ if (weight === undefined)
286
+ continue;
287
+ const weightedCount = count * weight;
288
+ normB += weightedCount * weightedCount;
289
+ }
290
+ if (normA === 0 || normB === 0)
291
+ return 0;
292
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
145
293
  }
@@ -4,4 +4,6 @@ export * from './expand.js';
4
4
  export * from './traceSignals.js';
5
5
  export * from './metaSignals.js';
6
6
  export * from './cycleHistoryFromEvents.js';
7
- export * from './curriculum.js';
7
+ export * from './curriculum.js';
8
+ export * from './taskDomain.js';
9
+ export * from './scopeVocabulary.js';
@@ -4,4 +4,6 @@ export * from './expand.js';
4
4
  export * from './traceSignals.js';
5
5
  export * from './metaSignals.js';
6
6
  export * from './cycleHistoryFromEvents.js';
7
- export * from './curriculum.js';
7
+ export * from './curriculum.js';
8
+ export * from './taskDomain.js';
9
+ export * from './scopeVocabulary.js';
@@ -0,0 +1,75 @@
1
+ import type { AssetStoreProvider, AssetRecord } from '../assetstore/provider.js';
2
+ /** A signal observed in the store, with how many assets declare it. */
3
+ export interface ScopeVocabularyEntry {
4
+ signal: string;
5
+ /** Number of Gene assets whose signals_match declares this signal (hard facets counted by their bare tag). */
6
+ assetCount: number;
7
+ }
8
+ export interface ScopeVocabulary {
9
+ /** Observed signals, most-declared first. */
10
+ entries: ScopeVocabularyEntry[];
11
+ /** Convenience set of the bare signal strings. */
12
+ signals: Set<string>;
13
+ }
14
+ /** Strip a `required:` facet prefix to get the bare signal a facet gates on. */
15
+ export declare function bareSignal(tag: string): string;
16
+ /**
17
+ * Build the scope vocabulary from a set of Gene asset records already in hand. Hard facets contribute their BARE
18
+ * signal, because `required:rounding-v2` and `rounding-v2` refer to the same scope dimension — one gates on it,
19
+ * one hints at it. Factored out of {@link deriveScopeVocabulary} so a caller that ALREADY holds the gene records
20
+ * (e.g. the distiller, which lists existing genes for dedup anyway) can build the same vocabulary without a second
21
+ * store round-trip, and so the prompt-injection path and the post-hoc resolve path share ONE derivation.
22
+ */
23
+ export declare function scopeVocabularyFromRecords(records: readonly AssetRecord[]): ScopeVocabulary;
24
+ /**
25
+ * Derive the scope vocabulary from the Gene assets in a store. Thin async wrapper over
26
+ * {@link scopeVocabularyFromRecords} that fetches the records first.
27
+ */
28
+ export declare function deriveScopeVocabulary(store: AssetStoreProvider, limit?: number): Promise<ScopeVocabulary>;
29
+ export type ScopeResolution =
30
+ /** The proposed tag names a signal that exists; usable as-is. */
31
+ {
32
+ status: 'exact';
33
+ proposed: string;
34
+ resolved: string;
35
+ }
36
+ /**
37
+ * The proposed tag does not exist, but exactly ONE observed signal is an unambiguous refinement of it — the
38
+ * autonomous-scope failure mode, where `v2` was proposed and `rounding-v2` is what exists. `resolved` is the
39
+ * real signal; a caller may adopt it, or surface it for review.
40
+ */
41
+ | {
42
+ status: 'resolved';
43
+ proposed: string;
44
+ resolved: string;
45
+ reason: string;
46
+ }
47
+ /** Several observed signals match equally well; resolving would be a guess, so we refuse to pick. */
48
+ | {
49
+ status: 'ambiguous';
50
+ proposed: string;
51
+ candidates: string[];
52
+ }
53
+ /** Nothing in the store resembles the tag. Not an error: a genuinely new scope looks like this. */
54
+ | {
55
+ status: 'unknown';
56
+ proposed: string;
57
+ };
58
+ /**
59
+ * Resolve a proposed scope tag against an observed vocabulary.
60
+ *
61
+ * Matching is deliberately conservative. Beyond an exact hit we accept only ONE relation: an observed signal that
62
+ * ends with `-<proposed>` or `_<proposed>` (or begins with `<proposed>-`/`<proposed>_`), i.e. the proposal is a
63
+ * bare qualifier and the real signal is that qualifier scoped to a domain. That is exactly the `v2` →
64
+ * `rounding-v2` shape. We do NOT do fuzzy/edit-distance matching: silently rewriting a scope key on a weak
65
+ * similarity signal would be a governance hazard far worse than an unresolved tag, since scope decides what gets
66
+ * injected into an agent's context. Multiple candidates yield `ambiguous` rather than an arbitrary pick.
67
+ */
68
+ export declare function resolveScopeTag(proposed: string, vocab: ScopeVocabulary): ScopeResolution;
69
+ /**
70
+ * Render the vocabulary as a compact, promptable list. This is what makes the vocabulary \emph{discoverable} to a
71
+ * distiller: it can be shown the signals that exist before being asked to choose a facet, instead of guessing.
72
+ * Bounded by `max` so a large store cannot blow a prompt budget; the most-declared signals come first, and the
73
+ * count is reported so a reader can tell a load-bearing scope from a one-off tag.
74
+ */
75
+ export declare function renderScopeVocabulary(vocab: ScopeVocabulary, max?: number): string;
@@ -0,0 +1,91 @@
1
+ const REQUIRED_PREFIX = 'required:';
2
+ /** Strip a `required:` facet prefix to get the bare signal a facet gates on. */
3
+ export function bareSignal(tag) {
4
+ const t = String(tag).trim();
5
+ return t.startsWith(REQUIRED_PREFIX) ? t.slice(REQUIRED_PREFIX.length).trim() : t;
6
+ }
7
+ function asStrings(v) {
8
+ return Array.isArray(v) ? v.filter((x) => typeof x === 'string') : [];
9
+ }
10
+ /**
11
+ * Build the scope vocabulary from a set of Gene asset records already in hand. Hard facets contribute their BARE
12
+ * signal, because `required:rounding-v2` and `rounding-v2` refer to the same scope dimension — one gates on it,
13
+ * one hints at it. Factored out of {@link deriveScopeVocabulary} so a caller that ALREADY holds the gene records
14
+ * (e.g. the distiller, which lists existing genes for dedup anyway) can build the same vocabulary without a second
15
+ * store round-trip, and so the prompt-injection path and the post-hoc resolve path share ONE derivation.
16
+ */
17
+ export function scopeVocabularyFromRecords(records) {
18
+ const counts = new Map();
19
+ for (const g of records) {
20
+ // One asset counts once per distinct signal, so a repeated tag within one asset cannot inflate the count.
21
+ const seen = new Set();
22
+ for (const raw of asStrings(g['signals_match'])) {
23
+ const s = bareSignal(raw);
24
+ if (!s || seen.has(s))
25
+ continue;
26
+ seen.add(s);
27
+ counts.set(s, (counts.get(s) ?? 0) + 1);
28
+ }
29
+ }
30
+ const entries = [...counts.entries()]
31
+ .map(([signal, assetCount]) => ({ signal, assetCount }))
32
+ .sort((a, b) => b.assetCount - a.assetCount || a.signal.localeCompare(b.signal));
33
+ return { entries, signals: new Set(entries.map((e) => e.signal)) };
34
+ }
35
+ /**
36
+ * Derive the scope vocabulary from the Gene assets in a store. Thin async wrapper over
37
+ * {@link scopeVocabularyFromRecords} that fetches the records first.
38
+ */
39
+ export async function deriveScopeVocabulary(store, limit = 500) {
40
+ return scopeVocabularyFromRecords(await store.list('Gene', limit));
41
+ }
42
+ /**
43
+ * Resolve a proposed scope tag against an observed vocabulary.
44
+ *
45
+ * Matching is deliberately conservative. Beyond an exact hit we accept only ONE relation: an observed signal that
46
+ * ends with `-<proposed>` or `_<proposed>` (or begins with `<proposed>-`/`<proposed>_`), i.e. the proposal is a
47
+ * bare qualifier and the real signal is that qualifier scoped to a domain. That is exactly the `v2` →
48
+ * `rounding-v2` shape. We do NOT do fuzzy/edit-distance matching: silently rewriting a scope key on a weak
49
+ * similarity signal would be a governance hazard far worse than an unresolved tag, since scope decides what gets
50
+ * injected into an agent's context. Multiple candidates yield `ambiguous` rather than an arbitrary pick.
51
+ */
52
+ export function resolveScopeTag(proposed, vocab) {
53
+ const bare = bareSignal(proposed);
54
+ if (!bare)
55
+ return { status: 'unknown', proposed };
56
+ if (vocab.signals.has(bare))
57
+ return { status: 'exact', proposed, resolved: bare };
58
+ const lower = bare.toLowerCase();
59
+ const candidates = [...vocab.signals].filter((s) => {
60
+ const sl = s.toLowerCase();
61
+ if (sl === lower)
62
+ return true;
63
+ return sl.endsWith(`-${lower}`) || sl.endsWith(`_${lower}`)
64
+ || sl.startsWith(`${lower}-`) || sl.startsWith(`${lower}_`);
65
+ });
66
+ if (candidates.length === 1) {
67
+ return {
68
+ status: 'resolved',
69
+ proposed,
70
+ resolved: candidates[0],
71
+ reason: `proposed '${bare}' is a bare qualifier; the store's vocabulary declares '${candidates[0]}'`,
72
+ };
73
+ }
74
+ if (candidates.length > 1)
75
+ return { status: 'ambiguous', proposed, candidates: candidates.sort() };
76
+ return { status: 'unknown', proposed };
77
+ }
78
+ /**
79
+ * Render the vocabulary as a compact, promptable list. This is what makes the vocabulary \emph{discoverable} to a
80
+ * distiller: it can be shown the signals that exist before being asked to choose a facet, instead of guessing.
81
+ * Bounded by `max` so a large store cannot blow a prompt budget; the most-declared signals come first, and the
82
+ * count is reported so a reader can tell a load-bearing scope from a one-off tag.
83
+ */
84
+ export function renderScopeVocabulary(vocab, max = 40) {
85
+ if (vocab.entries.length === 0)
86
+ return '(no signals observed in this store yet)';
87
+ const shown = vocab.entries.slice(0, max);
88
+ const body = shown.map((e) => `${e.signal} (${e.assetCount})`).join(', ');
89
+ const omitted = vocab.entries.length - shown.length;
90
+ return omitted > 0 ? `${body}, ... and ${omitted} more` : body;
91
+ }
@@ -0,0 +1,22 @@
1
+ export declare const TASK_DOMAIN_SIGNAL_PREFIX: "task_domain:";
2
+ export type TaskDomainResolution = {
3
+ status: 'absent';
4
+ } | {
5
+ status: 'resolved';
6
+ slug: string;
7
+ } | {
8
+ status: 'ambiguous';
9
+ } | {
10
+ status: 'invalid';
11
+ };
12
+ /** Identify the namespace even when the value is malformed, so it cannot leak into generic matching. */
13
+ export declare function isTaskDomainSignal(raw: string): boolean;
14
+ /** Remove task-domain tokens from generic matching while preserving their original wire representation elsewhere. */
15
+ export declare function withoutTaskDomainSignals(signals: readonly string[]): string[];
16
+ /**
17
+ * Resolve one canonical task domain from signal tokens. Parsing is order-independent and fail-closed:
18
+ * malformed tokens invalidate the whole dimension, and distinct valid slugs are ambiguous.
19
+ */
20
+ export declare function resolveTaskDomainSignals(signals: readonly string[]): TaskDomainResolution;
21
+ /** Emit a canonical wire token. Callers must supply an already-normalized lowercase slug. */
22
+ export declare function taskDomainSignal(slug: string): string;
@@ -0,0 +1,43 @@
1
+ export const TASK_DOMAIN_SIGNAL_PREFIX = 'task_domain:';
2
+ const TASK_DOMAIN_SLUG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
3
+ /** Identify the namespace even when the value is malformed, so it cannot leak into generic matching. */
4
+ export function isTaskDomainSignal(raw) {
5
+ return raw.trim().toLowerCase().startsWith(TASK_DOMAIN_SIGNAL_PREFIX);
6
+ }
7
+ /** Remove task-domain tokens from generic matching while preserving their original wire representation elsewhere. */
8
+ export function withoutTaskDomainSignals(signals) {
9
+ return signals.filter((signal) => !isTaskDomainSignal(signal));
10
+ }
11
+ /**
12
+ * Resolve one canonical task domain from signal tokens. Parsing is order-independent and fail-closed:
13
+ * malformed tokens invalidate the whole dimension, and distinct valid slugs are ambiguous.
14
+ */
15
+ export function resolveTaskDomainSignals(signals) {
16
+ const slugs = new Set();
17
+ let found = false;
18
+ for (const raw of signals) {
19
+ const signal = raw.trim();
20
+ const lower = signal.toLowerCase();
21
+ if (!lower.startsWith(TASK_DOMAIN_SIGNAL_PREFIX))
22
+ continue;
23
+ found = true;
24
+ if (signal !== raw)
25
+ return { status: 'invalid' };
26
+ const slug = lower.slice(TASK_DOMAIN_SIGNAL_PREFIX.length);
27
+ if (!TASK_DOMAIN_SLUG_RE.test(slug))
28
+ return { status: 'invalid' };
29
+ slugs.add(slug);
30
+ }
31
+ if (!found)
32
+ return { status: 'absent' };
33
+ if (slugs.size !== 1)
34
+ return { status: 'ambiguous' };
35
+ return { status: 'resolved', slug: slugs.values().next().value };
36
+ }
37
+ /** Emit a canonical wire token. Callers must supply an already-normalized lowercase slug. */
38
+ export function taskDomainSignal(slug) {
39
+ if (!TASK_DOMAIN_SLUG_RE.test(slug)) {
40
+ throw new Error(`invalid task_domain slug: ${slug}`);
41
+ }
42
+ return `${TASK_DOMAIN_SIGNAL_PREFIX}${slug}`;
43
+ }
@@ -107,6 +107,13 @@ export declare class AgentRunTraceRecorder {
107
107
  private readonly recorded;
108
108
  constructor(opts: AgentRunTraceRecorderOptions);
109
109
  get events(): readonly TraceEvent[];
110
+ get sessionId(): string | undefined;
111
+ /**
112
+ * Late-bind a session id discovered after run start (e.g. unique proxy llm_turn session).
113
+ * Backfills already-recorded events so the whole trajectory carries the join key.
114
+ * Fail closed on empty/whitespace values and conflicting rebinds.
115
+ */
116
+ bindSessionId(sessionId: string): void;
110
117
  runStarted(input?: RunStartedInput): TraceEvent;
111
118
  modelCalled(input?: ModelCalledInput): TraceEvent;
112
119
  toolCalled(input: ToolCalledInput): TraceEvent;
@@ -53,6 +53,28 @@ export class AgentRunTraceRecorder {
53
53
  this.opts = opts;
54
54
  }
55
55
  get events() { return this.recorded; }
56
+ get sessionId() { return this.opts.sessionId; }
57
+ /**
58
+ * Late-bind a session id discovered after run start (e.g. unique proxy llm_turn session).
59
+ * Backfills already-recorded events so the whole trajectory carries the join key.
60
+ * Fail closed on empty/whitespace values and conflicting rebinds.
61
+ */
62
+ bindSessionId(sessionId) {
63
+ const next = sessionId.trim();
64
+ if (next.length === 0) {
65
+ throw new Error('sessionId must be a non-empty string');
66
+ }
67
+ const current = this.opts.sessionId;
68
+ if (current !== undefined) {
69
+ if (current === next)
70
+ return;
71
+ throw new Error(`sessionId already bound to ${current}; refusing to rebind to ${next}`);
72
+ }
73
+ this.opts.sessionId = next;
74
+ for (const event of this.recorded) {
75
+ event.sessionId = next;
76
+ }
77
+ }
56
78
  runStarted(input = {}) {
57
79
  return this.record('run.started', {
58
80
  ...(input.taskSummary !== undefined ? { taskSummary: input.taskSummary } : {}),
@@ -78,5 +78,13 @@ export interface TraceTrajectoryDraft {
78
78
  coverage: TraceCoverageItem[];
79
79
  }
80
80
  export declare function traceRecordToTurnDraft(record: NativeTraceRecord): TraceTurnDraft | null;
81
+ /**
82
+ * Exact-join helper for Learning Ops: return the unique non-empty session id across turns.
83
+ * Fail closed — 0 or >1 distinct ids yield null. Matches the correlation key contract used by
84
+ * Darwin (`cc::<session_id>` ↔ packet `traceEvents[].sessionId`).
85
+ */
86
+ export declare function uniqueSessionId(turns: readonly {
87
+ session_id?: string | null;
88
+ }[]): string | null;
81
89
  export declare function buildTraceTrajectoryDraft(records: readonly NativeTraceRecord[]): TraceTrajectoryDraft;
82
90
  export declare function coverageForTurns(turns: readonly TraceTurnDraft[]): TraceCoverageItem[];
@@ -95,12 +95,24 @@ export function traceRecordToTurnDraft(record) {
95
95
  ...(validation !== undefined ? { validation } : {}),
96
96
  };
97
97
  }
98
+ /**
99
+ * Exact-join helper for Learning Ops: return the unique non-empty session id across turns.
100
+ * Fail closed — 0 or >1 distinct ids yield null. Matches the correlation key contract used by
101
+ * Darwin (`cc::<session_id>` ↔ packet `traceEvents[].sessionId`).
102
+ */
103
+ export function uniqueSessionId(turns) {
104
+ const sessionIds = [
105
+ ...new Set(turns
106
+ .map((turn) => turn.session_id)
107
+ .filter((value) => typeof value === 'string' && value.length > 0)),
108
+ ];
109
+ return sessionIds.length === 1 ? sessionIds[0] : null;
110
+ }
98
111
  export function buildTraceTrajectoryDraft(records) {
99
112
  const turns = records.map(traceRecordToTurnDraft).filter((turn) => turn !== null);
100
- const sessionIds = [...new Set(turns.map((turn) => turn.session_id).filter((value) => typeof value === 'string' && value.length > 0))];
101
113
  return {
102
114
  schema: 'evolver_trace_trajectory_draft.v1',
103
- session_id: sessionIds.length === 1 ? sessionIds[0] : null,
115
+ session_id: uniqueSessionId(turns),
104
116
  turns,
105
117
  coverage: coverageForTurns(turns),
106
118
  };
@@ -1,35 +1,122 @@
1
1
  export declare function syncSleep(ms: number): void;
2
+ export type FileLockProcessStartIdentity = {
3
+ readonly source: 'linux-proc';
4
+ readonly bootId: string;
5
+ readonly startTicks: string;
6
+ } | {
7
+ readonly source: 'windows-powershell';
8
+ readonly startTimeTicks: string;
9
+ } | {
10
+ readonly source: 'darwin-ps';
11
+ readonly startTime: string;
12
+ };
13
+ export interface FileLockOwnerRecord {
14
+ readonly pid: number;
15
+ readonly token: string;
16
+ readonly processStartIdentity: FileLockProcessStartIdentity;
17
+ }
18
+ export type TransferLockOwnershipReason = 'transferred' | 'not_owned' | 'ownership_changed' | 'mutation_busy' | 'guardian_exists' | 'guardian_missing' | 'target_process_dead' | 'target_process_pid_reused' | 'target_process_unverifiable' | 'path_changed';
19
+ export type TransferLockOwnershipResult = {
20
+ transferred: true;
21
+ reason: 'transferred';
22
+ guardian: FileLockOwnerRecord;
23
+ } | {
24
+ transferred: false;
25
+ reason: Exclude<TransferLockOwnershipReason, 'transferred'>;
26
+ };
27
+ export type AttachLockGuardianResult = {
28
+ attached: true;
29
+ reason: 'attached';
30
+ guardian: FileLockOwnerRecord;
31
+ } | {
32
+ attached: false;
33
+ reason: Exclude<TransferLockOwnershipReason, 'transferred'>;
34
+ };
35
+ export type RetainLockGuardianResult = {
36
+ retained: true;
37
+ reason: 'retained';
38
+ } | {
39
+ retained: false;
40
+ reason: 'not_owned' | 'ownership_changed' | 'mutation_busy' | 'guardian_missing' | 'path_changed';
41
+ };
42
+ export type ClearLockGuardianResult = {
43
+ cleared: true;
44
+ reason: 'cleared' | 'missing';
45
+ } | {
46
+ cleared: false;
47
+ reason: 'not_owned' | 'ownership_changed' | 'mutation_busy' | 'path_changed';
48
+ };
49
+ export type FileLockOwnerProcessStatus = 'current' | 'dead' | 'pid_reused' | 'unverifiable';
2
50
  export declare class LockTimeoutError extends Error {
3
51
  readonly code = "LOCK_TIMEOUT";
4
52
  constructor(_lockPath?: string);
5
53
  }
6
- export type UnsafeLockPathReason = 'symlink' | 'not_regular_file' | 'owner_too_large' | 'path_changed' | 'permission_denied' | 'invalid_owner';
54
+ export type UnsafeLockPathReason = 'symlink' | 'not_regular_file' | 'owner_too_large' | 'path_changed' | 'permission_denied' | 'process_identity_unavailable' | 'invalid_owner';
7
55
  export declare class UnsafeLockPathError extends Error {
8
56
  readonly reason: UnsafeLockPathReason;
9
57
  readonly code = "UNSAFE_LOCK_PATH";
10
58
  constructor(reason: UnsafeLockPathReason);
11
59
  }
12
60
  export declare const MAX_LOCK_OWNER_BYTES = 4096;
61
+ /** Fresh native process-start observation. Unlike lock acquisition, this does not use caches. */
62
+ export declare function readFileLockProcessStartIdentity(pid: number): FileLockProcessStartIdentity | null;
63
+ export declare function sameFileLockProcessStartIdentity(left: FileLockProcessStartIdentity, right: FileLockProcessStartIdentity): boolean;
64
+ export declare function parseFileLockProcessStartIdentity(value: unknown): FileLockProcessStartIdentity | undefined;
65
+ /**
66
+ * Conservative owner classification for crash recovery. PID reuse and unavailable identity
67
+ * remain distinct from a proven-dead PID so callers cannot reclaim ambiguous ownership.
68
+ */
69
+ export declare function inspectFileLockOwnerProcess(owner: Pick<FileLockOwnerRecord, 'pid' | 'processStartIdentity'>): FileLockOwnerProcessStatus;
70
+ export declare function _setFileLockTestHooksForTest(hooks?: {
71
+ processStartIdentity?: (pid: number) => FileLockProcessStartIdentity | null;
72
+ pidAlive?: (pid: number) => boolean;
73
+ beforeOwnerPublish?: (temporaryPath: string, lockPath: string) => void;
74
+ beforeGuardianPublish?: (temporaryPath: string, guardianPath: string) => void;
75
+ afterGuardianPublish?: (guardianPath: string, lockPath: string) => void;
76
+ beforeGuardianReclaim?: (guardianPath: string, lockPath: string) => void;
77
+ beforeMutationGuardRelease?: (mutationGuardPath: string, lockPath: string) => void;
78
+ }): void;
79
+ /**
80
+ * Pre-arm an acquired lock with the exact child PID generation while retaining local ownership.
81
+ * A controller can therefore keep mutating its journal, while a hard controller crash cannot make
82
+ * the primary owner reclaimable until the child exits.
83
+ */
84
+ export declare function attachLockGuardianToProcess(lockPath: string, expectedOwner: FileLockOwnerRecord, targetPid: number): AttachLockGuardianResult;
85
+ /**
86
+ * Drop local ownership only after an already-published guardian has been revalidated exactly.
87
+ * No file mutation is needed: the durable guardian receipt is the crash-safe owner binding.
88
+ */
89
+ export declare function retainLockGuardianForProcess(lockPath: string, expectedOwner: FileLockOwnerRecord, expectedGuardian: FileLockOwnerRecord): RetainLockGuardianResult;
90
+ /**
91
+ * Remove an exact attached guardian after the child has definitely exited, retaining the primary
92
+ * owner so rollback or another launch can continue under the same lifecycle lease.
93
+ */
94
+ export declare function clearLockGuardianForProcess(lockPath: string, expectedOwner: FileLockOwnerRecord, expectedGuardian: FileLockOwnerRecord): ClearLockGuardianResult;
95
+ /**
96
+ * Atomically bind an acquired lock to a still-running child and relinquish local ownership.
97
+ */
98
+ export declare function transferLockOwnershipToProcess(lockPath: string, expectedOwner: FileLockOwnerRecord, targetPid: number): TransferLockOwnershipResult;
13
99
  export interface AcquireLockOptions {
14
100
  maxTries?: number;
15
101
  waitMs?: number;
16
102
  }
17
103
  /**
18
- * Cross-process O_EXCL file lock with crashed-owner recovery.
104
+ * Cross-process file lock with crashed-owner recovery.
19
105
  *
20
106
  * The lock file records the owner pid and token. If a waiter finds the lock held by a pid that is no longer
21
107
  * alive (the owner crashed without releaseLock), it reclaims the stale lock instead of spinning
22
108
  * until timeout — otherwise one crashed process would deadlock every future writer until the file
23
109
  * is removed by hand. Empty or truncated locks are reclaimed only after the same inode and contents
24
110
  * remain malformed for a grace period, so a live creator can finish publishing its owner payload.
25
- * Acquisition stays atomic (O_EXCL), and stale reclaim moves the old lock aside under a mutation
26
- * guard, then verifies the inode snapshot before deletion. A live owner's lock (including this
111
+ * Acquisition publishes a fully synced temporary owner with an atomic no-replace hardlink, and stale
112
+ * reclaim moves the old lock aside under a mutation guard, then verifies the inode snapshot before
113
+ * deletion. A live owner's lock (including this
27
114
  * process's own) is never stolen.
28
115
  *
29
116
  * NOTE: still synchronous (blocks the event loop while waiting) by design — it guards short
30
117
  * synchronous critical sections (append-only writes).
31
118
  */
32
- export declare function acquireLock(lockPath: string, opts?: AcquireLockOptions): void;
119
+ export declare function acquireLock(lockPath: string, opts?: AcquireLockOptions): FileLockOwnerRecord;
33
120
  export type ReleaseLockReason = 'released' | 'missing' | 'ownership_changed' | 'not_owned' | 'released_with_cleanup_error' | UnsafeLockPathReason | 'release_failed';
34
121
  export interface ReleaseLockResult {
35
122
  released: boolean;