@evomap/evolver-adapter-public 2.0.0-beta.18 → 2.0.0-beta.19
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/dist/auth/credentialStore.d.ts +87 -3
- package/dist/auth/credentialStore.js +1008 -10
- package/dist/auth/oauthDeviceToken.d.ts +9 -6
- package/dist/auth/oauthDeviceToken.js +71 -18
- package/dist/auth/oauthHttpTransport.d.ts +4 -0
- package/dist/auth/oauthHttpTransport.js +66 -15
- package/dist/hubCapability.d.ts +9 -3
- package/dist/hubCapability.js +139 -28
- package/dist/hubFetch.d.ts +43 -10
- package/dist/hubFetch.js +311 -76
- package/dist/hubReuse.d.ts +40 -0
- package/dist/hubReuse.js +312 -32
- package/dist/learningPacketFeedback.js +2 -1
- package/dist/learningPacketSink.js +1 -0
- package/dist/wireMap.d.ts +3 -1
- package/dist/wireMap.js +20 -1
- package/package.json +5 -2
package/dist/hubReuse.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
// - search cache: signal-fingerprint → phase-1 metadata (short TTL). Repeat signal set → ZERO hub calls.
|
|
13
13
|
// - payload cache: assetId → phase-3 payload (content-addressed, long/permanent, bounded LRU). A cached
|
|
14
14
|
// payload → ZERO fetch. Both clocks are injected so TTL/eviction is deterministic and testable.
|
|
15
|
-
import {
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
import { hub, algo, wire } from '@evomap/evolver-core';
|
|
16
17
|
const { scoreSearchResults, decideReuse, DEFAULT_MIN_REUSE_SCORE, } = hub;
|
|
17
18
|
const GENE_WIRE_KEYS = new Set([
|
|
18
19
|
'type',
|
|
@@ -33,10 +34,52 @@ const GENE_WIRE_KEYS = new Set([
|
|
|
33
34
|
'generation_meta',
|
|
34
35
|
'asset_id',
|
|
35
36
|
]);
|
|
37
|
+
const HUB_DELIVERY_METADATA_KEYS = new Set([
|
|
38
|
+
'status',
|
|
39
|
+
'success_streak',
|
|
40
|
+
'reputation_score',
|
|
41
|
+
'gdi_score',
|
|
42
|
+
'gdi_score_mean',
|
|
43
|
+
'success_rate',
|
|
44
|
+
'reuse_count',
|
|
45
|
+
'ranking_score',
|
|
46
|
+
'credit_cost',
|
|
47
|
+
'source_node_id',
|
|
48
|
+
'fetched_at',
|
|
49
|
+
'receipt',
|
|
50
|
+
'hub_receipt',
|
|
51
|
+
'already_purchased',
|
|
52
|
+
'_semantic_similarity',
|
|
53
|
+
'semantic_similarity',
|
|
54
|
+
'similarity',
|
|
55
|
+
'semanticSimilarity',
|
|
56
|
+
'_search_score',
|
|
57
|
+
'search_score',
|
|
58
|
+
'payload_backfill_reason',
|
|
59
|
+
'asset_type',
|
|
60
|
+
'bundle_id',
|
|
61
|
+
'callable',
|
|
62
|
+
'payload_ready',
|
|
63
|
+
'bundle_capsule',
|
|
64
|
+
'bundle_events',
|
|
65
|
+
]);
|
|
36
66
|
// ── Cache config (ported from v1 hubSearch.js) ───────────────────────────────
|
|
37
67
|
export const SEARCH_CACHE_TTL_MS = 5 * 60 * 1000; // metadata is hot but staleable — short TTL
|
|
38
68
|
export const SEARCH_CACHE_MAX = 200;
|
|
39
69
|
export const PAYLOAD_CACHE_MAX = 100;
|
|
70
|
+
export const SEMANTIC_SEARCH_LIMIT = 10;
|
|
71
|
+
export const SEMANTIC_QUERY_MAX_TERMS = 12;
|
|
72
|
+
export const SEMANTIC_QUERY_MAX_CHARS = 512;
|
|
73
|
+
// Namespace filtering removes obviously private signal classes. The term allowlist below is still mandatory:
|
|
74
|
+
// even a public namespace can contain an arbitrary user-controlled value that must not enter a logged GET URL.
|
|
75
|
+
const PUBLIC_SEMANTIC_NAMESPACES = new Set(['area', 'cap', 'capability_gap', 'risk']);
|
|
76
|
+
const PUBLIC_SEMANTIC_TERMS = new Set([
|
|
77
|
+
'401', '403', '404', '409', '429', '500', '502', '503', '504',
|
|
78
|
+
'auth', 'cache', 'capability_gap', 'code_review', 'concurrency', 'database', 'debugging', 'go',
|
|
79
|
+
'javascript', 'latency', 'memory', 'network', 'performance', 'python',
|
|
80
|
+
'rate_limit', 'reliability', 'retry', 'rust', 'security', 'testing', 'timeout',
|
|
81
|
+
'typescript',
|
|
82
|
+
]);
|
|
40
83
|
export const DEFAULT_REUSE_MODE = 'reference';
|
|
41
84
|
/** Reads EVOLVER_MIN_REUSE_SCORE here (env is an ADAPTER concern — core never reads it). */
|
|
42
85
|
export function getMinReuseScore(env = process.env) {
|
|
@@ -47,10 +90,83 @@ export function getMinReuseScore(env = process.env) {
|
|
|
47
90
|
export function getReuseMode(env = process.env) {
|
|
48
91
|
return String(env['EVOLVER_REUSE_MODE'] ?? DEFAULT_REUSE_MODE).toLowerCase() === 'direct' ? 'direct' : 'reference';
|
|
49
92
|
}
|
|
93
|
+
/** V1-compatible kill-switch. Semantic recall is on unless explicitly disabled. */
|
|
94
|
+
export function isSemanticSearchEnabled(env = process.env) {
|
|
95
|
+
const value = String(env['HUBSEARCH_SEMANTIC'] ?? '').trim().toLowerCase();
|
|
96
|
+
return value !== '0' && value !== 'false';
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Derive a bounded public semantic query from structured signal tags. Error signatures, paths, prose, and other
|
|
100
|
+
* unstructured values are excluded so the vector-search leg cannot become a side channel for local diagnostics.
|
|
101
|
+
*/
|
|
102
|
+
export function buildSemanticQuery(signals) {
|
|
103
|
+
const terms = [];
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
for (const raw of signals) {
|
|
106
|
+
const signal = String(raw).trim();
|
|
107
|
+
const lower = signal.toLowerCase();
|
|
108
|
+
if (!signal || lower.startsWith('errsig:') || lower.startsWith('errsig_norm:') || lower.startsWith('recurring_errsig'))
|
|
109
|
+
continue;
|
|
110
|
+
const colon = signal.indexOf(':');
|
|
111
|
+
if (colon > 0 && !PUBLIC_SEMANTIC_NAMESPACES.has(lower.slice(0, colon)))
|
|
112
|
+
continue;
|
|
113
|
+
const candidate = (colon > 0 && colon < 30 ? signal.slice(colon + 1) : signal).trim().toLowerCase();
|
|
114
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(candidate)
|
|
115
|
+
// Signals are user-controlled. Only a fixed public taxonomy may enter the GET query because URLs are
|
|
116
|
+
// commonly retained by Hub and reverse-proxy access logs. Unknown terms remain in the structured POST leg.
|
|
117
|
+
|| !PUBLIC_SEMANTIC_TERMS.has(candidate)
|
|
118
|
+
|| seen.has(candidate))
|
|
119
|
+
continue;
|
|
120
|
+
const nextLength = terms.length === 0 ? candidate.length : terms.join(' ').length + 1 + candidate.length;
|
|
121
|
+
if (nextLength > SEMANTIC_QUERY_MAX_CHARS)
|
|
122
|
+
break;
|
|
123
|
+
seen.add(candidate);
|
|
124
|
+
terms.push(candidate);
|
|
125
|
+
if (terms.length >= SEMANTIC_QUERY_MAX_TERMS)
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
return terms.join(' ');
|
|
129
|
+
}
|
|
50
130
|
/** Stable signal fingerprint (ported from v1 _cacheKey: sort + join). */
|
|
51
131
|
export function signalFingerprint(signals) {
|
|
52
132
|
return [...signals].map((s) => String(s).trim()).filter(Boolean).sort().join('|');
|
|
53
133
|
}
|
|
134
|
+
export const TASK_DOMAIN_SIGNAL_PREFIX = 'task_domain:';
|
|
135
|
+
/**
|
|
136
|
+
* evolver domain slug → hub domain taxonomy (evomap-hub domainDetectionService VALID_DOMAINS).
|
|
137
|
+
* Only mapped slugs may ride the wire: the hub validates against its own taxonomy and silently
|
|
138
|
+
* ignores unknown values (fail-open), so an unmapped slug would just waste the fence. Slugs the
|
|
139
|
+
* hub has no counterpart for (pdf/mail/calendar) intentionally map to nothing.
|
|
140
|
+
*/
|
|
141
|
+
const HUB_DOMAIN_BY_SLUG = {
|
|
142
|
+
coding: 'software_engineering',
|
|
143
|
+
sql: 'software_engineering',
|
|
144
|
+
pptx: 'content_creation',
|
|
145
|
+
docx: 'content_creation',
|
|
146
|
+
xlsx: 'data_analysis',
|
|
147
|
+
marketing: 'marketing',
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Resolve the hub-side domain fence from this turn's signals. Exactly one domain is used and only
|
|
151
|
+
* when the turn is unambiguous: with two or more distinct task_domain:* signals the turn spans
|
|
152
|
+
* domains, and scoping recall to either one would hide the other's assets — so we return null and
|
|
153
|
+
* fall back to unscoped recall (today's behaviour).
|
|
154
|
+
*/
|
|
155
|
+
export function hubDomainFromSignals(signals) {
|
|
156
|
+
const slugs = new Set();
|
|
157
|
+
for (const raw of signals) {
|
|
158
|
+
const signal = String(raw).trim().toLowerCase();
|
|
159
|
+
if (!signal.startsWith(TASK_DOMAIN_SIGNAL_PREFIX))
|
|
160
|
+
continue;
|
|
161
|
+
const slug = signal.slice(TASK_DOMAIN_SIGNAL_PREFIX.length).trim();
|
|
162
|
+
if (slug)
|
|
163
|
+
slugs.add(slug);
|
|
164
|
+
}
|
|
165
|
+
if (slugs.size !== 1)
|
|
166
|
+
return null;
|
|
167
|
+
const [slug] = slugs;
|
|
168
|
+
return HUB_DOMAIN_BY_SLUG[slug] ?? null;
|
|
169
|
+
}
|
|
54
170
|
/**
|
|
55
171
|
* The two-layer reuse cache. Bounded + TTL'd, per-process. A search-cache hit means phase 1 makes ZERO hub
|
|
56
172
|
* calls; a payload-cache hit means phase 3 makes ZERO hub calls. The clock is injected for deterministic tests.
|
|
@@ -76,10 +192,13 @@ export class ReuseCache {
|
|
|
76
192
|
this.search.delete(key);
|
|
77
193
|
return null;
|
|
78
194
|
}
|
|
195
|
+
this.search.delete(key);
|
|
196
|
+
this.search.set(key, e);
|
|
79
197
|
return e.value;
|
|
80
198
|
}
|
|
81
199
|
setSearch(key, value) {
|
|
82
|
-
|
|
200
|
+
const exists = this.search.delete(key);
|
|
201
|
+
if (!exists && this.search.size >= this.searchMax) {
|
|
83
202
|
const oldest = this.search.keys().next().value;
|
|
84
203
|
if (oldest !== undefined)
|
|
85
204
|
this.search.delete(oldest);
|
|
@@ -90,15 +209,19 @@ export class ReuseCache {
|
|
|
90
209
|
const asset = this.payload.get(assetId) ?? null;
|
|
91
210
|
if (!asset)
|
|
92
211
|
return null;
|
|
93
|
-
if (assetMatchesId(asset, assetId))
|
|
212
|
+
if (assetMatchesId(asset, assetId)) {
|
|
213
|
+
this.payload.delete(assetId);
|
|
214
|
+
this.payload.set(assetId, asset);
|
|
94
215
|
return asset;
|
|
216
|
+
}
|
|
95
217
|
this.payload.delete(assetId);
|
|
96
218
|
return null;
|
|
97
219
|
}
|
|
98
220
|
setPayload(assetId, payload) {
|
|
99
221
|
if (!assetMatchesId(payload, assetId))
|
|
100
222
|
return;
|
|
101
|
-
|
|
223
|
+
const exists = this.payload.delete(assetId);
|
|
224
|
+
if (!exists && this.payload.size >= this.payloadMax) {
|
|
102
225
|
const oldest = this.payload.keys().next().value;
|
|
103
226
|
if (oldest !== undefined)
|
|
104
227
|
this.payload.delete(oldest);
|
|
@@ -132,6 +255,15 @@ function stripHubPayloadMetadata(rec) {
|
|
|
132
255
|
}
|
|
133
256
|
return out;
|
|
134
257
|
}
|
|
258
|
+
function stripHubDeliveryMetadataForIntegrity(rec) {
|
|
259
|
+
const out = { ...rec };
|
|
260
|
+
for (const key of HUB_DELIVERY_METADATA_KEYS)
|
|
261
|
+
delete out[key];
|
|
262
|
+
// Hub ranking confidence is metadata for Genes, while Capsule.confidence is canonical content.
|
|
263
|
+
if (out['type'] === 'Gene')
|
|
264
|
+
delete out['confidence'];
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
135
267
|
/**
|
|
136
268
|
* Map a hub search row (AssetRecord with arbitrary quality fields) → the core's price-free HubMetadata.
|
|
137
269
|
* Accepts both camelCase and the hub's snake_case (gdi_score / success_rate / reuse_count / ...). Drops any
|
|
@@ -153,7 +285,9 @@ export function toHubMetadata(rec) {
|
|
|
153
285
|
...(num(r['gdi_score'] ?? r['gdiScore']) !== undefined ? { gdiScore: num(r['gdi_score'] ?? r['gdiScore']) } : {}),
|
|
154
286
|
...(num(r['success_rate'] ?? r['successRate']) !== undefined ? { successRate: num(r['success_rate'] ?? r['successRate']) } : {}),
|
|
155
287
|
...(num(r['reuse_count'] ?? r['reuseCount']) !== undefined ? { reuseCount: num(r['reuse_count'] ?? r['reuseCount']) } : {}),
|
|
156
|
-
...(num(r['similarity'] ?? r['
|
|
288
|
+
...(num(r['similarity'] ?? r['semantic_similarity'] ?? r['_semantic_similarity'] ?? r['semanticSimilarity']) !== undefined
|
|
289
|
+
? { semanticSimilarity: num(r['similarity'] ?? r['semantic_similarity'] ?? r['_semantic_similarity'] ?? r['semanticSimilarity']) }
|
|
290
|
+
: {}),
|
|
157
291
|
...(updatedAt !== undefined ? { updatedAt } : {}),
|
|
158
292
|
};
|
|
159
293
|
}
|
|
@@ -179,6 +313,68 @@ export function toGeneCandidate(rec) {
|
|
|
179
313
|
hubAsset: stripHubPayloadMetadata(rec),
|
|
180
314
|
};
|
|
181
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* Run the complete free-search phase shared by reuse and economic miss probes. This function never performs the
|
|
318
|
+
* paid fetch. Only complete dual-leg results enter the cache, so a partial outage cannot become a verified miss.
|
|
319
|
+
*/
|
|
320
|
+
export async function searchHubMetadata(cap, cache, signals, opts = {}) {
|
|
321
|
+
const signalList = signals.map((signal) => String(signal).trim()).filter(Boolean);
|
|
322
|
+
const fingerprint = signalFingerprint(signalList);
|
|
323
|
+
if (signalList.length === 0) {
|
|
324
|
+
return { signals: signalList, fingerprint, metadata: [], searchCached: false, complete: true };
|
|
325
|
+
}
|
|
326
|
+
const env = opts.env ?? process.env;
|
|
327
|
+
const semanticQuery = isSemanticSearchEnabled(env) ? buildSemanticQuery(signalList) : '';
|
|
328
|
+
const semanticActive = semanticQuery.length >= 3;
|
|
329
|
+
const semanticQueryDigest = semanticActive
|
|
330
|
+
? createHash('sha256').update(semanticQuery).digest('hex')
|
|
331
|
+
: undefined;
|
|
332
|
+
// Domain fence: derived from the turn's own task_domain:* signals (never from prose), mapped to
|
|
333
|
+
// the hub taxonomy. Scopes the structured signal leg only — the semantic leg already carries its
|
|
334
|
+
// own allowlisted free-text and stays domain-agnostic as the discovery fallback.
|
|
335
|
+
const hubDomain = hubDomainFromSignals(signalList);
|
|
336
|
+
const signalSearchLimit = opts.searchLimit ? opts.searchLimit : undefined;
|
|
337
|
+
const limitKey = signalSearchLimit === undefined ? 'all' : String(signalSearchLimit);
|
|
338
|
+
const domainKey = hubDomain === null ? '' : `:domain:${hubDomain}`;
|
|
339
|
+
const key = semanticQueryDigest
|
|
340
|
+
? `semantic:${fingerprint}:${semanticQueryDigest}:limit:${limitKey}${domainKey}`
|
|
341
|
+
: `signals:${fingerprint}:limit:${limitKey}${domainKey}`;
|
|
342
|
+
const cached = cache.getSearch(key);
|
|
343
|
+
if (cached !== null) {
|
|
344
|
+
return { signals: signalList, fingerprint, metadata: cached, searchCached: true, complete: true };
|
|
345
|
+
}
|
|
346
|
+
// Enter a promise boundary before invoking an injected provider: interface implementations can still throw
|
|
347
|
+
// synchronously even though their declared return type is Promise, and reuse must remain best-effort.
|
|
348
|
+
const signalSearch = Promise.resolve().then(() => cap.search({
|
|
349
|
+
signalsAny: signalList,
|
|
350
|
+
...(hubDomain !== null ? { domain: hubDomain } : {}),
|
|
351
|
+
...(signalSearchLimit ? { limit: signalSearchLimit } : {}),
|
|
352
|
+
}));
|
|
353
|
+
const semanticSearch = semanticActive
|
|
354
|
+
? Promise.resolve().then(() => cap.search({ text: semanticQuery, kind: 'Gene', limit: SEMANTIC_SEARCH_LIMIT }))
|
|
355
|
+
: Promise.resolve([]);
|
|
356
|
+
const [signalResult, semanticResult] = await Promise.allSettled([signalSearch, semanticSearch]);
|
|
357
|
+
const signalRows = signalResult.status === 'fulfilled' ? signalResult.value : [];
|
|
358
|
+
const semanticRows = semanticResult.status === 'fulfilled' ? semanticResult.value : [];
|
|
359
|
+
const failedSearch = signalResult.status === 'rejected'
|
|
360
|
+
? signalResult
|
|
361
|
+
: semanticResult.status === 'rejected'
|
|
362
|
+
? semanticResult
|
|
363
|
+
: undefined;
|
|
364
|
+
const metadata = mergeSearchRows(signalRows, semanticRows)
|
|
365
|
+
.map(toHubMetadata)
|
|
366
|
+
.filter((candidate) => candidate.assetId.length > 0);
|
|
367
|
+
if (!failedSearch)
|
|
368
|
+
cache.setSearch(key, metadata);
|
|
369
|
+
return {
|
|
370
|
+
signals: signalList,
|
|
371
|
+
fingerprint,
|
|
372
|
+
metadata,
|
|
373
|
+
searchCached: false,
|
|
374
|
+
complete: failedSearch === undefined,
|
|
375
|
+
...(failedSearch ? { error: failedSearch.reason } : {}),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
182
378
|
/**
|
|
183
379
|
* The reuse-before-solve flow. Returns the single winner (already fetched) as a selection candidate, or a
|
|
184
380
|
* solve-fresh verdict. Never throws on a hub error — reuse is an optimization, not a hard dependency:
|
|
@@ -189,40 +385,58 @@ export function toGeneCandidate(rec) {
|
|
|
189
385
|
* @param signals the local problem signals.
|
|
190
386
|
*/
|
|
191
387
|
export async function reuseBeforeSolve(cap, cache, signals, opts = {}) {
|
|
192
|
-
const
|
|
193
|
-
const
|
|
388
|
+
const env = opts.env ?? process.env;
|
|
389
|
+
const mode = opts.mode ?? getReuseMode(env);
|
|
390
|
+
const threshold = opts.threshold ?? getMinReuseScore(env);
|
|
194
391
|
const runId = opts.runId ?? null;
|
|
195
392
|
const log = opts.log;
|
|
196
|
-
const
|
|
393
|
+
const searchResult = await searchHubMetadata(cap, cache, signals, {
|
|
394
|
+
env,
|
|
395
|
+
...(opts.searchLimit ? { searchLimit: opts.searchLimit } : {}),
|
|
396
|
+
});
|
|
397
|
+
const { signals: signalList, fingerprint, metadata, searchCached, complete: searchComplete, error: searchFailure, } = searchResult;
|
|
197
398
|
if (signalList.length === 0) {
|
|
198
399
|
return { action: 'solve-fresh', mode, zeroHubCalls: true, reason: 'no_signals' };
|
|
199
400
|
}
|
|
200
401
|
// ── Phase 1: free search (signal fingerprint cache → ZERO hub calls on hit) ──
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return { action: 'solve-fresh', mode, zeroHubCalls: false, reason: 'search_error' };
|
|
212
|
-
}
|
|
213
|
-
metadata = rows.map(toHubMetadata).filter((m) => m.assetId.length > 0);
|
|
214
|
-
cache.setSearch(key, metadata);
|
|
402
|
+
const searchIncomplete = !searchComplete;
|
|
403
|
+
if (searchIncomplete && metadata.length === 0) {
|
|
404
|
+
log?.append({
|
|
405
|
+
run_id: runId,
|
|
406
|
+
action: 'hub_search_miss',
|
|
407
|
+
signals: signalList,
|
|
408
|
+
reason: 'search_error',
|
|
409
|
+
error: errMsg(searchFailure),
|
|
410
|
+
});
|
|
411
|
+
return { action: 'solve-fresh', mode, zeroHubCalls: false, reason: 'search_error' };
|
|
215
412
|
}
|
|
216
413
|
if (metadata.length === 0) {
|
|
217
|
-
|
|
218
|
-
|
|
414
|
+
const reason = searchIncomplete ? 'search_error' : 'no_results';
|
|
415
|
+
log?.append({
|
|
416
|
+
run_id: runId,
|
|
417
|
+
action: 'hub_search_miss',
|
|
418
|
+
signals: signalList,
|
|
419
|
+
reason,
|
|
420
|
+
via: searchCached ? 'search_cached' : 'search',
|
|
421
|
+
...(searchIncomplete ? { error: errMsg(searchFailure) } : {}),
|
|
422
|
+
});
|
|
423
|
+
return { action: 'solve-fresh', mode, zeroHubCalls: searchCached, reason };
|
|
219
424
|
}
|
|
220
425
|
// ── Phase 2: PURE decision (core — no price) ──
|
|
221
426
|
const ranked = scoreSearchResults(signalList, metadata, opts.now !== undefined ? { now: opts.now } : {});
|
|
222
427
|
const decision = decideReuse(ranked, { threshold });
|
|
223
428
|
if (decision.action === 'solve-fresh' || !decision.candidate) {
|
|
224
|
-
|
|
225
|
-
|
|
429
|
+
const reason = searchIncomplete ? 'search_error' : 'below_threshold';
|
|
430
|
+
log?.append({
|
|
431
|
+
run_id: runId,
|
|
432
|
+
action: 'hub_search_miss',
|
|
433
|
+
signals: signalList,
|
|
434
|
+
reason,
|
|
435
|
+
candidates: metadata.length,
|
|
436
|
+
threshold,
|
|
437
|
+
...(searchIncomplete ? { error: errMsg(searchFailure) } : {}),
|
|
438
|
+
});
|
|
439
|
+
return { action: 'solve-fresh', mode, zeroHubCalls: searchCached, reason };
|
|
226
440
|
}
|
|
227
441
|
// ── Phase 3: paid fetch for the ONE winner (payload cache → ZERO hub calls on hit) ──
|
|
228
442
|
const winner = decision.candidate;
|
|
@@ -233,14 +447,14 @@ export async function reuseBeforeSolve(cap, cache, signals, opts = {}) {
|
|
|
233
447
|
if (asset === null) {
|
|
234
448
|
try {
|
|
235
449
|
if (isAssetByIdFetcher(cap)) {
|
|
236
|
-
asset = await cap.fetchAssetById(winnerId);
|
|
450
|
+
asset = normalizeMatchedAssetId(await cap.fetchAssetById(winnerId), winnerId);
|
|
237
451
|
creditCost = asset?.['credit_cost'];
|
|
238
452
|
}
|
|
239
453
|
else {
|
|
240
454
|
const results = await cap.fetch({ signalsAny: signalList, limit: metadata.length });
|
|
241
455
|
// The paid fetch returns full payloads; select the winner by id (content-addressed match).
|
|
242
|
-
|
|
243
|
-
|
|
456
|
+
const fetched = results.find((candidate) => assetMatchesId(candidate, winnerId));
|
|
457
|
+
asset = normalizeMatchedAssetId(fetched, winnerId);
|
|
244
458
|
// Economic receipt read-through (read-only): surface credit_cost if the hub attached one, never gate.
|
|
245
459
|
const carrier = results.credit_cost
|
|
246
460
|
?? asset?.['credit_cost'];
|
|
@@ -258,7 +472,7 @@ export async function reuseBeforeSolve(cap, cache, signals, opts = {}) {
|
|
|
258
472
|
payloadCached = false;
|
|
259
473
|
}
|
|
260
474
|
if (!asset) {
|
|
261
|
-
return { action: 'solve-fresh', mode, zeroHubCalls:
|
|
475
|
+
return { action: 'solve-fresh', mode, zeroHubCalls: false, reason: 'fetch_empty' };
|
|
262
476
|
}
|
|
263
477
|
const zeroHubCalls = searchCached && payloadCached;
|
|
264
478
|
log?.append({
|
|
@@ -270,7 +484,7 @@ export async function reuseBeforeSolve(cap, cache, signals, opts = {}) {
|
|
|
270
484
|
// payload/cache pull rather than a fresh LLM solve, so ≈0 here. Never lets an emission error break reuse.
|
|
271
485
|
if (opts.onReuseHit) {
|
|
272
486
|
try {
|
|
273
|
-
opts.onReuseHit({ assetId: winnerId, cycleId: opts.cycleId ?? '', signalFingerprint:
|
|
487
|
+
opts.onReuseHit({ assetId: winnerId, cycleId: opts.cycleId ?? '', signalFingerprint: fingerprint, fetchTokens: 0 });
|
|
274
488
|
}
|
|
275
489
|
catch { /* emission must never break the reuse path */ }
|
|
276
490
|
}
|
|
@@ -284,12 +498,78 @@ export async function reuseBeforeSolve(cap, cache, signals, opts = {}) {
|
|
|
284
498
|
zeroHubCalls,
|
|
285
499
|
};
|
|
286
500
|
}
|
|
501
|
+
function mergeSearchRows(signalRows, semanticRows) {
|
|
502
|
+
const merged = [];
|
|
503
|
+
const indexById = new Map();
|
|
504
|
+
for (const row of signalRows) {
|
|
505
|
+
const record = row;
|
|
506
|
+
const assetId = String(record['asset_id'] ?? record['assetId'] ?? '');
|
|
507
|
+
if (!assetId || indexById.has(assetId))
|
|
508
|
+
continue;
|
|
509
|
+
indexById.set(assetId, merged.length);
|
|
510
|
+
merged.push(row);
|
|
511
|
+
}
|
|
512
|
+
for (const row of semanticRows) {
|
|
513
|
+
const record = row;
|
|
514
|
+
const assetId = String(record['asset_id'] ?? record['assetId'] ?? '');
|
|
515
|
+
if (!assetId)
|
|
516
|
+
continue;
|
|
517
|
+
const existingIndex = indexById.get(assetId);
|
|
518
|
+
if (existingIndex === undefined) {
|
|
519
|
+
indexById.set(assetId, merged.length);
|
|
520
|
+
merged.push(row);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const similarity = num(record['similarity'] ?? record['semantic_similarity'] ?? record['_semantic_similarity'] ?? record['semanticSimilarity']);
|
|
524
|
+
if (similarity !== undefined) {
|
|
525
|
+
merged[existingIndex] = { ...merged[existingIndex], similarity };
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return merged;
|
|
529
|
+
}
|
|
287
530
|
function errMsg(e) {
|
|
288
531
|
return e instanceof Error ? e.message : String(e);
|
|
289
532
|
}
|
|
290
533
|
function isAssetByIdFetcher(value) {
|
|
291
534
|
return typeof value.fetchAssetById === 'function';
|
|
292
535
|
}
|
|
536
|
+
function normalizeMatchedAssetId(asset, assetId) {
|
|
537
|
+
if (!assetMatchesId(asset, assetId))
|
|
538
|
+
return null;
|
|
539
|
+
const record = asset;
|
|
540
|
+
if (typeof record['asset_id'] === 'string')
|
|
541
|
+
return asset;
|
|
542
|
+
const normalized = { ...record, asset_id: assetId };
|
|
543
|
+
if (record['assetId'] === assetId)
|
|
544
|
+
delete normalized['assetId'];
|
|
545
|
+
if (record['id'] === assetId)
|
|
546
|
+
delete normalized['id'];
|
|
547
|
+
return normalized;
|
|
548
|
+
}
|
|
293
549
|
export function assetMatchesId(asset, assetId) {
|
|
294
|
-
|
|
550
|
+
if (!asset)
|
|
551
|
+
return false;
|
|
552
|
+
const record = asset;
|
|
553
|
+
const canonicalAssetId = typeof record['asset_id'] === 'string' ? record['asset_id'] : undefined;
|
|
554
|
+
if (canonicalAssetId !== undefined && canonicalAssetId !== assetId)
|
|
555
|
+
return false;
|
|
556
|
+
const hasCamelAlias = typeof record['assetId'] === 'string';
|
|
557
|
+
if (canonicalAssetId === undefined && hasCamelAlias && record['assetId'] !== assetId)
|
|
558
|
+
return false;
|
|
559
|
+
if (canonicalAssetId === undefined && !hasCamelAlias && record['id'] !== assetId)
|
|
560
|
+
return false;
|
|
561
|
+
const content = { ...record };
|
|
562
|
+
if (record['assetId'] === assetId)
|
|
563
|
+
delete content['assetId'];
|
|
564
|
+
if (record['id'] === assetId)
|
|
565
|
+
delete content['id'];
|
|
566
|
+
try {
|
|
567
|
+
if (assetId.startsWith('sha256:') && !/^sha256:[0-9a-f]{64}$/.test(assetId))
|
|
568
|
+
return false;
|
|
569
|
+
const contentMatches = wire.computeAssetId(stripHubDeliveryMetadataForIntegrity(content)) === assetId;
|
|
570
|
+
return assetId.startsWith('sha256:') ? contentMatches : canonicalAssetId !== undefined || contentMatches;
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
295
575
|
}
|
|
@@ -35,6 +35,7 @@ export class HubLearningPacketFeedbackClient {
|
|
|
35
35
|
const res = await this.opts.fetchFn(url, {
|
|
36
36
|
method: 'POST',
|
|
37
37
|
headers,
|
|
38
|
+
redirect: 'manual',
|
|
38
39
|
body: JSON.stringify({
|
|
39
40
|
decision: feedback.decision,
|
|
40
41
|
...(feedback.feedbackType !== undefined ? { feedbackType: feedback.feedbackType } : {}),
|
|
@@ -66,7 +67,7 @@ export class HubLearningPacketFeedbackClient {
|
|
|
66
67
|
const url = `${this.opts.baseUrl}${path}`;
|
|
67
68
|
assertHubUrlSecure(url);
|
|
68
69
|
const headers = await learningOpsAuthHeaders(this.opts.auth, 'GET', path);
|
|
69
|
-
const res = await this.opts.fetchFn(url, { method: 'GET', headers });
|
|
70
|
+
const res = await this.opts.fetchFn(url, { method: 'GET', headers, redirect: 'manual' });
|
|
70
71
|
if (res.status !== 200)
|
|
71
72
|
return { ok: false, reason: await failureReason(res) };
|
|
72
73
|
const body = await res.json().catch(() => null);
|
|
@@ -113,6 +113,7 @@ export class HubLearningPacketSink {
|
|
|
113
113
|
const res = await this.opts.fetchFn(url, {
|
|
114
114
|
method: 'POST',
|
|
115
115
|
headers,
|
|
116
|
+
redirect: 'manual',
|
|
116
117
|
body: JSON.stringify(learningPacketWireBody(draft, this.opts.nodeId?.())),
|
|
117
118
|
});
|
|
118
119
|
if (res.status === 201) {
|
package/dist/wireMap.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export declare function inboundToAgentEvent(m: Record<string, unknown>): hub.Age
|
|
|
6
6
|
* payload.signals, #69)。text 不是 fetch 字段(自由文本走 semantic-search 端点, 见 hubCapability.search)。
|
|
7
7
|
*/
|
|
8
8
|
export declare function searchQueryToFetchWire(q: hub.HubQuery): Record<string, unknown>;
|
|
9
|
+
/** Free discovery phase on /a2a/fetch. Keep this separate from the paid/full fetch mapper. */
|
|
10
|
+
export declare function searchQueryToSearchOnlyWire(q: hub.HubQuery): Record<string, unknown>;
|
|
9
11
|
/** core AgentEvent(出站) → 公版 outbound 消息(id+type 必填). */
|
|
10
12
|
export declare function agentEventToOutbound(e: hub.AgentEvent): Record<string, unknown>;
|
|
11
13
|
/** Retry policy for a non-2xx hub status, shared by every money-touching caller (anti-drift, #177). */
|
|
@@ -25,4 +27,4 @@ export type AtpRetryClass = 'permanent' | 'cooldown' | 'recoverable';
|
|
|
25
27
|
*/
|
|
26
28
|
export declare function atpRetryClass(status: number): AtpRetryClass;
|
|
27
29
|
/** /a2a/publish 响应 → PublishReceipt. 200=accepted; 402/4xx=rejected 终态. */
|
|
28
|
-
export declare function publishRespToReceipt(status: number, body: Record<string, unknown
|
|
30
|
+
export declare function publishRespToReceipt(status: number, body: Record<string, unknown>, retryAfterMs?: number): hub.PublishReceipt;
|
package/dist/wireMap.js
CHANGED
|
@@ -24,10 +24,16 @@ export function searchQueryToFetchWire(q) {
|
|
|
24
24
|
out['category'] = q.category;
|
|
25
25
|
if (q.gene)
|
|
26
26
|
out['gene'] = q.gene;
|
|
27
|
+
if (q.domain)
|
|
28
|
+
out['domain'] = q.domain;
|
|
27
29
|
if (q.limit !== undefined)
|
|
28
30
|
out['limit'] = q.limit;
|
|
29
31
|
return out;
|
|
30
32
|
}
|
|
33
|
+
/** Free discovery phase on /a2a/fetch. Keep this separate from the paid/full fetch mapper. */
|
|
34
|
+
export function searchQueryToSearchOnlyWire(q) {
|
|
35
|
+
return { ...searchQueryToFetchWire(q), search_only: true };
|
|
36
|
+
}
|
|
31
37
|
/** core AgentEvent(出站) → 公版 outbound 消息(id+type 必填). */
|
|
32
38
|
export function agentEventToOutbound(e) {
|
|
33
39
|
return {
|
|
@@ -59,7 +65,7 @@ export function atpRetryClass(status) {
|
|
|
59
65
|
return 'recoverable';
|
|
60
66
|
}
|
|
61
67
|
/** /a2a/publish 响应 → PublishReceipt. 200=accepted; 402/4xx=rejected 终态. */
|
|
62
|
-
export function publishRespToReceipt(status, body) {
|
|
68
|
+
export function publishRespToReceipt(status, body, retryAfterMs) {
|
|
63
69
|
const payload = body['payload'] ?? body;
|
|
64
70
|
const assetIds = payload['asset_ids'];
|
|
65
71
|
const targetAssetId = payload['target_asset_id']
|
|
@@ -86,6 +92,15 @@ export function publishRespToReceipt(status, body) {
|
|
|
86
92
|
// M8-1: 按语义而非纯状态码区分(都终态不重试 = money-safety: 不反复打经济端点).
|
|
87
93
|
// 402=creditShortage(余额不足) / 403=node 失效需 rebind / 409=duplicate / 422=payload 须修 / 429=cooldown.
|
|
88
94
|
const reasonByStatus = { 402: 'credit_shortage', 403: 'node_unauthorized', 409: 'duplicate', 422: 'invalid_payload', 429: 'cooldown' };
|
|
95
|
+
const rejectionCodeByStatus = {
|
|
96
|
+
400: 'invalid_request',
|
|
97
|
+
402: 'credit_shortage',
|
|
98
|
+
403: 'node_unauthorized',
|
|
99
|
+
404: 'not_found',
|
|
100
|
+
409: 'duplicate',
|
|
101
|
+
422: 'invalid_payload',
|
|
102
|
+
429: 'cooldown',
|
|
103
|
+
};
|
|
89
104
|
const receipt = {
|
|
90
105
|
receiptId: String(payload['receipt_id'] ?? 'rejected'),
|
|
91
106
|
status: 'rejected',
|
|
@@ -93,6 +108,10 @@ export function publishRespToReceipt(status, body) {
|
|
|
93
108
|
...(assetId ? { assetId } : {}),
|
|
94
109
|
...(assetIds ? { assetIds } : {}),
|
|
95
110
|
terminal: true,
|
|
111
|
+
rejection: {
|
|
112
|
+
code: rejectionCodeByStatus[status] ?? 'hub_rejected',
|
|
113
|
+
...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
|
|
114
|
+
},
|
|
96
115
|
};
|
|
97
116
|
if (status === 402) {
|
|
98
117
|
receipt.economic = {
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evomap/evolver-adapter-public",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.19",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": "^22.13.0 || >=23.4.0"
|
|
8
|
+
},
|
|
6
9
|
"description": "公版 hub 适配器 (积分/治理)",
|
|
7
10
|
"main": "./dist/index.js",
|
|
8
11
|
"types": "./dist/index.d.ts",
|
|
@@ -14,7 +17,7 @@
|
|
|
14
17
|
},
|
|
15
18
|
"dependencies": {
|
|
16
19
|
"@evomap/atp-sdk": "^0.1.0",
|
|
17
|
-
"@evomap/evolver-core": "2.0.0-beta.
|
|
20
|
+
"@evomap/evolver-core": "2.0.0-beta.19",
|
|
18
21
|
"undici": "^6.27.0"
|
|
19
22
|
},
|
|
20
23
|
"optionalDependencies": {
|