@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.
- package/assets/gep/genes.jsonl +5 -5
- package/dist/algo/bans.d.ts +10 -1
- package/dist/algo/bans.js +56 -6
- package/dist/algo/candidateAssembly.d.ts +25 -2
- package/dist/algo/candidateAssembly.js +143 -20
- package/dist/algo/capabilityCandidates.js +10 -0
- package/dist/algo/cycleEngine.d.ts +40 -5
- package/dist/algo/cycleEngine.js +277 -37
- package/dist/algo/evolutionEvent.d.ts +26 -0
- package/dist/algo/evolutionEvent.js +32 -0
- package/dist/algo/exploration.d.ts +7 -0
- package/dist/algo/exploration.js +16 -3
- package/dist/algo/geneHealth.d.ts +34 -1
- package/dist/algo/geneHealth.js +42 -0
- package/dist/algo/geneIntake.d.ts +46 -1
- package/dist/algo/geneIntake.js +121 -10
- package/dist/algo/geneSelection.d.ts +115 -6
- package/dist/algo/geneSelection.js +459 -32
- package/dist/algo/index.d.ts +4 -1
- package/dist/algo/index.js +4 -1
- package/dist/algo/kautoProjection.d.ts +41 -0
- package/dist/algo/kautoProjection.js +95 -0
- package/dist/algo/kautoValidator.d.ts +68 -0
- package/dist/algo/kautoValidator.js +256 -0
- package/dist/algo/orchestrator.d.ts +14 -1
- package/dist/algo/orchestrator.js +9 -2
- package/dist/algo/solidify.d.ts +11 -2
- package/dist/algo/solidify.js +37 -7
- package/dist/algo/ucb1.d.ts +53 -0
- package/dist/algo/ucb1.js +156 -0
- package/dist/assetrepair/hubRejection.d.ts +12 -0
- package/dist/assetrepair/hubRejection.js +109 -0
- package/dist/assetrepair/index.d.ts +2 -0
- package/dist/assetrepair/index.js +2 -0
- package/dist/assetrepair/repair.d.ts +33 -0
- package/dist/assetrepair/repair.js +155 -0
- package/dist/assetstore/assetSidecarRecords.js +5 -1
- package/dist/assetstore/assetStoreHealth.d.ts +2 -3
- package/dist/assetstore/assetStoreHealth.js +4 -10
- package/dist/assetstore/foreignJsonlSource.d.ts +48 -0
- package/dist/assetstore/foreignJsonlSource.js +150 -0
- package/dist/assetstore/index.d.ts +3 -0
- package/dist/assetstore/index.js +3 -0
- package/dist/assetstore/learningHistory.js +3 -3
- package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
- package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
- package/dist/assetstore/localJsonl.d.ts +8 -0
- package/dist/assetstore/localJsonl.js +128 -3
- package/dist/assetstore/provenance.d.ts +38 -3
- package/dist/assetstore/provenance.js +103 -45
- package/dist/assetstore/provider.d.ts +4 -0
- package/dist/assetstore/reviewFilter.d.ts +19 -1
- package/dist/assetstore/reviewFilter.js +36 -0
- package/dist/assetstore/unionReadStore.d.ts +25 -0
- package/dist/assetstore/unionReadStore.js +119 -0
- package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
- package/dist/benchmark/antiGeneBenchmark.js +4 -3
- package/dist/benchmark/antiGeneRollout.d.ts +2 -0
- package/dist/benchmark/antiGeneRollout.js +4 -3
- package/dist/benchmark/index.d.ts +2 -1
- package/dist/benchmark/index.js +2 -1
- package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
- package/dist/benchmark/selectionFlatAbstention.js +481 -0
- package/dist/bootstrap/envFingerprint.d.ts +9 -0
- package/dist/bootstrap/envFingerprint.js +5 -0
- package/dist/bootstrap/index.d.ts +2 -1
- package/dist/bootstrap/index.js +2 -1
- package/dist/bootstrap/lifecycleBootstrap.d.ts +111 -0
- package/dist/bootstrap/lifecycleBootstrap.js +433 -0
- package/dist/bootstrap/v1EnvCompat.d.ts +3 -0
- package/dist/bootstrap/v1EnvCompat.js +48 -1
- package/dist/events/eventArchive.d.ts +2 -0
- package/dist/events/eventArchive.js +13 -3
- package/dist/events/eventSchema.d.ts +7 -7
- package/dist/events/eventStore.d.ts +2 -0
- package/dist/events/eventStore.js +5 -1
- package/dist/events/ingest.d.ts +2 -1
- package/dist/events/ingest.js +6 -0
- package/dist/exec/autoExec.d.ts +30 -3
- package/dist/exec/autoExec.js +294 -29
- package/dist/exec/autonomousCycle.d.ts +7 -0
- package/dist/exec/autonomousCycle.js +2 -0
- package/dist/exec/claudeBridge.d.ts +28 -6
- package/dist/exec/claudeBridge.js +411 -258
- package/dist/exec/executionBinding.d.ts +414 -0
- package/dist/exec/executionBinding.js +588 -0
- package/dist/exec/index.d.ts +1 -0
- package/dist/exec/index.js +1 -0
- package/dist/exec/proofOfWork.d.ts +1 -1
- package/dist/exec/proofOfWork.js +2 -2
- package/dist/exec/runnerRegistry.d.ts +5 -0
- package/dist/exec/runnerRegistry.js +54 -14
- package/dist/hub/capability.d.ts +40 -1
- package/dist/hub/conversationDistiller.d.ts +19 -0
- package/dist/hub/conversationDistiller.js +115 -37
- package/dist/hub/fake.d.ts +1 -0
- package/dist/hub/fake.js +1 -0
- package/dist/hub/index.d.ts +1 -0
- package/dist/hub/index.js +1 -0
- package/dist/hub/recipeCompose.d.ts +27 -0
- package/dist/hub/recipeCompose.js +90 -0
- package/dist/hub/sanitize.js +119 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mailbox/ipcServer.d.ts +2 -0
- package/dist/mailbox/ipcServer.js +20 -0
- package/dist/mailbox/store.d.ts +52 -2
- package/dist/mailbox/store.js +546 -81
- package/dist/modelCompatibility.d.ts +164 -0
- package/dist/modelCompatibility.js +309 -0
- package/dist/observers/valueDigestObserver.d.ts +9 -0
- package/dist/observers/valueDigestObserver.js +35 -2
- package/dist/ops/cleanup.js +1 -1
- package/dist/ops/evolutionGraphProjection.d.ts +20 -0
- package/dist/ops/evolutionGraphProjection.js +315 -0
- package/dist/ops/index.d.ts +2 -1
- package/dist/ops/index.js +2 -1
- package/dist/ops/selfUpdate.d.ts +8 -0
- package/dist/ops/selfUpdate.js +24 -8
- package/dist/ops/valueOutreach.d.ts +3 -1
- package/dist/ops/valueOutreach.js +5 -1
- package/dist/personality/schema.d.ts +18 -18
- package/dist/schema/evolutionGraph.d.ts +784 -0
- package/dist/schema/evolutionGraph.js +187 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/proofOfWork.d.ts +125 -6
- package/dist/schema/proofOfWork.js +102 -4
- package/dist/shadow/shadowHub.js +1 -0
- package/dist/signals/expand.d.ts +15 -1
- package/dist/signals/expand.js +148 -0
- package/dist/signals/index.d.ts +3 -1
- package/dist/signals/index.js +3 -1
- package/dist/signals/scopeVocabulary.d.ts +75 -0
- package/dist/signals/scopeVocabulary.js +91 -0
- package/dist/signals/taskDomain.d.ts +22 -0
- package/dist/signals/taskDomain.js +43 -0
- package/dist/trace/learningTrace.d.ts +7 -0
- package/dist/trace/learningTrace.js +22 -0
- package/dist/trace/trajectory.d.ts +8 -0
- package/dist/trace/trajectory.js +14 -2
- package/dist/util/fileLock.d.ts +92 -5
- package/dist/util/fileLock.js +809 -70
- package/dist/util/index.d.ts +2 -1
- package/dist/util/index.js +1 -1
- package/dist/verify/index.d.ts +2 -1
- package/dist/verify/index.js +1 -1
- package/dist/verify/sandboxRunner.d.ts +2 -0
- package/dist/verify/sandboxRunner.js +154 -10
- package/dist/verify/sandboxedValidation.d.ts +7 -2
- package/dist/verify/sandboxedValidation.js +102 -24
- package/dist/verify/validation.d.ts +24 -3
- package/dist/verify/validation.js +141 -24
- package/dist/wire/geneHints.d.ts +88 -16
- package/dist/wire/geneHints.js +124 -15
- package/dist/wire/index.d.ts +8 -3
- package/dist/wire/index.js +2 -2
- package/dist/wire/schemaGate.d.ts +21 -0
- package/dist/wire/schemaGate.js +119 -10
- package/package.json +5 -4
package/dist/signals/expand.js
CHANGED
|
@@ -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
|
}
|
package/dist/signals/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/signals/index.js
CHANGED
|
@@ -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[];
|
package/dist/trace/trajectory.js
CHANGED
|
@@ -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:
|
|
115
|
+
session_id: uniqueSessionId(turns),
|
|
104
116
|
turns,
|
|
105
117
|
coverage: coverageForTurns(turns),
|
|
106
118
|
};
|
package/dist/util/fileLock.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
26
|
-
* guard, then verifies the inode snapshot before
|
|
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):
|
|
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;
|